diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 6064058..b47f691 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: [3.7, 3.8, 3.9]
+ python-version: [3.8, 3.9, 3.11, 3.12]
steps:
# --- Install steps
diff --git a/README.md b/README.md
index ff2f914..5bf9206 100644
--- a/README.md
+++ b/README.md
@@ -1,140 +1,140 @@
-[](https://github.com/ebranlard/weio/actions?query=workflow%3A%22Tests%22)
-
-
-# weio
-
-Library to read and write files, in particular files used by the Wind Energy community.
-This library is for instance used by the GUI tool [pyDatView](https://github.com/ebranlard/pydatview/) to plot, export and compare these different files.
-
-## Typical file formats supported
-- Various CSV and delimited files
-- Simple Excel files
-- FAST input and output files (including some turbulence files)
-- HAWC2 and HawcStab2 input and output files (still some missing)
-- Bladed output files
-- FLEX output files
-- NetCDF files (partial support for 1D and 2D data for now)
-
-
-## Quickstart
-Download, install dependencies, install package and run tests:
-```bash
-git clone https://github.com/ebranlard/weio
-cd weio
-python -m pip install --user -r requirements.txt
-python -m pip install -e . # install
-python -m unittest discover -v # run test
-```
-
-## Python package usage
-```python
-import weio
-f=weio.read('file.csv')
-print(f.toDataFrame())
-f.write('out.csv')
-```
-Example for an OpenFAST binary file:
-```python
-import weio
-df=weio.read('Output.outb').toDataFrame()
-plt.plot(df['Time_[s]'], df['GenPwr_[kW']))
-```
-Example to change an OpenFAST input file:
-```python
-import weio
-ED=weio.read('ElastoDyn.dat')
-print(ED.keys())
-ED['NacMass'] = 100000 # changing nacelle mass value
-ED['HubMass'] = 10000 # changing hub mass value
-ED.write('ElastoDyn_modified.dat')
-```
-Example to change an OpenFAST aerodynamic blade file :
-```python
-import weio
-import numpy as np
-Bld=weio.read('NREL5MW_AD15_blade.dat')
-nSpan = 10
-Spn = np.linspace(0, 15, nSpan) # BlSpn, radial stations [m]
-CrvAC = np.zeros((nSpan,)) # BlCrvAC, prebend (usually <0) [m]
-SwpAC = np.zeros((nSpan,)) # BlSwpC, sweep [m]
-CrvAng = np.concatenate(([0], np.arctan2((CrvAC[1:]-CrvAC[:-1]),(Spn[1:]-Spn[:-1]))*180/np.pi))
-Twist = np.zeros((nSpan,)) + 1 # BlTwist [deg]
-Chord = np.zeros((nSpan,)) + 5 # BlChord [m]
-AFID = np.zeros((nSpan,)).astype(int) # BlAFID [-]
-ADProp = np.column_stack((Spn,CrvAC,SwpAC,CrvAng,Twist,Chord,AFID))
-Bld['NumBlNds'] = ADProp.shape[0]
-Bld['BldAeroNodes'] = ADProp
-Bld.write('AeroDyn_Blade_Modified.dat')
-```
-
-## Requirements
-The library is compatible python 2.7 and python 3, and has limited requirements.
-If you have pip installed on your system, you can install them by typing in a terminal:
-```bash
-pip install -r requirements.txt
-```
-or type `make dep` from the main directory.
-
-
-## Download
-From the github page, click on the "Clone or download" button, and you may chose to download as Zip.
-Alternatively, from a command line:
-```bash
-git clone https://github.com/ebranlard/weio
-cd weio
-```
-
-## Installation
-The python packages mentioned in the Requirements section need to be installed.
-```bash
-pip install -e .
-```
-or
-```bash
-python setup.py install
-```
-
-
-## Adding more file formats
-There are two ways to add a file format. If your file format is fairly generic (e.g. CSV, Excel) you can add it directly to weio (see Option 1 below). Otherwise, it is recommended to use Option 2 below.
-
-### Option 1: Adding a generic/wind energy file format to this repository
-Additional file formats (that are either generic, or commonly used by the wind energy community) can be added as follows:
-
-- Copy paste the template file `weio/_NEWFILE_TEMPLATE.py`, to, for instance `weio/my_format_file.py`
-- Edit this file. Adjust the classname and the default extensions. Implement the reader (function `_read()`) and optionally the writer. Look for XXX in this file and replace them with appropriate value for your file format.
-- Register the fileformat in `weio/__init__.py` by adding an import line in the function `fileFormats()`.
-Registering the fileformat is useful when using `weio` with `pyDatView`, or, when using the automatic reader functionality: `weio.read('any_file.ext')`
-
-That's it. If possible, add some unittests and examples files:
-
-- Unittests are found in the folder `weio/tests/`. You can create a file `test_myformat.py` in this folder, using existing tests for inspiration.
-- Examples files can be placed in the folder `weio/tests/example_files/`. Try to use a minimal size for the example files (e.g. a couple of bytes/Kb).
-- To run your test from the repository root, type `python -m weio.tests.tests_myformat`.
-
-### Option 2: Adding specific/confidential file formats
-Specific file formats can be added in the `` folder of weio.
-Depending on your platform, the `` directory will be:
-
-- `C:/Users//AppData/Roaming/weio` on Windows
-- `/.local/share/weio/` on Linux
-- `/Library/Application Support/weio/` on MacOS
-
-To add specfic file formats, follow the following steps:
-
-- Create the `` directory if it doesn't exist.
-
-- Copy paste the template file `weio/_NEWFILE_TEMPLATE.py`, to, for instance `/my_format_file.py`.
-
-- Edit this file. Adjust the classname and the default extensions. Implementing the reader (function `_read()`) and optionally the writer. Look for XXX in this file and replace them with appropriate values for your file format. NOTE: it's important to have "File" in your classname, for instance the class name could be "MyFormatFile". You can also adjust the priority level (the priority static method), which will define how early the fileformat will be tried in the list of fileformats.
-
-The fileformats should now be available within weio. The function `weio.read` will loop through all available file formats and attempt to read a given file. You can call `weio.fileFormats` to see the list of supported fileformats and see where your newly added format is located in this list. The added class may be accessed as follows `from weio.user import MyFormatFile".
-
-## Contributing
-Any contributions to this project are welcome! If you find this project useful, you can also buy me a coffee (donate a small amount) with the link below:
-
-
-
-
-
-
+[](https://github.com/ebranlard/weio/actions?query=workflow%3A%22Tests%22)
+
+
+# weio
+
+Library to read and write files, in particular files used by the Wind Energy community.
+This library is for instance used by the GUI tool [pyDatView](https://github.com/ebranlard/pydatview/) to plot, export and compare these different files.
+
+## Typical file formats supported
+- Various CSV and delimited files
+- Simple Excel files
+- FAST input and output files (including some turbulence files)
+- HAWC2 and HawcStab2 input and output files (still some missing)
+- Bladed output files
+- FLEX output files
+- NetCDF files (partial support for 1D and 2D data for now)
+
+
+## Quickstart
+Download, install dependencies, install package and run tests:
+```bash
+git clone https://github.com/ebranlard/weio
+cd weio
+python -m pip install --user -r requirements.txt
+python -m pip install -e . # install
+python -m unittest discover -v # run test
+```
+
+## Python package usage
+```python
+import weio
+f=weio.read('file.csv')
+print(f.toDataFrame())
+f.write('out.csv')
+```
+Example for an OpenFAST binary file:
+```python
+import weio
+df=weio.read('Output.outb').toDataFrame()
+plt.plot(df['Time_[s]'], df['GenPwr_[kW']))
+```
+Example to change an OpenFAST input file:
+```python
+import weio
+ED=weio.read('ElastoDyn.dat')
+print(ED.keys())
+ED['NacMass'] = 100000 # changing nacelle mass value
+ED['HubMass'] = 10000 # changing hub mass value
+ED.write('ElastoDyn_modified.dat')
+```
+Example to change an OpenFAST aerodynamic blade file :
+```python
+import weio
+import numpy as np
+Bld=weio.read('NREL5MW_AD15_blade.dat')
+nSpan = 10
+Spn = np.linspace(0, 15, nSpan) # BlSpn, radial stations [m]
+CrvAC = np.zeros((nSpan,)) # BlCrvAC, prebend (usually <0) [m]
+SwpAC = np.zeros((nSpan,)) # BlSwpC, sweep [m]
+CrvAng = np.concatenate(([0], np.arctan2((CrvAC[1:]-CrvAC[:-1]),(Spn[1:]-Spn[:-1]))*180/np.pi))
+Twist = np.zeros((nSpan,)) + 1 # BlTwist [deg]
+Chord = np.zeros((nSpan,)) + 5 # BlChord [m]
+AFID = np.zeros((nSpan,)).astype(int) # BlAFID [-]
+ADProp = np.column_stack((Spn,CrvAC,SwpAC,CrvAng,Twist,Chord,AFID))
+Bld['NumBlNds'] = ADProp.shape[0]
+Bld['BldAeroNodes'] = ADProp
+Bld.write('AeroDyn_Blade_Modified.dat')
+```
+
+## Requirements
+The library is compatible python 3, and has limited requirements.
+If you have pip installed on your system, you can install them by typing in a terminal:
+```bash
+pip install -r requirements.txt
+```
+
+
+
+## Download
+From the github page, click on the "Clone or download" button, and you may chose to download as Zip.
+Alternatively, from a command line:
+```bash
+git clone https://github.com/ebranlard/weio
+cd weio
+```
+
+## Installation
+The python packages mentioned in the Requirements section need to be installed.
+```bash
+pip install -e .
+```
+or
+```bash
+python setup.py install
+```
+
+
+## Adding more file formats
+There are two ways to add a file format. If your file format is fairly generic (e.g. CSV, Excel) you can add it directly to weio (see Option 1 below). Otherwise, it is recommended to use Option 2 below.
+
+### Option 1: Adding a generic/wind energy file format to this repository
+Additional file formats (that are either generic, or commonly used by the wind energy community) can be added as follows:
+
+- Copy paste the template file `weio/_NEWFILE_TEMPLATE.py`, to, for instance `weio/my_format_file.py`
+- Edit this file. Adjust the classname and the default extensions. Implement the reader (function `_read()`) and optionally the writer. Look for XXX in this file and replace them with appropriate value for your file format.
+- Register the fileformat in `weio/__init__.py` by adding an import line in the function `fileFormats()`.
+Registering the fileformat is useful when using `weio` with `pyDatView`, or, when using the automatic reader functionality: `weio.read('any_file.ext')`
+
+That's it. If possible, add some unittests and examples files:
+
+- Unittests are found in the folder `weio/tests/`. You can create a file `test_myformat.py` in this folder, using existing tests for inspiration.
+- Examples files can be placed in the folder `weio/tests/example_files/`. Try to use a minimal size for the example files (e.g. a couple of bytes/Kb).
+- To run your test from the repository root, type `python -m weio.tests.tests_myformat`.
+
+### Option 2: Adding specific/confidential file formats
+Specific file formats can be added in the `` folder of weio.
+Depending on your platform, the `` directory will be:
+
+- `C:/Users//AppData/Roaming/weio` on Windows
+- `/.local/share/weio/` on Linux
+- `/Library/Application Support/weio/` on MacOS
+
+To add specfic file formats, follow the following steps:
+
+- Create the `` directory if it doesn't exist.
+
+- Copy paste the template file `weio/_NEWFILE_TEMPLATE.py`, to, for instance `/my_format_file.py`.
+
+- Edit this file. Adjust the classname and the default extensions. Implementing the reader (function `_read()`) and optionally the writer. Look for XXX in this file and replace them with appropriate values for your file format. NOTE: it's important to have "File" in your classname, for instance the class name could be "MyFormatFile". You can also adjust the priority level (the priority static method), which will define how early the fileformat will be tried in the list of fileformats.
+
+The fileformats should now be available within weio. The function `weio.read` will loop through all available file formats and attempt to read a given file. You can call `weio.fileFormats` to see the list of supported fileformats and see where your newly added format is located in this list. The added class may be accessed as follows `from weio.user import MyFormatFile".
+
+## Contributing
+Any contributions to this project are welcome! If you find this project useful, you can also buy me a coffee (donate a small amount) with the link below:
+
+
+
+
+
+
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..2117b96
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1 @@
+from .weio import *
diff --git a/developper_notes.md b/developper_notes.md
new file mode 100644
index 0000000..4394bfc
--- /dev/null
+++ b/developper_notes.md
@@ -0,0 +1,77 @@
+# 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 file in the `data` directory are compatible with the new OpenFAST version
+3. Change the file VERSION (and/or setup.py)
+4. Merge pull request to main
+5. Tag the commit using `git tag -a vX.X.X`
+6. Upload to pypi and conda (see below)
+
+
+## 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/weio
+```
+
+### 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/examples/SubDynModes.py b/examples/SubDynModes.py
new file mode 100644
index 0000000..9923484
--- /dev/null
+++ b/examples/SubDynModes.py
@@ -0,0 +1,44 @@
+"""
+- Read a SubDyn summary file and extract the Guyan and Craig Bampton Modes
+- Convert the SubDyn file to JSON for 3D visualization
+"""
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+import os
+# Local
+from weio.fast_summary_file import FASTSummaryFile
+
+# Get current directory so this script can be called from any location
+scriptDir = os.path.dirname(__file__)
+
+# Read SubDyn summary file
+filename = os.path.join(scriptDir, '../weio/tests/example_files/FASTSum_Pendulum.SD.sum.yaml')
+# filename = os.path.join(scriptDir, 'FASTSum_5MW_OC3Mnpl.SD.sum.yaml')
+sds = FASTSummaryFile(filename)
+
+# Optional: Extract Guyan and Craig-Bampton modes
+#dispGY, posGY, _, dispCB, posCB, _ = sds.getModes()
+
+# Extract Guyan and Craig-Bampton modes and store them in a DataFrame
+df = sds.toDataFrame() #sortDim=2, removeZero=True)
+print(df.keys())
+plt.plot(df['z_[m]'], df['GuyanMode1x_[m]'], 'o')
+plt.plot(df['z_[m]'], df['GuyanMode5x_[m]'], 'o')
+
+
+# Store as JSON for 3d visualization with viz3danim
+sds.toJSON('_OUT.json') # method 1
+sds.toGraph().toJSON('_OUT2.json') # method 2
+
+
+if __name__ == '__main__':
+ plt.show()
+
+if __name__=='__test__':
+ try:
+ os.remove('_OUT.json')
+ os.remove('_OUT2.json')
+ except:
+ pass
+
diff --git a/requirements.txt b/requirements.txt
index 24c8aa8..64293b4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,10 @@
-matplotlib
-future
-chardet
-openpyxl ; python_version>"3.0"
-numpy>=1.16.5; python_version>"3.0"
-numpy ; python_version<"3.0"
-pandas>=1.0.1; python_version>"3.0"
-pandas; python_version<="3.0"
-xlrd==1.2.0; python_version<"3.0"
-pyarrow # for parquet files
-scipy
+matplotlib
+openpyxl
+numpy
+pandas
+pyarrow # for parquet files
+scipy
+chardet
+xarray # for netcdf files
+nptdms # for TDMS files
+pyyaml # for yaml
diff --git a/requirements_optional.txt b/requirements_optional.txt
deleted file mode 100644
index ddca95b..0000000
--- a/requirements_optional.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-xarray # for netcdf files
-nptdms # for TDMS files
diff --git a/setup.py b/setup.py
index 533b49d..9bc09de 100644
--- a/setup.py
+++ b/setup.py
@@ -1,72 +1,68 @@
-from setuptools import setup, find_packages
-
-VERSION='1.0.0'
-
-
-setup(
- name='weio',
- version=VERSION,
- url='http://github.com/elmanuelito/weio/',
- author='Emmanuel Branlard',
- author_email='lastname@gmail.com',
- license='MIT',
- python_requires=">=2.7",
- description='Library to read and write files for wind energy',
- long_description="""
-weio is a library to read and write files, in particular files used by the Wind Energy community.
-This library is for instance used by the GUI tool [pyDatView](https://github.com/ebranlard/pydatview/) to plot, export and compare these different files.
-
-##Typical file formats supported:
-- Various CSV and delimited files
-- Simple Excel files
-- FAST input and output files (including some turbulence files)
-- HAWC2 and HawcStab2 input and output files (still some missing)
-- Bladed output files
-- FLEX output files
-- NetCDF files (partial support for 1D and 2D data for now)
-
-
-##Quickstart:
-Download, install dependencies, install package and run tests:
-```bash
-git clone https://github.com/ebranlard/weio
-cd weio
-python -m pip install --user -r requirements.txt
-python -m pip install -e . # install
-python -m unittest discover -v # run test
-""",
- long_description_content_type = 'text/markdown',
- packages=find_packages(include=['weio'],exclude=['./__init__.py']),
- install_requires=[
- 'matplotlib',
- 'future',
- 'chardet',
- 'openpyxl ; python_version>"3.0"',
- 'numpy>=1.16.5; python_version>"3.0"',
- 'numpy ; python_version<"3.0"',
- 'pandas>=1.0.1; python_version>"3.0"',
- 'pandas; python_version<="3.0"',
- 'xlrd==1.2.0; python_version<"3.0"',
- 'pyarrow',
- 'scipy'],
- zip_safe=False,
- classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Environment :: Console',
- 'Intended Audience :: Science/Research',
- 'Intended Audience :: Education',
- 'Intended Audience :: End Users/Desktop',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: MIT License',
- 'Operating System :: MacOS :: MacOS X',
- 'Operating System :: Microsoft :: Windows',
- 'Operating System :: POSIX',
- 'Operating System :: Unix',
- 'Programming Language :: Python',
- 'Topic :: Scientific/Engineering',
- 'Topic :: Scientific/Engineering :: Atmospheric Science',
- 'Topic :: Scientific/Engineering :: Hydrology',
- 'Topic :: Scientific/Engineering :: Mathematics',
- 'Topic :: Scientific/Engineering :: Physics'
- ],
-)
+from setuptools import setup, find_packages
+
+VERSION='2.0.0'
+
+with open('requirements.txt') as f:
+ REQUIREMENTS = f.read().splitlines()
+
+DESCRIPTION="""
+weio is a library to read and write files, in particular files used by the Wind Energy community.
+This library is for instance used by the GUI tool [pyDatView](https://github.com/ebranlard/pydatview/) to plot, export and compare these different files.
+
+##Typical file formats supported:
+- Various CSV and delimited files
+- Simple Excel files
+- FAST input and output files (including some turbulence files)
+- HAWC2 and HawcStab2 input and output files (still some missing)
+- Bladed output files
+- FLEX output files
+- NetCDF files (partial support for 1D and 2D data for now)
+
+
+##Quickstart:
+Download, install dependencies, install package and run tests:
+```bash
+git clone https://github.com/ebranlard/weio
+cd weio
+python -m pip install --user -r requirements.txt
+python -m pip install -e . # install
+python -m unittest discover -v # run test
+"""
+
+
+
+setup(
+ name='weio',
+ version=VERSION,
+ url='http://github.com/ebranlard/weio/',
+ author='Emmanuel Branlard',
+ author_email='lastname@gmail.com',
+ license='MIT',
+ python_requires=">=3.6",
+ description='Library to read and write files for wind energy',
+ long_description="""
+""",
+ long_description_content_type = 'text/markdown',
+ packages=find_packages(include=['weio', 'weio.wetb*', 'weio.tools'],exclude=['./__init__.py']),
+ install_requires=REQUIREMENTS,
+ zip_safe=False,
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Environment :: Console',
+ 'Intended Audience :: Science/Research',
+ 'Intended Audience :: Education',
+ 'Intended Audience :: End Users/Desktop',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: MacOS :: MacOS X',
+ 'Operating System :: Microsoft :: Windows',
+ 'Operating System :: POSIX',
+ 'Operating System :: Unix',
+ 'Programming Language :: Python',
+ 'Topic :: Scientific/Engineering',
+ 'Topic :: Scientific/Engineering :: Atmospheric Science',
+ 'Topic :: Scientific/Engineering :: Hydrology',
+ 'Topic :: Scientific/Engineering :: Mathematics',
+ 'Topic :: Scientific/Engineering :: Physics'
+ ],
+)
diff --git a/weio/_NEWFILE_TEMPLATE.py b/weio/_NEWFILE_TEMPLATE.py
index 42ba251..27f4388 100644
--- a/weio/_NEWFILE_TEMPLATE.py
+++ b/weio/_NEWFILE_TEMPLATE.py
@@ -8,10 +8,10 @@
try:
from .file import File, WrongFormatError, BrokenFormatError
except:
+ File=dict
EmptyFileError = type('EmptyFileError', (Exception,),{})
WrongFormatError = type('WrongFormatError', (Exception,),{})
BrokenFormatError = type('BrokenFormatError', (Exception,),{})
- File=dict
class XXXFile(File):
"""
diff --git a/weio/__init__.py b/weio/__init__.py
index cc31316..717e691 100644
--- a/weio/__init__.py
+++ b/weio/__init__.py
@@ -1,4 +1,5 @@
-from .file import File, WrongFormatError, BrokenFormatError, FileNotFoundError, EmptyFileError
+# --- Generic reader / fileformat detection
+from .file import File, WrongFormatError, BrokenFormatError, FileNotFoundError, EmptyFileError, OptionalImportError
from .file_formats import FileFormat, isRightFormat
import sys
import os
@@ -17,10 +18,20 @@ def fileFormats(userpath=None, ignoreErrors=False, verbose=False):
""" return list of fileformats supported by the library
If userpath is provided,
+ OUTPUTS:
+ if ignoreErrors is True:
+ formats, errors
+ else:
+ formats
+
"""
global _FORMATS
+ errors=[]
if _FORMATS is not None:
- return _FORMATS
+ if ignoreErrors:
+ return _FORMATS, errors
+ else:
+ return _FORMATS
# --- Library formats
from .fast_input_file import FASTInputFile
from .fast_output_file import FASTOutputFile
@@ -37,6 +48,7 @@ def fileFormats(userpath=None, ignoreErrors=False, verbose=False):
from .hawcstab2_pwr_file import HAWCStab2PwrFile
from .hawcstab2_ind_file import HAWCStab2IndFile
from .hawcstab2_cmb_file import HAWCStab2CmbFile
+ from .gnuplot_file import GNUPlotFile
from .mannbox_file import MannBoxFile
from .flex_blade_file import FLEXBladeFile
from .flex_profile_file import FLEXProfileFile
@@ -52,9 +64,14 @@ def fileFormats(userpath=None, ignoreErrors=False, verbose=False):
from .vtk_file import VTKFile
from .bladed_out_file import BladedFile
from .parquet_file import ParquetFile
+ from .pickle_file import PickleFile
from .cactus_file import CactusFile
from .raawmat_file import RAAWMatFile
+ from .rosco_discon_file import ROSCODISCONFile
from .rosco_performance_file import ROSCOPerformanceFile
+ from .plot3d_file import Plot3DFile
+ from .yaml_file import YAMLFile
+ from .airfoil_file import AirfoilShapeFile
priorities = []
formats = []
def addFormat(priority, fmt):
@@ -62,6 +79,7 @@ def addFormat(priority, fmt):
formats.append(fmt)
addFormat(0, FileFormat(CSVFile))
addFormat(0, FileFormat(ExcelFile))
+ addFormat(0, FileFormat(YAMLFile))
addFormat(10, FileFormat(TecplotFile))
addFormat(10, FileFormat(BladedFile))
addFormat(20, FileFormat(FASTInputFile))
@@ -86,13 +104,18 @@ def addFormat(priority, fmt):
addFormat(40, FileFormat(FLEXWaveKinFile))
addFormat(40, FileFormat(FLEXDocFile))
addFormat(50, FileFormat(BModesOutFile))
+ addFormat(50, FileFormat(ROSCODISCONFile))
addFormat(50, FileFormat(ROSCOPerformanceFile))
addFormat(60, FileFormat(NetCDFFile))
addFormat(60, FileFormat(VTKFile))
addFormat(60, FileFormat(TDMSFile))
+ addFormat(60, FileFormat(GNUPlotFile))
addFormat(60, FileFormat(ParquetFile))
+ addFormat(60, FileFormat(PickleFile))
+ addFormat(60, FileFormat(Plot3DFile))
addFormat(70, FileFormat(CactusFile))
addFormat(70, FileFormat(RAAWMatFile))
+ addFormat(70, FileFormat(AirfoilShapeFile))
# --- User defined formats from user path
UserClasses, UserPaths, UserModules, UserModuleNames, errors = userFileClasses(userpath, ignoreErrors, verbose=verbose)
@@ -221,13 +244,14 @@ def detectFormat(filename, **kwargs):
extMatch = True
else:
# Try patterns if present
- extPatterns = [ef.replace('.','\.').replace('$','\$').replace('*','[.]*') for ef in myformat.extensions if '*' in ef]
+ extPatterns = [ef.replace('.',r'\.').replace('$',r'\$').replace('*','[.]*') for ef in myformat.extensions if '*' in ef]
if len(extPatterns)>0:
extPatMatch = [re.match(pat, ext) is not None for pat in extPatterns]
extMatch = any(extPatMatch)
else:
extMatch = False
if extMatch: # we have a match on the extension
+ #print('Trying format: ',myformat)
valid, F = isRightFormat(myformat, filename, **kwargs)
if valid:
#print('File detected as :',myformat)
@@ -241,6 +265,8 @@ def detectFormat(filename, **kwargs):
def read(filename, fileformat=None, **kwargs):
F = None
+ if not os.path.exists(filename):
+ raise FileNotFoundError('weio cannot read the following file because it does not exist:\n Inp. path: {}\n Abs. path: {}'.format(filename, os.path.abspath(filename)))
# Detecting format if necessary
if fileformat is None:
fileformat,F = detectFormat(filename, **kwargs)
@@ -250,15 +276,4 @@ def read(filename, fileformat=None, **kwargs):
return F
-# --- For legacy code
-def FASTInputFile(*args,**kwargs):
- from .fast_input_file import FASTInputFile as fi
- return fi(*args,**kwargs)
-def FASTOutputFile(*args,**kwargs):
- from .fast_output_file import FASTOutputFile as fo
- return fo(*args,**kwargs)
-def CSVFile(*args,**kwargs):
- from .csv_file import CSVFile as csv
- return csv(*args,**kwargs)
-
diff --git a/weio/airfoil_file.py b/weio/airfoil_file.py
new file mode 100644
index 0000000..9b26459
--- /dev/null
+++ b/weio/airfoil_file.py
@@ -0,0 +1,577 @@
+import os
+import pandas as pd
+import numpy as np
+from weio.csv_file import CSVFile, find_non_numeric_header_lines
+from weio.plot3d_file import read_plot3d, write_plot3d
+try:
+ from .file import File, WrongFormatError, BrokenFormatError, EmptyFileError
+except:
+ File = dict
+ EmptyFileError = type('EmptyFileError', (Exception,),{})
+ WrongFormatError = type('WrongFormatError', (Exception,),{})
+ BrokenFormatError = type('BrokenFormatError', (Exception,),{})
+
+FORMAT_TO_EXT = {
+ 'csv': 'csv',
+ 'plot3d': 'fmt',
+ 'fmt': 'fmt',
+ 'xyz': 'xyz',
+ 'xy': 'xy',
+ 'g': 'g',
+ 'pointwise': 'pwise',
+ 'geo': 'geo',
+}
+EXT_TO_FORMAT = {
+ 'csv': 'csv',
+ 'txt': 'csv',
+ 'dat': 'csv',
+ 'fmt': 'plot3d',
+ 'xyz': 'plot3d',
+ 'xy': 'plot3d',
+ 'g': 'plot3d',
+ 'pwise': 'pointwise',
+ 'pw': 'pointwise',
+ 'geo': 'geo',
+}
+# --------------------------------------------------------------------------------{
+# --- WEIO class
+# --------------------------------------------------------------------------------}
+class AirfoilShapeFile(File):
+ """
+ Read/write an airfoil shape (formatted, ASCII). The object behaves as a dictionary.
+
+ Note: the class does not make any manipulation of the data
+
+ Main methods
+ ------------
+ - read, write, toDataFrame, keys
+
+ Examplesy
+ --------
+ f = AirfoilShapeFile('file.fmt')
+ print(f.keys())
+ print(f.toDataFrame().columns)
+ """
+
+ @staticmethod
+ def defaultExtensions():
+ """ List of file extensions expected for this fileformat"""
+ return ['.csv', '.dat', '.fmt', '.txt']
+
+ @staticmethod
+ def formatName():
+ return 'Airfoil shape file'
+
+ @staticmethod
+ def priority(): return 60 # Priority in weio.read fileformat list between 0=high and 100:low
+
+ def __init__(self, filename=None, **kwargs):
+ """ Class constructor. If a `filename` is given, the file is read. """
+ self.filename = filename
+ self.data = None
+ self.format = None
+ if filename:
+ self.read(**kwargs)
+
+ def read(self, filename=None, verbose=False, format=None):
+ """ Reads the file self.filename, or `filename` if provided """
+ 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)
+
+ x, y, d = read_airfoil(self.filename, format=format, verbose=verbose)
+ if 'format' in d:
+ self.format = d['format']
+
+ # Store
+ self.data = pd.DataFrame({'x': x, 'y': y})
+ for k,v in d.items():
+ self[k] = v
+
+ def write(self, filename=None, format=None, **kwargs):
+ """ 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')
+ if format is None:
+ format = self.format # NOTE: not inferring from extension, change of behavior
+ for k,v in self.items():
+ if k not in kwargs and k not in ['x', 'y', 'format']:
+ kwargs[k] = v
+
+
+ write_airfoil(self.data['x'].values, self.data['y'].values, filename, format=format, **kwargs)
+
+ def toDataFrame(self):
+ """ Returns one DataFrame (single block) or a dict of DataFrames (multi-block) """
+ return self.data
+
+ def __repr__(self):
+ s = '<{} object>:\n'.format(type(self).__name__)
+ s += '|Main attributes:\n'
+ s += '| - filename: {}\n'.format(self.filename)
+ s += '| - format : {}\n'.format(self.format)
+ if self.data is not None:
+ x = self.data['x'].values
+ y = self.data['y'].values
+ s += '| - data: shape:{}\n'.format(self.data.shape)
+ s += '| x: len:{} type:{} values:[{}, {}, ...,{}]\n'.format(len(x), x.dtype, x[0], x[1], x[-1] )
+ s += '| y: len:{} type:{} values:[{}, {}, ...,{}]\n'.format(len(y), y.dtype, y[0], y[1], y[-1] )
+ s += '|Main keys from original input file:\n'
+ for k,v in self.items():
+ s += '| - {}: {}\n'.format(k, v)
+ s += '|Main methods:\n'
+ s += '| - read, write, toDataFrame, keys'
+ return s
+
+ def toString(self):
+ """ """
+ s = ''
+ return s
+
+
+
+
+# --------------------------------------------------------------------------------{
+# --- Main wrappers
+# --------------------------------------------------------------------------------}
+def read_airfoil(filename, format=None, verbose=False, **kwargs):
+ """ Read airfoil coordinates from a filename"""
+ if not os.path.exists(filename):
+ raise FileNotFoundError(f"File {filename} does not exist.")
+ if format is None:
+ ext = os.path.splitext(filename)[1].lower().strip('.')
+ format = EXT_TO_FORMAT[ext]
+ format = format.lower()
+ if verbose:
+ print(f"Reading airfoil from {filename} with format {format}")
+
+ if format in ['csv','tab']:
+ x, y, d = read_airfoil_csv_like(filename)
+ elif format in ['plot3d','fmt','g','xyz','xy','x']:
+ x, y, d = read_airfoil_plot3d(filename)
+ elif format in ['pointwise', 'pw','pwise']:
+ x, y, d = read_airfoil_pointwise(filename, plot=False)
+ else:
+ raise NotImplementedError(f"File type {ext} is not supported.")
+
+
+ if not np.issubdtype(x.dtype, np.floating) or not np.issubdtype(y.dtype, np.floating):
+ print('First values of x:',x[0:5], x.dtype)
+ print('First values of y:',y[0:5], y.dtype)
+ raise ValueError("Ffile must contain floating point numbers in both columns. Maybe the header was not detected correctly?")
+
+ return x, y, d
+
+def write_airfoil(x, y, filename, format=None, **kwargs):
+ """ Write airfoil coordinates to a file"""
+ if format is None:
+ ext = os.path.splitext(filename)[1].lower().strip('.')
+ format = EXT_TO_FORMAT[ext]
+ format = format.lower()
+ if format in ['csv','tab']:
+ write_airfoil_csv(x, y, filename)
+ elif format in ['plot3d','fmt','g','xyz','xy','x']:
+ write_airfoil_plot3d(x, y, filename, **kwargs)
+ elif format in ['pointwise', 'pw','pwise']:
+ write_airfoil_pointwise(x, y, filename)
+ elif format == 'geo':
+ write_airfoil_geo(x, y, filename, **kwargs)
+ elif format == 'openfast':
+ write_airfoil_openfast(x, y, filename, **kwargs)
+ else:
+ raise NotImplementedError(f"Format {format} is not supported.")
+
+# --------------------------------------------------------------------------------}
+# --- OpenFAST airfoil shape
+# --------------------------------------------------------------------------------{
+def read_airfoil_openfast(filename):
+ """
+ Reads a FAST-format airfoil file (with NumCoords and comments).
+ Returns a dict with keys: 'x', 'y', 'AirfoilRefPoint', 'NumCoords'.
+ """
+ d = {}
+ d['format'] = 'openfast'
+
+ with open(filename, 'r', encoding='utf-8', errors='surrogateescape') as f:
+ lines = f.readlines()
+
+ # Find the line with NumCoords (first non-comment, with an int at start)
+ numcoords = None
+ for i, line in enumerate(lines):
+ s = line.strip()
+ if not s or s.startswith('!') or s.startswith('#'): continue
+ try:
+ numcoords = int(s.split()[0])
+ idx_numcoords = i
+ break
+ except Exception:
+ continue
+ if numcoords is None:
+ raise BrokenFormatError("Could not find NumCoords in file.")
+
+ d['NumCoords'] = numcoords
+
+ # Find the reference point (first non-comment, non-empty line after NumCoords)
+ idx_ref = None
+ comments1 =[]
+ for j in range(idx_numcoords+1, len(lines)):
+ s = lines[j].strip()
+ if s.startswith('!') or s.startswith('#'):
+ comments1.append(s)
+ continue
+ try:
+ vals = [float(x) for x in s.replace(',', ' ').split()]
+ if len(vals) == 2:
+ d['AirfoilRefPoint'] = np.array(vals)
+ idx_ref = j
+ break
+ except Exception():
+ continue
+ if idx_ref is None:
+ raise BrokenFormatError("Could not find airfoil reference point.")
+
+ # Find the start of the coordinates (first non-comment, non-empty line after ref)
+ coords = []
+ d['comments'] =[]
+ for k in range(idx_ref+1, len(lines)):
+ s = lines[k].strip()
+ if s.startswith('!') or s.startswith('#'):
+ d['comments'].append(s)
+ continue
+ #try:
+ vals = [float(x) for x in s.replace(',', ' ').split()]
+ if len(vals) == 2:
+ coords.append(vals)
+ if len(coords) >= numcoords:
+ break
+ else:
+ raise BrokenFormatError('More than two values in line, expected only x and y coordinates.')
+ # except Exception:
+ # continue
+
+ coords = np.array(coords)
+ if coords.shape[0] != numcoords-1: # Note: +1 for the reference point..
+ raise BrokenFormatError(f"Expected {numcoords} coordinates, got {coords.shape[0]}.")
+
+ x = coords[:,0]
+ y = coords[:,1]
+ return x, y, d
+
+def write_airfoil_openfast(x, y, filename, AirfoilRefPoint=None, comments=None, NumCoords=None):
+ """
+ Writes airfoil coordinates to a FAST/OpenFAST airfoil shape file.
+ - x, y: arrays of coordinates
+ - filename: output file path
+ - AirfoilRefPoint: (optional) 2-element array for the reference point, defaults to [0.25, 0.0]
+ - comments: (optional) list of comment lines to write at the top of the file
+ - NumCoords : neglected
+ """
+ if AirfoilRefPoint is None:
+ AirfoilRefPoint = [0.25, 0.0]
+ print('[WARN] No AirfoilRefPoint provided, using default [0.25, 0.0].')
+
+ NumCoords = len(x) + 1
+
+ if comments is None:
+ comments = [
+ "! coordinates of the airfoil shape",
+ "! x/c y/c",
+ ]
+ with open(filename, 'w', encoding='utf-8') as f:
+ f.write(f"{NumCoords:<6d} NumCoords ! The number of coordinates in the airfoil shape file (including an extra coordinate for airfoil reference). Set to zero if coordinates not included.\n")
+ f.write("! ......... x-y coordinates are next if NumCoords > 0 .............\n")
+ f.write("! x-y coordinate of airfoil reference\n")
+ f.write("! x/c y/c\n")
+ f.write(f"{AirfoilRefPoint[0]:.8f}\t{AirfoilRefPoint[1]:.8f}\n")
+ for c in comments:
+ f.write(f"{c}\n")
+ for xi, yi in zip(x, y):
+ f.write(f"{xi:.6f}\t{yi:.6f}\n")
+
+# --------------------------------------------------------------------------------}
+# --- CSV
+# --------------------------------------------------------------------------------{
+def has_two_ints_on_second_line_and_third_empty(filename):
+ """
+ See for instance e850.dat
+
+ Returns True if the second line of the file contains exactly two floats that can be coerced to integers,
+ and the third line is empty (only whitespace or newline).
+ """
+ with open(filename, 'r', encoding='utf-8', errors='surrogateescape') as f:
+ lines = []
+ for _ in range(3):
+ line = f.readline()
+ if not line: break
+ lines.append(line.rstrip('\n\r'))
+
+ if len(lines) < 3: return False
+
+ # Check second line
+ parts = lines[1].strip().replace(',', ' ').split()
+ if len(parts) != 2: return False
+ try:
+ floats = [float(p) for p in parts]
+ ints = [int(f) for f in floats]
+ if not all(abs(f-i)<1e-8 for f,i in zip(floats,ints)):
+ return False
+ except Exception:
+ return False
+
+ # Check third line is empty
+ if lines[2].strip() != '':
+ return False
+
+ return True
+
+def is_OpenFAST_airfoil_shape(filename):
+ """ Detect if the file is in OpenFAST airfoil shape format """
+ with open(filename, 'r', encoding='utf-8', errors='surrogateescape') as f:
+ lines = []
+ for _ in range(2):
+ line = f.readline()
+ # check if line contains "NumCoords"
+ if "numcoords" in line.lower():
+ return True
+ if not line: break
+ return False
+
+
+def read_airfoil_csv_like(filename):
+
+ # --- Detect OpenFAST airfoil shape format
+ if is_OpenFAST_airfoil_shape(filename):
+ return read_airfoil_openfast(filename)
+
+ # --- Find non-numeric header lines
+ header_indices, header_lines = find_non_numeric_header_lines(filename)
+
+ # We expect mostly 0 or one line of header. If more than one header line is found, it's best if lines start with a '#'
+ if len(header_indices)> 1:
+ print("[INFO] Found more than one header line in file ", filename)
+ # # count lines that do not start with "#"
+ # not_comment_lines = [line for line in header_lines if not line.startswith('#')]
+ # nNotComment = len(not_comment_lines)
+ # print("[INFO] Found non-comment header lines:", not_comment_lines)
+ # #if nNotComment > 0:
+ # # raise Exception("Error: More than one header line found in the file. Please ensure the file has a single header line, no header lines at all, or that all header lines start with `#`.")
+
+
+ if has_two_ints_on_second_line_and_third_empty(filename):
+ raise BrokenFormatError('File format with separate Upper and Lower surfaces not yet supported, file {}.'.format(filename))
+
+ #print('>>>> commentLines:', header_indices, 'header_lines:', header_lines)
+ #csv = CSVFile(filename=filename, commentLines=header_indices, detectColumnNames=False, colNames=['x', 'y'], doRead=False)
+ csv = CSVFile(filename=filename, commentLines=header_indices, doRead=False) #, detectColumnNames=False, colNames=['x', 'y'], doRead=False)
+ try:
+ csv._read()
+ except WrongFormatError as e:
+ print("[FAIL] {}".format(str(e).strip()))
+ print(" > Trying to read the file with a slower method...")
+ #print(csv)
+ csv.read_slow_stop_at_first_empty_lines(numeric_only=True)
+
+ df = csv.toDataFrame()
+ #import pandas as pd
+ #df = pd.read_csv(filename)
+ #print(df)
+ if df.shape[1] == 2:
+ x = df.iloc[:, 0].values
+ y = df.iloc[:, 1].values
+ else:
+ raise ValueError("CSV file must have exactly two columns for x and y coordinates.")
+ # Check if numpy array are all floats, otherwise( e.g. if they are objects) raise an exception
+ if not np.issubdtype(x.dtype, np.floating) or not np.issubdtype(y.dtype, np.floating):
+ if x[-1] == 'ZZ':
+ print('[WARN] File {} Last value of x is "ZZ", removing it and converting to float.'.format(filename))
+ x=x[:-1].astype(float)
+ y=y[:-1]
+ else:
+ print(csv)
+ print('First values of x:',x[0:5], x.dtype)
+ print('First values of y:',y[0:5], y.dtype)
+ raise ValueError("CSV file must contain floating point numbers in both columns. Maybe the header was not detected correctly?")
+ d={}
+ d['format'] = 'csv'
+ return x, y, d
+
+def write_airfoil_csv(x, y, filename):
+ df = pd.DataFrame({'x': x, 'y': y})
+ df.to_csv(filename, index=False)
+
+# --------------------------------------------------------------------------------}
+# --- Plot3D
+# --------------------------------------------------------------------------------{
+def read_airfoil_plot3d(filename):
+ coords, dims = read_plot3d(filename, singleblock=True)
+ x = coords[:, 0]
+ y = coords[:, 1]
+ # Make sure we keep only the first slice in z-direction
+ nx = dims[0]
+ x = x[:nx]
+ y = y[:nx]
+ d = {}
+ d['format'] = 'plot3d'
+ return x, y, d
+
+# --------------------------------------------------------------------------------}
+# --- Pointwise
+# --------------------------------------------------------------------------------{
+def read_airfoil_pointwise(filename, plot=False, verbose=False):
+ # TODO this is horrible code, needs to be refactored
+ lower = []
+ upper = []
+ TE = []
+ d= {}
+ d['format'] = 'pointwise'
+
+ with open(filename, 'r') as file:
+ # Read the entire content of the file
+ lines = file.readlines()
+
+ current_section = 'lower' # Starting with the lower section
+ idx = 0 # Line index
+
+ while idx < len(lines):
+ line = lines[idx].strip()
+
+ if line.isdigit(): # When the line is a number (point count)
+ num_points = int(line) # Get the number of points in the section
+ idx += 1 # Move to the next line containing the coordinates
+
+ # Read the next `num_points` lines and store x, y, z coordinates
+ for _ in range(num_points):
+ if idx < len(lines):
+ x, y, z = map(float, lines[idx].strip().split()) # Parse x, y, z values
+ if current_section == 'lower':
+ lower.append((x, y, z)) # Append to the lower section
+ elif current_section == 'upper':
+ upper.append((x, y, z)) # Append to the upper section
+ elif current_section == 'TE':
+ TE.append((x, y, z)) # Append to the TE section
+ idx += 1 # Move to the next line containing coordinates
+
+ # Switch sections after processing each part
+ if current_section == 'lower':
+ current_section = 'upper'
+ elif current_section == 'upper':
+ current_section = 'TE'
+
+ else:
+ idx += 1 # Skip lines that are not point counts or coordinates
+ TE = np.asarray(TE)[:,:2]# Keep only x and y coordinates
+ lower = np.asarray(lower)[:,:2]
+ upper = np.asarray(upper)[:,:2]
+
+ from nalulib.curves import contour_is_clockwise
+ coords1 = np.vstack((lower[:-1], upper[:-1], TE))
+ assert contour_is_clockwise(coords1), "Pointwise format is expected to be clockwise."
+ assert np.allclose(coords1[0, :], coords1[-1, :], rtol=1e-10, atol=1e-12), "First and last points must be the same in Pointwise format."
+
+ # NOTE: Pointwise is assumed to be clockwise
+ TE = TE[::-1] # Reverse the order of TE points to match the convention
+ lower = lower[::-1] # Reverse the order of lower surface points
+ upper = upper[::-1] # Reverse the order of upper surface points
+
+ # NOTE: coords are anticlockwise with first and last point being the same
+ coords = np.vstack((upper[:-1], lower[:-1], TE))
+ assert np.allclose(coords[0, :], coords[-1, :], rtol=1e-10, atol=1e-12), "First and last points must be the same in Pointwise format."
+
+ if plot:
+ import matplotlib.pyplot as plt
+ plt.figure(figsize=(10, 5))
+ plt.plot(coords[:,0], coords[:,1], '.-', label='Airfoil Shape', color='black')
+ plt.plot(lower[:,0], lower[:,1], label='Lower Surface', color='blue')
+ plt.plot(upper[:,0], upper[:,1], label='Upper Surface', color='red')
+ plt.plot(TE[:,0], TE[:,1], label='Trailing Edge', color='green')
+ plt.title('Airfoil Shape with Upper and Lower Surfaces')
+ plt.xlabel('x')
+ plt.ylabel('y')
+ plt.axis('equal')
+ plt.legend()
+ plt.grid(True)
+ plt.show()
+
+ return coords[:,0], coords[:,1], d
+
+def write_airfoil_pointwise(x, y, output_file):
+ # === Load airfoil data ===
+ x_orig, y_orig = x, y
+
+ # === Find leading edge index (minimum x)
+ le_index = np.argmin(x_orig)
+
+ # === convert to .txt file format for Pointwise
+
+ # === 1. Split into upper and lower surfaces
+ x_orig_upper, y_orig_upper = x_orig[:le_index+1], y_orig[:le_index+1]
+ x_orig_lower, y_orig_lower = x_orig[le_index:], y_orig[le_index:]
+
+ # === 2. Sort both surfaces to save as .dat file without interpolation
+ x_orig_lower_sorted, y_orig_lower_sorted = x_orig_lower[::-1], y_orig_lower[::-1]
+ x_orig_upper_sorted, y_orig_upper_sorted = x_orig_upper[::-1], y_orig_upper[::-1]
+
+
+ # === 3. Save original data in .dat format ===
+ with open(output_file, 'w') as f:
+ # Write lower surface
+ f.write(f"{len(x_orig_lower_sorted)}\n")
+ for x, y in zip(x_orig_lower_sorted, y_orig_lower_sorted):
+ f.write(f"{x:.6f} {y:.6f} 0.000000\n")
+
+ # Write upper surface
+ f.write(f"{len(x_orig_upper_sorted)}\n")
+ for x, y in zip(x_orig_upper_sorted, y_orig_upper_sorted):
+ f.write(f"{x:.6f} {y:.6f} 0.000000\n")
+
+ # Write TE surface
+ f.write(f"{3}\n")
+ f.write(f"{x_orig_upper_sorted[-1]:.6f} {y_orig_upper_sorted[-1]:.6f} 0.000000\n")
+ f.write(f"{((x_orig_upper_sorted[-1]+x_orig_lower_sorted[0])/2):.6f} {((y_orig_lower_sorted[0]+y_orig_upper_sorted[-1])/2):.6f} 0.000000\n")
+ f.write(f"{x_orig_lower_sorted[0]:.6f} {y_orig_lower_sorted[0]:.6f} 0.000000\n")
+
+
+# --------------------------------------------------------------------------------}
+# --- gmesh
+# --------------------------------------------------------------------------------{
+def write_airfoil_geo(x, y, output_file, lc=1.0):
+ with open(output_file, 'w') as f_out:
+ f_out.write("// Gmsh .geo file generated from 2D airfoil .txt\n\n")
+ zz=0
+ point_ids = np.arange(len(x), dtype=int) + 1 # Point IDs start from 1 in Gmsh
+ for i, (xx, yy)in enumerate(zip(x, y)):
+ f_out.write(f"Point({point_ids[i]}) = {{{xx}, {yy}, {zz}, {lc}}};\n")
+
+ # Connect all points in a closed loop
+ f_out.write("\n// Single Line connecting all points in a loop\n")
+ line_str = ", ".join(str(pid) for pid in point_ids + [point_ids[0]])
+ f_out.write(f"Line(1) = {{{line_str}}};\n")
+
+
+def write_airfoil_plot3d(x, y, filename, thick=False):
+ """ Write airfoil coordinates to a Plot3D file"""
+ if thick:
+ # We duplicate the x y coordiantes and have z=0 and z=1
+ coords = np.column_stack((x, y, np.zeros_like(x)))
+ coords = np.concatenate((coords, coords + np.array([0, 0, 1])))
+ dims = (len(x), 2, 1) # Two slices in the z-direction
+ else:
+ coords = np.column_stack((x, y, np.zeros_like(x))) # Assuming z=0 for 2D airfoil
+ dims = (len(x), 1, 1) # Assuming a single slice in the z-direction
+ write_plot3d(filename, coords, dims)
+
+
+if __name__ == '__main__':
+ shp = AirfoilShapeFile(os.path.join(os.path.dirname(__file__), '../../data/airfoils/tests/FAST_naca64618.txt'))
+ print(shp)
+ shp.write('test_openfast.txt')
+ shp = AirfoilShapeFile('test_openfast.txt')
+ print(shp)
+
diff --git a/weio/amrwind_file.py b/weio/amrwind_file.py
new file mode 100644
index 0000000..3a9bbfd
--- /dev/null
+++ b/weio/amrwind_file.py
@@ -0,0 +1,125 @@
+"""Read AMR-Wind NETCDF file
+
+"""
+import xarray as xr
+import numpy as np
+
+class AMRWindFile(dict):
+ """
+ Read a AMR-Wind output file (.nc)
+ """
+
+ @staticmethod
+ def defaultExtensions():
+ """ List of file extensions expected for this fileformat"""
+ return ['.nc']
+
+ @staticmethod
+ def formatName():
+ """ Short string (~100 char) identifying the file format"""
+ return 'NetCDF plane sampling file from AMRWind'
+
+ @staticmethod
+ def priority(): return 60 # Priority in weio.read fileformat list between 0=high and 100:low
+
+ def __init__(self, filename=None, timestep=None, output_frequency=None, **kwargs):
+ self.filename = filename
+ self.amrwind_dt = timestep
+ self.output_dt = timestep * output_frequency
+
+ if filename:
+ self.read(**kwargs)
+
+ def read(self, group_name):
+ """
+ Parameters
+ ----------
+
+ group_name : str,
+ group name inside netcdf file that you want to read, e.g. p_slice
+
+ TODO: see if group_name can be avoided, and add a read_group function
+ """
+ # --- 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 Exception('File is empty:',self.filename)
+
+
+ ds = xr.open_dataset(self.filename,group=group_name)
+
+ coordinates = {"x":(0,"axial"), "y":(1,"lateral"),"z":(2,"vertical")}
+ c = {}
+ for coordinate,(i,desc) in coordinates.items():
+ c[coordinate] = xr.IndexVariable(
+ dims=[coordinate],
+ data=np.sort(np.unique(ds['coordinates'].isel(ndim=i))),
+ attrs={"description":"{0} coordinate".format(desc),"units":"m"}
+ )
+ c["t"] = xr.IndexVariable(
+ dims=["t"],
+ data=ds.num_time_steps*self.output_dt,
+ attrs={"description":"time from start of simulation","units":"s"}
+ )
+
+ self.nt = len(c["t"])
+ self.nx = len(c["x"])
+ self.ny = len(c["y"])
+ self.nz = len(c["z"])
+
+ coordinates = {"x":(0,"axial","u"), "y":(1,"lateral","v"),"z":(2,"vertical","w")}
+ v = {}
+ for coordinate,(i,desc,u) in coordinates.items():
+ v[u] = xr.DataArray(np.reshape(getattr(ds,"velocity{0}".format(coordinate)).values,(self.nt,self.nx,self.ny,self.nz)),
+ coords=c,
+ dims=["t","x","y","z"],
+ name="{0} velocity".format(desc),
+ attrs={"description":"velocity along {0}".format(coordinate),"units":"m/s"})
+
+ ds = xr.Dataset(data_vars=v, coords=v[u].coords)
+ ds.attrs = {"original file":self.filename}
+
+ self.data = ds
+
+
+ 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')
+ raise NotImplementedError()
+
+ def toDataFrame(self):
+ """ Returns object into one DataFrame, or a dictionary of DataFrames"""
+ # --- Example (returning one DataFrame):
+ # return pd.DataFrame(data=np.zeros((10,2)),columns=['Col1','Col2'])
+ # --- Example (returning dict of DataFrames):
+ #dfs={}
+ #cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]']
+ #dfs['Polar1'] = pd.DataFrame(data=..., columns=cols)
+ #dfs['Polar1'] = pd.DataFrame(data=..., columns=cols)
+ # return dfs
+ raise NotImplementedError()
+
+ # --- 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, write, toDataFrame, keys'
+ return s
+
diff --git a/weio/bladed_out_file.py b/weio/bladed_out_file.py
index 1315fb2..83fe832 100644
--- a/weio/bladed_out_file.py
+++ b/weio/bladed_out_file.py
@@ -1,17 +1,15 @@
-# -*- coding: utf-8 -*-
import os
import numpy as np
import re
import pandas as pd
import glob
import shlex
-# try:
-from .file import File, WrongFormatError, BrokenFormatError, isBinary
-# except:
-# EmptyFileError = type('EmptyFileError', (Exception,),{})
-# WrongFormatError = type('WrongFormatError', (Exception,),{})
-# BrokenFormatError = type('BrokenFormatError', (Exception,),{})
-# File=dict
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File = dict
+ class WrongFormatError(Exception): pass
+ class BrokenFormatError(Exception): pass
# --------------------------------------------------------------------------------}
@@ -72,13 +70,17 @@ def read_bladed_sensor_file(sensorfile):
# sometimes, the info is written on "AXIVAL"
# Check next line, we concatenate if doesnt start with AXISLAB (Might need more cases)
try:
- nextLine=sensorLines[i+1].strip()
- if not nextLine.startswith('AXISLAB'):
- t_line = t_line.strip()+' '+nextLine
+ # Combine the strings into one string
+ combined_string = ''.join(sensorLines)
+ # Search for a regex pattern that spans across multiple strings
+ line = re.search(r'(?<=AXITICK).+?(?=(AXISLAB|NVARS))', combined_string, flags=re.DOTALL)
+ line=line.group(0)
+ # Replace consecutive whitespace characters with a single space
+ t_line = re.sub(r'\s+', ' ', line)
except:
pass
- temp = t_line[7:].strip()
+ temp = t_line.strip()
temp = temp.strip('\'').split('\' \'')
dat['SectionList'] = np.array(temp, dtype=str)
dat['nSections'] = len(dat['SectionList'])
@@ -104,6 +106,7 @@ def read_bladed_sensor_file(sensorfile):
except:
pass
def repUnits(s):
+ s = s.replace('[[','[').replace(']]',']')
s = s.replace('TT','s^2').replace('T','s').replace('A','rad')
s = s.replace('P','W').replace('L','m').replace('F','N').replace('M','kg')
return s
@@ -181,6 +184,10 @@ def read_bladed_output(sensorFilename, readTimeFilesOnly=False):
data = np.fromfile(fid_2, sensorInfo['Precision'])
try:
+ if nMajor==0:
+ nMajor=int(np.floor(len(data)/nSections/nSensors))
+ data=data[0:nMajor*nSections*nSensors]
+ sensorInfo['nMajor']=nMajor
if sensorInfo['NDIMENS'] == 3:
data = np.reshape(data,(nMajor, nSections, nSensors), order='C')
@@ -371,6 +378,21 @@ def toDataFrame(self):
else:
return dfs
+
+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
+
if __name__ == '__main__':
pass
#filename = r'E:\Work_Google Drive\Bladed_Sims\Bladed_out_binary.$41'
diff --git a/weio/bmodes_out_file.py b/weio/bmodes_out_file.py
index 9022ccf..f9f2bbe 100644
--- a/weio/bmodes_out_file.py
+++ b/weio/bmodes_out_file.py
@@ -8,10 +8,10 @@
try:
from .file import File, WrongFormatError, BrokenFormatError
except:
+ File=dict
EmptyFileError = type('EmptyFileError', (Exception,),{})
WrongFormatError = type('WrongFormatError', (Exception,),{})
BrokenFormatError = type('BrokenFormatError', (Exception,),{})
- File=dict
class BModesOutFile(File):
"""
diff --git a/weio/cactus_file.py b/weio/cactus_file.py
index 7745a39..be95928 100644
--- a/weio/cactus_file.py
+++ b/weio/cactus_file.py
@@ -5,10 +5,10 @@
try:
from .file import File, WrongFormatError, BrokenFormatError, EmptyFileError
except:
+ File=dict
EmptyFileError = type('EmptyFileError', (Exception,),{})
WrongFormatError = type('WrongFormatError', (Exception,),{})
BrokenFormatError = type('BrokenFormatError', (Exception,),{})
- File=dict
class CactusFile(File):
diff --git a/weio/converters.py b/weio/converters.py
new file mode 100644
index 0000000..a2ec48b
--- /dev/null
+++ b/weio/converters.py
@@ -0,0 +1,94 @@
+import os
+
+
+# --------------------------------------------------------------------------------
+# --- Writing pandas DataFrame to different formats
+# --------------------------------------------------------------------------------
+def writeDataFrameToFormat(df, filename, fformat):
+ """
+ Write a dataframe to disk based on user-specified fileformat
+ - df: pandas dataframe
+ - filename: filename
+ - fformat: fileformat in: ['csv', 'outb', 'parquet']
+ """
+
+ if fformat=='outb':
+ dataFrameToOUTB(df, filename)
+ elif fformat=='parquet':
+ dataFrameToParquet(df, filename)
+ elif fformat=='csv':
+ dataFrameToCSV(df, filename, sep=',', index=False)
+ else:
+ raise Exception('File format not supported for dataframe export `{}`'.format(fformat))
+
+def writeDataFrameAutoFormat(df, filename, fformat=None):
+ """
+ Write a dataframe to disk based on extension
+ - df: pandas dataframe
+ - filename: filename
+ """
+ if fformat is not None:
+ raise Exception()
+ base, ext = os.path.splitext(filename)
+ ext = ext.lower()
+ if ext in ['.outb']:
+ fformat = 'outb'
+ elif ext in ['.parquet']:
+ fformat = 'parquet'
+ elif ext in ['.csv']:
+ fformat = 'csv'
+ else:
+ print('[WARN] defaulting to csv, extension unknown: `{}`'.format(ext))
+ fformat = 'csv'
+
+ writeDataFrameToFormat(df, filename, fformat)
+
+def writeFileDataFrames(fileObject, writer, extension='.conv', filename=None, **kwargs):
+ """
+ From a fileObejct, extract dataframes and write them to disk.
+
+ - fileObject: object inheriting from weio.File with at least
+ - the attributes .filename
+ - the method .toDataFrame()
+ - writer: function with the interface: writer ( dataframe, filename, **kwargs )
+ """
+ if filename is None:
+ base, _ = os.path.splitext(fileObject.filename)
+ filename = base + extension
+ else:
+ base, ext = os.path.splitext(filename)
+ if len(ext)!=0:
+ extension = ext
+ if filename == fileObject.filename:
+ raise Exception('Not overwritting {}. Specify a filename or an extension.'.format(filename))
+
+ dfs = fileObject.toDataFrame()
+ if isinstance(dfs, dict):
+ for name,df in dfs.items():
+ filename = base + name + extension
+ if filename == fileObject.filename:
+ raise Exception('Not overwritting {}. Specify a filename or an extension.'.format(filename))
+ writeDataFrame(df=df, writer=writer, filename=filename, **kwargs)
+ else:
+ writeDataFrame(df=dfs, writer=writer, filename=filename, **kwargs)
+
+def writeDataFrame(df, writer, filename, **kwargs):
+ """
+ Write a dataframe to disk based on a "writer" function.
+ - df: pandas dataframe
+ - writer: function with the interface: writer ( dataframe, filename, **kwargs )
+ - filename: filename
+ """
+ writer(df, filename, **kwargs)
+
+# --- Low level writers
+def dataFrameToCSV(df, filename, sep=',', index=False, **kwargs):
+ df.to_csv(filename, sep=sep, index=index, **kwargs)
+
+def dataFrameToOUTB(df, filename, **kwargs):
+ from .fast_output_file import writeDataFrame as writeDataFrameToOUTB
+ writeDataFrameToOUTB(df, filename, binary=True)
+
+def dataFrameToParquet(df, filename, **kwargs):
+ df.to_parquet(path=filename, **kwargs)
+
diff --git a/weio/csv_file.py b/weio/csv_file.py
index 5e8197c..a688f4e 100644
--- a/weio/csv_file.py
+++ b/weio/csv_file.py
@@ -1,13 +1,14 @@
-from __future__ import division,unicode_literals,print_function,absolute_import
-from builtins import map, range, chr, str
-from io import open
-from future import standard_library
-standard_library.install_aliases()
import os
-
-from .file import File, WrongFormatError
+import re
import pandas as pd
+try:
+ from .file import File, WrongFormatError
+except:
+ File=dict
+ WrongFormatError = type('WrongFormatError', (Exception,),{})
+ EmptyFileError = type('EmptyFileError', (Exception,),{})
+
class CSVFile(File):
"""
Read/write a CSV file.
@@ -34,10 +35,11 @@ def formatName():
return 'CSV file'
def __init__(self, filename=None, sep=None, colNames=None, commentChar=None, commentLines=None,\
- colNamesLine=None, detectColumnNames=True, header=None, **kwargs):
+ colNamesLine=None, detectColumnNames=True, header=None, doRead=True, **kwargs):
colNames = [] if colNames is None else colNames
commentLines = [] if commentLines is None else commentLines
self.sep = sep
+ self.skipRows = []
self.colNames = colNames
self.commentChar = commentChar
self.commentLines = commentLines
@@ -56,9 +58,26 @@ def __init__(self, filename=None, sep=None, colNames=None, commentChar=None, com
raise Exception('Provide either `commentChar` or `commentLines` for CSV file types')
if (len(self.colNames)>0) and (self.colNamesLine is not None):
raise Exception('Provide either `colNames` or `colNamesLine` for CSV file types')
- super(CSVFile, self).__init__(filename=filename,**kwargs)
+ if filename:
+ self.read(filename, doRead=doRead, **kwargs)
+ else:
+ self.filename = None
- def _read(self):
+ def read(self, filename=None, doRead=True, **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.detect()
+ if doRead:
+ self._read(**kwargs)
+
+ def detect(self):
COMMENT_CHAR=['#','!',';']
# --- Detecting encoding
# NOTE: done by parent class method
@@ -233,15 +252,16 @@ def strIsFloat(s):
skiprows.append(self.colNamesLine)
if (self.commentLines is not None) and len(self.commentLines)>0:
skiprows = skiprows + self.commentLines
- skiprows =list(sorted(set(skiprows)))
+ self.skiprows =list(sorted(set(skiprows)))
if self.sep is not None:
if self.sep=='\t':
self.sep=r'\s+'
#print(skiprows)
+
+ def _read(self):
try:
-# self.data = pd.read_csv(self.filename,sep=self.sep,skiprows=skiprows,header=None,comment=self.commentChar,encoding=self.encoding)
with open(self.filename,'r',encoding=self.encoding) as f:
- self.data = pd.read_csv(f,sep=self.sep,skiprows=skiprows,header=None,comment=self.commentChar)
+ self.data = pd.read_csv(f,sep=self.sep,skiprows=self.skiprows,header=None,comment=self.commentChar)
except pd.errors.ParserError as e:
raise WrongFormatError('CSV File {}: '.format(self.filename)+e.args[0])
@@ -249,8 +269,49 @@ def strIsFloat(s):
self.colNames=['C{}'.format(i) for i in range(len(self.data.columns))]
self.data.columns = self.colNames;
self.data.rename(columns=lambda x: x.strip(),inplace=True)
- #import pdb
- #pdb.set_trace()
+
+
+ def read_slow_stop_at_first_empty_lines(self, skiprows=None, sep=None, numeric_only=True, colNames=None):
+ """
+ HACKY function
+ Reads a CSV file line by line, stopping at the first empty line.
+ This is a slower method but can be useful for large files or when you want to avoid loading the entire file into memory.
+ """
+ if skiprows is None:
+ skiprows = self.skipRows + self.commentLines
+ if sep is None:
+ sep = self.sep if self.sep is not None else ','
+ if colNames is None:
+ colNames = self.colNames
+
+ def smart_split(line, sep):
+ """Splits a line using a separator, which can be a regex (e.g. '\\s+') or a normal string."""
+ if sep == r'\s+':
+ return re.split(r'\s+', line.strip())
+ else:
+ return [c.strip() for c in line.strip().split(sep)]
+ data = []
+ with open(self.filename, 'r', encoding=self.encoding) as f:
+ for i, line in enumerate(f):
+ if i in skiprows:
+ continue
+ line = line.strip()
+ if not line:
+ break
+ cols = smart_split(line, sep)
+ if len(colNames)>0 and len(cols) != len(self.colNames):
+ raise Exception("Error: Wrong number of columns on line {}.".format(i))
+ data.append(cols)
+ if len(colNames)>0:
+ self.data= pd.DataFrame(data, columns=self.colNames)
+ else:
+ self.data= pd.DataFrame(data)
+ self.colNames=['C{}'.format(i) for i in range(len(self.data.columns))]
+ self.data.columns = self.colNames;
+
+ if numeric_only:
+ self.data = self.data.apply(pd.to_numeric, errors='coerce')
+ #self.data.dropna(inplace=True)
def _write(self):
# --- Safety
@@ -275,18 +336,84 @@ def _write(self):
self.data.to_csv(self.filename,sep=self.sep,index=False)
def __repr__(self):
- s = 'CSVFile: {}\n'.format(self.filename)
- s += 'sep=`{}` commentChar=`{}`\ncolNamesLine={}'.format(self.sep,self.commentChar,self.colNamesLine)
- s += ', encoding={}'.format(self.encoding)+'\n'
- s += 'commentLines={}'.format(self.commentLines)+'\n'
- s += 'colNames={}'.format(self.colNames)
- s += '\n'
+ s = '\n'.format(self.filename)
+ s += '| - sep =`{}`\n'.format(self.sep)
+ s += '| - commentChar =`{}`\n'.format(self.commentChar)
+ s += '| - colNamesLine= {}\n'.format(self.colNamesLine)
+ s += '| - encoding = {}\n'.format(self.encoding)
+ s += '| - commentLines= {}\n'.format(self.commentLines)
+ s += '| - skipRows = {}\n'.format(self.skipRows)
+ s += '| - colNames = {}\n'.format(self.colNames)
if len(self.header)>0:
- s += 'header:\n'+ '\n'.join(self.header)+'\n'
+ s += '| - header:\n'+ '\n'.join(self.header)+'\n'
if len(self.data)>0:
- s += 'size: {}x{}'.format(len(self.data),len(self.data.columns))
+ s += '| - size: {}x{}'.format(len(self.data),len(self.data.columns))
return s
- def _toDataFrame(self):
+ def toDataFrame(self):
return self.data
+ def to2DFields(self, **kwargs):
+ import xarray as xr
+ if len(kwargs.keys())>0:
+ print('[WARN] CSVFile: to2DFields: ignored keys: ',kwargs.keys())
+ if len(self.data)==0:
+ return None
+ M = self.data.values
+ if self.data.columns[0].lower()=='index':
+ M = M[:,1:]
+ s1 = 'rows'
+ s2 = 'columns'
+ ds = xr.Dataset(coords={s1: range(M.shape[0]), s2: range(M.shape[1])})
+ ds['data'] = ([s1, s2], M)
+ return ds
+
+ # --------------------------------------------------------------------------------
+ # --- Properties NOTE: copy pasted from file.py to make this file standalone..
+ # --------------------------------------------------------------------------------
+ @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']
+
+
+def find_non_numeric_header_lines(filename):
+ """
+ Reads a file line by line and returns a list of indices for lines that are headers,
+ i.e., lines that contain anything other than numbers, commas, tabs, spaces, or scientific notation.
+ Stops at the first line that is purely numeric (with allowed delimiters).
+ """
+ header_indices = []
+ header_lines = []
+ numeric_line_pattern = re.compile(r'^[\s,]*([+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?[\s,]*)+$')
+ with open(filename, 'r', errors="surrogateescape") as f:
+ for idx, line in enumerate(f):
+ s = line.strip()
+ if not s: continue
+ if numeric_line_pattern.fullmatch(s):
+ break
+ header_lines.append(s)
+ header_indices.append(idx)
+ return header_indices, header_lines
+
+if __name__ == '__main__':
+ f = CSVFile('C:/Work/Courses/440/project_solution/data/CFD_u.dat')
+ print(f)
+ ds = f.to2DFields()
+ print(ds)
+
diff --git a/weio/excel_file.py b/weio/excel_file.py
index df28931..54ee8c8 100644
--- a/weio/excel_file.py
+++ b/weio/excel_file.py
@@ -1,88 +1,81 @@
-from __future__ import division,unicode_literals,print_function,absolute_import
-from builtins import map, range, chr, str
-from io import open
-from future import standard_library
-standard_library.install_aliases()
-
-from .file import File, WrongFormatError, BrokenFormatError
-import numpy as np
-import pandas as pd
-
-# from pandas import ExcelWriter
-from pandas import ExcelFile
-
-class ExcelFile(File):
-
- @staticmethod
- def defaultExtensions():
- return ['.xls','.xlsx']
-
- @staticmethod
- def formatName():
- return 'Excel file'
-
- def _read(self):
- self.data=dict()
- # Reading all sheets
- try:
- xls = pd.ExcelFile(self.filename, engine='openpyxl')
- except:
- xls = pd.ExcelFile(self.filename)
- dfs = {}
- for sheet_name in xls.sheet_names:
- # Reading sheet
- df = xls.parse(sheet_name, header=None)
- # TODO detect sub tables
- # Dropping empty rows and cols
- df.dropna(how='all',axis=0,inplace=True)
- df.dropna(how='all',axis=1,inplace=True)
- #print(df.shape)
- if df.shape[0]>0:
- # Setting first row as header
- df=df.rename(columns=df.iloc[0]).drop(df.index[0]).reset_index(drop=True)
- #print(df)
- self.data[sheet_name]=df
-
- #def toString(self):
- # s=''
- # return s
-
- def _write(self):
- # Create a Pandas Excel writer using XlsxWriter as the engine.
- writer = pd.ExcelWriter(self.filename, engine='xlsxwriter')
- # Convert the dataframe to an XlsxWriter Excel object.
- for k,_ in self.data.items():
- df = self.data[k]
- df.to_excel(writer, sheet_name=k, index=False)
- # # Account info columns (set size)
- # worksheet.set_column('B:D', 20)
- # # Total formatting
- # total_fmt = workbook.add_format({'align': 'right', 'num_format': '$#,##0',
- # 'bold': True, 'bottom':6})
- # # Total percent format
- # total_percent_fmt = workbook.add_format({'align': 'right', 'num_format': '0.0%',
- # 'bold': True, 'bottom':6})
- # workbook = writer.book
- # worksheet = writer.sheets['report']
- # Highlight the top 5 values in Green
- #worksheet.conditional_format(color_range, {'type': 'top',
- # 'value': '5',
- # 'format': format2})
- ## Highlight the bottom 5 values in Red
- #worksheet.conditional_format(color_range, {'type': 'bottom',
- # 'value': '5',
- # 'format': format1})
- # Close the Pandas Excel writer and output the Excel file.
- writer.save()
-
- def __repr__(self):
- s ='Class XXXX (attributes: data)\n'
- return s
-
-
- def _toDataFrame(self):
- #cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]']
- #dfs[name] = pd.DataFrame(data=..., columns=cols)
- #df=pd.DataFrame(data=,columns=)
- return self.data
-
+from .file import File, WrongFormatError, BrokenFormatError
+import numpy as np
+import pandas as pd
+
+# from pandas import ExcelWriter
+from pandas import ExcelFile
+
+class ExcelFile(File):
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.xls','.xlsx']
+
+ @staticmethod
+ def formatName():
+ return 'Excel file'
+
+ def _read(self):
+ self.data=dict()
+ # Reading all sheets
+ xls = pd.ExcelFile(self.filename, engine='openpyxl')
+ dfs = {}
+ for sheet_name in xls.sheet_names:
+ # Reading sheet
+ df = xls.parse(sheet_name, header=None)
+ # TODO detect sub tables
+ # Dropping empty rows and cols
+ df.dropna(how='all',axis=0,inplace=True)
+ df.dropna(how='all',axis=1,inplace=True)
+ #print(df.shape)
+ if df.shape[0]>0:
+ # Setting first row as header
+ df=df.rename(columns=df.iloc[0]).drop(df.index[0]).reset_index(drop=True)
+ #print(df)
+ self.data[sheet_name]=df
+
+ #def toString(self):
+ # s=''
+ # return s
+
+ def _write(self):
+ # Create a Pandas Excel writer using XlsxWriter as the engine.
+ writer = pd.ExcelWriter(self.filename, engine='xlsxwriter')
+ # Convert the dataframe to an XlsxWriter Excel object.
+ for k,_ in self.data.items():
+ df = self.data[k]
+ df.to_excel(writer, sheet_name=k, index=False)
+ # # Account info columns (set size)
+ # worksheet.set_column('B:D', 20)
+ # # Total formatting
+ # total_fmt = workbook.add_format({'align': 'right', 'num_format': '$#,##0',
+ # 'bold': True, 'bottom':6})
+ # # Total percent format
+ # total_percent_fmt = workbook.add_format({'align': 'right', 'num_format': '0.0%',
+ # 'bold': True, 'bottom':6})
+ # workbook = writer.book
+ # worksheet = writer.sheets['report']
+ # Highlight the top 5 values in Green
+ #worksheet.conditional_format(color_range, {'type': 'top',
+ # 'value': '5',
+ # 'format': format2})
+ ## Highlight the bottom 5 values in Red
+ #worksheet.conditional_format(color_range, {'type': 'bottom',
+ # 'value': '5',
+ # 'format': format1})
+ # Close the Pandas Excel writer and output the Excel file.
+ writer.save()
+
+ def __repr__(self):
+ s ='Class ExcelFile (attributes: data)\n'
+ return s
+
+
+ def _toDataFrame(self):
+ if len(self.data)==1:
+ # Return a single dataframe
+ return self.data[list(self.data.keys())[0]]
+ else:
+ # Return dictionary
+ return self.data
+
diff --git a/weio/fast_input_deck.py b/weio/fast_input_deck.py
index 1153ca5..5d71a55 100644
--- a/weio/fast_input_deck.py
+++ b/weio/fast_input_deck.py
@@ -1,12 +1,3 @@
-from __future__ import division
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import absolute_import
-from io import open
-from builtins import range
-from builtins import str
-from future import standard_library
-standard_library.install_aliases()
import os
import numpy as np
import re
@@ -21,13 +12,17 @@
class FASTInputDeck(dict):
"""Container for input files that make up a FAST input deck"""
+ @property
+ def readlist_default(self):
+ return ['Fst','ED','AD','BD','BDbld','EDtwr','EDbld','ADbld','AF','AC','OLAF','IW','HD','SrvD','SD','MD']
+
def __init__(self, fullFstPath='', readlist=['all'], verbose=False):
"""Read FAST master file and read inputs for FAST modules
INPUTS:
- fullFstPath:
- readlist: list of module files to be read, or ['all'], modules are identified as follows:
- ['Fst','ED','AD','BD','BDbld','EDtwr','EDbld','ADbld','AF','AC','IW','HD','SrvD','SD','MD']
+ ['Fst','ED','AD','BD','BDbld','EDtwr','EDbld','ADbld','AF','AC','OLAF','IW','HD','SrvD','SD','MD']
where:
AF: airfoil polars
AC: airfoil coordinates (if present)
@@ -37,17 +32,18 @@ def __init__(self, fullFstPath='', readlist=['all'], verbose=False):
if type(verbose) is not bool:
raise Exception('`verbose` arguments needs to be a boolean')
+ # Main Data
+ self.inputFilesRead = {}
self.filename = fullFstPath
self.verbose = verbose
self.readlist = readlist
if not type(self.readlist) is list:
self.readlist=[readlist]
if 'all' in self.readlist:
- self.readlist = ['Fst','ED','AD','BD','BDbld','EDtwr','EDbld','ADbld','AF','AC','IW','HD','SrvD','SD','MD']
+ self.readlist = self.readlist_default
else:
self.readlist = ['Fst']+self.readlist
- self.inputfiles = {}
# --- Harmonization with AeroElasticSE
self.FAST_ver = 'OPENFAST'
@@ -83,10 +79,23 @@ def __init__(self, fullFstPath='', readlist=['all'], verbose=False):
if len(fullFstPath)>0:
self.read()
+ @property
+ def ED(self):
+ ED = self.fst_vt['ElastoDyn']
+ if ED is None:
+ if 'ED' not in self.readlist:
+ self.readlist.append('ED')
+ if self.verbose:
+ print('>>> Reading ED', self.ED_path)
+ self.fst_vt['ElastoDyn'] = self._read(self.fst_vt['Fst']['EDFile'],'ED')
+ return self.fst_vt['ElastoDyn']
+ else:
+ return ED
+
def readAD(self, filename=None, readlist=None, verbose=False, key='AeroDyn15'):
"""
- readlist: 'AD','AF','AC'
+ readlist: 'AD','AF','AC','OLAF'
"""
if readlist is not None:
readlist_bkp = self.readlist
@@ -94,7 +103,7 @@ def readAD(self, filename=None, readlist=None, verbose=False, key='AeroDyn15'):
if not type(self.readlist) is list:
self.readlist=[readlist]
if 'all' in self.readlist:
- self.readlist = ['Fst','ED','AD','BD','BDbld','EDtwr','EDbld','ADbld','AF','AC','IW','HD','SrvD','SD','MD']
+ self.readlist = self.readlist_default
if filename is None:
filename = self.fst_vt['Fst']['AeroFile']
@@ -104,26 +113,40 @@ def readAD(self, filename=None, readlist=None, verbose=False, key='AeroDyn15'):
self.verbose = verbose
- self.fst_vt[key] = self._read(filename,'AD')
+ # AD
+ AD = self._read(filename,'AD')
+ self.fst_vt[key] = AD
+
+ if AD is not None:
+ # ADbld - AeroDyn Blades
+ #bld_file = os.path.join(baseDir, AD['ADBlFile(1)'])
+ #self.fst_vt['AeroDynBlade'] = self._read(bld_file,'ADbld')
+ for i in range(10):
+ try:
+ AD['ADBlFile({})'.format(i+1)]
+ except KeyError:
+ nBlades = i
+ break
+ self.fst_vt['AeroDynBlade'] = []
+ for i in range(nBlades):
+ bld_file = os.path.join(baseDir, self.fst_vt[key]['ADBlFile({})'.format(i+1)])
+ self.fst_vt['AeroDynBlade'].append(self._read(bld_file,'ADbld'))
+ # OLAF
+ #if os.path.exist(AD['OLAFInputFileName']):
+ self.fst_vt['OLAF'] = self._read(AD['OLAFInputFileName'], 'OLAF')
- if self.fst_vt[key] is not None:
- # Blades
- bld_file = os.path.join(baseDir, self.fst_vt[key]['ADBlFile(1)'])
- self.fst_vt['AeroDynBlade'] = self._read(bld_file,'ADbld')
- #self.fst_vt['AeroDynBlade'] = []
- #for i in range(3):
- # bld_file = os.path.join(os.path.dirname(self.fst_vt['Fst']['AeroFile']), self.fst_vt[key]['ADBlFile({})'.format(i+1)])
- # self.fst_vt['AeroDynBlade'].append(self._read(bld_file,'ADbld'))
# Polars
self.fst_vt['af_data']=[] # TODO add to "AeroDyn"
for afi, af_filename in enumerate(self.fst_vt['AeroDyn15']['AFNames']):
af_filename = os.path.join(baseDir,af_filename).replace('"','')
+ # AF - Airfoil file
try:
polar = self._read(af_filename, 'AF')
except:
polar=None
print('[FAIL] reading polar {}'.format(af_filename))
self.fst_vt['af_data'].append(polar)
+ # AC - Airfoil coordinates
if polar is not None:
coordFile = polar['NumCoords']
if isinstance(coordFile,str):
@@ -135,7 +158,7 @@ def readAD(self, filename=None, readlist=None, verbose=False, key='AeroDyn15'):
self.fst_vt['ac_data'].append(coords)
# --- Backward compatibility
- self.AD = self.fst_vt[key]
+ self.AD = AD
self.ADversion='AD15' if key=='AeroDyn15' else 'AD14'
if readlist is not None:
@@ -154,57 +177,37 @@ def inputFiles(self):
files=[]
files+=[self.ED_path, self.ED_twr_path, self.ED_bld_path]
files+=[self.BD_path, self.BD_bld_path]
+ files+=[self.SD_path]
return [f for f in files if f not in self.unusedNames]
-
- @property
- def ED_relpath(self):
+ def _relpath(self, k1, k2=None, k3=None):
try:
- return self.fst_vt['Fst']['EDFile'].replace('"','')
- except:
- return 'none'
-
- @property
- def ED_twr_relpath(self):
- try:
- return os.path.join(os.path.dirname(self.fst_vt['Fst']['EDFile']).replace('"',''), self.fst_vt['ElastoDyn']['TwrFile'].replace('"',''))
- except:
- return 'none'
-
- @property
- def ED_bld_relpath(self):
- try:
- if 'BldFile(1)' in self.fst_vt['ElastoDyn'].keys():
- return os.path.join(os.path.dirname(self.fst_vt['Fst']['EDFile'].replace('"','')), self.fst_vt['ElastoDyn']['BldFile(1)'].replace('"',''))
+ if k2 is None:
+ return self.fst_vt['Fst'][k1].replace('"','')
else:
- return os.path.join(os.path.dirname(self.fst_vt['Fst']['EDFile'].replace('"','')), self.fst_vt['ElastoDyn']['BldFile1'].replace('"',''))
+ parent = os.path.dirname(self.fst_vt['Fst'][k1]).replace('"','')
+ if type(k3)==list:
+ for k in k3:
+ if k in self.fst_vt[k2].keys():
+ child = self.fst_vt[k2][k].replace('"','')
+ else:
+ child = self.fst_vt[k2][k3].replace('"','')
+ return os.path.join(parent, child)
except:
return 'none'
@property
- def BD_relpath(self):
- try:
- return self.fst_vt['Fst']['BDBldFile(1)'].replace('"','')
- except:
- return 'none'
-
+ def ED_path(self): return self._fullpath(self._relpath('EDFile'))
@property
- def BD_bld_relpath(self):
- try:
- return os.path.join(os.path.dirname(self.fst_vt['Fst']['BDBldFile(1)'].replace('"','')), self.fst_vt['BeamDyn']['BldFile'].replace('"',''))
- except:
- return 'none'
-
+ def SD_path(self): return self._fullpath(self._relpath('SubFile'))
@property
- def ED_path(self): return self._fullpath(self.ED_relpath)
+ def BD_path(self): return self._fullpath(self._relpath('BDBldFile(1)'))
@property
- def BD_path(self): return self._fullpath(self.BD_relpath)
+ def BD_bld_path(self): return self._fullpath(self._relpath('BDBldFile(1)','BeamDyn','BldFile'))
@property
- def BD_bld_path(self): return self._fullpath(self.BD_bld_relpath)
+ def ED_twr_path(self): return self._fullpath(self._relpath('EDFile','ElastoDyn','TwrFile'))
@property
- def ED_twr_path(self): return self._fullpath(self.ED_twr_relpath)
- @property
- def ED_bld_path(self): return self._fullpath(self.ED_bld_relpath)
+ def ED_bld_path(self): return self._fullpath(self._relpath('EDFile','ElastoDyn',['BldFile(1)','BldFile1']))
@@ -218,18 +221,25 @@ def _fullpath(self, relfilepath):
def read(self, filename=None):
+ """
+ Read all OpenFAST inputs files, based on the requested list of modules `readlist`
+ """
if filename is not None:
self.filename = filename
- # Read OpenFAST files
+ # Read main file (.fst, or .drv) and store into key "Fst"
+ if self.verbose:
+ print('Reading:', self.FAST_InputFile)
self.fst_vt['Fst'] = self._read(self.FAST_InputFile, 'Fst')
if self.fst_vt['Fst'] is None:
raise Exception('Error reading main file {}'.format(self.filename))
keys = self.fst_vt['Fst'].keys()
-
+ # Detect driver or OpenFAST version
if 'NumTurbines' in keys:
self.version='AD_driver'
+ elif 'DynamicSolve' in keys:
+ self.version='BD_driver'
elif 'InterpOrder' in self.fst_vt['Fst'].keys():
self.version='OF2'
else:
@@ -244,14 +254,26 @@ def read(self, filename=None):
self.readAD(key='AeroDyn15')
+ elif self.version=='BD_driver':
+ # --- BD driver
+ self.fst_vt['BeamDyn'] = self._read(self.fst_vt['Fst']['InputFile'],'BD')
+ if self.fst_vt['BeamDyn'] is not None:
+ # Blades
+ bld_file = os.path.join(os.path.dirname(self.fst_vt['Fst']['InputFile']), self.fst_vt['BeamDyn']['BldFile'])
+ print('bld_file', bld_file)
+ self.fst_vt['BeamDynBlade']= self._read(bld_file,'BDbld')
+
+ del self.fst_vt['af_data']
+ del self.fst_vt['ac_data']
+
elif self.version=='OF2':
# ---- Regular OpenFAST file
# ElastoDyn
if 'EDFile' in self.fst_vt['Fst'].keys():
self.fst_vt['ElastoDyn'] = self._read(self.fst_vt['Fst']['EDFile'],'ED')
if self.fst_vt['ElastoDyn'] is not None:
- twr_file = self.ED_twr_relpath
- bld_file = self.ED_bld_relpath
+ twr_file = self.ED_twr_path
+ bld_file = self.ED_bld_path
self.fst_vt['ElastoDynTower'] = self._read(twr_file,'EDtwr')
self.fst_vt['ElastoDynBlade'] = self._read(bld_file,'EDbld')
@@ -275,7 +297,7 @@ def read(self, filename=None):
# SubDyn
if self.fst_vt['Fst']['CompSub'] == 1:
- self.fst_vt['SubDyn'] = self._read(self.fst_vt['Fst']['SubFile'],'HD')
+ self.fst_vt['SubDyn'] = self._read(self.fst_vt['Fst']['SubFile'], 'SD')
# Mooring
if self.fst_vt['Fst']['CompMooring']==1:
@@ -293,15 +315,16 @@ def read(self, filename=None):
# --- Backward compatibility
self.fst = self.fst_vt['Fst']
- self.ED = self.fst_vt['ElastoDyn']
+ self._ED = self.fst_vt['ElastoDyn']
if not hasattr(self,'AD'):
self.AD = None
if self.AD is not None:
- self.AD.Bld1 = self.fst_vt['AeroDynBlade']
+ self.AD.Bld1 = self.fst_vt['AeroDynBlade'][0]
self.AD.AF = self.fst_vt['af_data']
self.IW = self.fst_vt['InflowWind']
self.BD = self.fst_vt['BeamDyn']
self.BDbld = self.fst_vt['BeamDynBlade']
+ self.SD = self.fst_vt['SubDyn']
@ property
def unusedNames(self):
@@ -325,12 +348,15 @@ def _read(self, relfilepath, shortkey):
return None
# Attempt reading
- fullpath =os.path.join(self.FAST_directory, relfilepath)
+ if relfilepath.startswith(self.FAST_directory):
+ fullpath = relfilepath
+ else:
+ fullpath = os.path.join(self.FAST_directory, relfilepath)
try:
data = FASTInputFile(fullpath)
if self.verbose:
print('>>> Read: ',fullpath)
- self.inputfiles[shortkey] = fullpath
+ self.inputFilesRead[shortkey] = fullpath
return data
except FileNotFoundError:
print('[WARN] File not found '+fullpath)
@@ -345,95 +371,122 @@ def write(self, filename=None, prefix='', suffix='', directory=None):
self.filename=filename
if directory is None:
directory = os.path.dirname(filename)
+ else:
+ # Making sure filename is within directory
+ filename = os.path.join(directory, os.path.basename(filename))
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+
basename = os.path.splitext(os.path.basename(filename))[0]
fst = self.fst_vt['Fst']
- # Filenames
- filename_ED = os.path.join(directory,prefix+'ED'+suffix+'.dat') if fst['CompElast']>0 else 'none'
- filename_IW = os.path.join(directory,prefix+'IW'+suffix+'.dat') if fst['CompInflow']>0 else 'none'
- filename_BD = os.path.join(directory,prefix+'BD'+suffix+'.dat') if fst['CompElast']==2 else 'none'
- filename_AD = os.path.join(directory,prefix+'AD'+suffix+'.dat') if fst['CompAero']>0 else 'none'
- filename_HD = os.path.join(directory,prefix+'HD'+suffix+'.dat') if fst['CompHydro']>0 else 'none'
- filename_SD = os.path.join(directory,prefix+'SD'+suffix+'.dat') if fst['CompSub']>0 else 'none'
- filename_MD = os.path.join(directory,prefix+'MD'+suffix+'.dat') if fst['CompMooring']>0 else 'none'
- filename_SvD = os.path.join(directory,prefix+'SvD'+suffix+'.dat') if fst['CompServo']>0 else 'none'
- filename_Ice = os.path.join(directory,prefix+'Ice'+suffix+'.dat') if fst['CompIce']>0 else 'none'
- filename_ED_bld = os.path.join(directory,prefix+'ED_bld'+suffix+'.dat') if fst['CompElast']>0 else 'none'
- filename_ED_twr = os.path.join(directory,prefix+'ED_twr'+suffix+'.dat') if fst['CompElast']>0 else 'none'
- filename_BD_bld = os.path.join(directory,prefix+'BD_bld'+suffix+'.dat') if fst['CompElast']>0 else 'none'
- # TODO AD Profiles and OLAF
-
- fst['EDFile'] = '"' + os.path.basename(filename_ED) + '"'
- fst['BDBldFile(1)'] = '"' + os.path.basename(filename_BD) + '"'
- fst['BDBldFile(2)'] = '"' + os.path.basename(filename_BD) + '"'
- fst['BDBldFile(3)'] = '"' + os.path.basename(filename_BD) + '"'
- fst['InflowFile'] = '"' + os.path.basename(filename_IW) + '"'
- fst['AeroFile'] = '"' + os.path.basename(filename_AD) + '"'
- fst['ServoFile'] = '"' + os.path.basename(filename_AD) + '"'
- fst['HydroFile'] = '"' + os.path.basename(filename_HD) + '"'
- fst['SubFile'] = '"' + os.path.basename(filename_SD) + '"'
- fst['MooringFile'] = '"' + os.path.basename(filename_MD) + '"'
- fst['IceFile'] = '"' + os.path.basename(filename_Ice)+ '"'
- fst.write(filename)
-
-
- ED = self.fst_vt['ElastoDyn']
- if fst['CompElast']>0:
- ED['TwrFile'] = '"' + os.path.basename(filename_ED_twr)+ '"'
- self.fst_vt['ElastoDynTower'].write(filename_ED_twr)
- if fst['CompElast']==1:
- if 'BldFile1' in ED.keys():
- ED['BldFile1'] = '"' + os.path.basename(filename_ED_bld)+ '"'
- ED['BldFile2'] = '"' + os.path.basename(filename_ED_bld)+ '"'
- ED['BldFile3'] = '"' + os.path.basename(filename_ED_bld)+ '"'
- else:
- ED['BldFile(1)'] = '"' + os.path.basename(filename_ED_bld)+ '"'
- ED['BldFile(2)'] = '"' + os.path.basename(filename_ED_bld)+ '"'
- ED['BldFile(3)'] = '"' + os.path.basename(filename_ED_bld)+ '"'
- self.fst_vt['ElastoDynBlade'].write(filename_ED_bld)
- elif fst['CompElast']==2:
+ if self.version=='AD_driver':
+ raise NotImplementedError()
+
+ elif self.version=='BD_driver':
+ # --- BD driver
+ filename_BD = os.path.join(directory, prefix+'BD'+suffix+'.dat')
+ filename_BD_bld = os.path.join(directory, prefix+'BD_bld'+suffix+'.dat')
+ fst['InputFile'] = '"' + os.path.basename(filename_BD) + '"'
+ fst.write(filename)
BD = self.fst_vt['BeamDyn']
BD['BldFile'] = '"'+os.path.basename(filename_BD_bld)+'"'
self.fst_vt['BeamDynBlade'].write(filename_BD_bld) # TODO TODO pick up the proper blade file!
BD.write(filename_BD)
- ED.write(filename_ED)
-
-
- if fst['CompInflow']>0:
- self.fst_vt['InflowWind'].write(filename_IW)
- if fst['CompAero']>0:
- self.fst_vt['AeroDyn15'].write(filename_AD)
- # TODO other files
-
- if fst['CompServo']>0:
- self.fst_vt['ServoDyn'].write(filename_SvD)
-
- if fst['CompHydro']==1:
- self.fst_vt['HydroDyn'].write(filename_HD)
+ elif self.version=='OF2':
- if fst['CompSub']==1:
- self.fst_vt['SubDyn'].write(filename_SD)
- elif fst['CompSub']==2:
- raise NotImplementedError()
+ # Filenames
+ filename_ED = os.path.join(directory,prefix+'ED'+suffix+'.dat') if fst['CompElast']>0 else 'none'
+ filename_IW = os.path.join(directory,prefix+'IW'+suffix+'.dat') if fst['CompInflow']>0 else 'none'
+ filename_BD = os.path.join(directory,prefix+'BD'+suffix+'.dat') if fst['CompElast']==2 else 'none'
+ filename_AD = os.path.join(directory,prefix+'AD'+suffix+'.dat') if fst['CompAero']>0 else 'none'
+ filename_HD = os.path.join(directory,prefix+'HD'+suffix+'.dat') if fst['CompHydro']>0 else 'none'
+ filename_SD = os.path.join(directory,prefix+'SD'+suffix+'.dat') if fst['CompSub']>0 else 'none'
+ filename_MD = os.path.join(directory,prefix+'MD'+suffix+'.dat') if fst['CompMooring']>0 else 'none'
+ filename_SvD = os.path.join(directory,prefix+'SvD'+suffix+'.dat') if fst['CompServo']>0 else 'none'
+ filename_Ice = os.path.join(directory,prefix+'Ice'+suffix+'.dat') if fst['CompIce']>0 else 'none'
+ filename_ED_bld = os.path.join(directory,prefix+'ED_bld'+suffix+'.dat') if fst['CompElast']>0 else 'none'
+ filename_ED_twr = os.path.join(directory,prefix+'ED_twr'+suffix+'.dat') if fst['CompElast']>0 else 'none'
+ filename_BD_bld = os.path.join(directory,prefix+'BD_bld'+suffix+'.dat') if fst['CompElast']>0 else 'none'
+ # TODO AD Profiles and OLAF
+
+ fst['EDFile'] = '"' + os.path.basename(filename_ED) + '"'
+ fst['BDBldFile(1)'] = '"' + os.path.basename(filename_BD) + '"'
+ fst['BDBldFile(2)'] = '"' + os.path.basename(filename_BD) + '"'
+ fst['BDBldFile(3)'] = '"' + os.path.basename(filename_BD) + '"'
+ fst['InflowFile'] = '"' + os.path.basename(filename_IW) + '"'
+ fst['AeroFile'] = '"' + os.path.basename(filename_AD) + '"'
+ fst['ServoFile'] = '"' + os.path.basename(filename_AD) + '"'
+ fst['HydroFile'] = '"' + os.path.basename(filename_HD) + '"'
+ fst['SubFile'] = '"' + os.path.basename(filename_SD) + '"'
+ fst['MooringFile'] = '"' + os.path.basename(filename_MD) + '"'
+ fst['IceFile'] = '"' + os.path.basename(filename_Ice)+ '"'
+ fst.write(filename)
+
+
+ ED = self.fst_vt['ElastoDyn']
+ if fst['CompElast']>0:
+ ED['TwrFile'] = '"' + os.path.basename(filename_ED_twr)+ '"'
+ self.fst_vt['ElastoDynTower'].write(filename_ED_twr)
+ if fst['CompElast']==1:
+ if 'BldFile1' in ED.keys():
+ ED['BldFile1'] = '"' + os.path.basename(filename_ED_bld)+ '"'
+ ED['BldFile2'] = '"' + os.path.basename(filename_ED_bld)+ '"'
+ ED['BldFile3'] = '"' + os.path.basename(filename_ED_bld)+ '"'
+ else:
+ ED['BldFile(1)'] = '"' + os.path.basename(filename_ED_bld)+ '"'
+ ED['BldFile(2)'] = '"' + os.path.basename(filename_ED_bld)+ '"'
+ ED['BldFile(3)'] = '"' + os.path.basename(filename_ED_bld)+ '"'
+ self.fst_vt['ElastoDynBlade'].write(filename_ED_bld)
+
+ elif fst['CompElast']==2:
+ BD = self.fst_vt['BeamDyn']
+ BD['BldFile'] = '"'+os.path.basename(filename_BD_bld)+'"'
+ self.fst_vt['BeamDynBlade'].write(filename_BD_bld) # TODO TODO pick up the proper blade file!
+ BD.write(filename_BD)
+ ED.write(filename_ED)
+
+
+ if fst['CompInflow']>0:
+ self.fst_vt['InflowWind'].write(filename_IW)
+
+ if fst['CompAero']>0:
+ self.fst_vt['AeroDyn15'].write(filename_AD)
+ # TODO other files
+
+ if fst['CompServo']>0:
+ self.fst_vt['ServoDyn'].write(filename_SvD)
+
+ if fst['CompHydro']==1:
+ self.fst_vt['HydroDyn'].write(filename_HD)
+
+ if fst['CompSub']==1:
+ self.fst_vt['SubDyn'].write(filename_SD)
+ elif fst['CompSub']==2:
+ raise NotImplementedError()
+
+ if fst['CompMooring']==1:
+ self.fst_vt['MAP'].write(filename_MD)
+ if self.fst_vt['Fst']['CompMooring']==2:
+ self.fst_vt['MoorDyn'].write(filename_MD)
- if fst['CompMooring']==1:
- self.fst_vt['MAP'].write(filename_MD)
- if self.fst_vt['Fst']['CompMooring']==2:
- self.fst_vt['MoorDyn'].write(filename_MD)
+ return filename
def __repr__(self):
s=''+'\n'
s+='filename : '+self.filename+'\n'
+ s+='readlist : {}'.format(self.readlist)+'\n'
s+='version : '+self.version+'\n'
s+='AD version : '+self.ADversion+'\n'
s+='fst_vt : dict{'+','.join([k for k,v in self.fst_vt.items() if v is not None])+'}\n'
s+='inputFiles : {}\n'.format(self.inputFiles)
+ s+='inputFilesRead : {}\n'.format(self.inputFilesRead)
s+='\n'
return s
diff --git a/weio/fast_input_file.py b/weio/fast_input_file.py
index 51a6868..516fb49 100644
--- a/weio/fast_input_file.py
+++ b/weio/fast_input_file.py
@@ -1,25 +1,13 @@
-from __future__ import division
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import absolute_import
-from io import open
-from builtins import range
-from builtins import str
-from future import standard_library
-standard_library.install_aliases()
+import numpy as np
+import os
+import pandas as pd
+import re
try:
from .file import File, WrongFormatError, BrokenFormatError
except:
- # --- Allowing this file to be standalone..
- class WrongFormatError(Exception):
- pass
- class BrokenFormatError(Exception):
- pass
File = dict
-import os
-import numpy as np
-import re
-import pandas as pd
+ class WrongFormatError(Exception): pass
+ class BrokenFormatError(Exception): pass
__all__ = ['FASTInputFile']
@@ -28,8 +16,10 @@ class BrokenFormatError(Exception):
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_OUTLIST = 33
TABTYPE_FMT = 9999 # TODO
@@ -54,7 +44,7 @@ class FASTInputFile(File):
@staticmethod
def defaultExtensions():
- return ['.dat','.fst','.txt','.fstf']
+ return ['.dat','.fst','.txt','.fstf','.dvr']
@staticmethod
def formatName():
@@ -73,6 +63,7 @@ def fixedfile(self):
return self._fixedfile
else:
return self.basefile
+
@property
def module(self):
if self._fixedfile is None:
@@ -80,14 +71,32 @@ def module(self):
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 'MassMatrix' in KEYS and self.module =='ExtPtfm':
+ 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)
@@ -111,8 +120,8 @@ def toString(self):
def keys(self):
return self.fixedfile.keys()
- def toGraph(self):
- return self.fixedfile.toGraph()
+ def toGraph(self, **kwargs):
+ return self.fixedfile.toGraph(**kwargs)
@property
def filename(self):
@@ -124,7 +133,7 @@ def comment(self):
@comment.setter
def comment(self,comment):
- self.fixedfile.comment(comment)
+ self.fixedfile.comment = comment
def __iter__(self):
return self.fixedfile.__iter__()
@@ -132,10 +141,10 @@ def __iter__(self):
def __next__(self):
return self.fixedfile.__next__()
- def __setitem__(self,key,item):
- return self.fixedfile.__setitem__(key,item)
+ def __setitem__(self, key, item):
+ return self.fixedfile.__setitem__(key, item)
- def __getitem__(self,key):
+ def __getitem__(self, key):
return self.fixedfile.__getitem__(key)
def __repr__(self):
@@ -143,6 +152,56 @@ def __repr__(self):
#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)])
+ def delete(self, key, error=False):
+ self.pop(key, error=error)
+
+ def pop(self, key, error=False):
+ if isinstance(key, int):
+ i = key
+ else:
+ i = self.fixedfile.getIDSafe(key)
+ if i>=0:
+ d = self.data[i]
+ del self.data[i]
+ return d
+ else:
+ if error:
+ raise Exception('Key `{}` not found in file:{}'.format(key, self.filename))
+ else:
+ print('[WARN] Key `{}` not found in file:{}'.format(key, self.filename))
+ return None
+
+ def insertComment(self, i, comment='', error=False):
+ d = getDict()
+ d['value'] = comment
+ d['label'] = ''
+ d['descr'] = ''
+ d['isComment'] = True
+ try:
+ self.data.insert(i, d)
+ except:
+ import pdb; pdb.set_trace()
+
+ def insertKeyVal(self, i, key, value, description='', error=False):
+ d = getDict()
+ d['value'] = value
+ d['label'] = key
+ d['descr'] = description
+ d['isComment'] = False
+ self.data.insert(i, d)
+
+ def insertKeyValAfter(self, key_prev, key, value, description, error=False):
+ i = self.fixedfile.getIDSafe(key_prev)
+ if i<0:
+ if error:
+ raise Exception('Key `{}` not found in file:{}'.format(key_prev, self.filename))
+ else:
+ print('[WARN] Key `{}` not found in file:{}'.format(key_prev, self.filename))
+ self.insertKeyVal(i+1, key, value, description, error=error)
+
+
+
+
# --------------------------------------------------------------------------------}
# --- BASE INPUT FILE
@@ -169,14 +228,20 @@ class FASTInputFileBase(File):
f.write('AeroDyn_Changed.dat')
"""
+ @staticmethod
+ def defaultExtensions():
+ return ['.dat','.fst','.txt','.fstf','.dvr']
- def __init__(self, filename=None, **kwargs):
+ @staticmethod
+ def formatName():
+ return 'FAST input file Base'
+
+ def __init__(self, filename=None, IComment=None, **kwargs):
self._size=None
- self._encoding=None
self.setData() # Init data
if filename:
self.filename = filename
- self.read()
+ self.read(IComment=IComment)
def setData(self, filename=None, data=None, hasNodal=False, module=None):
""" Set the data of this object. This object shouldn't store anything else. """
@@ -198,7 +263,6 @@ def getID(self,label):
raise KeyError('Variable `'+ label+'` not found in FAST file:'+self.filename)
else:
return i
-
def getIDs(self,label):
I=[]
# brute force search
@@ -233,17 +297,28 @@ def __next__(self): # Python 2: def next(self)
return self.data[self.iCurrent]
# Making it behave like a dictionary
- def __setitem__(self,key,item):
+ 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):
+ 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)
+ s='<{} object - Base> with attributes:\n'.format(type(self).__name__)
+ s =' - filename: {}\n'.format(self.filename)
+ s =' - dict keys/values: \n'
return s+'\n'.join(['{:15s}: {}'.format(d['label'],d['value']) for i,d in enumerate(self.data)])
def addKeyVal(self, key, val, descr=None):
@@ -287,14 +362,17 @@ 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 [] # Typical OpenFAST files have comment on second line [1]
+ return [1] # Typical OpenFAST files have comment on second line [1]
- def read(self, filename=None):
+ def read(self, filename=None, IComment=None):
if filename:
self.filename = filename
if self.filename:
@@ -302,35 +380,49 @@ def read(self, filename=None):
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()
+ self._read(IComment=IComment)
else:
raise Exception('No filename provided')
- def _read(self):
+ def _read(self, IComment=None):
+ if IComment is None:
+ IComment=[]
# --- 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' , 'WndSpeed' , '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' , 'NumCases' , 'NHvCoef' , 'NAxCoef' , 'NJoints' , 'NCoefDpth' , 'NFillGroups' , 'NMGDepths' , 1 , 'BldNodes' , 'kp_total' , 1 , 'NTwrHt' , 'NTwrRe' , 'NumTurbines']
- NUMTAB_FROM_VAL_VARNAME = ['TowProp' , 'TowProp' , 'BldProp' , 'DLLProp' , 'BldAeroNodes' , 'Cases' , '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 , 2 , 1 , 2 , 2 , 1 , 1 , 2 ]
- NUMTAB_FROM_VAL_TYPE = ['num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'num' , 'mix' , 'num' , 'num' , 'num' , 'num' , 'mix']
+ 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' ,'Diam' ,'Type' ,'LineType' ]
- NUMTAB_FROM_LAB_DIM_VAR = ['NumAlf' , 'NKInpSt' , 'NCoefMembers' , 'NMembers' , 'NMOutputs' , 'NMOutputs' , 'NPropSets' ,'NTypes' ,'NConnects' ,'NLines' ]
- NUMTAB_FROM_LAB_VARNAME = ['AFCoeff' , 'TMDspProp' , 'MemberProp' , 'Members' , 'MemberOuts' , 'MemberOuts' , 'SectionProp' ,'LineTypes' ,'ConnectionProp' ,'LineProp' ]
- NUMTAB_FROM_LAB_NHEADER = [2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 ]
- NUMTAB_FROM_LAB_NOFFSET = [0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ]
- NUMTAB_FROM_LAB_TYPE = ['num' , 'num' , 'num' , 'mix' , 'num' , 'num' , 'num' ,'mix' ,'mix' ,'mix' ]
+ 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']
@@ -385,6 +477,11 @@ def _read(self):
labOffset=''
while i0 \
@@ -400,24 +497,24 @@ def _read(self):
# 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:
+ 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['tabType'] = TABTYPE_OUTLIST # TODO
d['value'] = ['']+OutList
self.data.append(d)
if i>=len(lines):
+ self.addComment('END of input file (the word "END" must appear in the first 3 columns of this last OutList line)')
+ self.addComment('---------------------------------------------------------------------------------------')
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
+ pass
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))
@@ -430,8 +527,8 @@ def _read(self):
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))
+ self.addComment('END of input file (the word "END" must appear in the first 3 columns of this last OutList line)')
+ self.addComment('---------------------------------------------------------------------------------------')
break
elif line.upper().find('ADDITIONAL STIFFNESS')>0:
# TODO, lazy implementation so far, MAKE SUB FUNCTION
@@ -453,6 +550,34 @@ def _read(self):
i+=1;
self.readBeamDynProps(lines,i)
return
+ elif line.upper().find('GLBDCM')>0:
+ # BeamDyn DCM has no label.....
+ self.addComment(lines[i]); i+=1
+ self.addComment(lines[i]); i+=1
+ d = getDict()
+ d['label'] = 'GlbDCM'
+ d['tabType'] = TABTYPE_NUM_NO_HEADER
+ nTabLines = 3
+ nHeaders = 0
+ d['value'], d['tabColumnNames'], d['tabUnits'] = parseFASTNumTable(self.filename, lines[i:i+nTabLines],nTabLines, i, nHeaders, tableType='num', nOffset=0)
+ i=i+3
+ self.data.append(d)
+ continue
+
+ #---The following 3 by 3 matrix is the direction cosine matirx ,GlbDCM(3,3),
+ 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_OUTLIST
+ d['value'] = OutList
+ self.addComment('------------------------ OUTPUTS --------------------------------------------')
+ self.data.append(d)
+ self.addComment('END of input file (the word "END" must appear in the first 3 columns of this last OutList line)')
+ self.addComment('---------------------------------------------------------------------------------------')
+ return
# --- Parsing of standard lines: value(s) key comment
line = lines[i]
@@ -502,16 +627,22 @@ def _read(self):
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['descr'] = '' #
d['tabUnits'] = ['(-)','(-)']
self.data.append(d)
break
elif labelRaw=='re':
- nAirfoilTab = self['NumTabs']
- iTab +=1
- if nAirfoilTab>1:
- labOffset ='_'+str(iTab)
- d['label']=labelRaw+labOffset
+ 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']));
@@ -538,6 +669,7 @@ def _read(self):
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'])
+ _, d['descr'] = splitAfterChar(lines[i], '!')
i += nTabLines+nHeaders-1
# --- Temporary hack for e.g. SubDyn, that has duplicate table, impossible to detect in the current way...
@@ -569,22 +701,42 @@ def _read(self):
self.data.append(dd)
d['label'] = NUMTAB_FROM_LAB_VARNAME[ii]
- d['tabDimVar'] = NUMTAB_FROM_LAB_DIM_VAR[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
- if isinstance(d['tabDimVar'],int):
+ # 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:
- nTabLines = self[d['tabDimVar']+labOffset]
+ # 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'])
+ d['descr'] = '' #
i += nTabLines+1-nOffset
# --- Temporary hack for e.g. SubDyn, that has duplicate table, impossible to detect in the current way...
@@ -656,11 +808,22 @@ def toString(self):
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().strip('-').strip()+'\n'
+ # Trying to reproduce WISDEM format
+ if len(val)<22:
+ val='{:22s}'.format(val)
+ if len(lab)<11:
+ lab='{:11s}'.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'):
@@ -676,45 +839,64 @@ def mat_tostring(M,fmt='24.16e'):
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):
+ if isinstance(d['value'], list) or isinstance(d['value'],np.ndarray):
sList=', '.join([str(x) for x in d['value']])
- s+='{} {} {}'.format(sList,d['label'],d['descr'])
+ s+=toStringVLD(sList, d['label'], d['descr'])
else:
- s+=toStringVLD(d['value'],d['label'],d['descr']).strip()
+ s+=toStringVLD(d['value'],d['label'],d['descr'])
elif d['tabType']==TABTYPE_NUM_WITH_HEADER:
+ PrettyCols= d['label']!='TowProp' # Temporary hack for AeroDyn tower table
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 PrettyCols:
+ s+='{}'.format(' '.join(['{:^15s}'.format(s) for s in d['tabColumnNames']]))
+ else:
+ s+='{}'.format(' '.join(['{:14s}'.format(s) for s in d['tabColumnNames']]))
+ if len(d['descr'])>0 and d['descr'][0]=='!':
+ s+=d['descr']
if d['tabUnits'] is not None:
s+='\n'
- s+='{}'.format(' '.join(['{:15s}'.format(s) for s in d['tabUnits']]))
+ if PrettyCols:
+ s+='{}'.format(' '.join(['{:^15s}'.format(s) for s in d['tabUnits']]))
+ else:
+ s+='{}'.format(' '.join(['{:14s}'.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'])
+ if PrettyCols:
+ 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'])
+ else:
+ s+='\n'.join(' '.join( ('{:13.7E}'.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']]))
+ 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('{}'.format(x) for x in y) for y in d['value'])
+ 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'.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']))
- s+='{} {} {}\n'.format(d['value'][0],d['label'],d['descr']) # TODO?
+ 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_OUTLIST:
+ label = d['label']
+ s+='{:22s} {:11s} - {}\n'.format('', label, d['descr'])
s+='\n'.join(fil for fil in d['value'][1:])
elif d['tabType']==TABTYPE_NUM_BEAMDYN:
# TODO use dedicated sub-class
@@ -727,6 +909,16 @@ def mat_tostring(M,fmt='24.16e'):
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)
+ elif d['tabType']==TABTYPE_NUM_NO_HEADER:
+ data = d['value']
+ 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'])
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')
@@ -771,32 +968,25 @@ def _toDataFrame(self):
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)']
+ 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))
- Cols = Cols + ['ShapeForeAft1_[-]','ShapeForeAft2_[-]','ShapeSideSide1_[-]','ShapeSideSide2_[-]']
+ ShapeCols = [c+'_[-]' for c in ['ShapeForeAft1','ShapeForeAft2','ShapeSideSide1','ShapeSideSide2']]
+ Cols = Cols + ShapeCols
name=d['label']
+ if 'AFCoeff' in name:
+ if '_' in name:
+ i = int(name.split('_')[1])
+ Re = self['Re_'+str(i)]
+ else:
+ Re = self['Re']
+ name = 'AFCoeff_Re{:.2f}'.format(Re)
if name=='DampingCoeffs':
pass
@@ -826,9 +1016,9 @@ def _toDataFrame(self):
dfs=dfs[list(dfs.keys())[0]]
return dfs
- def toGraph(self):
+ def toGraph(self, **kwargs):
from .fast_input_file_graph import fastToGraph
- return fastToGraph(self)
+ return fastToGraph(self, **kwargs)
@@ -913,7 +1103,6 @@ def readBeamDynProps(self,lines,iStart):
i+=7
except:
raise WrongFormatError('An error occured while reading section {}/{}'.format(j+1,nStations))
-
d = getDict()
d['label'] = 'BeamProperties'
d['descr'] = ''
@@ -925,26 +1114,6 @@ def readBeamDynProps(self,lines,iStart):
# --- Helper functions
# --------------------------------------------------------------------------------{
def isStr(s):
- # Python 2 and 3 compatible
- # Two options below
- # NOTE: all this avoided since we import str from builtins
- # --- Version 2
- # isString = False;
- # if(isinstance(s, str)):
- # isString = True;
- # try:
- # if(isinstance(s, basestring)): # todo unicode as well
- # isString = True;
- # except NameError:
- # pass;
- # return isString
- # --- Version 1
- # try:
- # basestring # python 2
- # return isinstance(s, basestring) or isinstance(s,unicode)
- # except NameError:
- # basestring=str #python 3
- # return isinstance(s, str)
return isinstance(s, str)
def strIsFloat(s):
@@ -980,12 +1149,18 @@ def cleanLine(l):
return l
def cleanAfterChar(l,c):
- # remove whats after a character
+ # remove what's after a character
n = l.find(c);
if n>0:
return l[:n]
else:
return l
+def splitAfterChar(l, c):
+ n = l.find(c);
+ if n>0:
+ return l[:n], l[n:]
+ else:
+ return l, ''
def getDict():
return {'value':None, 'label':'', 'isComment':False, 'descr':'', 'tabType':TABTYPE_NOT_A_TAB}
@@ -1003,6 +1178,12 @@ def _merge_value(splits):
def parseFASTInputLine(line_raw,i,allowSpaceSeparatedList=False):
d = getDict()
+ line_low = line_raw.lower()
+ if line_low=='end' or line_low.startswith('end of') or line_low.startswith('end ('):
+ d['isComment'] = True
+ d['value'] = line_raw
+ d['label'] = 'END'
+ return d
#print(line_raw)
try:
# preliminary cleaning (Note: loss of formatting)
@@ -1108,17 +1289,14 @@ def parseFASTInputLine(line_raw,i,allowSpaceSeparatedList=False):
def parseFASTOutList(lines,iStart):
OutList=[]
i = iStart
- MAX=200
- while iMAX :
- raise Exception('More that 200 lines found in outlist')
if i>=len(lines):
print('[WARN] End of file reached while reading Outlist')
#i=min(i+1,len(lines))
- return OutList,iStart+len(OutList)
+ return OutList, iStart+len(OutList)
def extractWithinParenthesis(s):
@@ -1150,6 +1328,17 @@ def detectUnits(s,nRef):
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
@@ -1160,6 +1349,7 @@ def parseFASTNumTable(filename,lines,n,iStart,nHeaders=2,tableType='num',nOffset
Units = None
+ i = 0
if len(lines)!=n+nHeaders+nOffset:
raise BrokenFormatError('Not enough lines in table: {} lines instead of {}\nFile:{}'.format(len(lines)-nHeaders,n,filename))
try:
@@ -1257,6 +1447,13 @@ def parseFASTNumTable(filename,lines,n,iStart,nHeaders=2,tableType='num',nOffset
# 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')
@@ -1297,8 +1494,101 @@ def parseFASTFilTable(lines,n,iStart):
# --------------------------------------------------------------------------------{
# --------------------------------------------------------------------------------{
+
# --------------------------------------------------------------------------------}
-# --- AeroDyn Blade
+# --- 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
@@ -1332,11 +1622,11 @@ def __init__(self, filename=None, **kwargs):
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( 1.0 , 'BldFl2Sh(2)', 'Flap mode 2, coeff of x^2')
+ 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( 0.0 , 'BldFl2Sh(4)', ' , coeff of x^4')
- self.addValKey( 0.0 , 'BldFl2Sh(5)', ' , coeff of x^5')
- self.addValKey( 0.0 , 'BldFl2Sh(6)', ' , coeff of x^6')
+ 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')
@@ -1344,7 +1634,7 @@ def __init__(self, filename=None, **kwargs):
self.addValKey( 0.0 , 'BldEdgSh(6)', ' , coeff of x^6')
else:
# fix some stuff that generic reader fail at
- self.data[1] = self._lines[1]
+ self.data[1] = {'value':self._lines[1], 'label':'', 'isComment':True, 'descr':'', 'tabType':0}
self.module='EDBlade'
def _writeSanityChecks(self):
@@ -1361,27 +1651,101 @@ def _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)']
+ 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
@@ -1411,7 +1775,7 @@ def _writeSanityChecks(self):
# TODO double check this calculation with gradient
dr = np.gradient(aeroNodes[:,0])
dx = np.gradient(aeroNodes[:,1])
- crvAng = np.degrees(np.arctan2(dx,dr))*np.pi/180
+ 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')
@@ -1470,16 +1834,62 @@ def _toDataFrame(self):
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]
+ def resample(self, n=10, r=None):
+ def multiInterp(x, xp, fp, extrap='bounded'):
+ """ See welib.tools.signal_analysis """
+ x = np.asarray(x)
+ xp = np.asarray(xp)
+ j = np.searchsorted(xp, x, 'left') - 1
+ dd = np.zeros(len(x)) #*np.nan
+ bOK = np.logical_and(j>=0, j< len(xp)-1)
+ jOK = j[bOK]
+ dd[bOK] = (x[bOK] - xp[jOK]) / (xp[jOK + 1] - xp[jOK])
+ jBef=j
+ jAft=j+1
+ bLower =j<0
+ bUpper =j>=len(xp)-1
+ jAft[bUpper] = len(xp)-1
+ jBef[bUpper] = len(xp)-1
+ jAft[bLower] = 0
+ jBef[bLower] = 0
+ return (1 - dd) * fp[:,jBef] + fp[:,jAft] * dd
+ # current data
+ M_old= self['BldAeroNodes']
+ r_old = M_old[:,0]
+ if r is None:
+ r_new = np.linspace(r_old[0], r_old[-1], n)
+ else:
+ r_new = r
+ M_new = multiInterp(r_new, r_old, M_old.T).T
+ # Handling precision, but keeping first and last value
+ M_new = np.around(M_new, 5)
+ if r_new[0]==r_old[0]:
+ M_new[0,:] = M_old[0,:]
+ if r_new[-1]==r_old[-1]:
+ M_new[-1,:] = M_old[-1,:]
+ M_new[:,6] = np.around(M_new[:,6],0).astype(int)
+ self['BldAeroNodes']=M_new
+ self['NumBlNds']=len(r_new)
+ return self
+
# --------------------------------------------------------------------------------}
# --- AeroDyn Polar
# --------------------------------------------------------------------------------{
class ADPolarFile(FASTInputFileBase):
+ @staticmethod
+ def formatName():
+ return 'FAST AeroDyn polar file'
+
@classmethod
def from_fast_input_file(cls, parent):
self = cls()
@@ -1588,30 +1998,48 @@ def _write(self):
self.data[i]['label'] = labFull
def _toDataFrame(self):
+ # --- We rely on parent class, it has an if statement for AFCoeff already...
dfs = FASTInputFileBase._toDataFrame(self)
if not isinstance(dfs, dict):
dfs={'AFCoeff':dfs}
- for k,df in dfs.items():
+ # --- Adding more columns
+ for i,(k,df) in enumerate(dfs.items()):
sp = k.split('_')
- if len(sp)==2:
- labOffset='_'+sp[1]
+ if len(dfs)>1:
+ labOffset='_'+str(i+1)
else:
labOffset=''
+ #if len(sp)==2:
+ # labOffset='_'+sp[1]
+ #else:
+ # labOffset=''
alpha = df['Alpha_[deg]'].values*np.pi/180.
Cl = df['Cl_[-]'].values
Cd = df['Cd_[-]'].values
- Cd0 = self['Cd0'+labOffset]
- # Cn (with or without Cd0)
- Cn1 = Cl*np.cos(alpha)+ (Cd-Cd0)*np.sin(alpha)
+
+ # 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
- df['Cn_Cd0off_[-]'] = Cn1
- CnLin = self['C_nalpha'+labOffset]*(alpha-self['alpha0'+labOffset]*np.pi/180.)
- CnLin[alpha<-20*np.pi/180]=np.nan
- CnLin[alpha> 30*np.pi/180]=np.nan
- df['Cn_pot_[-]'] = CnLin
+ # 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
@@ -1626,13 +2054,19 @@ def _toDataFrame(self):
except:
pass
+ # Cnf
+ try:
+ df['Cn_f_[-]'] = CnLin_ * ((1 + np.sqrt(0.7)) / 2) ** 2
+ except:
+ pass
+
if len(dfs)==1:
dfs=dfs[list(dfs.keys())[0]]
return dfs
@property
def comment(self):
- return FASTInputFileBase.comment
+ return '\n'.join([self.data[i]['value'] for i in self._IComment])
@comment.setter
def comment(self, comment):
@@ -1651,6 +2085,81 @@ def _IComment(self):
return I
+ # --- Helper functions
+ @property
+ def reynolds(self):
+ return self.getAll('re')
+
+ def getPolar(self, i):
+ if i not in range(0, self['NumTabs']):
+ raise IndexError('Index {} for Polar should be between 0 and {}'.format(i, self['NumTabs']-1))
+ if self['NumTabs']==1:
+ return self[f'AFCoeff'].copy()
+ else:
+ return self[f'AFCoeff_{i+1}'].copy()
+
+ def setPolar(self, i, M):
+ if i not in range(0, self['NumTabs']):
+ raise IndexError('Index {} for Polar should be between 0 and {}'.format(i, self['NumTabs']-1))
+ if self['NumTabs']==1:
+ M_old = self[f'AFCoeff']
+ else:
+ M_old = self[f'AFCoeff_{i+1}']
+ if M_old.shape[1] != M.shape[1]:
+ # Actually, does it?
+ raise Exception('Number of columns must match previous data when setting a polar')
+ #self[f'AFCoeff_{i+1}']=M
+ if self['NumTabs']==1:
+ self[f'AFCoeff']=M
+ else:
+ ID = self.getID(f'AFCoeff_{i+1}')
+ self[f'NumAlf_{i+1}']=M.shape[0]
+ self.data[ID]['value']=M
+
+
+ def getAll(self, key):
+ """
+ Examples:
+ pol.getAll('re')
+ """
+ if self['NumTabs']==1:
+ return np.array([self[key]])
+ else:
+ return np.array([self[key + f'_{i}'] for i in range(1, self['NumTabs']+1)])
+
+ def setAll(self, key, value=None, offset=None):
+ """
+ Examples:
+ pol.setAll('T_f0', 6 )
+ pol.setAll('alpha1', offset=+2 )
+ pol.setAll('alpha2', offset=-2 )
+ """
+ if self['NumTabs']==1:
+ if value is not None:
+ self[f'{key}'] = value
+ if offset is not None:
+ self[f'{key}'] += offset
+ else:
+ for i in range(1, self['NumTabs']+1):
+ ID = self.getID(f'{key}_{i}')
+ if value is not None:
+ self.data[ID]['value'] = value
+ if offset is not None:
+ self.data[ID]['value']+= offset
+
+ def calcUnsteadyParams(self):
+ from welib.airfoils.Polar import Polar
+ for i in range(1, self['NumTabs']+1):
+ offset=f'_{i}' if self['NumTabs']>1 else ''
+ M = self['AFCoeff'+offset]
+ pol = Polar(alpha=M[:,0], cl=M[:,1], cd=M[:,2], cm=M[:,3], radians=False, name=os.path.basename(self.filename)+f'_Table{i}')
+ d = pol.unsteadyParams(dictOut=True)
+ for k,v in d.items():
+ self[k+offset] = v
+
+
+
+
# --------------------------------------------------------------------------------}
# --- ExtPtfm
# --------------------------------------------------------------------------------{
@@ -1748,7 +2257,7 @@ def __init__(self, filename=None, **kwargs):
self.module='ExtPtfm'
- def _read(self):
+ def _read(self, IComment=None):
with open(self.filename, 'r', errors="surrogateescape") as f:
lines=f.read().splitlines()
detectAndReadExtPtfmSE(self, lines)
@@ -1817,7 +2326,8 @@ def _toDataFrame(self):
if __name__ == "__main__":
- f = FASTInputFile()
+ f = FASTInputFile('tests/example_files/FASTIn_HD_SeaState.dat')
+ print(f)
pass
#B=FASTIn('Turbine.outb')
diff --git a/weio/fast_input_file_graph.py b/weio/fast_input_file_graph.py
index eccc069..593a4d0 100644
--- a/weio/fast_input_file_graph.py
+++ b/weio/fast_input_file_graph.py
@@ -3,33 +3,34 @@
import matplotlib.pyplot as plt
# Local
-try:
- from .tools.graph import *
-except ImportError:
- from welib.FEM.graph import *
+from weio.tools.graph import *
+
# --------------------------------------------------------------------------------}
# --- Wrapper to convert a "fast" input file dictionary into a graph
# --------------------------------------------------------------------------------{
-def fastToGraph(data):
+def fastToGraph(data, **kwargs):
if 'BeamProp' in data.keys():
- return subdynToGraph(data)
+ return subdynToGraph(data, **kwargs)
if 'SmplProp' in data.keys():
- return hydrodynToGraph(data)
+ return hydrodynToGraph(data, **kwargs)
if 'DOF2Nodes' in data.keys():
- return subdynSumToGraph(data)
+ return subdynSumToGraph(data, **kwargs)
raise NotImplementedError('Graph for object with keys: {}'.format(data.keys()))
# --------------------------------------------------------------------------------}
# --- SubDyn
# --------------------------------------------------------------------------------{
-def subdynToGraph(sd):
+def subdynToGraph(sd, propToNodes=False, propToElem=False):
"""
sd: dict-like object as returned by weio
+
+ -propToNodes: if True, the element properties are also transferred to the nodes for convenience.
+ NOTE: this is not the default because a same node can have two different diameters in SubDyn (it's by element)
"""
type2Color=[
(0.1,0.1,0.1), # Watchout based on background
@@ -81,7 +82,11 @@ def subdynToGraph(sd):
elem.data['color'] = type2Color[Type]
Graph.addElement(elem)
# Nodal prop data
- #Graph.setElementNodalProp(elem, propset=PropSets[Type-1], propIDs=E[3:5])
+ if propToNodes:
+ # NOTE: this is disallowed by default because a same node can have two different diameters in SubDyn (it's by element)
+ Graph.setElementNodalProp(elem, propset=PropSets[Type-1], propIDs=E[3:5])
+ if propToElem:
+ Graph.setElementNodalPropToElem(elem) # TODO, this shouldn't be needed
# --- Concentrated Masses (in global coordinates), node data
for iC, CM in enumerate(sd['ConcentratedMasses']):
@@ -127,9 +132,14 @@ def subdynToGraph(sd):
# --------------------------------------------------------------------------------}
# --- HydroDyn
# --------------------------------------------------------------------------------{
-def hydrodynToGraph(hd):
+def hydrodynToGraph(hd, propToNodes=False, propToElem=False, verbose=False):
"""
hd: dict-like object as returned by weio
+
+ -propToNodes: if True, the element properties are also transferred to the nodes for convenience.
+ NOTE: this is not the default because a same node can have two different diameters in SubDyn (it's by element)
+
+ - propToElem: This might be due to misunderstanding of graph..
"""
def type2Color(Pot):
if Pot:
@@ -177,17 +187,19 @@ def type2Color(Pot):
Graph.addMiscPropertySet('MemberCoefs')
for ip,P in enumerate(hd['MemberProp']):
# MemberID MemberCd1 MemberCd2 MemberCdMG1 MemberCdMG2 MemberCa1 MemberCa2 MemberCaMG1 MemberCaMG2 MemberCp1 MemberCp2 MemberCpMG1 MemberCpMG2 MemberAxCd1 MemberAxCd2 MemberAxCdMG1 MemberAxCdMG2 MemberAxCa1 MemberAxCa2 MemberAxCaMG1 MemberAxCaMG2 MemberAxCp1 MemberAxCp2 MemberAxCpMG1 MemberAxCpMG2
- prop = Property(ID=ip+1, MemberID=P[0], Cd1=P[1], Cd2=P[2], CdMG1=P[3], CdMG2=P[4], Ca1=P[5], Ca2=P[6], CaMG1=P[7], CaMG2=P[8], Cp1=P[9], Cp2=P[10], CpMG1=P[11], CpMG2=P[12], AxCd1=P[14], AxCd2=P[15], axCdMG1=P[16], axCdMG2=P[17], AxCa1=P[18], AxCa2=P[19], AxCaMG1=P[20], AxCaMG2=P[21], AxCp1=P[22], AxCp2=P[23])
+ prop = ElemProperty(ID=ip+1, MemberID=P[0], Cd1=P[1], Cd2=P[2], CdMG1=P[3], CdMG2=P[4], Ca1=P[5], Ca2=P[6], CaMG1=P[7], CaMG2=P[8], Cp1=P[9], Cp2=P[10], CpMG1=P[11], CpMG2=P[12], AxCd1=P[14], AxCd2=P[15], axCdMG1=P[16], axCdMG2=P[17], AxCa1=P[18], AxCa2=P[19], AxCaMG1=P[20], AxCaMG2=P[21], AxCp1=P[22], AxCp2=P[23])
Graph.addMiscProperty('MemberCoefs',prop)
# ---
if 'FillGroups' in hd.keys():
# Filled members
Graph.addMiscPropertySet('FillGroups')
- for ip,P in enumerate(hd['FillGroups']):
- # FillNumM FillMList FillFSLoc FillDens
- raise NotImplementedError('hydroDynToGraph, Fill List might not be properly set, verify below')
- prop = MiscProperty(ID=ip+1, FillNumM=P[0], FillMList=P[1], FillFSLoc=P[2], FillDens=P[3])
- Graph.addMiscProperty('FillGroups',prop)
+ if verbose:
+ print('>>> TODO Filled Groups')
+ #for ip,P in enumerate(hd['FillGroups']):
+ # # FillNumM FillMList FillFSLoc FillDens
+ # raise NotImplementedError('hydroDynToGraph, Fill List might not be properly set, verify below')
+ # prop = MiscProperty(ID=ip+1, FillNumM=P[0], FillMList=P[1], FillFSLoc=P[2], FillDens=P[3])
+ # Graph.addMiscProperty('FillGroups',prop)
if 'MGProp' in hd.keys():
# Marine Growth
@@ -218,12 +230,19 @@ def type2Color(Pot):
elem.data['color'] = type2Color(Pot)
Graph.addElement(elem)
# Nodal prop data NOTE: can't do that anymore for memebrs with different diameters at the same node
- #Graph.setElementNodalProp(elem, propset='Section', propIDs=EE[3:5])
+ if propToNodes:
+ # NOTE: not by default because of feature with members with different diameters at the same node
+ Graph.setElementNodalProp(elem, propset='Section', propIDs=EE[3:5])
+ if propToElem:
+ Graph.setElementNodalPropToElem(elem) # TODO, this shouldn't be needed
+
if Type==1:
# Simple
Graph.setElementNodalProp(elem, propset='SimpleCoefs', propIDs=[1,1])
else:
print('>>> TODO type DepthCoefs and MemberCoefs')
+ # NOTE: this is disallowed by default because a same node can have two different diameters in SubDyn (it's by element)
+ #Graph.setElementNodalProp(elem, propset=PropSets[Type-1], propIDs=E[3:5])
return Graph
@@ -231,7 +250,7 @@ def type2Color(Pot):
# --------------------------------------------------------------------------------}
# --- SubDyn Summary file
# --------------------------------------------------------------------------------{
-def subdynSumToGraph(data):
+def subdynSumToGraph(data, Graph=None):
"""
data: dict-like object as returned by weio
"""
@@ -247,7 +266,8 @@ def subdynSumToGraph(data):
DOF2Nodes = data['DOF2Nodes']
nDOF = data['nDOF_red']
- Graph = GraphModel()
+ if Graph is None:
+ Graph = GraphModel()
# --- Nodes and DOFs
Nodes = data['Nodes']
diff --git a/weio/fast_linearization_file.py b/weio/fast_linearization_file.py
index 374d432..8477d62 100644
--- a/weio/fast_linearization_file.py
+++ b/weio/fast_linearization_file.py
@@ -1,354 +1,615 @@
-from __future__ import division
-from __future__ import print_function
-from __future__ import absolute_import
-from io import open
-from .file import File, isBinary, WrongFormatError, BrokenFormatError
-import pandas as pd
-import numpy as np
-import re
-
-class FASTLinearizationFile(File):
- """
- Read/write an OpenFAST linearization file. The object behaves like a dictionary.
-
- Main keys
- ---------
- - 'x', 'xdot' 'u', 'y', 'A', 'B', 'C', 'D'
-
- Main methods
- ------------
- - read, write, toDataFrame, keys, xdescr, ydescr, udescr
-
- Examples
- --------
-
- f = FASTLinearizationFile('5MW.1.lin')
- print(f.keys())
- print(f['u']) # input operating point
- print(f.udescr()) # description of inputs
-
- # use a dataframe with "named" columns and rows
- df = f.toDataFrame()
- print(df['A'].columns)
- print(df['A'])
-
- """
- @staticmethod
- def defaultExtensions():
- return ['.lin']
-
- @staticmethod
- def formatName():
- return 'FAST linearization output'
-
- def _read(self, *args, **kwargs):
- self['header']=[]
-
- def extractVal(lines, key):
- for l in lines:
- if l.find(key)>=0:
- return l.split(key)[1].split()[0]
- return None
-
- def readToMarker(fid, marker, nMax):
- lines=[]
- for i, line in enumerate(fid):
- if i>nMax:
- raise BrokenFormatError('`{}` not found in file'.format(marker))
- if line.find(marker)>=0:
- break
- lines.append(line.strip())
- return lines, line
-
- def readOP(fid, n):
- OP=[]
- Var = {'RotatingFrame': [], 'DerivativeOrder': [], 'Description': []}
- colNames=fid.readline().strip()
- dummy= fid.readline().strip()
- bHasDeriv= colNames.find('Derivative Order')>=0
- for i, line in enumerate(fid):
- sp=line.strip().split()
- if sp[1].find(',')>=0:
- # Most likely this OP has three values (e.g. orientation angles)
- # For now we discard the two other values
- OP.append(float(sp[1][:-1]))
- iRot=4
- else:
- OP.append(float(sp[1]))
- iRot=2
- Var['RotatingFrame'].append(sp[iRot])
- if bHasDeriv:
- Var['DerivativeOrder'].append(int(sp[iRot+1]))
- Var['Description'].append(' '.join(sp[iRot+2:]).strip())
- else:
- Var['DerivativeOrder'].append(-1)
- Var['Description'].append(' '.join(sp[iRot+1:]).strip())
- if i>=n-1:
- break
- return OP, Var
-
- def readMat(fid, n, m):
- vals=[f.readline().strip().split() for i in np.arange(n)]
-# try:
- return np.array(vals).astype(float)
-# except ValueError:
-# import pdb; pdb.set_trace()
-
- # Reading
- with open(self.filename, 'r', errors="surrogateescape") as f:
- # --- Reader header
- self['header'], lastLine=readToMarker(f, 'Jacobians included', 30)
- self['header'].append(lastLine)
- nx = int(extractVal(self['header'],'Number of continuous states:'))
- nxd = int(extractVal(self['header'],'Number of discrete states:' ))
- nz = int(extractVal(self['header'],'Number of constraint states:'))
- nu = int(extractVal(self['header'],'Number of inputs:' ))
- ny = int(extractVal(self['header'],'Number of outputs:' ))
- bJac = extractVal(self['header'],'Jacobians included in this file?')
- try:
- self['Azimuth'] = float(extractVal(self['header'],'Azimuth:'))
- except:
- self['Azimuth'] = None
- try:
- self['RotSpeed'] = float(extractVal(self['header'],'Rotor Speed:')) # rad/s
- except:
- self['RotSpeed'] = None
- try:
- self['WindSpeed'] = float(extractVal(self['header'],'Wind Speed:'))
- except:
- self['WindSpeed'] = None
-
- KEYS=['Order of','A:','B:','C:','D:','ED M:', 'dUdu','dUdy']
-
- for i, line in enumerate(f):
- line = line.strip()
- KeyFound=any([line.find(k)>=0 for k in KEYS])
- if KeyFound:
- if line.find('Order of continuous states:')>=0:
- self['x'], self['x_info'] = readOP(f, nx)
- elif line.find('Order of continuous state derivatives:')>=0:
- self['xdot'], self['xdot_info'] = readOP(f, nx)
- elif line.find('Order of inputs')>=0:
- self['u'], self['u_info'] = readOP(f, nu)
- elif line.find('Order of outputs')>=0:
- self['y'], self['y_info'] = readOP(f, ny)
- elif line.find('A:')>=0:
- self['A'] = readMat(f, nx, nx)
- elif line.find('B:')>=0:
- self['B'] = readMat(f, nx, nu)
- elif line.find('C:')>=0:
- self['C'] = readMat(f, ny, nx)
- elif line.find('D:')>=0:
- self['D'] = readMat(f, ny, nu)
- elif line.find('dUdu:')>=0:
- self['dUdu'] = readMat(f, nu, nu)
- elif line.find('dUdy:')>=0:
- self['dUdy'] = readMat(f, nu, ny)
- elif line.find('ED M:')>=0:
- self['EDDOF'] = line[5:].split()
- self['M'] = readMat(f, 24, 24)
-
- def toString(self):
- s=''
- return s
-
- def _write(self):
- with open(self.filename,'w') as f:
- f.write(self.toString())
-
- def short_descr(self,slist):
- def shortname(s):
- s=s.strip()
- s = s.replace('(m/s)' , '_[m/s]' );
- s = s.replace('(kW)' , '_[kW]' );
- s = s.replace('(deg)' , '_[deg]' );
- s = s.replace('(N)' , '_[N]' );
- s = s.replace('(kN-m)' , '_[kNm]' );
- s = s.replace('(N-m)' , '_[Nm]' );
- s = s.replace('(kN)' , '_[kN]' );
- s = s.replace('(rpm)' , '_[rpm]' );
- s = s.replace('(rad)' , '_[rad]' );
- s = s.replace('(rad/s)' , '_[rad/s]' );
- s = s.replace('(rad/s^2)', '_[rad/s^2]' );
- s = s.replace('(m/s^2)' , '_[m/s^2]');
- s = s.replace('(deg/s^2)','_[deg/s^2]');
- s = s.replace('(m)' , '_[m]' );
- s = s.replace(', m/s/s','_[m/s^2]');
- s = s.replace(', m/s^2','_[m/s^2]');
- s = s.replace(', m/s','_[m/s]');
- s = s.replace(', m','_[m]');
- s = s.replace(', rad/s/s','_[rad/s^2]');
- s = s.replace(', rad/s^2','_[rad/s^2]');
- s = s.replace(', rad/s','_[rad/s]');
- s = s.replace(', rad','_[rad]');
- s = s.replace(', -','_[-]');
- s = s.replace(', Nm/m','_[Nm/m]');
- s = s.replace(', Nm','_[Nm]');
- s = s.replace(', N/m','_[N/m]');
- s = s.replace(', N','_[N]');
- s = s.replace('(1)','1')
- s = s.replace('(2)','2')
- s = s.replace('(3)','3')
- s= re.sub(r'\([^)]*\)','', s) # remove parenthesis
- s = s.replace('ED ','');
- s = s.replace('BD_','BD_B');
- s = s.replace('IfW ','');
- s = s.replace('Extended input: ','')
- s = s.replace('1st tower ','qt1');
- s = s.replace('2nd tower ','qt2');
- nd = s.count('First time derivative of ')
- if nd>=0:
- s = s.replace('First time derivative of ' ,'');
- if nd==1:
- s = 'd_'+s.strip()
- elif nd==2:
- s = 'dd_'+s.strip()
- s = s.replace('Variable speed generator DOF ','psi_rot'); # NOTE: internally in FAST this is the azimuth of the rotor
- s = s.replace('fore-aft bending mode DOF ' ,'FA' );
- s = s.replace('side-to-side bending mode DOF','SS' );
- s = s.replace('bending-mode DOF of blade ' ,'' );
- s = s.replace(' rotational-flexibility DOF, rad','-ROT' );
- s = s.replace('rotational displacement in ','rot' );
- s = s.replace('Drivetrain','DT' );
- s = s.replace('translational displacement in ','trans' );
- s = s.replace('finite element node ','N' );
- s = s.replace('-component position of node ','posN')
- s = s.replace('-component inflow on tower node','TwrN')
- s = s.replace('-component inflow on blade 1, node','Bld1N')
- s = s.replace('-component inflow on blade 2, node','Bld2N')
- s = s.replace('-component inflow on blade 3, node','Bld3N')
- s = s.replace('-component inflow velocity at node','N')
- s = s.replace('X translation displacement, node','TxN')
- s = s.replace('Y translation displacement, node','TyN')
- s = s.replace('Z translation displacement, node','TzN')
- s = s.replace('X translation velocity, node','TVxN')
- s = s.replace('Y translation velocity, node','TVyN')
- s = s.replace('Z translation velocity, node','TVzN')
- s = s.replace('X translation acceleration, node','TAxN')
- s = s.replace('Y translation acceleration, node','TAyN')
- s = s.replace('Z translation acceleration, node','TAzN')
- s = s.replace('X orientation angle, node' ,'RxN')
- s = s.replace('Y orientation angle, node' ,'RyN')
- s = s.replace('Z orientation angle, node' ,'RzN')
- s = s.replace('X rotation velocity, node' ,'RVxN')
- s = s.replace('Y rotation velocity, node' ,'RVyN')
- s = s.replace('Z rotation velocity, node' ,'RVzN')
- s = s.replace('X rotation acceleration, node' ,'RAxN')
- s = s.replace('Y rotation acceleration, node' ,'RAyN')
- s = s.replace('Z rotation acceleration, node' ,'RAzN')
- s = s.replace('X force, node','FxN')
- s = s.replace('Y force, node','FyN')
- s = s.replace('Z force, node','FzN')
- s = s.replace('X moment, node','MxN')
- s = s.replace('Y moment, node','MyN')
- s = s.replace('Z moment, node','MzN')
- s = s.replace('FX', 'Fx')
- s = s.replace('FY', 'Fy')
- s = s.replace('FZ', 'Fz')
- s = s.replace('MX', 'Mx')
- s = s.replace('MY', 'My')
- s = s.replace('MZ', 'Mz')
- s = s.replace('FKX', 'FKx')
- s = s.replace('FKY', 'FKy')
- s = s.replace('FKZ', 'FKz')
- s = s.replace('MKX', 'MKx')
- s = s.replace('MKY', 'MKy')
- s = s.replace('MKZ', 'MKz')
- s = s.replace('Nodes motion','')
- s = s.replace('cosine','cos' );
- s = s.replace('sine','sin' );
- s = s.replace('collective','coll.');
- s = s.replace('Blade','Bld');
- s = s.replace('rotZ','TORS-R');
- s = s.replace('transX','FLAP-D');
- s = s.replace('transY','EDGE-D');
- s = s.replace('rotX','EDGE-R');
- s = s.replace('rotY','FLAP-R');
- s = s.replace('flapwise','FLAP');
- s = s.replace('edgewise','EDGE');
- s = s.replace('horizontal surge translation DOF','Surge');
- s = s.replace('horizontal sway translation DOF','Sway');
- s = s.replace('vertical heave translation DOF','Heave');
- s = s.replace('roll tilt rotation DOF','Roll');
- s = s.replace('pitch tilt rotation DOF','Pitch');
- s = s.replace('yaw rotation DOF','Yaw');
- s = s.replace('vertical power-law shear exponent','alpha')
- s = s.replace('horizontal wind speed ','WS')
- s = s.replace('propagation direction','WD')
- s = s.replace(' pitch command','pitch')
- s = s.replace('HSS_','HSS')
- s = s.replace('Bld','B')
- s = s.replace('tower','Twr')
- s = s.replace('Tower','Twr')
- s = s.replace('Nacelle','Nac')
- s = s.replace('Platform','Ptfm')
- s = s.replace('SrvD','SvD')
- s = s.replace('Generator torque','Qgen')
- s = s.replace('coll. blade-pitch command','PitchColl')
- s = s.replace('wave elevation at platform ref point','WaveElevRefPoint')
- s = s.replace('1)','1');
- s = s.replace('2)','2');
- s = s.replace('3)','3');
- s = s.replace(',','');
- s = s.replace(' ','');
- s=s.strip()
- return s
- return [shortname(s) for s in slist]
-
- def xdescr(self):
- if 'x_info' in self.keys():
- return self.short_descr(self['x_info']['Description'])
- else:
- return []
-
- def xdotdescr(self):
- if 'xdot_info' in self.keys():
- return self.short_descr(self['xdot_info']['Description'])
- else:
- return []
-
- def ydescr(self):
- if 'y_info' in self.keys():
- return self.short_descr(self['y_info']['Description'])
- else:
- return []
- def udescr(self):
- if 'u_info' in self.keys():
- return self.short_descr(self['u_info']['Description'])
- else:
- return []
-
- def _toDataFrame(self):
- dfs={}
-
- xdescr_short = self.xdescr()
- xdotdescr_short = self.xdotdescr()
- ydescr_short = self.ydescr()
- udescr_short = self.udescr()
-
- if 'A' in self.keys():
- dfs['A'] = pd.DataFrame(data = self['A'], index=xdescr_short, columns=xdescr_short)
- if 'B' in self.keys():
- dfs['B'] = pd.DataFrame(data = self['B'], index=xdescr_short, columns=udescr_short)
- if 'C' in self.keys():
- dfs['C'] = pd.DataFrame(data = self['C'], index=ydescr_short, columns=xdescr_short)
- if 'D' in self.keys():
- dfs['D'] = pd.DataFrame(data = self['D'], index=ydescr_short, columns=udescr_short)
- if 'x' in self.keys():
- dfs['x'] = pd.DataFrame(data = np.asarray(self['x']).reshape((1,-1)), columns=xdescr_short)
- if 'xdot' in self.keys():
- dfs['xdot'] = pd.DataFrame(data = np.asarray(self['xdot']).reshape((1,-1)), columns=xdotdescr_short)
- if 'u' in self.keys():
- dfs['u'] = pd.DataFrame(data = np.asarray(self['u']).reshape((1,-1)), columns=udescr_short)
- if 'y' in self.keys():
- dfs['y'] = pd.DataFrame(data = np.asarray(self['y']).reshape((1,-1)), columns=ydescr_short)
- if 'M' in self.keys():
- dfs['M'] = pd.DataFrame(data = self['M'], index=self['EDDOF'], columns=self['EDDOF'])
- if 'dUdu' in self.keys():
- dfs['dUdu'] = pd.DataFrame(data = self['dUdu'], index=udescr_short, columns=udescr_short)
- if 'dUdy' in self.keys():
- dfs['dUdy'] = pd.DataFrame(data = self['dUdy'], index=udescr_short, columns=ydescr_short)
-
- return dfs
-
-
+import os
+import numpy as np
+import re
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File = dict
+ class BrokenFormatError(Exception): pass
+
+class SlowReaderNeededError(Exception):
+ pass
+
+
+_lin_vec = ['x','xd','xdot','u','y','z','header']
+_lin_mat = ['A','B','C','D','dUdu','dUdy', 'StateRotation', 'M']
+_lin_dict = ['x_info','xdot_info','u_info','y_info']
+
+class FASTLinearizationFile(File):
+ """
+ Read/write an OpenFAST linearization file. The object behaves like a dictionary.
+
+ Main keys
+ ---------
+ - 'x', 'xdot', 'xd', 'u', 'y', 'z', 'A', 'B', 'C', 'D'
+
+ Main methods
+ ------------
+ - read, write, toDataFrame, keys, xdescr, ydescr, udescr
+
+ Examples
+ --------
+
+ f = FASTLinearizationFile('5MW.1.lin')
+ print(f.keys())
+ print(f['u']) # input operating point
+ print(f.udescr()) # description of inputs
+
+ # use a dataframe with "named" columns and rows
+ df = f.toDataFrame()
+ print(df['A'].columns)
+ print(df['A'])
+
+ """
+ @staticmethod
+ def defaultExtensions():
+ return ['.lin']
+
+ @staticmethod
+ def formatName():
+ return 'FAST linearization output'
+
+ 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, starSub=None, removeStatesPattern=None):
+ """ Reads the file self.filename, or `filename` if provided
+
+ - starSub: if None, raise an error if `****` are present
+ otherwise replace *** with `starSub` (e.g. 0)
+ - removeStatesPattern: if None, do nothing
+ otherwise search for states matching a pattern and remove them
+ e.g: 'tower|Drivetrain' or '^AD'
+ see removeStates in this file.
+ """
+
+ # --- 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)
+
+ # --- Main Data
+ self['header']=[]
+
+ # --- StarValues replacement `*****` -> inf
+ starPattern = re.compile(r"[\*]+")
+ starSubStr = ' inf '
+ starSubFn = lambda si: starPattern.sub(starSubStr, si)
+
+ # Reading function, with slow or fast reader. See sub functions at end of this file
+ def doRead(slowReader=False):
+ with open(self.filename, 'r', errors="surrogateescape") as f:
+ # --- Reader header
+ self['header'], lastLine=readToMarker(f, 'Jacobians included', 30)
+ self['header'].append(lastLine)
+ nx = extractVal(self['header'],'Number of continuous states:' , dtype=int, NA=np.nan, missing=None)
+ nxd = extractVal(self['header'],'Number of discrete states:' , dtype=int, NA=np.nan, missing=None)
+ nz = extractVal(self['header'],'Number of constraint states:' , dtype=int, NA=np.nan, missing=None)
+ nu = extractVal(self['header'],'Number of inputs:' , dtype=int, NA=np.nan, missing=None)
+ ny = extractVal(self['header'],'Number of outputs:' , dtype=int, NA=np.nan, missing=None)
+ bJac = extractVal(self['header'],'Jacobians included in this file?', dtype=bool, NA=False, missing=None)
+ self['Azimuth'] = extractVal(self['header'], 'Azimuth:' , dtype=float, NA=np.nan, missing=None)
+ self['RotSpeed'] = extractVal(self['header'], 'Rotor Speed:', dtype=float, NA=np.nan, missing=None) # rad/s
+ self['WindSpeed'] = extractVal(self['header'], 'Wind Speed:' , dtype=float, NA=np.nan, missing=None)
+ self['t'] = extractVal(self['header'],'Simulation time:' , dtype=float, NA=np.nan, missing=None)
+ for i, line in enumerate(f):
+ line = line.strip()
+ if line.find('Order of continuous states:')>=0:
+ self['x'], self['x_info'] = readOP(f, nx, 'x', defaultDerivOrder=1, starSubFn=starSubFn, starSub=starSub)
+ elif line.find('Order of continuous state derivatives:')>=0:
+ self['xdot'], self['xdot_info'] = readOP(f, nx, 'xdot', defaultDerivOrder=2, starSubFn=starSubFn, starSub=starSub)
+ elif line.find('Order of discrete states:')>=0:
+ self['xd'], self['xd_info'] = readOP(f, nxd, 'xd', defaultDerivOrder=2, starSubFn=starSubFn, starSub=starSub)
+ elif line.find('Order of inputs')>=0:
+ self['u'], self['u_info'] = readOP(f, nu, 'u', defaultDerivOrder=0, starSubFn=starSubFn, starSub=starSub)
+ elif line.find('Order of outputs')>=0:
+ self['y'], self['y_info'] = readOP(f, ny, 'y', defaultDerivOrder=0, starSubFn=starSubFn, starSub=starSub)
+ elif line.find('Order of constraint states:')>=0:
+ self['z'], self['z_info'] = readOP(f, nz, 'z', defaultDerivOrder=0, starSubFn=starSubFn, starSub=starSub)
+ elif line.find('A:')>=0:
+ self['A'] = readMat(f, nx, nx, 'A', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub)
+ elif line.find('B:')>=0:
+ self['B'] = readMat(f, nx, nu, 'B', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub)
+ elif line.find('C:')>=0:
+ self['C'] = readMat(f, ny, nx, 'C', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub)
+ elif line.find('D:')>=0:
+ self['D'] = readMat(f, ny, nu, 'D', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub)
+ elif line.find('dUdu:')>=0:
+ self['dUdu'] = readMat(f, nu, nu,'dUdu', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub)
+ elif line.find('dUdy:')>=0:
+ self['dUdy'] = readMat(f, nu, ny,'dUdy', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub)
+ elif line.find('StateRotation:')>=0:
+ pass
+ # TODO
+ #StateRotation:
+ elif line.find('ED M:')>=0:
+ self['EDDOF'] = line[5:].split()
+ self['M'] = readMat(f, 24, 24,'M', slowReader=slowReader, filename=self.filename, starSubFn=starSubFn, starSub=starSub)
+ try:
+ doRead(slowReader=False)
+ except SlowReaderNeededError:
+ doRead(slowReader=True)
+
+ if removeStatesPattern is not None:
+ self.removeStates(pattern=removeStatesPattern)
+
+ def toString(self):
+ s=''
+ return s
+
+ def _write(self):
+ with open(self.filename,'w') as f:
+ f.write(self.toString())
+
+ @property
+ def nx(self):
+ if 'x' in self.keys():
+ return len(self['x'])
+ return 0
+
+ @property
+ def nxd(self):
+ if 'xd' in self.keys():
+ return len(self['xd'])
+ return 0
+
+ @property
+ def nu(self):
+ if 'u' in self.keys():
+ return len(self['u'])
+ return 0
+
+ @property
+ def ny(self):
+ if 'y' in self.keys():
+ return len(self['y'])
+ return 0
+
+ @property
+ def nz(self):
+ if 'z' in self.keys():
+ return len(self['z'])
+ return 0
+
+ @property
+ def u_descr(self):
+ if self.nu>0:
+ return self['u_info']['Description']
+ else:
+ return []
+
+ @property
+ def x_descr(self):
+ if self.nx>0:
+ return self['x_info']['Description']
+ else:
+ return []
+
+ @property
+ def xd_descr(self): # Discrete states not derivative!
+ if self.nxd>0:
+ return self['xd_info']['Description']
+ else:
+ return []
+
+ @property
+ def xdot_descr(self):
+ if self.nx>0:
+ return self['xdot_info']['Description']
+ else:
+ return []
+
+ @property
+ def y_descr(self):
+ if self.ny>0:
+ return self['y_info']['Description']
+ else:
+ return []
+
+ @property
+ def z_descr(self):
+ if self.nz>0:
+ return self['z_info']['Description']
+ else:
+ return []
+
+ def __repr__(self):
+ s='<{} object> with attributes:\n'.format(type(self).__name__)
+ s+=' - filename: {}\n'.format(self.filename)
+ s+=' * nx : {}\n'.format(self.nx)
+ s+=' * nxd : {}\n'.format(self.nxd)
+ s+=' * nu : {}\n'.format(self.nu)
+ s+=' * ny : {}\n'.format(self.ny)
+ s+=' * nz : {}\n'.format(self.nz)
+ s+='keys:\n'
+ for k,v in self.items():
+ if k in _lin_vec:
+ s+=' - {:15s}: shape: ({}) \n'.format(k,len(v))
+ elif k in _lin_mat:
+ s+=' - {:15s}: shape: ({} x {})\n'.format(k,v.shape[0], v.shape[1])
+ elif k in _lin_dict:
+ s+=' - {:15s}: dict with keys: {} \n'.format(k,list(v.keys()))
+ else:
+ s+=' - {:15s}: {}\n'.format(k,v)
+ s+='methods:\n'
+ s+=' - toDataFrame: convert A,B,C,D to dataframes\n'
+ s+=' - removeStates: remove states\n'
+ s+=' - eva: eigenvalue analysis\n'
+
+ return s
+
+ def toDataFrame(self):
+ import pandas as pd
+ dfs={}
+
+ xdescr_short = short_descr(self.x_descr)
+ xddescr_short = short_descr(self.xd_descr)
+ xdotdescr_short = short_descr(self.xdot_descr)
+ udescr_short = short_descr(self.u_descr)
+ ydescr_short = short_descr(self.y_descr)
+ zdescr_short = short_descr(self.z_descr)
+
+ if 'A' in self.keys():
+ dfs['A'] = pd.DataFrame(data = self['A'], index=xdescr_short, columns=xdescr_short)
+ if 'B' in self.keys():
+ dfs['B'] = pd.DataFrame(data = self['B'], index=xdescr_short, columns=udescr_short)
+ if 'C' in self.keys():
+ dfs['C'] = pd.DataFrame(data = self['C'], index=ydescr_short, columns=xdescr_short)
+ if 'D' in self.keys():
+ dfs['D'] = pd.DataFrame(data = self['D'], index=ydescr_short, columns=udescr_short)
+ if 'x' in self.keys():
+ dfs['x'] = pd.DataFrame(data = np.asarray(self['x']).reshape((1,-1)), columns=xdescr_short)
+ if 'xd' in self.keys():
+ dfs['xd'] = pd.DataFrame(data = np.asarray(self['xd']).reshape((1,-1)))
+ if 'xdot' in self.keys():
+ dfs['xdot'] = pd.DataFrame(data = np.asarray(self['xdot']).reshape((1,-1)), columns=xdotdescr_short)
+ if 'u' in self.keys():
+ dfs['u'] = pd.DataFrame(data = np.asarray(self['u']).reshape((1,-1)), columns=udescr_short)
+ if 'y' in self.keys():
+ dfs['y'] = pd.DataFrame(data = np.asarray(self['y']).reshape((1,-1)), columns=ydescr_short)
+ if 'z' in self.keys():
+ dfs['z'] = pd.DataFrame(data = np.asarray(self['z']).reshape((1,-1)), columns=zdescr_short)
+ if 'M' in self.keys():
+ dfs['M'] = pd.DataFrame(data = self['M'], index=self['EDDOF'], columns=self['EDDOF'])
+ if 'dUdu' in self.keys():
+ dfs['dUdu'] = pd.DataFrame(data = self['dUdu'], index=udescr_short, columns=udescr_short)
+ if 'dUdy' in self.keys():
+ dfs['dUdy'] = pd.DataFrame(data = self['dUdy'], index=udescr_short, columns=ydescr_short)
+
+ return dfs
+
+ def removeStates(self, pattern=None, Irm=None, verbose=True):
+ """
+ remove states based on pattern or index
+
+ - pattern: e.g: 'tower|Drivetrain' or '^AD'
+ """
+ if self.nx==0:
+ return
+ desc = self['x_info']['Description']
+ Iall = set(range(len(desc)))
+ sInfo=''
+ if pattern is not None:
+ Irm = [i for i, s in enumerate(desc) if re.search(pattern, s)]
+ sInfo=' with pattern `{}`'.format(pattern)
+ if verbose:
+ print('[INFO] removing {}/{} states{}'.format(len(Irm), len(Iall), sInfo))
+ Ikeep = list(Iall.difference(Irm))
+ Ikeep.sort() # safety
+ if len(Ikeep)==0:
+ raise Exception('All states have been removed{}!'.format(sInfo))
+ # Remove states and info in vectors
+ self['x'] = self['x'][Ikeep]
+ self['xdot'] = self['xdot'][Ikeep]
+ for k in self['x_info'].keys():
+ self['x_info'][k] = self['x_info'][k][Ikeep]
+ self['xdot_info'][k] = self['xdot_info'][k][Ikeep]
+ # Remove states in matrices
+ if 'A' in self.keys():
+ self['A'] = self['A'][np.ix_(Ikeep,Ikeep)]
+ if 'B' in self.keys():
+ self['B'] = self['B'][Ikeep,:]
+ if 'C' in self.keys():
+ self['C'] = self['C'][:, Ikeep]
+
+
+ def eva(self, normQ=None, sort=True, discardIm=True):
+ """ Perform eigenvalue analysis of A matrix and return frequencies and damping """
+ # --- Excerpt from welib.tools.eva.eigA
+ A = self['A']
+ n,m = A.shape
+ if m!=n:
+ raise Exception('Matrix needs to be square')
+ # Basic EVA
+ D,Q = np.linalg.eig(A)
+ Lambda = np.diag(D)
+ v = np.diag(Lambda)
+
+ # Selecting eigenvalues with positive imaginary part (frequency)
+ if discardIm:
+ 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 short_descr(slist):
+ """ Shorten and "unify" the description from lin file """
+ def shortname(s):
+ s=s.strip()
+ s = s.replace('(m/s)' , '_[m/s]' );
+ s = s.replace('(kW)' , '_[kW]' );
+ s = s.replace('(deg)' , '_[deg]' );
+ s = s.replace('(N)' , '_[N]' );
+ s = s.replace('(kN-m)' , '_[kNm]' );
+ s = s.replace('(N-m)' , '_[Nm]' );
+ s = s.replace('(kN)' , '_[kN]' );
+ s = s.replace('(rpm)' , '_[rpm]' );
+ s = s.replace('(rad)' , '_[rad]' );
+ s = s.replace('(rad/s)' , '_[rad/s]' );
+ s = s.replace('(rad/s^2)', '_[rad/s^2]' );
+ s = s.replace('(m/s^2)' , '_[m/s^2]');
+ s = s.replace('(deg/s^2)','_[deg/s^2]');
+ s = s.replace('(m)' , '_[m]' );
+ s = s.replace(', m/s/s','_[m/s^2]');
+ s = s.replace(', m/s^2','_[m/s^2]');
+ s = s.replace(', m/s','_[m/s]');
+ s = s.replace(', m','_[m]');
+ s = s.replace(', rad/s/s','_[rad/s^2]');
+ s = s.replace(', rad/s^2','_[rad/s^2]');
+ s = s.replace(', rad/s','_[rad/s]');
+ s = s.replace(', rad','_[rad]');
+ s = s.replace(', -','_[-]');
+ s = s.replace(', Nm/m','_[Nm/m]');
+ s = s.replace(', Nm','_[Nm]');
+ s = s.replace(', N/m','_[N/m]');
+ s = s.replace(', N','_[N]');
+ s = s.replace('(1)','1')
+ s = s.replace('(2)','2')
+ s = s.replace('(3)','3')
+ s= re.sub(r'\([^)]*\)','', s) # remove parenthesis
+ s = s.replace('ED ','');
+ s = s.replace('BD_','BD_B');
+ s = s.replace('IfW ','');
+ s = s.replace('Extended input: ','')
+ s = s.replace('1st tower ','qt1');
+ s = s.replace('2nd tower ','qt2');
+ nd = s.count('First time derivative of ')
+ if nd>=0:
+ s = s.replace('First time derivative of ' ,'');
+ if nd==1:
+ s = 'd_'+s.strip()
+ elif nd==2:
+ s = 'dd_'+s.strip()
+ s = s.replace('Variable speed generator DOF ','psi_rot'); # NOTE: internally in FAST this is the azimuth of the rotor
+ s = s.replace('fore-aft bending mode DOF ' ,'FA' );
+ s = s.replace('side-to-side bending mode DOF','SS' );
+ s = s.replace('bending-mode DOF of blade ' ,'' );
+ s = s.replace(' rotational-flexibility DOF, rad','-ROT' );
+ s = s.replace('rotational displacement in ','rot' );
+ s = s.replace('Drivetrain','DT' );
+ s = s.replace('translational displacement in ','trans' );
+ s = s.replace('finite element node ','N' );
+ s = s.replace('-component position of node ','posN')
+ s = s.replace('-component inflow on tower node','TwrN')
+ s = s.replace('-component inflow on blade 1, node','Bld1N')
+ s = s.replace('-component inflow on blade 2, node','Bld2N')
+ s = s.replace('-component inflow on blade 3, node','Bld3N')
+ s = s.replace('-component inflow velocity at node','N')
+ s = s.replace('X translation displacement, node','TxN')
+ s = s.replace('Y translation displacement, node','TyN')
+ s = s.replace('Z translation displacement, node','TzN')
+ s = s.replace('X translation velocity, node','TVxN')
+ s = s.replace('Y translation velocity, node','TVyN')
+ s = s.replace('Z translation velocity, node','TVzN')
+ s = s.replace('X translation acceleration, node','TAxN')
+ s = s.replace('Y translation acceleration, node','TAyN')
+ s = s.replace('Z translation acceleration, node','TAzN')
+ s = s.replace('X orientation angle, node' ,'RxN')
+ s = s.replace('Y orientation angle, node' ,'RyN')
+ s = s.replace('Z orientation angle, node' ,'RzN')
+ s = s.replace('X rotation velocity, node' ,'RVxN')
+ s = s.replace('Y rotation velocity, node' ,'RVyN')
+ s = s.replace('Z rotation velocity, node' ,'RVzN')
+ s = s.replace('X rotation acceleration, node' ,'RAxN')
+ s = s.replace('Y rotation acceleration, node' ,'RAyN')
+ s = s.replace('Z rotation acceleration, node' ,'RAzN')
+ s = s.replace('X force, node','FxN')
+ s = s.replace('Y force, node','FyN')
+ s = s.replace('Z force, node','FzN')
+ s = s.replace('X moment, node','MxN')
+ s = s.replace('Y moment, node','MyN')
+ s = s.replace('Z moment, node','MzN')
+ s = s.replace('FX', 'Fx')
+ s = s.replace('FY', 'Fy')
+ s = s.replace('FZ', 'Fz')
+ s = s.replace('MX', 'Mx')
+ s = s.replace('MY', 'My')
+ s = s.replace('MZ', 'Mz')
+ s = s.replace('FKX', 'FKx')
+ s = s.replace('FKY', 'FKy')
+ s = s.replace('FKZ', 'FKz')
+ s = s.replace('MKX', 'MKx')
+ s = s.replace('MKY', 'MKy')
+ s = s.replace('MKZ', 'MKz')
+ s = s.replace('Nodes motion','')
+ s = s.replace('cosine','cos' );
+ s = s.replace('sine','sin' );
+ s = s.replace('collective','coll.');
+ s = s.replace('Blade','Bld');
+ s = s.replace('rotZ','TORS-R');
+ s = s.replace('transX','FLAP-D');
+ s = s.replace('transY','EDGE-D');
+ s = s.replace('rotX','EDGE-R');
+ s = s.replace('rotY','FLAP-R');
+ s = s.replace('flapwise','FLAP');
+ s = s.replace('edgewise','EDGE');
+ s = s.replace('horizontal surge translation DOF','Surge');
+ s = s.replace('horizontal sway translation DOF','Sway');
+ s = s.replace('vertical heave translation DOF','Heave');
+ s = s.replace('roll tilt rotation DOF','Roll');
+ s = s.replace('pitch tilt rotation DOF','Pitch');
+ s = s.replace('yaw rotation DOF','Yaw');
+ s = s.replace('vertical power-law shear exponent','alpha')
+ s = s.replace('horizontal wind speed ','WS')
+ s = s.replace('propagation direction','WD')
+ s = s.replace(' pitch command','pitch')
+ s = s.replace('HSS_','HSS')
+ s = s.replace('Bld','B')
+ s = s.replace('tower','Twr')
+ s = s.replace('Tower','Twr')
+ s = s.replace('Nacelle','Nac')
+ s = s.replace('Platform','Ptfm')
+ s = s.replace('SrvD','SvD')
+ s = s.replace('Generator torque','Qgen')
+ s = s.replace('coll. blade-pitch command','PitchColl')
+ s = s.replace('wave elevation at platform ref point','WaveElevRefPoint')
+ s = s.replace('1)','1');
+ s = s.replace('2)','2');
+ s = s.replace('3)','3');
+ s = s.replace(',','');
+ s = s.replace(' ','');
+ s=s.strip()
+ return s
+ return [shortname(s) for s in slist]
+
+
+
+def extractVal(lines, key, NA=None, missing=None, dtype=float):
+ for l in lines:
+ if l.find(key)>=0:
+ #l = starPattern.sub(starSubStr, l)
+ try:
+ return dtype(l.split(key)[1].split()[0])
+ except:
+ return NA
+ return missing
+
+def readToMarker(fid, marker, nMax):
+ lines=[]
+ for i, line in enumerate(fid):
+ if i>nMax:
+ raise BrokenFormatError('`{}` not found in file'.format(marker))
+ if line.find(marker)>=0:
+ break
+ lines.append(line.strip())
+ return lines, line
+
+def readOP(fid, n, name='', defaultDerivOrder=1, filename='', starSubFn=None, starSub=None):
+ OP=[]
+ Var = {'RotatingFrame': [], 'DerivativeOrder': [], 'Description': []}
+ colNames=fid.readline().strip()
+ dummy= fid.readline().strip()
+ bHasDeriv= colNames.find('Derivative Order')>=0
+ for i, line in enumerate(fid):
+ line = line.strip()
+ line = starSubFn(line)
+ sp = line.split()
+ if sp[1].find(',')>=0:
+ # Most likely this OP has three values (e.g. orientation angles)
+ # For now we discard the two other values
+ OP.append(float(sp[1][:-1]))
+ iRot=4
+ else:
+ OP.append(float(sp[1]))
+ iRot=2
+ Var['RotatingFrame'].append(sp[iRot])
+ if bHasDeriv:
+ Var['DerivativeOrder'].append(int(sp[iRot+1]))
+ Var['Description'].append(' '.join(sp[iRot+2:]).strip())
+ else:
+ Var['DerivativeOrder'].append(defaultDerivOrder)
+ Var['Description'].append(' '.join(sp[iRot+1:]).strip())
+ if i>=n-1:
+ break
+ OP = np.asarray(OP)
+ nInf = np.sum(np.isinf(OP))
+ if nInf>0:
+ sErr = 'Some ill-formated/infinite values (e.g. `*******`) were found in the vector `{}`\n\tin linflile: {}'.format(name, filename)
+ if starSub is None:
+ raise Exception(sErr)
+ else:
+ print('[WARN] '+sErr)
+ OP[np.isinf(OP)] = starSub
+
+ Var['RotatingFrame'] = np.asarray(Var['RotatingFrame'])
+ Var['DerivativeOrder'] = np.asarray(Var['DerivativeOrder'])
+ Var['Description'] = np.asarray(Var['Description'])
+ return OP, Var
+
+
+
+def readMat(fid, n, m, name='', slowReader=False, filename='', starSubFn=None, starSub=None):
+ if not slowReader:
+ try:
+ return np.array([fid.readline().strip().split() for i in np.arange(n)],dtype=float)
+ except:
+ print('[INFO] Failed to read some value in matrix {}, trying slower reader'.format(name))
+ raise SlowReaderNeededError()
+ else:
+ #vals = vals.ravel()
+ #vals = np.array(list(map(starSubFn, vals))).reshape(n,m)
+ vals=np.array([starSubFn( fid.readline().strip() ).split() for i in np.arange(n)], dtype=str)
+ try:
+ vals = vals.astype(float) # This could potentially fail
+ except:
+ raise Exception('Failed to convert into an array of float the matrix `{}`\n\tin linfile: {}'.format(name, filename))
+ if vals.shape[0]!=n or vals.shape[1]!=m:
+ shape1 = vals.shape
+ shape2 = (n,m)
+ raise Exception('Shape of matrix `{}` has wrong dimension ({} instead of {})\n\tin linfile: {}'.format(name, shape1, shape2, name, filename))
+
+ nNaN = np.sum(np.isnan(vals.ravel()))
+ nInf = np.sum(np.isinf(vals.ravel()))
+ if nInf>0:
+ sErr = 'Some ill-formated/infinite values (e.g. `*******`) were found in the matrix `{}`\n\tin linflile: {}'.format(name, filename)
+ if starSub is None:
+ raise Exception(sErr)
+ else:
+ print('[WARN] '+sErr)
+ vals[np.isinf(vals)] = starSub
+ if nNaN>0:
+ raise Exception('Some NaN values were found in the matrix `{}`\n\tin linfile: `{}`.'.format(name, filename))
+ return vals
+
+if __name__ == '__main__':
+ f = FASTLinearizationFile('../../data/example_files/StandstillSemi_ForID_EDHD.1.lin')
+ print(f)
+ _, zeta1, _, freq1 = f.eva()
+ f.removeStates(pattern=r'^AD')
+ print(f)
+ dfs = f.toDataFrame()
+ _, zeta2, _, freq2 = f.eva()
+ print('f',freq1)
+ print('f',freq2)
+ print('d',zeta1)
+ print('d',zeta2)
+
diff --git a/weio/fast_output_file.py b/weio/fast_output_file.py
index 94ee22c..15556a8 100644
--- a/weio/fast_output_file.py
+++ b/weio/fast_output_file.py
@@ -1,498 +1,845 @@
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import unicode_literals
-from __future__ import print_function
-from io import open
-from builtins import map
-from builtins import range
-from builtins import chr
-from builtins import str
-from future import standard_library
-standard_library.install_aliases()
-
-from itertools import takewhile
-
-try:
- from .file import File, WrongFormatError, BrokenReaderError, EmptyFileError
-except:
- # --- Allowing this file to be standalone..
- class WrongFormatError(Exception):
- pass
- class WrongReaderError(Exception):
- pass
- class EmptyFileError(Exception):
- pass
- File = dict
-try:
- from .csv_file import CSVFile
-except:
- print('CSVFile not available')
-import numpy as np
-import pandas as pd
-import struct
-import os
-import re
-
-
-# --------------------------------------------------------------------------------}
-# --- OUT FILE
-# --------------------------------------------------------------------------------{
-class FASTOutputFile(File):
- """
- Read an OpenFAST ouput file (.out, .outb, .elev).
-
- Main methods
- ------------
- - read, write, toDataFrame
-
- Examples
- --------
-
- # read an output file, convert it to pandas dataframe, modify it, write it back
- f = FASTOutputFile('5MW.outb')
- df=f.toDataFrame()
- time = df['Time_[s]']
- Omega = df['RotSpeed_[rpm]']
- df['Time_[s]'] -=100
- f.writeDataFrame(df, '5MW_TimeShifted.outb')
-
- """
-
- @staticmethod
- def defaultExtensions():
- return ['.out','.outb','.elm','.elev']
-
- @staticmethod
- def formatName():
- return 'FAST output file'
-
- def _read(self):
- def readline(iLine):
- with open(self.filename) as f:
- for i, line in enumerate(f):
- if i==iLine-1:
- return line.strip()
- elif i>=iLine:
- break
-
- ext = os.path.splitext(self.filename.lower())[1]
- self.info={}
- self['binary']=False
- try:
- if ext in ['.out','.elev']:
- self.data, self.info = load_ascii_output(self.filename)
- elif ext=='.outb':
- self.data, self.info = load_binary_output(self.filename)
- self['binary']=True
- elif ext=='.elm':
- F=CSVFile(filename=self.filename, sep=' ', commentLines=[0,2],colNamesLine=1)
- self.data = F.data
- del F
- self.info['attribute_units']=readline(3).replace('sec','s').split()
- self.info['attribute_names']=self.data.columns.values
- else:
- self.data, self.info = load_output(self.filename)
- except MemoryError as e:
- raise BrokenReaderError('FAST Out File {}: Memory error encountered\n{}'.format(self.filename,e))
- except Exception as e:
- raise WrongFormatError('FAST Out File {}: {}'.format(self.filename,e.args))
- if self.data.shape[0]==0:
- raise EmptyFileError('This FAST output file contains no data: {}'.format(self.filename))
-
- if self.info['attribute_units'] is not None:
- self.info['attribute_units'] = [re.sub(r'[()\[\]]','',u) for u in self.info['attribute_units']]
-
-
- def _write(self):
- if self['binary']:
- channels = self.data
- chanNames = self.info['attribute_names']
- chanUnits = self.info['attribute_units']
- descStr = self.info['description']
- writeBinary(self.filename, channels, chanNames, chanUnits, fileID=2, descStr=descStr)
- else:
- # ascii output
- with open(self.filename,'w') as f:
- f.write('\t'.join(['{:>10s}'.format(c) for c in self.info['attribute_names']])+'\n')
- f.write('\t'.join(['{:>10s}'.format('('+u+')') for u in self.info['attribute_units']])+'\n')
- # TODO better..
- f.write('\n'.join(['\t'.join(['{:10.4f}'.format(y[0])]+['{:10.3e}'.format(x) for x in y[1:]]) for y in self.data]))
-
- def _toDataFrame(self):
- if self.info['attribute_units'] is not None:
- cols=[n+'_['+u.replace('sec','s')+']' for n,u in zip(self.info['attribute_names'],self.info['attribute_units'])]
- else:
- cols=self.info['attribute_names']
- if isinstance(self.data, pd.DataFrame):
- df= self.data
- df.columns=cols
- else:
- df = pd.DataFrame(data=self.data,columns=cols)
-
- return df
-
- def writeDataFrame(self, df, filename, binary=True):
- writeDataFrame(df, filename, binary=binary)
-
-# --------------------------------------------------------------------------------
-# --- Helper low level functions
-# --------------------------------------------------------------------------------
-def load_output(filename):
- """Load a FAST binary or ascii output file
-
- Parameters
- ----------
- filename : str
- filename
-
- Returns
- -------
- data : ndarray
- data values
- info : dict
- info containing:
- - name: filename
- - description: description of dataset
- - attribute_names: list of attribute names
- - attribute_units: list of attribute units
- """
-
- assert os.path.isfile(filename), "File, %s, does not exists" % filename
- with open(filename, 'r') as f:
- try:
- f.readline()
- except UnicodeDecodeError:
- return load_binary_output(filename)
- return load_ascii_output(filename)
-
-def load_ascii_output(filename):
- with open(filename) as f:
- info = {}
- info['name'] = os.path.splitext(os.path.basename(filename))[0]
- # Header is whatever is before the keyword `time`
- in_header = True
- header = []
- while in_header:
- l = f.readline()
- if not l:
- raise Exception('Error finding the end of FAST out file header. Keyword Time missing.')
- in_header= (l+' dummy').lower().split()[0] != 'time'
- if in_header:
- header.append(l)
- else:
- info['description'] = header
- info['attribute_names'] = l.split()
- info['attribute_units'] = [unit[1:-1] for unit in f.readline().split()]
- # ---
- # Data, up to end of file or empty line (potential comment line at the end)
-# data = np.array([l.strip().split() for l in takewhile(lambda x: len(x.strip())>0, f.readlines())]).astype(np.float)
- # ---
- data = np.loadtxt(f, comments=('This')) # Adding "This" for the Hydro Out files..
- return data, info
-
-
-def load_binary_output(filename, use_buffer=True):
- """
- 03/09/15: Ported from ReadFASTbinary.m by Mads M Pedersen, DTU Wind
- 24/10/18: Low memory/buffered version by E. Branlard, NREL
- 18/01/19: New file format for exctended channels, by E. Branlard, NREL
-
- Info about ReadFASTbinary.m:
- % Author: Bonnie Jonkman, National Renewable Energy Laboratory
- % (c) 2012, National Renewable Energy Laboratory
- %
- % Edited for FAST v7.02.00b-bjj 22-Oct-2012
- """
- def fread(fid, n, type):
- fmt, nbytes = {'uint8': ('B', 1), 'int16':('h', 2), 'int32':('i', 4), 'float32':('f', 4), 'float64':('d', 8)}[type]
- return struct.unpack(fmt * n, fid.read(nbytes * n))
-
- def freadRowOrderTableBuffered(fid, n, type_in, nCols, nOff=0, type_out='float64'):
- """
- Reads of row-ordered table from a binary file.
-
- Read `n` data of type `type_in`, assumed to be a row ordered table of `nCols` columns.
- Memory usage is optimized by allocating the data only once.
- Buffered reading is done for improved performances (in particular for 32bit python)
-
- `nOff` allows for additional column space at the begining of the storage table.
- Typically, `nOff=1`, provides a column at the beginning to store the time vector.
-
- @author E.Branlard, NREL
-
- """
- fmt, nbytes = {'uint8': ('B', 1), 'int16':('h', 2), 'int32':('i', 4), 'float32':('f', 4), 'float64':('d', 8)}[type_in]
- nLines = int(n/nCols)
- GoodBufferSize = 4096*40
- nLinesPerBuffer = int(GoodBufferSize/nCols)
- BufferSize = nCols * nLinesPerBuffer
- nBuffer = int(n/BufferSize)
- # Allocation of data
- data = np.zeros((nLines,nCols+nOff), dtype = type_out)
- # Reading
- try:
- nIntRead = 0
- nLinesRead = 0
- while nIntRead0:
- op,cl = chars
- iu=c.rfind(op)
- if iu>1:
- name = c[:iu]
- unit = c[iu+1:].replace(cl,'')
- if name[-1]=='_':
- name=name[:-1]
-
- chanNames.append(name)
- chanUnits.append(unit)
-
- if binary:
- writeBinary(filename, channels, chanNames, chanUnits)
- else:
- NotImplementedError()
-
-
-def writeBinary(fileName, channels, chanNames, chanUnits, fileID=2, descStr=''):
- """
- Write an OpenFAST binary file.
-
- Based on contributions from
- Hugo Castro, David Schlipf, Hochschule Flensburg
-
- Input:
- FileName - string: contains file name to open
- Channels - 2-D array: dimension 1 is time, dimension 2 is channel
- ChanName - cell array containing names of output channels
- ChanUnit - cell array containing unit names of output channels, preferably surrounded by parenthesis
- FileID - constant that determines if the time is stored in the
- output, indicating possible non-constant time step
- DescStr - String describing the file
- """
- # Data sanitization
- chanNames = list(chanNames)
- channels = np.asarray(channels)
- if chanUnits[0][0]!='(':
- chanUnits = ['('+u+')' for u in chanUnits] # units surrounded by parenthesis to match OpenFAST convention
-
- nT, nChannelsWithTime = np.shape(channels)
- nChannels = nChannelsWithTime - 1
-
- # For FileID =2, time needs to be present and at the first column
- try:
- iTime = chanNames.index('Time')
- except ValueError:
- raise Exception('`Time` needs to be present in channel names' )
- if iTime!=0:
- raise Exception('`Time` needs to be the first column of `chanName`' )
-
- time = channels[:,iTime]
- timeStart = time[0]
- timeIncr = time[1]-time[0]
- dataWithoutTime = channels[:,1:]
-
- # Compute data range, scaling and offsets to convert to int16
- # To use the int16 range to its fullest, the max float is matched to 2^15-1 and the
- # the min float is matched to -2^15. Thus, we have the to equations we need
- # to solve to get scaling and offset, see line 120 of ReadFASTbinary:
- # Int16Max = FloatMax * Scaling + Offset
- # Int16Min = FloatMin * Scaling + Offset
- int16Max = np.single( 32767.0) # Largest integer represented in 2 bytes, 2**15 - 1
- int16Min = np.single(-32768.0) # Smallest integer represented in 2 bytes -2**15
- int16Rng = np.single(int16Max - int16Min) # Max Range of 2 byte integer
- mins = np.min(dataWithoutTime, axis=0)
- ranges = np.single(np.max(dataWithoutTime, axis=0) - mins)
- ranges[ranges==0]=1 # range set to 1 for constant channel. In OpenFAST: /sqrt(epsilon(1.0_SiKi))
- ColScl = np.single(int16Rng/ranges)
- ColOff = np.single(int16Min - np.single(mins)*ColScl)
-
- #Just available for fileID
- if fileID != 2:
- print("current version just works with FileID = 2")
-
- else:
- with open(fileName,'wb') as fid:
- # Notes on struct:
- # @ is used for packing in native byte order
- # B - unsigned integer 8 bits
- # h - integer 16 bits
- # i - integer 32 bits
- # f - float 32 bits
- # d - float 64 bits
-
- # Write header informations
- fid.write(struct.pack('@h',fileID))
- fid.write(struct.pack('@i',nChannels))
- fid.write(struct.pack('@i',nT))
- fid.write(struct.pack('@d',timeStart))
- fid.write(struct.pack('@d',timeIncr))
- fid.write(struct.pack('@{}f'.format(nChannels), *ColScl))
- fid.write(struct.pack('@{}f'.format(nChannels), *ColOff))
- descStrASCII = [ord(char) for char in descStr]
- fid.write(struct.pack('@i',len(descStrASCII)))
- fid.write(struct.pack('@{}B'.format(len((descStrASCII))), *descStrASCII))
-
- # Write channel names
- for chan in chanNames:
- ordchan = [ord(char) for char in chan]+ [32]*(10-len(chan))
- fid.write(struct.pack('@10B', *ordchan))
-
- # Write channel units
- for unit in chanUnits:
- ordunit = [ord(char) for char in unit]+ [32]*(10-len(unit))
- fid.write(struct.pack('@10B', *ordunit))
-
- # Pack data
- packedData=np.zeros((nT, nChannels), dtype=np.int16)
- for iChan in range(nChannels):
- packedData[:,iChan] = np.clip( ColScl[iChan]*dataWithoutTime[:,iChan]+ColOff[iChan], int16Min, int16Max)
-
- # Write data
- fid.write(struct.pack('@{}h'.format(packedData.size), *packedData.flatten()))
- fid.close()
-
-
-if __name__ == "__main__":
- B=FASTOutputFile('tests/example_files/FASTOutBin.outb')
- df=B.toDataFrame()
- B.writeDataFrame(df, 'tests/example_files/FASTOutBin_OUT.outb')
-
-
+"""
+Tools to read/write OpenFAST output files
+
+Main content:
+
+- class FASTOutputFile()
+- data, info = def load_output(filename)
+- data, info = def load_ascii_output(filename)
+- data, info = def load_binary_output(filename, use_buffer=True)
+- def writeDataFrame(df, filename, binary=True)
+- def writeBinary(fileName, channels, chanNames, chanUnits, fileID=2, descStr='')
+
+NOTE:
+ - load_binary and writeBinary are not "fully reversible" for now.
+ Some small numerical errors are introduced in the conversion.
+ Some of the error is likely due to the fact that Python converts to "int" and "float" (double).
+ Maybe all the operations should be done in single. I tried but failed.
+ I simply wonder if the operation is perfectly reversible.
+
+
+"""
+from itertools import takewhile
+import numpy as np
+import pandas as pd
+import struct
+import ctypes
+import os
+import re
+try:
+ from .file import File, WrongFormatError, BrokenReaderError, EmptyFileError, BrokenFormatError
+except:
+ File = dict
+ class WrongFormatError(Exception): pass
+ class WrongReaderError(Exception): pass
+ class BrokenFormatError(Exception): pass
+ class EmptyFileError(Exception): pass
+try:
+ from .csv_file import CSVFile
+except:
+ print('CSVFile not available')
+
+
+
+FileFmtID_WithTime = 1 # File identifiers used in FAST
+FileFmtID_WithoutTime = 2
+FileFmtID_NoCompressWithoutTime = 3
+FileFmtID_ChanLen_In = 4 # Channel length included in file
+
+
+# --------------------------------------------------------------------------------}
+# --- OUT FILE
+# --------------------------------------------------------------------------------{
+class FASTOutputFile(File):
+ """
+ Read an OpenFAST ouput file (.out, .outb, .elev).
+
+ Main methods
+ ------------
+ - read, write, toDataFrame
+
+ Examples
+ --------
+
+ # read an output file, convert it to pandas dataframe, modify it, write it back
+ f = FASTOutputFile('5MW.outb')
+ df=f.toDataFrame()
+ time = df['Time_[s]']
+ Omega = df['RotSpeed_[rpm]']
+ df['Time_[s]'] -=100
+ f.writeDataFrame(df, '5MW_TimeShifted.outb')
+
+ """
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.out','.outb','.elm','.elev','.dbg','.dbg2']
+
+ @staticmethod
+ def formatName():
+ return 'FAST output file'
+
+ def __init__(self, filename=None, **kwargs):
+ """ Class constructor. If a `filename` is given, the file is read. """
+ # Data
+ self.filename = filename
+ self.data = None # pandas.DataFrame
+ self.description = '' # string
+ 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)
+
+ # --- Actual reading
+ def readline(iLine):
+ with open(self.filename) as f:
+ for i, line in enumerate(f):
+ if i==iLine-1:
+ return line.strip()
+ elif i>=iLine:
+ break
+
+ ext = os.path.splitext(self.filename.lower())[1]
+ info={}
+ self['binary']=False
+ try:
+ if ext in ['.out','.elev','.dbg','.dbg2']:
+ self.data, info = load_ascii_output(self.filename, **kwargs)
+ elif ext=='.outb':
+ self.data, info = load_binary_output(self.filename, **kwargs)
+ self['binary']=True
+ elif ext=='.elm':
+ F=CSVFile(filename=self.filename, sep=' ', commentLines=[0,2],colNamesLine=1)
+ self.data = F.data
+ del F
+ info['attribute_units']=readline(3).replace('sec','s').split()
+ info['attribute_names']=self.data.columns.values
+ else:
+ if isBinary(self.filename):
+ self.data, info = load_binary_output(self.filename, **kwargs)
+ self['binary']=True
+ else:
+ self.data, info = load_ascii_output(self.filename, **kwargs)
+ self['binary']=False
+ except MemoryError as e:
+ raise BrokenReaderError('FAST Out File {}: Memory error encountered\n{}'.format(self.filename,e))
+ except Exception as e:
+ raise WrongFormatError('FAST Out File {}: {}'.format(self.filename,e.args))
+ if self.data.shape[0]==0:
+ raise EmptyFileError('This FAST output file contains no data: {}'.format(self.filename))
+
+
+
+ # --- Convert to DataFrame
+ if info['attribute_units'] is not None:
+ info['attribute_units'] = [re.sub(r'[()\[\]]','',u) for u in info['attribute_units']]
+ if len(info['attribute_names'])!=len(info['attribute_units']):
+ cols=info['attribute_names']
+ print('[WARN] not all columns have units! Skipping units')
+ else:
+ cols=[n+'_['+u.replace('sec','s')+']' for n,u in zip(info['attribute_names'], info['attribute_units'])]
+ else:
+ cols=info['attribute_names']
+ self.description = info.get('description', '')
+ self.description = ''.join(self.description) if isinstance(self.description,list) else self.description
+ if isinstance(self.data, pd.DataFrame):
+ self.data.columns = cols
+ else:
+ if len(cols)!=self.data.shape[1]:
+ raise BrokenFormatError('Inconstistent number of columns between headers ({}) and data ({}) for file {}'.format(len(cols), self.data.shape[1], self.filename))
+ self.data = pd.DataFrame(data=self.data, columns=cols)
+
+
+ def write(self, filename=None, binary=None, fileID=4):
+ if filename:
+ self.filename = filename
+ if not self.filename:
+ raise Exception('No filename provided')
+ # Calling children function
+ if binary is None:
+ binary = self['binary']
+
+ if binary:
+ # NOTE: user provide a filename, we allow overwrite
+ self.toOUTB(filename=self.filename, fileID=fileID, noOverWrite=False)
+ else:
+ # ascii output
+ with open(self.filename,'w') as f:
+ f.write(self.description) # add description to the begining of the file
+ f.write('\t'.join(['{:>10s}'.format(c) for c in self.channels])+'\n')
+ f.write('\t'.join(['{:>10s}'.format('('+u+')') for u in self.units])+'\n')
+ # TODO better..
+ if self.data is not None:
+ if isinstance(self.data, pd.DataFrame) and not self.data.empty:
+ f.write('\n'.join(['\t'.join(['{:10.4f}'.format(y.iloc[0])]+['{: .5e}'.format(x) for x in y.iloc[1:]]) for _, y in self.data.iterrows()]))
+ else: # in case data beeing array or list of list.
+ f.write('\n'.join(['\t'.join(['{:10.4f}'.format(y)]+['{: .5e}'.format(x) for x in y]) for y in self.data]))
+
+ @property
+ def channels(self):
+ if self.data is None:
+ return []
+ def no_unit(s):
+ s=s.replace('(','[').replace(')',']').replace(' [','_[').strip(']')
+ try:
+ return s.split('_[')[0].strip()
+ except:
+ return s.strip()
+ channels = [no_unit(c) for c in self.data.columns]
+ return channels
+
+ @property
+ def units(self):
+ if self.data is None:
+ return []
+ def unit(s):
+ s=s.replace('(','[').replace(')',']').replace(' [','_[').strip(']')
+ try:
+ return s.split('_[')[1].strip()
+ except:
+ return s.strip()
+ units = [unit(c) for c in self.data.columns]
+ return units
+
+ def toDataFrame(self):
+ """ Returns object into one DataFrame, or a dictionary of DataFrames"""
+ return self.data
+
+ def writeDataFrame(self, df, filename, binary=True):
+ writeDataFrame(df, filename, binary=binary)
+
+ def __repr__(self):
+ s='<{} object> with attributes:\n'.format(type(self).__name__)
+ s+=' - filename: {}\n'.format(self.filename)
+ s+=' - data ({})\n'.format(type(self.data))
+ s+=' - description: {}\n'.format(self.description)
+ s+='and keys: {}\n'.format(self.keys())
+ return s
+
+ # --------------------------------------------------------------------------------
+ # --- 2D fields
+ # --------------------------------------------------------------------------------
+ @property
+ def driverFile(self):
+ try:
+ driver, FFKind = findDriverFile(self.filename, df=None)
+ return driver
+ except:
+ return None
+
+ def to2DFields(self, DeltaAzi=5, nPeriods=3, rcoords=None, kinds=['(t,r)','(psi,r)'], **kwargs):
+ raise Exception('to2DFields is not available in weio')
+
+
+ def insertName(ds, name, dims):
+ for var in ds.variables:
+ # Create a new name, for example, add "_new" to each variable name
+ if ds[var].dims == dims:
+ #chan = var.split('_[')[0]
+ #unit = var.split('_[')[1]
+ #new_var = chan+name+'_['+unit
+ new_var = name+'_'+var
+ ds = ds.rename({var: new_var})
+ return ds
+
+ driverFile = self.driverFile
+
+ if len(kwargs.keys())>0:
+ print('[WARN] FASTOutputFile: to2DFields: ignored keys: ',kwargs.keys())
+ # Sanity check
+ DeltaAzi=float(DeltaAzi)
+ df = self.toDataFrame()
+ ds_AD1 = None
+ ds_AD2 = None
+ # --- Radial coordinate
+ def get_rvals(ds, rcoords):
+ n = len(ds['i/n_[-]'].values)
+ if rcoords is not None: # priority to user input
+ runit = 'm'
+ rvals = rcoords
+ if len(rcoords)!=n:
+ raise Exception('Length of rcoords should be: {}'.format(n))
+ elif 'r_[m]' in ds.keys():
+ rvals = ds['r_[m]'].values
+ runit = 'm'
+ else:
+ rvals = ds['i/n_[-]'].values
+ runit = '-'
+ return rvals, runit
+ # --- Time wise
+ if '(t,r)' in kinds:
+ ds_AD1, ds_ED, ds_BD = fastlib.spanwisePostProRows(df, driverFile, si1='t', sir='r')
+ if ds_AD1 is None:
+ return None # No Hope
+ rvals, runit = get_rvals(ds_AD1, rcoords)
+ # Rename columns to make them unique
+ ds_AD1 = insertName(ds_AD1, '(t,r)', ('t','r'))
+ try:
+ ds_AD1.coords['t'] = ('t', df['Time_[s]'].values)
+ except:
+ pass
+ ds_AD1.coords['r'] = ('r', rvals)
+ ds_AD1.r.attrs['unit'] = runit
+ ds_AD1.t.attrs['unit'] = 's'
+
+ # --- Azimuthal Radial postpro
+ if '(psi,r)' in kinds:
+ psi = np.arange(0, 360+DeltaAzi/10, DeltaAzi)
+ dfPsi = fastlib.azimuthal_average_DF(df, psiBin=psi, periodic=True, nPeriods=nPeriods) #, tStart = time[-1]-20)
+ ds_AD2, ds_ED2, ds_BD2 = fastlib.spanwisePostProRows(dfPsi, driverFile, si1='psi', sir='r')
+ rvals, runit = get_rvals(ds_AD2, rcoords)
+ ds_AD2.coords['psi'] = ('psi', psi) # TODO hack from bin to bin edges...
+ ds_AD2.coords['r'] = ('r', rvals)
+ # Rename columns to make them unique
+ ds_AD2= insertName(ds_AD2, '(psi,r)', ('psi','r'))
+ ds_AD2.psi.attrs['unit'] = 'deg'
+ ds_AD2.r.attrs['unit'] = runit
+
+ # --- Combine into one field (need unique variables and dimension)
+ if ds_AD1 is not None and ds_AD2 is not None:
+ ds_AD = ds_AD1.merge(ds_AD2, compat='override')
+ elif ds_AD1 is not None:
+ ds_AD = ds_AD1
+ else:
+ ds_AD = ds_AD2
+
+ # ---
+# # ds_AD = ds_AD.swap_dims(dims_dict={'it': 't'})
+# # ds_AD = ds_AD.drop_vars('it')
+# # ds_AD.coords['r'] = ('ir', rcoords)
+# # ds_AD = ds_AD.swap_dims(dims_dict={'ir': 'r'})
+# # ds_AD = ds_AD.drop_vars('ir')
+ return ds_AD
+
+ # --------------------------------------------------------------------------------
+ # --- Converters
+ # --------------------------------------------------------------------------------
+ def toOUTB(self, filename=None, extension='.outb', fileID=4, noOverWrite=True, **kwargs):
+ #NOTE: we override the File class here
+ if filename is None:
+ base, _ = os.path.splitext(self.filename)
+ filename = base + extension
+ else:
+ base, ext = os.path.splitext(filename)
+ if len(ext)!=0:
+ extension = ext
+ if (filename==self.filename) and noOverWrite:
+ raise Exception('Not overwritting {}. Specify a filename or an extension.'.format(filename))
+
+ # NOTE: fileID=2 will chop the channels name of long channels use fileID4 instead
+ channels = self.data
+ chanNames = self.channels
+ chanUnits = self.units
+ descStr = self.description
+ if isinstance(descStr, list):
+ descStr=(''.join(descStr[:2])).replace('\n','')
+ writeBinary(filename, channels, chanNames, chanUnits, fileID=fileID, descStr=descStr)
+
+
+# --------------------------------------------------------------------------------
+# --- Helper low level 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 load_ascii_output(filename, method='numpy', encoding='ascii', **kwargs):
+
+
+ if method in ['forLoop','pandas']:
+ from .file import numberOfLines
+ nLines = numberOfLines(filename, method=2)
+
+ with open(filename, encoding=encoding, errors='ignore') as f:
+ info = {}
+ info['name'] = os.path.splitext(os.path.basename(filename))[0]
+ # Header is whatever is before the keyword `time`
+ header = []
+ maxHeaderLines=35
+ headerRead = False
+ for i in range(maxHeaderLines):
+ l = f.readline()
+ if not l:
+ raise Exception('Error finding the end of FAST out file header. Keyword Time missing.')
+ # Check for utf-16
+ if l[:3] == '\x00 \x00':
+ f.close()
+ encoding=''
+ print('[WARN] Attempt to re-read the file with encoding utf-16')
+ return load_ascii_output(filename=filename, method=method, encoding='utf-16')
+ first_word = (l+' dummy').lower().split()[0]
+ in_header= (first_word != 'time') and (first_word != 'alpha')
+ if in_header:
+ header.append(l)
+ else:
+ info['description'] = header
+ info['attribute_names'] = l.split()
+ info['attribute_units'] = [unit[1:-1] for unit in f.readline().split()]
+ headerRead=True
+ break
+ if not headerRead:
+ raise WrongFormatError('Could not find the keyword "Time" or "Alpha" in the first {} lines of the file {}'.format(maxHeaderLines, filename))
+
+ nHeader = len(header)+1
+ nCols = len(info['attribute_names'])
+
+ if method=='numpy':
+ # The most efficient, and will remove empty lines and the lines that starts with "This"
+ # ("This" is found at the end of some Hydro Out files..)
+ data = np.loadtxt(f, comments=('This'))
+
+ elif method =='pandas':
+ # Could probably be made more efficient, but
+ f.close()
+ nRows = nLines-nHeader
+ sep=r'\s+'
+ cols= ['C{}'.format(i) for i in range(nCols)]
+ df = pd.read_csv(filename, sep=sep, header=0, skiprows=nHeader, names=cols, dtype=float, na_filter=False, nrows=nRows, engine='pyarrow'); print(df)
+ data=df.values
+
+ elif method == 'forLoop':
+ # The most inefficient
+ nRows = nLines-nHeader
+ sep=r'\s+'
+ data = np.zeros((nRows, nCols))
+ for i in range(nRows):
+ l = f.readline().strip()
+ sp = np.array(l.split()).astype(float)
+ data[i,:] = sp[:nCols]
+
+ elif method == 'listCompr':
+ # --- Method 4 - List comprehension
+ # Data, up to end of file or empty line (potential comment line at the end)
+ data = np.array([l.strip().split() for l in takewhile(lambda x: len(x.strip())>0, f.readlines())]).astype(float)
+ else:
+ raise NotImplementedError()
+
+ return data, info
+
+
+def load_binary_output(filename, use_buffer=False, method='mix', **kwargs):
+ """
+ 03/09/15: Ported from ReadFASTbinary.m by Mads M Pedersen, DTU Wind
+ 24/10/18: Low memory/buffered version by E. Branlard, NREL
+ 18/01/19: New file format for extended channels, by E. Branlard, NREL
+ 20/11/23: Improved performances using np.fromfile, by E. Branlard, NREL
+ """
+ StructDict = {
+ 'uint8': ('B', 1, np.uint8),
+ 'int16': ('h', 2, np.int16),
+ 'int32': ('i', 4, np.int32),
+ 'float32': ('f', 4, np.float32),
+ 'float64': ('d', 8, np.float64)
+ }
+ def getFileSizeMB(filename):
+ return os.path.getsize(filename)/(1024.0**2)
+
+ def freadStruct(fid, n, dtype):
+ fmt, nbytes, npdtype = StructDict[dtype]
+ return struct.unpack(fmt * n, fid.read(nbytes * n))
+
+ def freadStructArray(fid, n, dtype):
+ fmt, nbytes, npdtype = StructDict[dtype]
+ return np.array(struct.unpack(fmt * n, fid.read(nbytes * n)))
+
+ def freadNumpy(fid, n, dtype):
+ fmt, nbytes, npdtype = StructDict[dtype]
+ return np.fromfile(fid, count=n, dtype=npdtype) # Improved performances
+
+ if method=='numpy':
+ fread = freadNumpy
+ freadLarge = freadNumpy
+ elif method=='struct':
+ fread = freadStruct
+ freadLarge = freadStructArray
+ elif method=='mix':
+ fread = freadStruct
+ freadLarge = freadNumpy
+ elif method=='optim':
+ # Decide on method on the fly
+ #MB = getFileSizeMB(filename)
+ use_buffer = False
+ fread = freadStruct
+ freadLarge = freadNumpy
+ else:
+ raise NotImplementedError
+
+ def freadRowOrderTableBuffered(fid, n, type_in, nCols, nOff=0, type_out='float64'):
+ """
+ Reads of row-ordered table from a binary file.
+
+ Read `n` data of type `type_in`, assumed to be a row ordered table of `nCols` columns.
+ Memory usage is optimized by allocating the data only once.
+ Buffered reading is done for improved performances (in particular for 32bit python)
+
+ `nOff` allows for additional column space at the begining of the storage table.
+ Typically, `nOff=1`, provides a column at the beginning to store the time vector.
+
+ @author E.Branlard, NREL
+
+ """
+ fmt, nbytes = StructDict[type_in][:2]
+ nLines = int(n/nCols)
+ GoodBufferSize = 4096*40
+ nLinesPerBuffer = int(GoodBufferSize/nCols)
+ BufferSize = nCols * nLinesPerBuffer
+ nBuffer = int(n/BufferSize)
+ # Allocation of data
+ data = np.zeros((nLines,nCols+nOff), dtype = type_out)
+ # Reading
+ try:
+ nIntRead = 0
+ nLinesRead = 0
+ while nIntRead0:
+ op,cl = chars
+ iu=c.rfind(op)
+ if iu>1:
+ name = c[:iu]
+ unit = c[iu+1:].replace(cl,'')
+ if name[-1]=='_':
+ name=name[:-1]
+
+ chanNames.append(name)
+ chanUnits.append(unit)
+
+ if binary:
+ writeBinary(filename, channels, chanNames, chanUnits, fileID=FileFmtID_ChanLen_In)
+ else:
+ NotImplementedError()
+
+def findDriverFileFAST(base):
+ Files=[base+ext for ext in ['.fst','.FST','.Fst','.dvr','.Dvr','.DVR'] if os.path.exists(base+ext)]
+ if len(Files)>0:
+ return Files[0]
+ return None
+
+def findDriverFileFASTFarm(base):
+ Files=[base+ext for ext in ['.fstf','.FSTF','.Fstf','.fmas','.FMAS','.Fmas'] if os.path.exists(base+ext)]
+ if len(Files)>0:
+ return Files[0]
+ return None
+
+def findDriverFile(filename, df=None):
+ """
+ OUTPUTS:
+ - driver : filename of driver
+ - FASTFarm: logical
+ """
+ base,out_ext = os.path.splitext(filename)
+ # HACK for AD file to find the right .fst file
+ iDotAD = base.lower().find('.ad')
+ if iDotAD>1:
+ base=base[:iDotAD]
+
+ #FASTFarmKind = None
+ if df is not None:
+ sCols = ''.join(df.columns)
+ FASTFarmKind = sCols.find('WkDf')>1 or sCols.find('CtT')>0
+ if FASTFarmKind:
+ return findDriverFileFASTFarm(base), FASTFarmKind
+ else:
+ return findDriverFileFAST(base), FASTFarmKind
+ else:
+ driver = findDriverFileFASTFarm(base)
+ if driver:
+ return driver, True
+ else:
+ driver = findDriverFileFAST(base)
+ return driver, False
+
+
+
+
+
+
+if __name__ == "__main__":
+ scriptDir = os.path.dirname(__file__)
+ B=FASTOutputFile(os.path.join(scriptDir, 'tests/example_files/FASTOutBin.outb'))
+ B.to2DFields()
+ df=B.toDataFrame()
+ B.writeDataFrame(df, os.path.join(scriptDir, 'tests/example_files/FASTOutBin_OUT.outb'))
+ B.toOUTB(extension='.dat.outb')
+ B.toParquet()
+ B.toCSV()
+
+
diff --git a/weio/fast_summary_file.py b/weio/fast_summary_file.py
index 86a75f2..54f15dc 100644
--- a/weio/fast_summary_file.py
+++ b/weio/fast_summary_file.py
@@ -1,6 +1,5 @@
import numpy as np
import pandas as pd
-from io import open
import os
# Local
from .mini_yaml import yaml_read
@@ -57,15 +56,38 @@ def read(self, filename=None, header_only=False):
raise OSError(2,'File not found:',self.filename)
if os.stat(self.filename).st_size == 0:
raise EmptyFileError('File is empty:',self.filename)
-
+ # Children class
+ self._read()
+
+ def _read(self):
+ def readFirstLines(fid, nLines):
+ lines=[]
+ for i, line in enumerate(fid):
+ lines.append(line.strip())
+ if i==nLines:
+ break
+ return lines
with open(self.filename, 'r', errors="surrogateescape") as fid:
header= readFirstLines(fid, 4)
if any(['subdyn' in s.lower() for s in header]):
self['module']='SubDyn'
- readSubDynSum(self)
+ fsum = SubDynSummaryFile(self.filename)
+ self.update_from_child(fsum, attrList=fsum.attributes())
+ elif any(['beamdyn' in s.lower() for s in header]):
+ self['module']='BeamDyn'
+ fsum = BeamDynSummaryFile(self.filename)
+ self.update_from_child(fsum, attrList=fsum.attributes())
else:
raise NotImplementedError('This summary file format is not yet supported')
+ def update_from_child(self, child, attrList=None):
+ self.update(child)
+ if attrList is None:
+ attrList = [c for c in child.__dir__() if not c.startswith('_')]
+ for attr in attrList:
+ print('FASTSummaryFile from child {}, setting `{}`'.format(type(child).__name__, attr))
+ setattr(self, attr, getattr(child, attr))
+
def toDataFrame(self):
if 'module' not in self.keys():
raise Exception('');
@@ -81,38 +103,46 @@ def toGraph(self):
# --------------------------------------------------------------------------------}
-# --- Helper functions
+# --- SubDyn Summary File
# --------------------------------------------------------------------------------{
-def readFirstLines(fid, nLines):
- lines=[]
- for i, line in enumerate(fid):
- lines.append(line.strip())
- if i==nLines:
- break
- return lines
+class SubDynSummaryFile(FASTSummaryFile):
-# --------------------------------------------------------------------------------}
-# --- Sub-reader/class for SubDyn summary files
-# --------------------------------------------------------------------------------{
-def readSubDynSum(self):
-
- # Read data
- #T=yaml.load(fid, Loader=yaml.SafeLoader)
- yaml_read(self.filename, self)
-
- # --- Treatement of useful data
- if self['DOF2Nodes'].shape[1]==3:
- self['DOF2Nodes']=np.column_stack((np.arange(self['DOF2Nodes'].shape[0])+1,self['DOF2Nodes']))
- # NOTE: DOFs are reindexed to start at 0
- self['DOF2Nodes'][:,0]-=1
- self['DOF___L'] -=1 # internal DOFs
- self['DOF___B'] -=1 # internal
- self['DOF___F'] -=1 # fixed DOFs
-
- self['CB_frequencies']=self['CB_frequencies'].ravel()
- self['X'] = self['Nodes'][:,1].astype(float)
- self['Y'] = self['Nodes'][:,2].astype(float)
- self['Z'] = self['Nodes'][:,3].astype(float)
+ @staticmethod
+ def formatName():
+ return 'SubDyn summary file'
+
+ @staticmethod
+ def attributes():
+ attr = ['formatName']
+ attr += ['_read']
+ attr += ['toDataFrame']
+ attr += ['NodesDisp' ]
+ attr += ['toDataFrame']
+ attr += ['toJSON' ]
+ attr += ['getModes' ]
+ return attr
+
+ def _read(self):
+ """ """
+ #T=yaml.load(fid, Loader=yaml.SafeLoader)
+ yaml_read(self.filename, self)
+
+ # --- Treatement of useful data
+ if self['DOF2Nodes'].shape[1]==3:
+ self['DOF2Nodes']=np.column_stack((np.arange(self['DOF2Nodes'].shape[0])+1,self['DOF2Nodes']))
+ # NOTE: DOFs are reindexed to start at 0
+ self['DOF2Nodes'][:,0]-=1
+ self['DOF___L'] -=1 # internal DOFs
+ self['DOF___B'] -=1 # internal
+ self['DOF___F'] -=1 # fixed DOFs
+
+ self['CB_frequencies']=self['CB_frequencies'].ravel()
+ self['X'] = self['Nodes'][:,1].astype(float)
+ self['Y'] = self['Nodes'][:,2].astype(float)
+ self['Z'] = self['Nodes'][:,3].astype(float)
+
+ def toDataFrame(self):
+ return None
# --- Useful methods that will be added to the class
def NodesDisp(self, IDOF, UDOF, maxDisp=None, sortDim=None):
@@ -150,7 +180,7 @@ def NodesDisp(self, IDOF, UDOF, maxDisp=None, sortDim=None):
pos = pos[I,:]
return disp, pos, INodes
- def getModes(data, maxDisp=None, sortDim=2):
+ def getModes(self, maxDisp=None, sortDim=None):
""" return Guyan and CB modes"""
if maxDisp is None:
#compute max disp such as it's 10% of maxdimension
@@ -160,37 +190,38 @@ def getModes(data, maxDisp=None, sortDim=2):
maxDisp = np.max([dx,dy,dz])*0.1
# NOTE: DOF have been reindexed -1
- DOF_B = data['DOF___B'].ravel()
- DOF_F = data['DOF___F'].ravel()
- DOF_K = (np.concatenate((DOF_B,data['DOF___L'].ravel(), DOF_F))).astype(int)
+ DOF_B = self['DOF___B'].ravel()
+ DOF_F = self['DOF___F'].ravel()
+ DOF_K = (np.concatenate((DOF_B,self['DOF___L'].ravel(), DOF_F))).astype(int)
# CB modes
- PhiM = data['PhiM']
+ PhiM = self['PhiM']
Phi_CB = np.vstack((np.zeros((len(DOF_B),PhiM.shape[1])),PhiM, np.zeros((len(DOF_F),PhiM.shape[1]))))
- dispCB, posCB, INodesCB = data.NodesDisp(DOF_K, Phi_CB, maxDisp=maxDisp, sortDim=sortDim)
+ dispCB, posCB, INodesCB = self.NodesDisp(DOF_K, Phi_CB, maxDisp=maxDisp, sortDim=sortDim)
# Guyan modes
- PhiR = data['PhiR']
+ PhiR = self['PhiR']
Phi_Guyan = np.vstack((np.eye(len(DOF_B)),PhiR, np.zeros((len(DOF_F),PhiR.shape[1]))))
- dispGy, posGy, INodesGy = data.NodesDisp(DOF_K, Phi_Guyan, maxDisp=maxDisp, sortDim=sortDim)
+ dispGy, posGy, INodesGy = self.NodesDisp(DOF_K, Phi_Guyan, maxDisp=maxDisp, sortDim=sortDim)
return dispGy, posGy, INodesGy, dispCB, posCB, INodesCB
- def subDynToJson(data, outfile=None):
+ def toJSON(self, outfile=None):
""" Convert to a "JSON" format
TODO: convert to graph and use graph.toJSON
"""
+ #return self.toGraph().toJSON(outfile)
- dispGy, posGy, _, dispCB, posCB, _ = data.getModes()
+ dispGy, posGy, _, dispCB, posCB, _ = self.getModes(sortDim=None) # Sorting mess things up
- Nodes = self['Nodes']
- Elements = self['Elements']
+ Nodes = self['Nodes'].copy()
+ Elements = self['Elements'].copy()
Elements[:,0]-=1
Elements[:,1]-=1
Elements[:,2]-=1
- CB_freq = data['CB_frequencies'].ravel()
+ CB_freq = self['CB_frequencies'].ravel()
d=dict();
d['Connectivity']=Elements[:,[1,2]].astype(int).tolist();
@@ -210,7 +241,7 @@ def subDynToJson(data, outfile=None):
'omega':CB_freq[iMode]*2*np.pi, #in [rad/s]
'Displ':dispCB[:,:,iMode].tolist()
} for iMode in range(dispCB.shape[2]) ]
- d['groundLevel']=np.min(data['Z']) # TODO
+ d['groundLevel']=np.min(self['Z']) # TODO
if outfile is not None:
import json
@@ -222,7 +253,7 @@ def subDynToJson(data, outfile=None):
return d
- def subDynToDataFrame(data):
+ def toDataFrame(self, sortDim=2, removeZero=True):
""" Convert to DataFrame containing nodal displacements """
def toDF(pos,disp,preffix=''):
disp[np.isnan(disp)]=0
@@ -235,14 +266,15 @@ def toDF(pos,disp,preffix=''):
disptot= np.moveaxis(disptot,2,1).reshape(disptot.shape[0],disptot.shape[1]*disptot.shape[2])
disp = np.moveaxis(disp,2,1).reshape(disp.shape[0],disp.shape[1]*disp.shape[2])
df= pd.DataFrame(data = disptot ,columns = columns)
- # remove zero
dfDisp= pd.DataFrame(data = disp ,columns = columns)
- df = df.loc[:, (dfDisp != 0).any(axis=0)]
- dfDisp = dfDisp.loc[:, (dfDisp != 0).any(axis=0)]
+ # remove mode components that are fully zero
+ if removeZero:
+ df = df.loc[:, (dfDisp != 0).any(axis=0)]
+ dfDisp = dfDisp.loc[:, (dfDisp != 0).any(axis=0)]
dfDisp.columns = [c.replace('Mode','Disp') for c in dfDisp.columns.values]
return df, dfDisp
- dispGy, posGy, _, dispCB, posCB, _ = data.getModes()
+ dispGy, posGy, _, dispCB, posCB, _ = self.getModes(sortDim=sortDim)
columns = ['z_[m]','x_[m]','y_[m]']
dataZXY = np.column_stack((posGy[:,2],posGy[:,0],posGy[:,1]))
@@ -252,13 +284,42 @@ def toDF(pos,disp,preffix=''):
df = pd.concat((dfZXY, df1, df2, df1d, df2d), axis=1)
return df
- # adding method to class dynamically to give it a "SubDyn Summary flavor"
- setattr(FASTSummaryFile, 'NodesDisp' , NodesDisp)
- setattr(FASTSummaryFile, 'toDataFrame', subDynToDataFrame)
- setattr(FASTSummaryFile, 'toJSON' , subDynToJson)
- setattr(FASTSummaryFile, 'getModes' , getModes)
- return self
+
+
+# --------------------------------------------------------------------------------}
+# --- BeamDyn
+# --------------------------------------------------------------------------------{
+class BeamDynSummaryFile(FASTSummaryFile):
+
+ @staticmethod
+ def formatName():
+ return 'BeamDyn summary file'
+
+ @staticmethod
+ def attributes():
+ attr = ['formatName']
+ attr += ['_read']
+ attr += ['toDataFrame']
+ return attr
+
+ def _read(self, filename=None):
+ """ """
+ if not filename:
+ filename = self.filename
+ # Put data into self dict
+ yaml_read(filename, self)
+
+ def toDataFrame(self):
+ #self['Init_QP_E1']
+ #self['M_IEC']
+ #self['K_IEC']
+ df = pd.DataFrame(data=self['Init_Nodes_E1'])
+
+
+ return df
+
+
if __name__=='__main__':
diff --git a/weio/fast_wind_file.py b/weio/fast_wind_file.py
index b642d89..7c46c6e 100644
--- a/weio/fast_wind_file.py
+++ b/weio/fast_wind_file.py
@@ -1,84 +1,72 @@
-from __future__ import division
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import absolute_import
-from io import open
-from builtins import map
-from builtins import range
-from builtins import chr
-from builtins import str
-from future import standard_library
-standard_library.install_aliases()
-
-from .csv_file import CSVFile
-from .file import isBinary, WrongFormatError
-import numpy as np
-import pandas as pd
-
-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 i=nColsStruct:
- struct[iSec,:]=np.array(vals[0:nColsStruct]).astype(float)
- #elif self.version==1:
- # # version 1 has either 8 or 9 columns
- # nColsStruct=nColsStruct-1
- # struct_headers=struct_headers[0:-1]
- # struct =struct[:,:-1]
- # struct[iSec,:]=np.array(vals[0:nColsStruct]).astype(float)
- except:
- raise WrongFormatError('Unable to read structural data')
- try:
- self.BetaC = float(f.readline().strip().split()[0])
- if self.version==3:
- f.readline()
- self.FlapDamping = [float(v) for v in f.readline().strip().split(';')[0].split()]
- self.EdgeDamping = [float(v) for v in f.readline().strip().split(';')[0].split()]
- self.TorsDamping = [float(v) for v in f.readline().strip().split(';')[0].split()]
- f.readline()
- f.readline()
- else:
- Damping = [float(v) for v in f.readline().strip().split()[0:4]]
- self.FlapDamping = Damping[0:2]
- self.EdgeDamping = Damping[2:4]
- self.TorsDamping = []
- except:
- raise
- raise WrongFormatError('Unable to read damping data')
-
- # --- Aero
- try:
- for iSec in range(nSections):
- vals=f.readline().split()[0:nColsAero]
- aero[iSec,:]=np.array(vals).astype(float)
- except:
- raise WrongFormatError('Unable to read aerodynamic data')
-
- self.ProfileFile=f.readline().strip()
-
- # --- Concatenating aero and structural data
- self._cols = struct_headers+aero_headers[1:]
- data = np.column_stack((struct,aero[:,1:]))
- dataMiss=pd.DataFrame(data=data, columns=self._cols)
- self._nColsStruct=nColsStruct # to remember where to split
- # --- Making sure all columns are present, irrespectively of version
- self.data=pd.DataFrame(data=[], columns=headers_all)
- for c in self._cols:
- self.data[c]=dataMiss[c]
-
-# def toString(self):
-# s=''
-# if len(self.ProfileSets)>0:
-# prefix='PROFILE SET '
-# else:
-# prefix=''
-# for pset in self.ProfileSets:
-# s+=pset.toString(prefix)
-# return s
-#
-# def _write(self):
-# with open(self.filename,'w') as f:
-# f.write(self.toString)
-#
- def __repr__(self):
- s ='Class FLEXBladeFile (attributes: data, BetaC, FlapDamping, EdgeDamping, ProfileFile)\n'
- return s
-
- def _toDataFrame(self):
- return self.data
-
+import numpy as np
+import pandas as pd
+import os
+
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File = dict
+ class WrongFormatError(Exception): pass
+ class BrokenFormatError(Exception): pass
+
+class FLEXBladeFile(File):
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.bld','.bla','.00X'] #'.001 etc..'
+
+ @staticmethod
+ def formatName():
+ return 'FLEX blade file'
+
+ def _read(self):
+ headers_all = ['r_[m]','EIFlap_[Nm2]','EIEdge_[Nm2]','GKt_[Nm2]','Mass_[kg/m]','Jxx_[kg.m]','PreBendFlap_[m]','PreBendEdge_[m]'\
+ ,'Str.Twist_[deg]','PhiOut_[deg]','Ycog_[m]','Yshc_[m]','CalcOutput_[0/1]'\
+ ,'Chord_[m]','AeroTwist_[deg]','RelThickness_[%]','AeroCenter_[m]','AeroTorsion_[0/1]','ProfileSet_[#]']
+ with open(self.filename, 'r', errors="surrogateescape") as f:
+ try:
+ firstline = f.readline().strip()
+ nSections = int(f.readline().strip().split()[0])
+ except:
+ raise WrongFormatError('Unable to read first two lines of blade file')
+ try:
+ self.version=int(firstline[1:4])
+ except:
+ self.version=0
+ # --- Different handling depending on version
+ if self.version==0:
+ # Version 0 struct has no GKt
+ # Version 0 aero has no profile set, no TorsionAero
+ nColsStruct = 8
+ nColsAero = 5
+ struct_headers = ['r_[m]','EIFlap_[Nm2]','EIEdge_[Nm2]','Mass_[kg/m]','Str.Twist_[deg]','CalcOutput_[0/1]','PreBendFlap_[m]','PreBendEdge_[m]']
+ aero_headers = ['X_BladeRoot_[m]','Chord_[m]','AeroTwist_[deg]','RelThickness_[%]','AeroCenter_[m]']
+ elif self.version==1:
+ # Version 1 struct has GKt
+ # Version 1 aero has no profile set
+ nColsStruct = 8
+ nColsAero = 6
+ struct_headers = ['r_[m]','EIFlap_[Nm2]','EIEdge_[Nm2]','Mass_[kg/m]','Str.Twist_[deg]','CalcOutput_[0/1]','PreBendFlap_[m]','PreBendEdge_[m]']
+ aero_headers = ['X_BladeRoot_[m]','Chord_[m]','AeroTwist_[deg]','RelThickness_[%]','AeroCenter_[m]','AeroTorsion_[0/1]']
+ elif self.version==2:
+ nColsStruct = 9
+ nColsAero = 7
+ struct_headers = ['r_[m]','EIFlap_[Nm2]','EIEdge_[Nm2]','Mass_[kg/m]','Str.Twist_[deg]','CalcOutput_[0/1]','PreBendFlap_[m]','PreBendEdge_[m]','GKt_[Nm2]']
+ aero_headers = ['X_BladeRoot_[m]','Chord_[m]','AeroTwist_[deg]','RelThickness_[%]','AeroCenter_[m]','AeroTorsion_[0/1]','ProfileSet_[#]']
+ elif self.version==3:
+ nColsStruct = 13
+ nColsAero = 7
+ struct_headers = ['r_[m]','EIFlap_[Nm2]','EIEdge_[Nm2]','GKt_[Nm2]','Mass_[kg/m]','Jxx_[kg.m]','PreBendFlap_[m]','PreBendEdge_[m]','Str.Twist_[deg]','PhiOut_[deg]','Ycog_[m]','Yshc_[m]','CalcOutput_[0/1]']
+ aero_headers = ['X_BladeRoot_[m]','Chord_[m]','AeroTwist_[deg]','RelThickness_[%]','AeroCenter_[m]','AeroTorsion_[0/1]','ProfileSet_[#]']
+ else:
+ raise BrokenFormatError('Blade format not implemented')
+
+ struct = np.zeros((nSections,nColsStruct))
+ aero = np.zeros((nSections,nColsAero))
+
+ # --- Structural data
+ try:
+ for iSec in range(nSections):
+ vals=f.readline().split()
+ #if len(vals)>=nColsStruct:
+ struct[iSec,:]=np.array(vals[0:nColsStruct]).astype(float)
+ #elif self.version==1:
+ # # version 1 has either 8 or 9 columns
+ # nColsStruct=nColsStruct-1
+ # struct_headers=struct_headers[0:-1]
+ # struct =struct[:,:-1]
+ # struct[iSec,:]=np.array(vals[0:nColsStruct]).astype(float)
+ except:
+ raise WrongFormatError('Unable to read structural data')
+ try:
+ self.BetaC = float(f.readline().strip().split()[0])
+ if self.version==3:
+ f.readline()
+ self.FlapDamping = [float(v) for v in f.readline().strip().split(';')[0].split()]
+ self.EdgeDamping = [float(v) for v in f.readline().strip().split(';')[0].split()]
+ self.TorsDamping = [float(v) for v in f.readline().strip().split(';')[0].split()]
+ f.readline()
+ f.readline()
+ else:
+ Damping = [float(v) for v in f.readline().strip().split()[0:4]]
+ self.FlapDamping = Damping[0:2]
+ self.EdgeDamping = Damping[2:4]
+ self.TorsDamping = []
+ except:
+ raise
+ raise WrongFormatError('Unable to read damping data')
+
+ # --- Aero
+ try:
+ for iSec in range(nSections):
+ vals=f.readline().split()[0:nColsAero]
+ aero[iSec,:]=np.array(vals).astype(float)
+ except:
+ raise WrongFormatError('Unable to read aerodynamic data')
+
+ self.ProfileFile=f.readline().strip()
+
+ # --- Concatenating aero and structural data
+ self._cols = struct_headers+aero_headers[1:]
+ data = np.column_stack((struct,aero[:,1:]))
+ dataMiss=pd.DataFrame(data=data, columns=self._cols)
+ self._nColsStruct=nColsStruct # to remember where to split
+ # --- Making sure all columns are present, irrespectively of version
+ self.data=pd.DataFrame(data=[], columns=headers_all)
+ for c in self._cols:
+ self.data[c]=dataMiss[c]
+
+# def toString(self):
+# s=''
+# if len(self.ProfileSets)>0:
+# prefix='PROFILE SET '
+# else:
+# prefix=''
+# for pset in self.ProfileSets:
+# s+=pset.toString(prefix)
+# return s
+#
+# def _write(self):
+# with open(self.filename,'w') as f:
+# f.write(self.toString)
+#
+ def __repr__(self):
+ s ='Class FLEXBladeFile (attributes: data, BetaC, FlapDamping, EdgeDamping, ProfileFile)\n'
+ return s
+
+ def _toDataFrame(self):
+ return self.data
+
diff --git a/weio/flex_doc_file.py b/weio/flex_doc_file.py
index aeac114..6fd0107 100644
--- a/weio/flex_doc_file.py
+++ b/weio/flex_doc_file.py
@@ -1,14 +1,13 @@
-from __future__ import division,unicode_literals,print_function,absolute_import
-from builtins import map, range, chr, str
-from io import open
-from future import standard_library
-standard_library.install_aliases()
-
-from .file import File, WrongFormatError, BrokenFormatError
import numpy as np
import pandas as pd
import os
import re
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File = dict
+ class WrongFormatError(Exception): pass
+ class BrokenFormatError(Exception): pass
class FLEXDocFile(File):
diff --git a/weio/flex_out_file.py b/weio/flex_out_file.py
index d9c95e5..8b88bd0 100644
--- a/weio/flex_out_file.py
+++ b/weio/flex_out_file.py
@@ -1,200 +1,205 @@
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import unicode_literals
-from __future__ import print_function
-from io import open
-from builtins import map
-from builtins import range
-from builtins import chr
-from builtins import str
-from future import standard_library
-standard_library.install_aliases()
-
-from .file import File, WrongFormatError, BrokenFormatError
-import numpy as np
-import pandas as pd
-import os
-
-#from .wetb.fast import fast_io
-
-
-
-# --------------------------------------------------------------------------------}
-# --- OUT FILE
-# --------------------------------------------------------------------------------{
-class FLEXOutFile(File):
-
- @staticmethod
- def defaultExtensions():
- return ['.res','.int']
-
- @staticmethod
- def formatName():
- return 'FLEX output file'
-
- def _read(self):
- # --- First read the binary file
- dtype=np.float32; # Flex internal data is stored in single precision
- try:
- self.data,self.tmin,self.dt,self.Version,self.DateID,self.title=read_flex_res(self.filename, dtype=dtype)
- except WrongFormatError as e:
- raise WrongFormatError('FLEX File {}: '.format(self.filename)+'\n'+e.args[0])
- self.nt = np.size(self.data,0)
- self.nSensors = np.size(self.data,1)
- self.time = np.arange(self.tmin, self.tmin + self.nt * self.dt, self.dt).reshape(self.nt,1).astype(dtype)
-
- # --- Then the sensor file
- sensor_filename = os.path.join(os.path.dirname(self.filename), "sensor")
- if not os.path.isfile(sensor_filename):
- # we are being nice and create some fake sensors info
- self.sensors=read_flex_sensor_fake(self.nSensors)
- else:
- self.sensors=read_flex_sensor(sensor_filename)
- if len(self.sensors['ID'])!=self.nSensors:
- raise BrokenFormatError('Inconsistent number of sensors: {} (sensor file) {} (out file), for file: {}'.format(len(self.sensors['ID']),self.nSensors,self.filename))
-
- #def _write(self): # TODO
- # pass
-
- def __repr__(self):
- return 'Flex Out File: {}\nVersion:{} - DateID:{} - Title:{}\nSize:{}x{} - tmin:{} - dt:{}]\nSensors:{}'.format(self.filename,self.Version,self.DateID,self.title,self.nt,self.nSensors,self.tmin,self.dt,self.sensors['Name'])
-
- def _toDataFrame(self):
- # Appending time to form the dataframe
- names = ['Time'] + self.sensors['Name']
- units = ['s'] + self.sensors['Unit']
- units = [u.replace('(','').replace(')','').replace('[','').replace(']','') for u in units]
- data = np.concatenate((self.time, self.data), axis=1)
- cols=[n+'_['+u+']' for n,u in zip(names,units)]
- return pd.DataFrame(data=data,columns=cols)
-
-# --------------------------------------------------------------------------------}
-# --- Helper Functions
-# --------------------------------------------------------------------------------{
-def read_flex_res(filename, dtype=np.float32):
- # Read flex file
- with open(filename,'rb') as fid:
- #_ = struct.unpack('i', fid.read(4)) # Dummy
- _ = np.fromfile(fid, 'int32', 1) # Dummy
- # --- Trying to get DateID
- fid.seek(4) #
- DateID=np.fromfile(fid, 'int32', 6)
- if DateID[0]<32 and DateID[1]<13 and DateID[3]<25 and DateID[4]<61:
- # OK, DateID was present
- title = fid.read(40).strip()
- else:
- fid.seek(4) #
- DateID = np.fromfile(fid, 'int32', 1)
- title = fid.read(60).strip()
- _ = np.fromfile(fid, 'int32', 2) # Dummy
- # FILE POSITION <<< fid.seek(4 * 19)
- nSensors = np.fromfile(fid, 'int32', 1)[0]
- IDs = np.fromfile(fid, 'int32', nSensors)
- _ = np.fromfile(fid, 'int32', 1) # Dummy
- # FILE POSITION <<< fid.seek(4*nSensors+4*21)
- Version = np.fromfile(fid, 'int32', 1)[0]
- # FILE POSITION <<< fid.seek(4*(nSensors)+4*22)
- if Version == 12:
- raise NotImplementedError('Flex out file with version 12, TODO. Implement it!')
- # TODO
- #fseek(o.fid,4*(21+o.nSensors),-1);% seek to the data from beginning of file
- #RL=o.nSensors+5; % calculate the length of each row
- #A = fread(o.fid,[RL,inf],'single'); % read whole file
- #t=A(2,:);% time vector contained in row 2
- #o.SensorData=A(5:end,:);
- # save relevant information
- #o.tmin = t(1) ;
- #o.dt = t(2)-t(1);
- #o.t = t ;
- #o.nt = length(t);
- elif Version in [0,2,3]:
- tmin = np.fromfile(fid, 'f', 1)[0] # Dummy
- dt = np.fromfile(fid, 'f', 1)[0] # Dummy
- scale_factors = np.fromfile(fid, 'f', nSensors).astype(dtype)
- # --- Reading Time series
- # FILE POSITION <<< fid.seek(8*nSensors + 48*2)
- data = np.fromfile(fid, 'int16').astype(dtype) #data = np.fromstring(fid.read(), 'int16').astype(dtype)
- nt = int(len(data) / nSensors)
- try:
- if Version ==3:
- data = data.reshape(nSensors, nt).transpose()
- else:
- data = data.reshape(nt, nSensors)
- except ValueError:
- raise WrongFormatError("Flat data length {} is not compatible with {}x{} (nt x nSensors)".format(len(data),nt,nSensors))
- for i in range(nSensors):
- data[:, i] *= scale_factors[i]
-
- return (data,tmin,dt,Version,DateID,title)
-
-
-def read_flex_sensor(sensor_file):
- with open(sensor_file, encoding="utf-8") as fid:
- sensor_info_lines = fid.readlines()[2:]
- sensor_info = []
- d=dict({ 'ID':[],'Gain':[],'Offset':[],'Unit':[],'Name':[],'Description':[]});
- for line in sensor_info_lines:
- line = line.strip().split()
- d['ID'] .append(int(line[0]))
- d['Gain'] .append(float(line[1]))
- d['Offset'] .append(float(line[2]))
- d['Unit'] .append(line[5])
- d['Name'] .append(line[6])
- d['Description'] .append(' '.join(line[7:]))
- return d
-
-def read_flex_sensor_fake(nSensors):
- d=dict({ 'ID':[],'Gain':[],'Offset':[],'Unit':[],'Name':[],'Description':[]});
- for i in range(nSensors):
- d['ID'] .append(i+1)
- d['Gain'] .append(1.0)
- d['Offset'] .append(0.0)
- d['Unit'] .append('(NA)')
- d['Name'] .append('S{:04d}'.format(i+1))
- d['Description'] .append('NA')
- return d
-
-
-
-
-
-
-# def write_flex_file(filename,data,tmin,dt):
-# ds = dataset
-# # Write int data file
-# f = open(filename, 'wb')
-# f.write(struct.pack('ii', 0, 0)) # 2x empty int
-# title = ("%-60s" % str(ds.name)).encode()
-# f.write(struct.pack('60s', title)) # title
-# f.write(struct.pack('ii', 0, 0)) # 2x empty int
-# ns = len(sensors)
-# f.write(struct.pack('i', ns))
-# f.write(struct.pack('i' * ns, *range(1, ns + 1))) # sensor number
-# f.write(struct.pack('ii', 0, 0)) # 2x empty int
-# time = ds.basis_attribute()
-# f.write(struct.pack('ff', time[0], time[1] - time[0])) # start time and time step
-#
-# scale_factors = np.max(np.abs(data), 0) / 32000
-# f.write(struct.pack('f' * len(scale_factors), *scale_factors))
-# # avoid dividing by zero
-# not0 = np.where(scale_factors != 0)
-# data[:, not0] /= scale_factors[not0]
-# #flatten and round
-# data = np.round(data.flatten()).astype(np.int16)
-# f.write(struct.pack('h' * len(data), *data.tolist()))
-# f.close()
-#
-# # write sensor file
-# f = open(os.path.join(os.path.dirname(filename), 'sensor'), 'w')
-# f.write("Sensor list for %s\n" % filename)
-# f.write(" No forst offset korr. c Volt Unit Navn Beskrivelse------------\n")
-# sensorlineformat = "%3s %.3f %.3f 1.00 0.00 %7s %-8s %s\n"
-#
-# if isinstance(ds, FLEX4Dataset):
-# gains = np.r_[ds.gains[1:], np.ones(ds.shape[1] - len(ds.gains))]
-# offsets = np.r_[ds.offsets[1:], np.zeros(ds.shape[1] - len(ds.offsets))]
-# sensorlines = [sensorlineformat % ((nr + 1), gain, offset, att.unit[:7], att.name.replace(" ", "_")[:8], att.description[:512]) for nr, att, gain, offset in zip(range(ns), sensors, gains, offsets)]
-# else:
-# sensorlines = [sensorlineformat % ((nr + 1), 1, 0, att.unit[:7], att.name.replace(" ", "_")[:8], att.description[:512]) for nr, att in enumerate(sensors)]
-# f.writelines(sensorlines)
-# f.close()
+import numpy as np
+import pandas as pd
+import os
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File = dict
+ class WrongFormatError(Exception): pass
+ class BrokenFormatError(Exception): pass
+
+# --------------------------------------------------------------------------------}
+# --- OUT FILE
+# --------------------------------------------------------------------------------{
+class FLEXOutFile(File):
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.res','.int']
+
+ @staticmethod
+ def formatName():
+ return 'FLEX output file'
+
+ def _read(self):
+ # --- First read the binary file
+ dtype=np.float32; # Flex internal data is stored in single precision
+ try:
+ self.data,self.tmin,self.dt,self.Version,self.DateID,self.title=read_flex_res(self.filename, dtype=dtype)
+ except WrongFormatError as e:
+ raise WrongFormatError('FLEX File {}: '.format(self.filename)+'\n'+e.args[0])
+ self.nt = np.size(self.data,0)
+ self.nSensors = np.size(self.data,1)
+ self.time = np.arange(self.tmin, self.tmin + self.nt * self.dt, self.dt).reshape(self.nt,1).astype(dtype)
+
+ # --- Then the sensor file
+ parentdir = os.path.dirname(self.filename)
+ basename = os.path.splitext(os.path.basename(self.filename))[0]
+ #print(parentdir)
+ #print(basename)
+ PossibleFiles=[]
+ PossibleFiles+=[os.path.join(parentdir, basename+'.Sensor')]
+ PossibleFiles+=[os.path.join(parentdir, 'Sensor_'+basename)]
+ PossibleFiles+=[os.path.join(parentdir, 'sensor')]
+ # We try allow for other files
+ Found =False
+ for sf in PossibleFiles:
+ if os.path.isfile(sf):
+ self.sensors=read_flex_sensor(sf)
+ if len(self.sensors['ID'])!=self.nSensors:
+ Found = False
+ else:
+ Found = True
+ break
+ if not Found:
+ # we are being nice and create some fake sensors info
+ self.sensors=read_flex_sensor_fake(self.nSensors)
+
+ if len(self.sensors['ID'])!=self.nSensors:
+ raise BrokenFormatError('Inconsistent number of sensors: {} (sensor file) {} (out file), for file: {}'.format(len(self.sensors['ID']),self.nSensors,self.filename))
+
+ #def _write(self): # TODO
+ # pass
+
+ def __repr__(self):
+ return 'Flex Out File: {}\nVersion:{} - DateID:{} - Title:{}\nSize:{}x{} - tmin:{} - dt:{}]\nSensors:{}'.format(self.filename,self.Version,self.DateID,self.title,self.nt,self.nSensors,self.tmin,self.dt,self.sensors['Name'])
+
+ def _toDataFrame(self):
+ # Appending time to form the dataframe
+ names = ['Time'] + self.sensors['Name']
+ units = ['s'] + self.sensors['Unit']
+ units = [u.replace('(','').replace(')','').replace('[','').replace(']','') for u in units]
+ data = np.concatenate((self.time, self.data), axis=1)
+ cols=[n+'_['+u+']' for n,u in zip(names,units)]
+ return pd.DataFrame(data=data,columns=cols)
+
+# --------------------------------------------------------------------------------}
+# --- Helper Functions
+# --------------------------------------------------------------------------------{
+def read_flex_res(filename, dtype=np.float32):
+ # Read flex file
+ with open(filename,'rb') as fid:
+ #_ = struct.unpack('i', fid.read(4)) # Dummy
+ _ = np.fromfile(fid, 'int32', 1) # Dummy
+ # --- Trying to get DateID
+ fid.seek(4) #
+ DateID=np.fromfile(fid, 'int32', 6)
+ if DateID[0]<32 and DateID[1]<13 and DateID[3]<25 and DateID[4]<61:
+ # OK, DateID was present
+ title = fid.read(40).strip()
+ else:
+ fid.seek(4) #
+ DateID = np.fromfile(fid, 'int32', 1)
+ title = fid.read(60).strip()
+ _ = np.fromfile(fid, 'int32', 2) # Dummy
+ # FILE POSITION <<< fid.seek(4 * 19)
+ nSensors = np.fromfile(fid, 'int32', 1)[0]
+ IDs = np.fromfile(fid, 'int32', nSensors)
+ _ = np.fromfile(fid, 'int32', 1) # Dummy
+ # FILE POSITION <<< fid.seek(4*nSensors+4*21)
+ Version = np.fromfile(fid, 'int32', 1)[0]
+ # FILE POSITION <<< fid.seek(4*(nSensors)+4*22)
+ if Version == 12:
+ raise NotImplementedError('Flex out file with version 12, TODO. Implement it!')
+ # TODO
+ #fseek(o.fid,4*(21+o.nSensors),-1);% seek to the data from beginning of file
+ #RL=o.nSensors+5; % calculate the length of each row
+ #A = fread(o.fid,[RL,inf],'single'); % read whole file
+ #t=A(2,:);% time vector contained in row 2
+ #o.SensorData=A(5:end,:);
+ # save relevant information
+ #o.tmin = t(1) ;
+ #o.dt = t(2)-t(1);
+ #o.t = t ;
+ #o.nt = length(t);
+ elif Version in [0,2,3]:
+ tmin = np.fromfile(fid, 'f', 1)[0] # Dummy
+ dt = np.fromfile(fid, 'f', 1)[0] # Dummy
+ scale_factors = np.fromfile(fid, 'f', nSensors).astype(dtype)
+ # --- Reading Time series
+ # FILE POSITION <<< fid.seek(8*nSensors + 48*2)
+ data = np.fromfile(fid, 'int16').astype(dtype) #data = np.fromstring(fid.read(), 'int16').astype(dtype)
+ nt = int(len(data) / nSensors)
+ try:
+ if Version ==3:
+ data = data.reshape(nSensors, nt).transpose()
+ else:
+ data = data.reshape(nt, nSensors)
+ except ValueError:
+ raise WrongFormatError("Flat data length {} is not compatible with {}x{} (nt x nSensors)".format(len(data),nt,nSensors))
+ for i in range(nSensors):
+ data[:, i] *= scale_factors[i]
+
+ return (data,tmin,dt,Version,DateID,title)
+
+
+def read_flex_sensor(sensor_file):
+ with open(sensor_file, 'r') as fid:
+ sensor_info_lines = fid.readlines()[2:]
+ sensor_info = []
+ d=dict({ 'ID':[],'Gain':[],'Offset':[],'Unit':[],'Name':[],'Description':[]});
+ for line in sensor_info_lines:
+ line = line.strip().split()
+ d['ID'] .append(int(line[0]))
+ d['Gain'] .append(float(line[1]))
+ d['Offset'] .append(float(line[2]))
+ d['Unit'] .append(line[5])
+ d['Name'] .append(line[6])
+ d['Description'] .append(' '.join(line[7:]))
+ return d
+
+def read_flex_sensor_fake(nSensors):
+ d=dict({ 'ID':[],'Gain':[],'Offset':[],'Unit':[],'Name':[],'Description':[]});
+ for i in range(nSensors):
+ d['ID'] .append(i+1)
+ d['Gain'] .append(1.0)
+ d['Offset'] .append(0.0)
+ d['Unit'] .append('(NA)')
+ d['Name'] .append('S{:04d}'.format(i+1))
+ d['Description'] .append('NA')
+ return d
+
+
+
+
+
+
+# def write_flex_file(filename,data,tmin,dt):
+# ds = dataset
+# # Write int data file
+# f = open(filename, 'wb')
+# f.write(struct.pack('ii', 0, 0)) # 2x empty int
+# title = ("%-60s" % str(ds.name)).encode()
+# f.write(struct.pack('60s', title)) # title
+# f.write(struct.pack('ii', 0, 0)) # 2x empty int
+# ns = len(sensors)
+# f.write(struct.pack('i', ns))
+# f.write(struct.pack('i' * ns, *range(1, ns + 1))) # sensor number
+# f.write(struct.pack('ii', 0, 0)) # 2x empty int
+# time = ds.basis_attribute()
+# f.write(struct.pack('ff', time[0], time[1] - time[0])) # start time and time step
+#
+# scale_factors = np.max(np.abs(data), 0) / 32000
+# f.write(struct.pack('f' * len(scale_factors), *scale_factors))
+# # avoid dividing by zero
+# not0 = np.where(scale_factors != 0)
+# data[:, not0] /= scale_factors[not0]
+# #flatten and round
+# data = np.round(data.flatten()).astype(np.int16)
+# f.write(struct.pack('h' * len(data), *data.tolist()))
+# f.close()
+#
+# # write sensor file
+# f = open(os.path.join(os.path.dirname(filename), 'sensor'), 'w')
+# f.write("Sensor list for %s\n" % filename)
+# f.write(" No forst offset korr. c Volt Unit Navn Beskrivelse------------\n")
+# sensorlineformat = "%3s %.3f %.3f 1.00 0.00 %7s %-8s %s\n"
+#
+# if isinstance(ds, FLEX4Dataset):
+# gains = np.r_[ds.gains[1:], np.ones(ds.shape[1] - len(ds.gains))]
+# offsets = np.r_[ds.offsets[1:], np.zeros(ds.shape[1] - len(ds.offsets))]
+# sensorlines = [sensorlineformat % ((nr + 1), gain, offset, att.unit[:7], att.name.replace(" ", "_")[:8], att.description[:512]) for nr, att, gain, offset in zip(range(ns), sensors, gains, offsets)]
+# else:
+# sensorlines = [sensorlineformat % ((nr + 1), 1, 0, att.unit[:7], att.name.replace(" ", "_")[:8], att.description[:512]) for nr, att in enumerate(sensors)]
+# f.writelines(sensorlines)
+# f.close()
diff --git a/weio/flex_profile_file.py b/weio/flex_profile_file.py
index 9a29f17..30d2133 100644
--- a/weio/flex_profile_file.py
+++ b/weio/flex_profile_file.py
@@ -1,147 +1,144 @@
-from __future__ import division,unicode_literals,print_function,absolute_import
-from builtins import map, range, chr, str
-from io import open
-from future import standard_library
-standard_library.install_aliases()
-
-from .file import File, WrongFormatError, BrokenFormatError
-import numpy as np
-import pandas as pd
-import os
-
-#from .wetb.fast import fast_io
-
-class ProfileSet():
- def __init__(self,header,thickness,polars,polar_headers):
- self.header = header
- self.polars = polars
- self.thickness = thickness
- self.polar_headers = polar_headers
-
- def toString(self,PREFIX=''):
- s =PREFIX+self.header+'\n'
- s+=' '.join([str(t) for t in self.thickness])+'\n'
- s+=str(self.polars[0].shape[0])+'\n'
- for ph,t,polar in zip(self.polar_headers,self.thickness,self.polars):
- s+=ph+'\n'
- s+='\n'.join([' '.join(['{:15.7e}'.format(v) for v in line]) for line in polar])
-# s+=ph+'\n'
- return s
-
- def __repr__(self):
- s ='Class ProfileSet (attributes: header, polars, thickness, polar_headers)\n'
- s+=' header : '+self.header+'\n'
- s+=' thickness : '+str(self.thickness)+'\n'
- s+=' Number of polars: '+str(len(self.thickness))+'\n'
- s+=' Alpha values : '+str(self.polars[0].shape[0])+'\n'
- for ip,(ph,t) in enumerate(zip(self.polar_headers,self.thickness)):
- s+= ' Polar: {}, Thickness: {}, Header: {}\n'.format(ip+1,t,ph)
- return s
-
-class FLEXProfileFile(File):
-
- @staticmethod
- def defaultExtensions():
- return ['.pro','.00X'] #'.001 etc..'
-
- @staticmethod
- def formatName():
- return 'FLEX profile file'
-
- def _read(self):
- self.ProfileSets=[]
- setNumber=1
- with open(self.filename, 'r', errors="surrogateescape") as f:
- def read_header(allow_empty=False):
- """ Reads the header of a profile set (4 first lines)
- - The first line may start with "Profile set I:" to indicate a set number
- - Second line is number of thicknesses
- - Third is thicnkesses
- - Fourth is number of alpha values
- """
- header=[]
- for i, line in enumerate(f):
- header.append(line.strip())
- if i==3:
- break
- if len(header)<4:
- if allow_empty:
- return [],[],'',False
- else:
- raise WrongFormatError('A Flex profile file needs at leats 4 lines of headers')
- try:
- nThickness=int(header[1])
- except:
- raise WrongFormatError('Number of thicknesses (integer) should be on line 2')
- try:
- thickness=np.array(header[2].split()).astype(float)
- except:
- raise WrongFormatError('Number of thicknesses (integer) should be on line 2')
- if len(thickness)!=nThickness:
- raise WrongFormatError('Number of thicknesses read ({}) different from the number reported ({})'.format(len(thickness),nThickness))
- try:
- nAlpha=int(header[3])
- except:
- raise WrongFormatError('Number of alpha values (integer) should be on line 4')
- if header[0].lower().find('profile set')==0:
- header[0]=header[0][11:]
- bHasSets=True
- else:
- bHasSets=False
- return nAlpha,thickness,header[0],bHasSets
-
- def read_polars(nAlpha,thickness):
- polars=[]
- polar_headers=[]
- for it,t in enumerate(thickness):
- polar_headers.append(f.readline().strip())
- polars.append(np.zeros((nAlpha,4)))
- try:
- for ia in range(nAlpha):
- polars[it][ia,:]=np.array([f.readline().split()]).astype(float)
- except:
- raise BrokenFormatError('An error occured while reading set number {}, polar number {}, (thickness {}), value number {}.'.format(setNumber,it+1,t,ia+1))
-
- return polars,polar_headers
-
- # Reading headers and polars
- while True:
- nAlpha,thickness,Header,bHasSets = read_header(allow_empty=setNumber>1)
- if len(thickness)==0:
- break
- polars,polar_headers = read_polars(nAlpha,thickness)
- PSet= ProfileSet(Header,thickness,polars,polar_headers)
- self.ProfileSets.append(PSet)
- setNumber=setNumber+1
-
- def toString(self):
- s=''
- if len(self.ProfileSets)>0:
- prefix='PROFILE SET '
- else:
- prefix=''
- for pset in self.ProfileSets:
- s+=pset.toString(prefix)
- return s
-
- def _write(self):
- with open(self.filename,'w') as f:
- f.write(self.toString)
-
- def __repr__(self):
- s ='Class FlexProfileFile (attributes: ProfileSets)\n'
- s+=' Number of profiles sets: '+str(len(self.ProfileSets))+'\n'
- for ps in self.ProfileSets:
- s+=ps.__repr__()
- return s
-
-
- def _toDataFrame(self):
- cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]']
- dfs = {}
- for iset,pset in enumerate(self.ProfileSets):
- for ipol,(thickness,polar) in enumerate(zip(pset.thickness,pset.polars)):
- name='pc_set_{}_t_{}'.format(iset+1,thickness)
- dfs[name] = pd.DataFrame(data=polar, columns=cols)
- return dfs
-
+import numpy as np
+import pandas as pd
+import os
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File = dict
+ class WrongFormatError(Exception): pass
+ class BrokenFormatError(Exception): pass
+
+class ProfileSet():
+ def __init__(self,header,thickness,polars,polar_headers):
+ self.header = header
+ self.polars = polars
+ self.thickness = thickness
+ self.polar_headers = polar_headers
+
+ def toString(self,PREFIX=''):
+ s =PREFIX+self.header+'\n'
+ s+=' '.join([str(t) for t in self.thickness])+'\n'
+ s+=str(self.polars[0].shape[0])+'\n'
+ for ph,t,polar in zip(self.polar_headers,self.thickness,self.polars):
+ s+=ph+'\n'
+ s+='\n'.join([' '.join(['{:15.7e}'.format(v) for v in line]) for line in polar])
+# s+=ph+'\n'
+ return s
+
+ def __repr__(self):
+ s ='Class ProfileSet (attributes: header, polars, thickness, polar_headers)\n'
+ s+=' header : '+self.header+'\n'
+ s+=' thickness : '+str(self.thickness)+'\n'
+ s+=' Number of polars: '+str(len(self.thickness))+'\n'
+ s+=' Alpha values : '+str(self.polars[0].shape[0])+'\n'
+ for ip,(ph,t) in enumerate(zip(self.polar_headers,self.thickness)):
+ s+= ' Polar: {}, Thickness: {}, Header: {}\n'.format(ip+1,t,ph)
+ return s
+
+class FLEXProfileFile(File):
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.pro','.00X'] #'.001 etc..'
+
+ @staticmethod
+ def formatName():
+ return 'FLEX profile file'
+
+ def _read(self):
+ self.ProfileSets=[]
+ setNumber=1
+ with open(self.filename, 'r', errors="surrogateescape") as f:
+ def read_header(allow_empty=False):
+ """ Reads the header of a profile set (4 first lines)
+ - The first line may start with "Profile set I:" to indicate a set number
+ - Second line is number of thicknesses
+ - Third is thicnkesses
+ - Fourth is number of alpha values
+ """
+ header=[]
+ for i, line in enumerate(f):
+ header.append(line.strip())
+ if i==3:
+ break
+ if len(header)<4:
+ if allow_empty:
+ return [],[],'',False
+ else:
+ raise WrongFormatError('A Flex profile file needs at leats 4 lines of headers')
+ try:
+ nThickness=int(header[1])
+ except:
+ raise WrongFormatError('Number of thicknesses (integer) should be on line 2')
+ try:
+ thickness=np.array(header[2].split()).astype(float)
+ except:
+ raise WrongFormatError('Number of thicknesses (integer) should be on line 2')
+ if len(thickness)!=nThickness:
+ raise WrongFormatError('Number of thicknesses read ({}) different from the number reported ({})'.format(len(thickness),nThickness))
+ try:
+ nAlpha=int(header[3])
+ except:
+ raise WrongFormatError('Number of alpha values (integer) should be on line 4')
+ if header[0].lower().find('profile set')==0:
+ header[0]=header[0][11:]
+ bHasSets=True
+ else:
+ bHasSets=False
+ return nAlpha,thickness,header[0],bHasSets
+
+ def read_polars(nAlpha,thickness):
+ polars=[]
+ polar_headers=[]
+ for it,t in enumerate(thickness):
+ polar_headers.append(f.readline().strip())
+ polars.append(np.zeros((nAlpha,4)))
+ try:
+ for ia in range(nAlpha):
+ polars[it][ia,:]=np.array([f.readline().split()]).astype(float)
+ except:
+ raise BrokenFormatError('An error occured while reading set number {}, polar number {}, (thickness {}), value number {}.'.format(setNumber,it+1,t,ia+1))
+
+ return polars,polar_headers
+
+ # Reading headers and polars
+ while True:
+ nAlpha,thickness,Header,bHasSets = read_header(allow_empty=setNumber>1)
+ if len(thickness)==0:
+ break
+ polars,polar_headers = read_polars(nAlpha,thickness)
+ PSet= ProfileSet(Header,thickness,polars,polar_headers)
+ self.ProfileSets.append(PSet)
+ setNumber=setNumber+1
+
+ def toString(self):
+ s=''
+ if len(self.ProfileSets)>0:
+ prefix='PROFILE SET '
+ else:
+ prefix=''
+ for pset in self.ProfileSets:
+ s+=pset.toString(prefix)
+ return s
+
+ def _write(self):
+ with open(self.filename,'w') as f:
+ f.write(self.toString)
+
+ def __repr__(self):
+ s ='Class FlexProfileFile (attributes: ProfileSets)\n'
+ s+=' Number of profiles sets: '+str(len(self.ProfileSets))+'\n'
+ for ps in self.ProfileSets:
+ s+=ps.__repr__()
+ return s
+
+
+ def _toDataFrame(self):
+ cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]']
+ dfs = {}
+ for iset,pset in enumerate(self.ProfileSets):
+ for ipol,(thickness,polar) in enumerate(zip(pset.thickness,pset.polars)):
+ name='pc_set_{}_t_{}'.format(iset+1,thickness)
+ dfs[name] = pd.DataFrame(data=polar, columns=cols)
+ return dfs
+
diff --git a/weio/flex_wavekin_file.py b/weio/flex_wavekin_file.py
index 684e8f8..51761a6 100644
--- a/weio/flex_wavekin_file.py
+++ b/weio/flex_wavekin_file.py
@@ -1,104 +1,101 @@
-from __future__ import division,unicode_literals,print_function,absolute_import
-from builtins import map, range, chr, str
-from io import open
-from future import standard_library
-standard_library.install_aliases()
-
-from .file import File, WrongFormatError, BrokenFormatError
-from .csv_file import CSVFile
-import numpy as np
-import pandas as pd
-import os
-import re
-
-#from .wetb.fast import fast_io
-
-
-class FLEXWaveKinFile(File):
-
- @staticmethod
- def defaultExtensions():
- return ['.wko'] #'.001 etc..'
-
- @staticmethod
- def formatName():
- return 'FLEX WaveKin file'
-
- def _read(self):
- numeric_const_pattern = r'[-+]? (?: (?: \d* \. \d+ ) | (?: \d+ \.? ) )(?: [Ee] [+-]? \d+ ) ?'
- rx = re.compile(numeric_const_pattern, re.VERBOSE)
- def extract_floats(s):
- v=np.array(rx.findall(s))
- return v
-
-
- try:
- csv = CSVFile(self.filename, sep=' ', commentLines=list(np.arange(11)),detectColumnNames=False)
- except:
- raise WrongFormatError('Unable to parse Flex WaveKin file as CSV with 11 header lines')
-
- header = csv.header
- self['header'] = csv.header[0:2]
- self['data'] = csv.data
- try:
- self['MaxCrestHeight'] = float(extract_floats(header[2])[0])
- self['MaxLongiVel'] = float(extract_floats(header[3])[0])
- self['MaxLongiAcc'] = float(extract_floats(header[4])[0])
- dat = extract_floats(header[5]).astype(float)
- self['WaterDepth'] = dat[0]
- self['Hs'] = dat[1]
- self['Tp'] = dat[2]
- self['SpecType'] = dat[3]
- except:
- raise BrokenFormatError('Unable to parse floats from header lines 3-6')
-
- try:
- nDisp = int(extract_floats(header[6])[0])
- nRelD = int(extract_floats(header[8])[0])
- except:
- raise BrokenFormatError('Unable to parse int from header lines 7 and 9')
-
- try:
- displ = extract_floats(header[7]).astype(float)
- depth = extract_floats(header[9]).astype(float)
- except:
- raise BrokenFormatError('Unable to parse displacements or depths from header lines 8 and 10')
- if len(displ)!=nDisp:
- print(displ)
- raise BrokenFormatError('Number of displacements ({}) does not match number provided ({})'.format(nDisp, len(displ)))
- if len(depth)!=nRelD:
- print(depth)
- raise BrokenFormatError('Number of rel depth ({}) does not match number provided ({})'.format(nRelD, len(depth)))
-
- self['RelDepth'] = depth
- self['Displacements'] = displ
-
- cols=['Time_[s]', 'WaveElev_[m]']
- for j,x in enumerate(displ):
- for i,z in enumerate(depth):
- cols+=['u_z={:.1f}_x={:.1f}_[m/s]'.format(z*self['WaterDepth']*-1,x)]
- for i,z in enumerate(depth):
- cols+=['a_z={:.1f}_x={:.1f}_[m/s^2]'.format(z*self['WaterDepth'],x)]
-
- if len(cols)!=len(self['data'].columns):
- raise BrokenFormatError('Number of columns not valid')
- self['data'].columns = cols
-
-# def _write(self):
-# with open(self.filename,'w') as f:
-# f.write(self.toString)
-
- def __repr__(self):
- s='<{} object> with keys:\n'.format(type(self).__name__)
-
- for k in ['MaxCrestHeight','MaxLongiVel','MaxLongiAcc','WaterDepth','Hs','Tp','SpecType','RelDepth','Displacements']:
- s += '{:15s}: {}\n'.format(k,self[k])
- if len(self['header'])>0:
- s += 'header : '+ ' ,'.join(self['header'])+'\n'
- if len(self['data'])>0:
- s += 'data size : {}x{}'.format(self['data'].shape[0],self['data'].shape[1])
- return s
-
- def _toDataFrame(self):
- return self['data']
-
+import numpy as np
+import pandas as pd
+import os
+import re
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File = dict
+ class WrongFormatError(Exception): pass
+ class BrokenFormatError(Exception): pass
+from .csv_file import CSVFile
+
+
+class FLEXWaveKinFile(File):
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.wko'] #'.001 etc..'
+
+ @staticmethod
+ def formatName():
+ return 'FLEX WaveKin file'
+
+ def _read(self):
+ numeric_const_pattern = r'[-+]? (?: (?: \d* \. \d+ ) | (?: \d+ \.? ) )(?: [Ee] [+-]? \d+ ) ?'
+ rx = re.compile(numeric_const_pattern, re.VERBOSE)
+ def extract_floats(s):
+ v=np.array(rx.findall(s))
+ return v
+
+
+ try:
+ csv = CSVFile(self.filename, sep=' ', commentLines=list(np.arange(11)),detectColumnNames=False)
+ except:
+ raise WrongFormatError('Unable to parse Flex WaveKin file as CSV with 11 header lines')
+
+ header = csv.header
+ self['header'] = csv.header[0:2]
+ self['data'] = csv.data
+ try:
+ self['MaxCrestHeight'] = float(extract_floats(header[2])[0])
+ self['MaxLongiVel'] = float(extract_floats(header[3])[0])
+ self['MaxLongiAcc'] = float(extract_floats(header[4])[0])
+ dat = extract_floats(header[5]).astype(float)
+ self['WaterDepth'] = dat[0]
+ self['Hs'] = dat[1]
+ self['Tp'] = dat[2]
+ self['SpecType'] = dat[3]
+ except:
+ raise BrokenFormatError('Unable to parse floats from header lines 3-6')
+
+ try:
+ nDisp = int(extract_floats(header[6])[0])
+ nRelD = int(extract_floats(header[8])[0])
+ except:
+ raise BrokenFormatError('Unable to parse int from header lines 7 and 9')
+
+ try:
+ displ = extract_floats(header[7]).astype(float)
+ depth = extract_floats(header[9]).astype(float)
+ except:
+ raise BrokenFormatError('Unable to parse displacements or depths from header lines 8 and 10')
+ if len(displ)!=nDisp:
+ print(displ)
+ raise BrokenFormatError('Number of displacements ({}) does not match number provided ({})'.format(nDisp, len(displ)))
+ if len(depth)!=nRelD:
+ print(depth)
+ raise BrokenFormatError('Number of rel depth ({}) does not match number provided ({})'.format(nRelD, len(depth)))
+
+ self['RelDepth'] = depth
+ self['Displacements'] = displ
+
+ cols=['Time_[s]', 'WaveElev_[m]']
+ for j,x in enumerate(displ):
+ for i,z in enumerate(depth):
+ cols+=['u_z={:.1f}_x={:.1f}_[m/s]'.format(z*self['WaterDepth']*-1,x)]
+ for i,z in enumerate(depth):
+ cols+=['a_z={:.1f}_x={:.1f}_[m/s^2]'.format(z*self['WaterDepth'],x)]
+
+ if len(cols)!=len(self['data'].columns):
+ raise BrokenFormatError('Number of columns not valid')
+ self['data'].columns = cols
+
+# def _write(self):
+# with open(self.filename,'w') as f:
+# f.write(self.toString)
+
+ def __repr__(self):
+ s='<{} object> with keys:\n'.format(type(self).__name__)
+
+ for k in ['MaxCrestHeight','MaxLongiVel','MaxLongiAcc','WaterDepth','Hs','Tp','SpecType','RelDepth','Displacements']:
+ s += '{:15s}: {}\n'.format(k,self[k])
+ if len(self['header'])>0:
+ s += 'header : '+ ' ,'.join(self['header'])+'\n'
+ if len(self['data'])>0:
+ s += 'data size : {}x{}'.format(self['data'].shape[0],self['data'].shape[1])
+ return s
+
+ def _toDataFrame(self):
+ return self['data']
+
diff --git a/weio/gnuplot_file.py b/weio/gnuplot_file.py
new file mode 100644
index 0000000..29c8a32
--- /dev/null
+++ b/weio/gnuplot_file.py
@@ -0,0 +1,373 @@
+"""
+Input/output class for the fileformat supported by GNU Plot
+"""
+import numpy as np
+import pandas as pd
+import os
+
+
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File=dict
+ EmptyFileError = type('EmptyFileError', (Exception,),{})
+ WrongFormatError = type('WrongFormatError', (Exception,),{})
+ BrokenFormatError = type('BrokenFormatError', (Exception,),{})
+
+class GNUPlotFile(File):
+ """
+ Read/write a GnuPlot file. The object behaves as a dictionary.
+
+ Main methods
+ ------------
+ - read, write, toDataFrame, to2DFields, keys
+
+ Examples
+ --------
+ f = GNUPlotFile('file.dat')
+ print(f.keys())
+ print(f.toDataFrame().columns)
+
+ """
+
+ @staticmethod
+ def defaultExtensions():
+ """ List of file extensions expected for this fileformat"""
+ return ['.dat','.raw']
+
+ @staticmethod
+ def formatName():
+ """ Short string (~100 char) identifying the file format"""
+ return 'GNUPlot file'
+
+ @staticmethod
+ def priority(): return 60 # Priority in weio.read fileformat list between 0=high and 100:low
+
+ 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"""
+ # NOTE: for ascii files only
+ with open(self.filename, 'r') as fid:
+ data = []
+ column_names = None
+ headers = []
+ current_dataset = []
+
+ for line in fid:
+ line = line.strip()
+ # header
+ if line.startswith('#'):
+ headers.append(line[1:].strip())
+ continue
+ if not line:
+ # a new line triggers GNUPlot to "lift the pen"
+ if current_dataset:
+ data.append(np.array(current_dataset))
+ current_dataset = []
+ continue
+ current_dataset.append(np.array(line.split()).astype(float))
+
+ if current_dataset:
+ data.append(np.array(current_dataset))
+
+ # Try to detect column names from header
+ same_columns, same_rows, n_cols, n_rows = check_same_shape(data)
+ if same_columns:
+ column_names = find_string_with_n_splits(headers, n_cols)
+
+ self['data'] = data
+ self['headers'] = headers
+ self['column_names']= column_names
+
+ def plot(self):
+ import matplotlib.pyplot as plt
+ data, column_names = self['data'], self['column_names']
+
+ if not data:
+ print("No data found in the file.")
+ return
+
+ shapes = [dataset.shape for dataset in data]
+ if len(set(shapes)) != 1:
+ print("Datasets have different shapes.")
+ return
+
+ data = data[0] # Assuming all datasets have the same shape, take the first one
+ x = data[:, 0]
+ y = data[:, 1]
+ z_columns = data[:, 2:]
+
+ xu = np.unique(x)
+ yu = np.unique(y)
+
+ if len(xu) * len(yu) != len(x):
+ print("The data does not form a proper grid.")
+ return
+
+ X, Y = np.meshgrid(xu, yu)
+
+ for i, z_column in enumerate(z_columns.T):
+ Z = z_column.reshape(len(yu), len(xu))
+ plt.figure()
+ cp = plt.contourf(X, Y, Z)
+ plt.colorbar(cp)
+ plt.title(f'Plot for column {i + 3}' + (f' ({column_names[i + 2]})' if column_names else ''))
+ plt.xlabel('X')
+ plt.ylabel('Y')
+ plt.show()
+
+
+ def _write(self):
+ """ Writes to self.filename"""
+ # --- Example:
+ #with open(self.filename,'w') as f:
+ # f.write(self.toString)
+ raise NotImplementedError()
+
+
+ # --------------------------------------------------------------------------------
+ # --- Convenient properties
+ # --------------------------------------------------------------------------------
+ @property
+ def common_shape(self):
+ same_columns, same_rows, n_cols, n_rows = check_same_shape(self['data'])
+ if same_columns and same_rows:
+ return n_rows, n_cols
+ else:
+ return None
+
+ @property
+ def is_meshgrid(self):
+ b1 = self.common_shape
+ b2 = check_first_column_same(self['data'])
+ b3 = check_second_column_unique(self['data'])
+ return b1 is not None and b2 is not None and b3 is not None
+
+ @property
+ def x_values(self):
+ if len(self['data'])==1:
+ x = self['data'][0][:,0]
+ else:
+ x = check_first_column_same(self['data'])
+ if x is None:
+ # TODO, potential concat but this will make the check above unclear
+ pass
+ return x
+
+ @property
+ def y_values(self):
+ if len(self['data'])==1:
+ if self['data'][0].shape[1]<2:
+ return None
+ y= self['data'][0][:,1]
+ else:
+ y = check_second_column_unique(self['data'])
+ if y is None:
+ # TODO, potential concat but this will make the check above unclear
+ pass
+ return y
+
+ def toDataFrame(self):
+ """ Returns object into one DataFrame, or a dictionary of DataFrames"""
+ data = self['data']
+ shape = self.common_shape
+ x = self.x_values
+ y = self.y_values
+ colNames=self['column_names']
+ if self['column_names'] is None:
+ colNames = default_colnames(self['data'][0].shape[1])
+
+ # --- Only one data set
+ if len(data)==1:
+ M = self['data'][0]
+ return pd.DataFrame(data=M, columns=colNames)
+
+ # --- Mesh grid dataset
+ if self.is_meshgrid:
+ # We concatenate..
+ M = np.vstack(self['data'])
+ return pd.DataFrame(data=M, columns=colNames)
+
+ # --- A bunch a different lines
+ dfs={}
+ same_columns, same_rows, n_cols, n_rows = check_same_shape(self['data'])
+ for i in range(len(self['data'])):
+ if same_columns:
+ cols = colNames
+ else:
+ colNames = default_colnames(self['data'][i].shape[1])
+ dfs['set'+str(i)] = pd.DataFrame(data=self['data'][i], columns=cols)
+ return dfs
+
+ def to2DFields(self, **kwargs):
+ import xarray as xr
+ is_meshgrid = self.is_meshgrid
+ shape = self.common_shape
+ x = self.x_values
+ y = self.y_values
+ colNames=self['column_names']
+ if self['column_names'] is None:
+ colNames = default_colnames(self['data'][0].shape[1])
+
+ if len(kwargs.keys())>0:
+ print('[WARN] GNUPlotFile: to2DFields: ignored keys: ',kwargs.keys())
+
+ if not self.is_meshgrid:
+ if len(self['data'])>1:
+ # Probably hard to do, or have to provide different sets
+ return None
+ # --- Single table..., likely dubious results
+ M = self['data'][0]
+ if M.shape[1]<2:
+ return None
+ s1 = 'rows'
+ s2 = 'columns'
+ ds = xr.Dataset(coords={s1: range(M.shape[0]), s2: range(M.shape[1])})
+ ds['data'] = ([s1, s2], M)
+ return ds
+
+ # --- Mesh grid
+ ds = xr.Dataset()
+ ds = xr.Dataset(coords={colNames[0]: x, colNames[1]: y})
+ data = np.array(self['data'])
+ #X, Y = np.meshgrid(x,y)
+ for i,c in enumerate(colNames[2:]):
+ ds[c] = ((colNames[0],colNames[1]), np.squeeze(data[:,:,i+2]).T)
+ return ds
+
+ # --- 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.
+ """
+ def axisvec_to_string(v, s=''):
+ if v is None:
+ return 'None'
+ else:
+ deltas = np.diff(v)
+ if len(v)==0:
+ return '[], n:0'
+ elif len(v)==1:
+ return '[{}], n:1'.format(v[0])
+ elif len(v)==2:
+ return '[{}, {}], n:2'.format(v[0], v[1])
+ else: # len(np.unique(deltas))==1:
+ # TODO improve me
+ return '[{} ... {}], d{}:{}, n:{}'.format(v[0], v[-1], s, v[1]-v[0], len(v))
+ #else:
+ # return '[{} {} ... {}], d{}:{}, n:{}'.format(v[0], v[1], v[-1], s, v[-1]-v[-2], len(v))
+
+ s='<{} object>:\n'.format(type(self).__name__)
+ s+='|Main attributes:\n'
+ s+='| - filename: {}\n'.format(self.filename)
+
+ common_shape = self.common_shape
+ s+='| * common_shape: {}\n'.format(common_shape)
+ s+='| * is_meshgrid: {}\n'.format(self.is_meshgrid)
+ s+='| * x_values: {}\n'.format(axisvec_to_string(self.x_values, 'x'))
+ s+='| * y_values: {}\n'.format(axisvec_to_string(self.y_values, 'y'))
+ s+='|Main keys:\n'
+ if common_shape:
+ s+='| - data : length {}, shape {}\n'.format(len(self['data']), common_shape)
+ else:
+ s+='| - data : length {}, shapes {}\n'.format(len(self['data']), [x.shape for x in self['data']])
+ s+='| - headers : {}\n'.format(self['headers'])
+ s+='| - column_names : {}\n'.format(self['column_names'])
+ s+='|Main methods:\n'
+ s+='| - read, write, toDataFrame, to2DFields, keys'
+ return s
+
+ def toString(self):
+ """ """
+ s=''
+ return s
+
+# --------------------------------------------------------------------------------}
+# --- Some hlper functions
+# --------------------------------------------------------------------------------{
+def check_same_shape(arrays):
+ if not arrays:
+ return False, False
+ num_columns = arrays[0].shape[1]
+ num_rows = arrays[0].shape[0]
+ same_columns = all(array.shape[1] == num_columns for array in arrays)
+ same_rows = all(array.shape[0] == num_rows for array in arrays)
+ return same_columns, same_rows, num_columns, num_rows
+
+def check_first_column_same(arrays):
+ if not arrays:
+ return None
+ first_column = arrays[0][:, 0]
+ for array in arrays[1:]:
+ if not np.array_equal(array[:, 0], first_column):
+ return None
+ return first_column
+
+def check_second_column_unique(arrays):
+ if not arrays:
+ return None
+ unique_values = []
+ if len(arrays[0].shape)!=2:
+ return None
+ for array in arrays:
+ if array.shape[1]<2:
+ return None
+ second_column = array[:, 1]
+ unique_value = np.unique(second_column)
+ if len(unique_value) != 1:
+ return None
+ unique_values.append(unique_value[0])
+ return np.array(unique_values)
+
+def find_string_with_n_splits(strings, n):
+ for string in strings:
+ splits = string.split()
+ if len(splits) == n:
+ return splits
+ return None
+
+def default_colnames(n):
+ colNames=['C{}'.format(i) for i in range(n)]
+ colNames[0] = 'x'
+ if len(colNames)>1:
+ colNames[1] = 'y'
+ return colNames
+
+if __name__ == '__main__':
+ from welib.essentials import *
+ #plt = GNUPlotFile('diff.000000.dat')
+ plt = GNUPlotFile('C:/Work/Courses/440/project_solution/data/CFD_x.dat')
+ print(plt)
+ df =plt.toDataFrame()
+ print(df)
+ ds = plt.to2DFields()
+ print(ds)
diff --git a/weio/hawc2_dat_file.py b/weio/hawc2_dat_file.py
index 3a3bab4..44de75f 100644
--- a/weio/hawc2_dat_file.py
+++ b/weio/hawc2_dat_file.py
@@ -1,156 +1,144 @@
-from __future__ import division
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import absolute_import
-from io import open
-from builtins import map
-from builtins import range
-from builtins import chr
-from builtins import str
-from future import standard_library
-standard_library.install_aliases()
-import os
-import numpy as np
-
-from .file import File, WrongFormatError, FileNotFoundError
-import pandas as pd
-
-from .wetb.hawc2.Hawc2io import ReadHawc2
-
-
-class HAWC2DatFile(File):
-
- @staticmethod
- def defaultExtensions():
- return ['.dat','.sel']
-
- @staticmethod
- def formatName():
- return 'HAWC2 dat file'
-
- def __init__(self, filename=None, **kwargs):
- self.info={}
- self.data=np.array([])
- self.bHawc=False
- super(HAWC2DatFile, self).__init__(filename=filename,**kwargs)
-
- def _read(self):
- try:
- res_file = ReadHawc2(self.filename)
- self.data = res_file.ReadAll()
- self.info['attribute_names'] = res_file.ChInfo[0]
- self.info['attribute_units'] = res_file.ChInfo[1]
- self.info['attribute_descr'] = res_file.ChInfo[2]
- if res_file.FileFormat=='BHAWC_ASCII':
- self.bHawc=True
- except FileNotFoundError:
- raise
- #raise WrongFormatError('HAWC2 dat File {}: '.format(self.filename)+' File Not Found:'+e.filename)
- except Exception as e:
-# raise e
- raise WrongFormatError('HAWC2 dat File {}: '.format(self.filename)+e.args[0])
-
- #def _write(self):
- #self.data.to_csv(self.filename,sep=self.false,index=False)
-
- def _toDataFrame(self):
- import re
-
- # Simplify output names
- names=self.info['attribute_names']
- for i,desc in enumerate(self.info['attribute_descr']):
- elem = re.findall(r'E-nr:\s*(\d+)', desc)
- zrel = re.findall(r'Z-rel:\s*(\d+.\d+)', desc)
- node = re.findall(r'nodenr:\s*(\d+)', desc)
- mbdy = re.findall(r'Mbdy:([-a-zA-Z0-9_.]*) ', desc)
- s = re.findall(r's=\s*(\d+.\d+)\[m\]', desc)
- sS = re.findall(r's/S=\s*(\d+.\d+)', desc)
-
- pref=''
- names[i] = names[i].replace(' ','')
- names[i] = names[i].replace('coo:global','g').strip()
- names[i] = names[i].replace('Statepos','').strip()
- names[i] = names[i].replace('axisangle','rot_').strip()
-
- if len(mbdy)==1:
- names[i] = names[i].replace('coo:'+mbdy[0],'b').strip()
- pref += mbdy[0]
-
- if len(zrel)==1 and len(elem)==1:
- ielem=int(elem[0])
- fzrel=float(zrel[0])
- if fzrel==0:
- pref+= 'N'+str(ielem)
- elif fzrel==1:
- pref+='N'+str(ielem+1)
- else:
- pref+='N'+str(ielem+fzrel)
- if len(s)==1 and len(sS)==1:
- pref+='r'+str(sS[0])
-
- if len(node)==1:
- pref+='N'+node[0]
- names[i]=pref+names[i]
-
- if self.info['attribute_units'] is not None:
- units = [u.replace('(','').replace(')','').replace('[','').replace(']','') for u in self.info['attribute_units']]
-
- cols=[n+'_['+u+']' for n,u in zip(names,units)]
- else:
- cols=names
-
- return pd.DataFrame(data=self.data,columns=cols)
-#
- def _write(self):
- filename=self.filename
- ext = os.path.splitext(filename)[-1].lower()
- if ext=='.dat':
- datfilename=filename
- elif ext=='.sel':
- datfilename=filename.replace(ext,'.dat')
- else:
- datfilename=filename+'.dat'
- selfilename=datfilename[:-4]+'.sel'
- nScans = self.data.shape[0]
- nChannels = self.data.shape[1]
- SimTime = self.data[-1,0] #-self.data[0,0]
- # --- dat file
- np.savetxt(datfilename, self.data, fmt=b'%16.8e')
- # --- Sel file
- with open(selfilename, 'w') as f:
- if self.bHawc:
- f.write('BHawC channel reference file (sel):\n')
- f.write('+===================== (Name) =================== (Time stamp) ========= (Path) ==========================================================+\n')
- f.write('Original BHAWC file : NA.dat 2001.01.01 00:00:00 C:\\\n')
- f.write('Channel reference file : NA.sel 2001.01.01 00:00:00 C:\\\n')
- f.write('Result file : NA.dat 2001.01.01 00:00:00 C:\\\n')
- f.write('+=========================================================================================================================================+\n')
- f.write('Scans \tChannels \tTime [sec] \tCoordinate convention: Siemens\n')
- f.write('{:19s}\t{:25s}\t{:25s}\n\n'.format(str(nScans),str(nChannels),'{:.3f}'.format(SimTime)))
- f.write('{:19s}\t{:25s}\t{:25s}\t{:25s}\n'.format('Channel','Variable descriptions','Labels','Units'))
- for chan,(label,descr,unit) in enumerate(zip(self.info['attribute_names'],self.info['attribute_descr'],self.info['attribute_units'])):
- unit=unit.replace('(','').replace(')','').replace('[','').replace(']','')
- f.write('{:19s}\t{:25s}\t{:25s}\t{:25s}\n'.format(str(chan+1),descr[0:26],label[0:26],'['+unit+']'))
- else:
-
- f.write('________________________________________________________________________________________________________________________\n')
- f.write(' Version ID : NA\n')
- f.write(' Time : 00:00:00\n')
- f.write(' Date : 01:01.2001\n')
- f.write('________________________________________________________________________________________________________________________\n')
- f.write(' Result file : {:s}\n'.format(os.path.basename(datfilename)))
- f.write('________________________________________________________________________________________________________________________\n')
- f.write(' Scans Channels Time [sec] Format\n')
- f.write('{:12s}{:12s}{:16s}{:s}\n'.format(str(nScans),str(nChannels),'{:.3f}'.format(SimTime),'ASCII'))
- f.write('\n')
- f.write('{:12s}{:31s}{:11s}{:s}'.format('Channel','Name','Unit','Variable Description\n'))
- f.write('\n')
- for chan,(label,descr,unit) in enumerate(zip(self.info['attribute_names'],self.info['attribute_descr'],self.info['attribute_units'])):
- unit=unit.replace('(','').replace(')','').replace('[','').replace(']','')
- f.write('{:12s}{:31s}{:11s}{:s}\n'.format(str(chan+1),label[0:30],unit,descr))
- f.write('________________________________________________________________________________________________________________________\n');
-
-class BHAWCDatFile(HAWC2DatFile):
- def __init__(self, filename=None, **kwargs):
- super(HAWC2DatFile, self).__init__(filename=filename,**kwargs)
- self.bHawc=False
+import numpy as np
+import pandas as pd
+import os
+
+from .file import File, WrongFormatError, FileNotFoundError
+from .wetb.hawc2.Hawc2io import ReadHawc2
+
+
+class HAWC2DatFile(File):
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.dat','.sel']
+
+ @staticmethod
+ def formatName():
+ return 'HAWC2 dat file'
+
+ def __init__(self, filename=None, **kwargs):
+ self.info={}
+ self.data=np.array([])
+ self.bHawc=False
+ super(HAWC2DatFile, self).__init__(filename=filename,**kwargs)
+
+ def _read(self):
+ try:
+ res_file = ReadHawc2(self.filename)
+ self.data = res_file.ReadAll()
+ self.info['attribute_names'] = res_file.ChInfo[0]
+ self.info['attribute_units'] = res_file.ChInfo[1]
+ self.info['attribute_descr'] = res_file.ChInfo[2]
+ if res_file.FileFormat=='BHAWC_ASCII':
+ self.bHawc=True
+ except FileNotFoundError:
+ raise
+ #raise WrongFormatError('HAWC2 dat File {}: '.format(self.filename)+' File Not Found:'+e.filename)
+ except Exception as e:
+# raise e
+ raise WrongFormatError('HAWC2 dat File {}: '.format(self.filename)+e.args[0])
+
+ #def _write(self):
+ #self.data.to_csv(self.filename,sep=self.false,index=False)
+
+ def _toDataFrame(self):
+ import re
+
+ # Simplify output names
+ names=self.info['attribute_names']
+ for i,desc in enumerate(self.info['attribute_descr']):
+ elem = re.findall(r'E-nr:\s*(\d+)', desc)
+ zrel = re.findall(r'Z-rel:\s*(\d+.\d+)', desc)
+ node = re.findall(r'nodenr:\s*(\d+)', desc)
+ mbdy = re.findall(r'Mbdy:([-a-zA-Z0-9_.]*) ', desc)
+ s = re.findall(r's=\s*(\d+.\d+)\[m\]', desc)
+ sS = re.findall(r's/S=\s*(\d+.\d+)', desc)
+
+ pref=''
+ names[i] = names[i].replace(' ','')
+ names[i] = names[i].replace('coo:global','g').strip()
+ names[i] = names[i].replace('Statepos','').strip()
+ names[i] = names[i].replace('axisangle','rot_').strip()
+
+ if len(mbdy)==1:
+ names[i] = names[i].replace('coo:'+mbdy[0],'b').strip()
+ pref += mbdy[0]
+
+ if len(zrel)==1 and len(elem)==1:
+ ielem=int(elem[0])
+ fzrel=float(zrel[0])
+ if fzrel==0:
+ pref+= 'N'+str(ielem)
+ elif fzrel==1:
+ pref+='N'+str(ielem+1)
+ else:
+ pref+='N'+str(ielem+fzrel)
+ if len(s)==1 and len(sS)==1:
+ pref+='r'+str(sS[0])
+
+ if len(node)==1:
+ pref+='N'+node[0]
+ names[i]=pref+names[i]
+
+ if self.info['attribute_units'] is not None:
+ units = [u.replace('(','').replace(')','').replace('[','').replace(']','') for u in self.info['attribute_units']]
+
+ cols=[n+'_['+u+']' for n,u in zip(names,units)]
+ else:
+ cols=names
+
+ return pd.DataFrame(data=self.data,columns=cols)
+#
+ def _write(self):
+ filename=self.filename
+ ext = os.path.splitext(filename)[-1].lower()
+ if ext=='.dat':
+ datfilename=filename
+ elif ext=='.sel':
+ datfilename=filename.replace(ext,'.dat')
+ else:
+ datfilename=filename+'.dat'
+ selfilename=datfilename[:-4]+'.sel'
+ nScans = self.data.shape[0]
+ nChannels = self.data.shape[1]
+ SimTime = self.data[-1,0] #-self.data[0,0]
+ # --- dat file
+ np.savetxt(datfilename, self.data, fmt='%16.8e')
+ # --- Sel file
+ with open(selfilename, 'w') as f:
+ if self.bHawc:
+ f.write('BHawC channel reference file (sel):\n')
+ f.write('+===================== (Name) =================== (Time stamp) ========= (Path) ==========================================================+\n')
+ f.write('Original BHAWC file : NA.dat 2001.01.01 00:00:00 C:\\\n')
+ f.write('Channel reference file : NA.sel 2001.01.01 00:00:00 C:\\\n')
+ f.write('Result file : NA.dat 2001.01.01 00:00:00 C:\\\n')
+ f.write('+=========================================================================================================================================+\n')
+ f.write('Scans \tChannels \tTime [sec] \tCoordinate convention: Siemens\n')
+ f.write('{:19s}\t{:25s}\t{:25s}\n\n'.format(str(nScans),str(nChannels),'{:.3f}'.format(SimTime)))
+ f.write('{:19s}\t{:25s}\t{:25s}\t{:25s}\n'.format('Channel','Variable descriptions','Labels','Units'))
+ for chan,(label,descr,unit) in enumerate(zip(self.info['attribute_names'],self.info['attribute_descr'],self.info['attribute_units'])):
+ unit=unit.replace('(','').replace(')','').replace('[','').replace(']','')
+ f.write('{:19s}\t{:25s}\t{:25s}\t{:25s}\n'.format(str(chan+1),descr[0:26],label[0:26],'['+unit+']'))
+ else:
+
+ f.write('________________________________________________________________________________________________________________________\n')
+ f.write(' Version ID : NA\n')
+ f.write(' Time : 00:00:00\n')
+ f.write(' Date : 01:01.2001\n')
+ f.write('________________________________________________________________________________________________________________________\n')
+ f.write(' Result file : {:s}\n'.format(os.path.basename(datfilename)))
+ f.write('________________________________________________________________________________________________________________________\n')
+ f.write(' Scans Channels Time [sec] Format\n')
+ f.write('{:12s}{:12s}{:16s}{:s}\n'.format(str(nScans),str(nChannels),'{:.3f}'.format(SimTime),'ASCII'))
+ f.write('\n')
+ f.write('{:12s}{:31s}{:11s}{:s}'.format('Channel','Name','Unit','Variable Description\n'))
+ f.write('\n')
+ for chan,(label,descr,unit) in enumerate(zip(self.info['attribute_names'],self.info['attribute_descr'],self.info['attribute_units'])):
+ unit=unit.replace('(','').replace(')','').replace('[','').replace(']','')
+ f.write('{:12s}{:31s}{:11s}{:s}\n'.format(str(chan+1),label[0:30],unit,descr))
+ f.write('________________________________________________________________________________________________________________________\n');
+
+class BHAWCDatFile(HAWC2DatFile):
+ def __init__(self, filename=None, **kwargs):
+ super(HAWC2DatFile, self).__init__(filename=filename,**kwargs)
+ self.bHawc=False
diff --git a/weio/hawc2_htc_file.py b/weio/hawc2_htc_file.py
index 0a7449c..684db1e 100644
--- a/weio/hawc2_htc_file.py
+++ b/weio/hawc2_htc_file.py
@@ -114,9 +114,13 @@ def _toDataFrame(self):
tim = bdy.timoschenko_input
H2_stfile = os.path.join(simdir, tim.filename[0])
if not os.path.exists(H2_stfile):
- print('[WARN] st file referenced in htc file was not found. St file: {}, htc file {}'.format(H2_stfile, self.filename))
+ # Try with a parent directory..
+ H2_stfile = os.path.join(simdir, '../',tim.filename[0])
+
+ if not os.path.exists(H2_stfile):
+ print('[WARN] st file referenced in htc file was not found for body {}.\nSt file: {}\nhtc file {}'.format(name, H2_stfile, self.filename))
else:
- dfs_st = HAWC2StFile(H2_stfile).toDataFrame()
+ dfs_st = HAWC2StFile(H2_stfile).toDataFrame(extraCols=False)
if 'set' in tim.keys():
mset = tim.set[0]
iset = tim.set[1]
diff --git a/weio/hawc2_pc_file.py b/weio/hawc2_pc_file.py
index 74e69bd..1f97160 100644
--- a/weio/hawc2_pc_file.py
+++ b/weio/hawc2_pc_file.py
@@ -1,97 +1,97 @@
-"""
-Hawc2 pc file
-
-"""
-import pandas as pd
-import numpy as np
-import os
-
-try:
- from .file import File, WrongFormatError, EmptyFileError
-except:
- EmptyFileError = type('EmptyFileError', (Exception,),{})
- WrongFormatError = type('WrongFormatError', (Exception,),{})
-
-from .wetb.hawc2.pc_file import PCFile
-
-class HAWC2PCFile(File):
-
- @staticmethod
- def defaultExtensions():
- return ['.dat','.pc','.txt']
-
- @staticmethod
- def formatName():
- return 'HAWC2 PC file'
-
- def __init__(self,filename=None,**kwargs):
- if filename:
- self.filename = filename
- self.read(**kwargs)
- else:
- self.filename = None
- self.data = PCFile()
-
- 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)
- # ---
- try:
- self.data = PCFile(self.filename)
- except Exception as e:
- raise WrongFormatError('PC File {}: '.format(self.filename)+e.args[0])
-
- def write(self, filename=None):
- if filename:
- self.filename = filename
- if not self.filename:
- raise Exception('No filename provided')
- # ---
- self.data.save(self.filename)
-
- def toDataFrame(self):
- cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]']
-
- dfs = {}
- for iset in self.data.pc_sets.keys():
- vt, vpolar = self.data.pc_sets[iset]
- for ipol in range(len(vt)):
- name='pc_set_{}_t_{}'.format(iset,vt[ipol])
- dfs[name] = pd.DataFrame(data=vpolar[ipol], columns=cols)
- return dfs
-
-
- # --- Useful function
- def add_set(self, set_label, thicknesses, profiles):
- """
- thicknesses: list of thicknesses
- profiles: list of polars, where each polar is an array (nx4) of alpha(deg), Cl, Cd, Cm
- """
- self.data.pc_sets[set_label] = (np.array(thicknesses), profiles)
-
- @property
- def sets(self):
- return self.pc_sets
-
- def __repr__(self):
- cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]']
- #nRows = 0
- s='<{} object>\n'.format(type(self).__name__)
- s+='| Attributes:\n'
- s+='| - filename: {}\n'.format(self.filename)
- s+='| - data: PCFile, with attributes `pc_sets`\n'
- s+='| Derived attributes:\n'
- s+='| * sets: dict of length: {}:\n'.format(len(self.data.pc_sets))
- for iset in self.data.pc_sets.keys():
- vt, vpolar = self.data.pc_sets[iset]
- nRows = [np.asarray(s).shape[0] for s in vpolar]
- s+='| key: {}, len: {}, thicknesses: {}, rows: {}\n'.format(iset, len(vt), vt, nRows)
- s+='| Methods: add_set, toDataFrame\n'
- return s
-
+"""
+Hawc2 pc file
+
+"""
+import pandas as pd
+import numpy as np
+import os
+
+try:
+ from .file import File, WrongFormatError, EmptyFileError
+except:
+ EmptyFileError = type('EmptyFileError', (Exception,),{})
+ WrongFormatError = type('WrongFormatError', (Exception,),{})
+
+from .wetb.hawc2.pc_file import PCFile
+
+class HAWC2PCFile(File):
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.dat','.pc','.txt']
+
+ @staticmethod
+ def formatName():
+ return 'HAWC2 PC file'
+
+ def __init__(self,filename=None,**kwargs):
+ if filename:
+ self.filename = filename
+ self.read(**kwargs)
+ else:
+ self.filename = None
+ self.data = PCFile()
+
+ 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)
+ # ---
+ try:
+ self.data = PCFile(self.filename)
+ except Exception as e:
+ raise WrongFormatError('PC File {}: '.format(self.filename)+e.args[0])
+
+ def write(self, filename=None):
+ if filename:
+ self.filename = filename
+ if not self.filename:
+ raise Exception('No filename provided')
+ # ---
+ self.data.save(self.filename)
+
+ def toDataFrame(self):
+ cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]']
+
+ dfs = {}
+ for iset in self.data.pc_sets.keys():
+ vt, vpolar = self.data.pc_sets[iset]
+ for ipol in range(len(vt)):
+ name='pc_set_{}_t_{}'.format(iset,vt[ipol])
+ dfs[name] = pd.DataFrame(data=vpolar[ipol], columns=cols)
+ return dfs
+
+
+ # --- Useful function
+ def add_set(self, set_label, thicknesses, profiles):
+ """
+ thicknesses: list of thicknesses
+ profiles: list of polars, where each polar is an array (nx4) of alpha(deg), Cl, Cd, Cm
+ """
+ self.data.pc_sets[set_label] = (np.array(thicknesses), profiles)
+
+ @property
+ def sets(self):
+ return self.data.pc_sets
+
+ def __repr__(self):
+ cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]']
+ #nRows = 0
+ s='<{} object>\n'.format(type(self).__name__)
+ s+='| Attributes:\n'
+ s+='| - filename: {}\n'.format(self.filename)
+ s+='| - data: PCFile, with attributes `pc_sets`\n'
+ s+='| Derived attributes:\n'
+ s+='| * sets: dict of length: {}:\n'.format(len(self.data.pc_sets))
+ for iset in self.data.pc_sets.keys():
+ vt, vpolar = self.data.pc_sets[iset]
+ nRows = [np.asarray(s).shape[0] for s in vpolar]
+ s+='| key: {}, len: {}, thicknesses: {}, rows: {}\n'.format(iset, len(vt), vt, nRows)
+ s+='| Methods: add_set, toDataFrame\n'
+ return s
+
diff --git a/weio/hawc2_st_file.py b/weio/hawc2_st_file.py
index 0b484f9..571daef 100644
--- a/weio/hawc2_st_file.py
+++ b/weio/hawc2_st_file.py
@@ -47,7 +47,7 @@ def __repr__(self):
s='<{} object> with attribute `data`\n'.format(type(self).__name__)
return s
- def toDataFrame(self):
+ def toDataFrame(self, extraCols=True):
col_reg=['r_[m]','m_[kg/m]','x_cg_[m]','y_cg_[m]','ri_x_[m]','ri_y_[m]', 'x_sh_[m]','y_sh_[m]','E_[N/m^2]','G_[N/m^2]','I_x_[m^4]','I_y_[m^4]','I_p_[m^4]','k_x_[-]','k_y_[-]','A_[m^2]','pitch_[deg]','x_e_[m]','y_e_[m]']
col_fpm=['r_[m]','m_[kg/m]','x_cg_[m]','y_cg_[m]','ri_x_[m]','ri_y_[m]','pitch_[deg]','x_e_[m]','y_e_[m]','K11','K12','K13','K14','K15','K16','K22','K23','K24','K25','K26','K33','K34','K35','K36','K44','K45','K46','K55','K56','K66']
@@ -57,8 +57,25 @@ def toDataFrame(self):
for iset in self.data.main_data_sets[mset].keys():
tab = self.data.main_data_sets[mset][iset]
if tab.shape[1]==19:
- col=col_reg
+ FPM = False
+ col = col_reg
else:
- col=col_fpm
- dfs['{}_{}'.format(mset,iset)] = pd.DataFrame(data =tab, columns=col )
+ FPM = True
+ col = col_fpm
+ df = pd.DataFrame(data =tab, columns=col )
+ if extraCols:
+ df['Ixi_[kg.m]'] = df['ri_x_[m]']**2 * df['m_[kg/m]']
+ df['Iyi_[kg.m]'] = df['ri_y_[m]']**2 * df['m_[kg/m]']
+ df['StrcTwst_[deg]'] = -df['pitch_[deg]']
+ if not FPM:
+ df['EIx_[Nm^2]'] = df['E_[N/m^2]']*df['I_x_[m^4]']
+ df['EIy_[Nm^2]'] = df['E_[N/m^2]']*df['I_y_[m^4]']
+ df['GKt_[Nm^2]'] = df['G_[N/m^2]']*df['I_p_[m^4]']
+ df['EA_[N]'] = df['E_[N/m^2]']*df['A_[m^2]']
+ df['GA_[N]'] = df['G_[N/m^2]']*df['A_[m^2]']
+ df['r_bar_[-]'] = df['r_[m]']/df['r_[m]'].values[-1]
+
+ dfs['{}_{}'.format(mset,iset)] = df
+
+
return dfs
diff --git a/weio/hawcstab2_ind_file.py b/weio/hawcstab2_ind_file.py
index 3d41940..244e366 100644
--- a/weio/hawcstab2_ind_file.py
+++ b/weio/hawcstab2_ind_file.py
@@ -1,68 +1,60 @@
-from __future__ import division
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import absolute_import
-from io import open
-from builtins import map
-from builtins import range
-from builtins import chr
-from builtins import str
-from future import standard_library
-standard_library.install_aliases()
-import os
-import re
-
-from .file import File, WrongFormatError
-import numpy as np
-import pandas as pd
-
-
-class HAWCStab2IndFile(File):
-
- @staticmethod
- def defaultExtensions():
- return ['.ind', '.txt']
-
- @staticmethod
- def formatName():
- return 'HAWCStab2 induction file'
-
- def _read(self, *args, **kwargs):
- # Reading header line
- with open(self.filename,'r',encoding=self.encoding) as f:
- header = f.readline().strip()
- if len(header)<=0 or header[0]!='#':
- raise WrongFormatError('Ind File {}: header line does not start with `#`.'.format(self.filename)+e.args[0])
- # Extracting column names
- header = '00'+header[1:].strip()
- num_and_cols = [s.strip()+']' for s in header.split(']')[:-1]]
- cols = [col[2:].strip().replace(' ','_') for col in num_and_cols]
- cols = [col.replace('[','_[').replace('__','_') for col in cols]
- # Determining type based on number of columns (NOTE: could use col names as well maybe)
- NumCol2Type = {38: 'ind', 14: 'fext', 18: 'defl'}
- try:
- self.type = NumCol2Type[len(cols)]
- except Exception as e:
- raise WrongFormatError('Ind File {}: '.format(self.filename))
- self.colNames=cols
-
- # Reading numerical data
- try:
- self.data = np.loadtxt(self.filename, skiprows=1)
- except Exception as e:
- raise BrokenFormatError('Ind File {}: '.format(self.filename)+e.args[0])
-
- if self.data.shape[1]!=len(cols):
- raise BrokenFormatError('Ind File {}: inconsistent number of header columns and data columns.'.format(self.filename)+e.args[0])
-
- # Extracting wind speed from filename
- self.wsp = float(self.filename.lower().split('_')[-1].rstrip('.ind').lstrip('u'))/1000
-
- def _toDataFrame(self):
- key = '{:s} - ws={:06.3f}'.format(self.type,self.wsp)
- df= pd.DataFrame(data=self.data, columns=self.colNames)
- df.columns.name=key
- return df
- #dfs = {key: pd.read_csv(self.filename, delim_whitespace=True, names=cols, skiprows=1)}
- #return dfs
-
+import numpy as np
+import pandas as pd
+import os
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File = dict
+ class WrongFormatError(Exception): pass
+ class BrokenFormatError(Exception): pass
+
+
+class HAWCStab2IndFile(File):
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.ind', '.txt']
+
+ @staticmethod
+ def formatName():
+ return 'HAWCStab2 induction file'
+
+ def _read(self, *args, **kwargs):
+ # Reading header line
+ with open(self.filename,'r',encoding=self.encoding) as f:
+ header = f.readline().strip()
+ if len(header)<=0 or header[0]!='#':
+ raise WrongFormatError('Ind File {}: header line does not start with `#`.'.format(self.filename)+e.args[0])
+ # Extracting column names
+ header = '00'+header[1:].strip()
+ num_and_cols = [s.strip()+']' for s in header.split(']')[:-1]]
+ cols = [col[2:].strip().replace(' ','_') for col in num_and_cols]
+ cols = [col.replace('[','_[').replace('__','_') for col in cols]
+ # Determining type based on number of columns (NOTE: could use col names as well maybe)
+ NumCol2Type = {38: 'ind', 14: 'fext', 18: 'defl'}
+ try:
+ self.type = NumCol2Type[len(cols)]
+ except Exception as e:
+ raise WrongFormatError('Ind File {}: '.format(self.filename))
+ self.colNames=cols
+
+ # Reading numerical data
+ try:
+ self.data = np.loadtxt(self.filename, skiprows=1)
+ except Exception as e:
+ raise BrokenFormatError('Ind File {}: '.format(self.filename)+e.args[0])
+
+ if self.data.shape[1]!=len(cols):
+ raise BrokenFormatError('Ind File {}: inconsistent number of header columns and data columns.'.format(self.filename)+e.args[0])
+
+ # Extracting wind speed from filename
+ self.wsp = float(self.filename.lower().split('_')[-1].rstrip('.ind').lstrip('u'))/1000
+
+ def _toDataFrame(self):
+ key = '{:s} - ws={:06.3f}'.format(self.type,self.wsp)
+ df= pd.DataFrame(data=self.data, columns=self.colNames)
+ df.columns.name=key
+ return df
+ #dfs = {key: pd.read_csv(self.filename, delim_whitespace=True, names=cols, skiprows=1)}
+ #return dfs
+
diff --git a/weio/hawcstab2_pwr_file.py b/weio/hawcstab2_pwr_file.py
index add7a22..1c62d26 100644
--- a/weio/hawcstab2_pwr_file.py
+++ b/weio/hawcstab2_pwr_file.py
@@ -1,56 +1,49 @@
-from __future__ import division
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import absolute_import
-from io import open
-from builtins import map
-from builtins import range
-from builtins import chr
-from builtins import str
-from future import standard_library
-standard_library.install_aliases()
-
-from .file import File, WrongFormatError
-import numpy as np
-import pandas as pd
-
-
-class HAWCStab2PwrFile(File):
-
- @staticmethod
- def defaultExtensions():
- return ['.pwr', '.txt']
-
- @staticmethod
- def formatName():
- return 'HAWCStab2 power file'
-
- def _read(self):
- # Reading header line
- with open(self.filename,'r',encoding=self.encoding) as f:
- header = f.readline().strip()
- if len(header)<=0 or header[0]!='#':
- raise WrongFormatError('Pwr File {}: header line does not start with `#`'.format(self.filename)+e.args[0])
- # Extracting column names
- header = '0 '+header[1:].strip()
- num_and_cols = [s.strip()+']' for s in header.split(']')[:-1]]
- cols = [(' '.join(col.split(' ')[1:])).strip().replace(' ','_') for col in num_and_cols]
- # Determining type based on number of columns (NOTE: could use col names as well maybe)
- if len(cols)!=15:
- raise WrongFormatError('Pwr File {}: '.format(self.filename))
- self.colNames=cols
- # Reading numerical data
- try:
- self.data = np.loadtxt(self.filename, skiprows=1)
- except Exception as e:
- raise BrokenFormatError('Pwr File {}: '.format(self.filename)+e.args[0])
-
- if self.data.shape[1]!=len(cols):
- raise BrokenFormatError('Pwr File {}: inconsistent number of header columns and data columns.'.format(self.filename)+e.args[0])
-
- #def _write(self):
- #self.data.to_csv(self.filename,sep=self.false,index=False)
-
- def _toDataFrame(self):
- return pd.DataFrame(data=self.data, columns=self.colNames)
-
+import numpy as np
+import pandas as pd
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File = dict
+ class WrongFormatError(Exception): pass
+ class BrokenFormatError(Exception): pass
+
+
+class HAWCStab2PwrFile(File):
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.pwr', '.txt']
+
+ @staticmethod
+ def formatName():
+ return 'HAWCStab2 power file'
+
+ def _read(self):
+ # Reading header line
+ with open(self.filename,'r',encoding=self.encoding) as f:
+ header = f.readline().strip()
+ if len(header)<=0 or header[0]!='#':
+ raise WrongFormatError('Pwr File {}: header line does not start with `#`'.format(self.filename)+e.args[0])
+ # Extracting column names
+ header = '0 '+header[1:].strip()
+ num_and_cols = [s.strip()+']' for s in header.split(']')[:-1]]
+ cols = [(' '.join(col.split(' ')[1:])).strip().replace(' ','_') for col in num_and_cols]
+ # Determining type based on number of columns (NOTE: could use col names as well maybe)
+ if len(cols)!=15:
+ raise WrongFormatError('Pwr File {}: '.format(self.filename))
+ self.colNames=cols
+ # Reading numerical data
+ try:
+ self.data = np.loadtxt(self.filename, skiprows=1)
+ except Exception as e:
+ raise BrokenFormatError('Pwr File {}: '.format(self.filename)+e.args[0])
+
+ if self.data.shape[1]!=len(cols):
+ raise BrokenFormatError('Pwr File {}: inconsistent number of header columns and data columns.'.format(self.filename)+e.args[0])
+
+ #def _write(self):
+ #self.data.to_csv(self.filename,sep=self.false,index=False)
+
+ def _toDataFrame(self):
+ return pd.DataFrame(data=self.data, columns=self.colNames)
+
diff --git a/weio/mannbox_file.py b/weio/mannbox_file.py
index 4df52dc..b2b51d7 100644
--- a/weio/mannbox_file.py
+++ b/weio/mannbox_file.py
@@ -47,8 +47,8 @@ class MannBoxFile(File):
print(mb['field'].shape)
# Use methods to extract relevant values
- u,v,w = my.valuesAt(y=10.5, z=90)
- z, means, stds = mb.vertProfile()
+ u = mb.valuesAt(y=10.5, z=90)
+ z, means, stds = mb.vertProfile
# Write to a new file
mb.write('Output_1024x16x16.u')
@@ -144,10 +144,6 @@ def _read_nonbuffered():
self['y0']=y0
self['z0']=z0
self['zMid']=zMid
-# print('1',self['field'][:,-1,0])
-# print('2',self['field'][0,-1::-1,0])
-# print('3',self['field'][0,-1,:])
-
def write(self, filename=None):
""" Write mann box """
@@ -170,11 +166,13 @@ def __repr__(self):
s+='| min: {}, max: {}, mean: {} \n'.format(np.min(self['field']), np.max(self['field']), np.mean(self['field']))
s+='| - dy, dz: {}, {}\n'.format(self['dy'], self['dz'])
s+='| - y0, z0 zMid: {}, {}, {}\n'.format(self['y0'], self['z0'], self['zMid'])
- s+='|useful getters: y, z, _iMid, fromTurbSim\n'
z=self.z
y=self.y
- s+='| y: [{} ... {}], dy: {}, n: {} \n'.format(y[0],y[-1],self['dy'],len(y))
- s+='| z: [{} ... {}], dz: {}, n: {} \n'.format(z[0],z[-1],self['dz'],len(z))
+ s+='| * y: [{} ... {}], dy: {}, n: {} \n'.format(y[0],y[-1],self['dy'],len(y))
+ s+='| * z: [{} ... {}], dz: {}, n: {} \n'.format(z[0],z[-1],self['dz'],len(z))
+ s+='|useful functions:\n'
+ s+='| - t(dx, U)\n'
+ s+='| - valuesAt(y,z), vertProfile, fromTurbSim(*), _iMid()\n'
return s
diff --git a/weio/mannbox_input_file.py b/weio/mannbox_input_file.py
new file mode 100644
index 0000000..12ba9ce
--- /dev/null
+++ b/weio/mannbox_input_file.py
@@ -0,0 +1,191 @@
+"""
+Read/Write Mann Box input file
+
+"""
+import os
+import numpy as np
+try:
+ from .file import File, EmptyFileError, BrokenFormatError
+except:
+ EmptyFileError = type('EmptyFileError', (Exception,),{})
+ BrokenFormatError = type('BrokenFormatError', (Exception,),{})
+ File=dict
+
+class MannBoxInputFile(File):
+ def __init__(self, filename=None, **kwargs):
+ self.filename = None
+ # Default Init
+ self['fieldDim'] = None
+ self['nComp'] = None
+ self['nx'] = None
+ self['ny'] = None
+ self['nz'] = None
+ self['Lx'] = None
+ self['Ly'] = None
+ self['Lz'] = None
+ self['type'] = None
+ self['U'] = None
+ self['z'] = None
+ self['zNone'] = None
+ self['spectrum'] = None
+ # Basic
+ self['alpha_eps'] = None
+ self['L'] = None
+ self['Gamma'] = None
+ self['seed'] = -1
+ self['file_u'] = None
+ self['file_v'] = None
+ self['file_w'] = None
+ for k,v in kwargs.items():
+ print('>>> Setting',k,v)
+
+ if filename:
+ self.read(filename=filename)
+
+ def read(self, filename):
+ 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)
+
+ with open(self.filename) as f:
+ print('IO: Reading Mann input file: '+self.filename)
+ self['fieldDim'] = int(f.readline())
+ self['nComp'] = int(f.readline())
+ if(self['nComp']==3):
+ self['nx'] = int(f.readline())
+ self['ny'] = int(f.readline())
+ self['nz'] = int(f.readline())
+ self['Lx'] = float(f.readline())
+ self['Ly'] = float(f.readline())
+ self['Lz'] = float(f.readline())
+ else:
+ raise Exception('nComp=2')
+ self['type']=f.readline().strip()
+ if(self['type']=='basic'):
+ self['alpha_eps'] = float(f.readline())
+ self['L'] = float(f.readline())
+ self['Gamma'] = float(f.readline())
+ else:
+ raise Exception('not basic')
+ self['seed']=int(f.readline())
+ if(self['nComp']>=1):
+ self['file_u']=f.readline().strip()
+ if(self['nComp']>=2):
+ self['file_v']=f.readline().strip()
+ if(self['nComp']>=3):
+ self['file_w']=f.readline().strip()
+
+ def write(self, filename):
+ """ Write mann box """
+ if filename:
+ self.filename = filename
+
+ self.defaultFileNames(self.filename)
+
+ with open(self.filename, 'w') as f:
+ f.write(str(self['fieldDim'])+'\n')
+ f.write(str(self['nComp'])+'\n')
+ if(self['nComp']==3):
+ f.write(str(self['nx'])+'\n')
+ f.write(str(self['ny'])+'\n')
+ f.write(str(self['nz'])+'\n')
+ f.write(str(self['Lx'])+'\n')
+ f.write(str(self['Ly'])+'\n')
+ f.write(str(self['Lz'])+'\n')
+ else:
+ raise Exception('nComp=2')
+
+ f.write(self['type']+'\n')
+ if(self['type']=='basic'):
+ f.write(str(self['alpha_eps'])+'\n');
+ f.write(str(self['L'])+'\n');
+ f.write(str(self['Gamma'])+'\n');
+ else:
+ raise Exception('not basic')
+
+ f.write(str(self['seed'])+'\n');
+ if(self['nComp']>=1):
+ f.write(self['file_u']+'\n');
+ if(self['nComp']>=2):
+ f.write(self['file_v']+'\n');
+ if(self['nComp']>=3):
+ f.write(self['file_w']+'\n');
+
+ def defaultFileNames(self, inpfile):
+ base = os.path.splitext(os.path.basename(inpfile))[0]
+ nx,ny,nz = self['nx'], self['ny'], self['nz']
+ self['file_u'] = base+'_{}x{}x{}.u'.format(nx,ny,nz)
+ self['file_v'] = base+'_{}x{}x{}.v'.format(nx,ny,nz)
+ self['file_w'] = base+'_{}x{}x{}.w'.format(nx,ny,nz)
+
+# def set3Disotropic(self,nx,ny,nz,Lx,Ly,Lz,alpha_eps,L):
+# self.fieldDim=3;
+# self.NComp=3;
+# self.nx=nx;
+# self.ny=ny;
+# self.nz=nz;
+# self.Lx=Lx;
+# self.Ly=Ly;
+# self.Lz=Lz;
+# self.alpha_eps=alpha_eps;
+# self.L=L;
+# # Isotropic
+# self.Gamma=0.0;
+# self.type='basic';
+#
+# def auto_file_names(self,folder=''):
+# if folder is None:
+# folder=''
+# import os
+# if(self.type=='basic' and self.Gamma==0):
+# if self.fieldDim==3:
+# filenameBase='Isotropic3D_'+str(self.nx)+'_'+str(self.ny)+'_'+str(self.nz)+'_h'+str(int(100*float(self.Lx)/float(self.nx-1))/100.)
+# self.filename = os.path.join(folder,filenameBase+'.maninp')
+# self.file_u = os.path.join(folder,filenameBase+'_u.dat')
+# self.file_v = os.path.join(folder,filenameBase+'_v.dat')
+# self.file_w = os.path.join(folder,filenameBase+'_w.dat')
+#
+
+ def __repr__(self):
+ s = '<{} object> with keys:\n'.format(type(self).__name__)
+ for k,v in self.items():
+ s += '{:15s}: {}\n'.format(k,v)
+ return s
+
+
+
+#
+# def get_outfile_u(self):
+# return self.file_u
+# def get_outfile_v(self):
+# return self.file_v
+# def get_outfile_w(self):
+# return self.file_w
+#
+# if __name__ == "__main__":
+# import sys
+# if len(sys.argv)>1:
+# print 'called with: ', int(sys.argv[1])
+#
+# nx=1024;
+# ny=256;
+# nz=256;
+# Lx=100;
+# Ly=10;
+# Lz=10;
+# alpha_eps=0.01
+# L=10;
+#
+# inp=MannInputFile();
+# inp.set3Disotropic(nx,ny,nz,Lx,Ly,Lz,alpha_eps,L)
+# inp.auto_file_names()
+# inp.write();
+# # inp2=MannInputFile(inp.filename);
+# # inp2.filename='out10.inp'
+# # inp2.write()
+#
diff --git a/weio/matlabmat_file.py b/weio/matlabmat_file.py
new file mode 100644
index 0000000..b4b29ed
--- /dev/null
+++ b/weio/matlabmat_file.py
@@ -0,0 +1,111 @@
+"""
+Input/output class for the matlab .mat fileformat
+"""
+import numpy as np
+import pandas as pd
+import os
+import scipy.io
+
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File=dict
+ EmptyFileError = type('EmptyFileError', (Exception,),{})
+ WrongFormatError = type('WrongFormatError', (Exception,),{})
+ BrokenFormatError = type('BrokenFormatError', (Exception,),{})
+
+class MatlabMatFile(File):
+ """
+ Read/write a mat file. The object behaves as a dictionary.
+
+ Main methods
+ ------------
+ - read, write, toDataFrame, keys
+
+ Examples
+ --------
+ f = MatlabMatFile('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 'Matlab mat file'
+
+ @staticmethod
+ def priority(): return 60 # Priority in weio.read fileformat list between 0=high and 100:low
+
+ 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)
+
+ mfile = scipy.io.loadmat(self.filename)
+
+ 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')
+ #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"""
+ # --- Example (returning one DataFrame):
+ # return pd.DataFrame(data=np.zeros((10,2)),columns=['Col1','Col2'])
+ # --- Example (returning dict of DataFrames):
+ #dfs={}
+ #cols=['Alpha_[deg]','Cl_[-]','Cd_[-]','Cm_[-]']
+ #dfs['Polar1'] = pd.DataFrame(data=..., columns=cols)
+ #dfs['Polar1'] = pd.DataFrame(data=..., columns=cols)
+ # return dfs
+ raise NotImplementedError()
+
+ # --- 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, write, toDataFrame, keys'
+ return s
+
+ def toString(self):
+ """ """
+ s=''
+ return s
+
+
+
diff --git a/weio/mini_yaml.py b/weio/mini_yaml.py
index 04269b9..1046950 100644
--- a/weio/mini_yaml.py
+++ b/weio/mini_yaml.py
@@ -1,98 +1,202 @@
-from __future__ import unicode_literals
-from __future__ import print_function
-from io import open
-import numpy as np
-
-def yaml_read(filename,dictIn=None):
- """
- read yaml files only supports:
- - Key value pairs:
- key: value
- - Key with lists of lists:
- key:
- - [0,1]
- - [0,1]
- - Comments are stripped based on first # found (in string or not)
- - Keys are found based on first : found (in string or not)
- """
- # Read all lines at once
- with open(filename, 'r', errors="surrogateescape") as f:
- lines=f.read().splitlines()
-
-
- if dictIn is None:
- d=dict()
- else:
- d=dictIn
-
- def cleanComment(l):
- """ remove comments from a line"""
- return l.split('#')[0].strip()
-
- def readDashList(iStart):
- """ """
- i=iStart
- while i0])
- try:
- FirstElems=FirstElems.astype(int)
- mytype=int
- except:
- try:
- FirstElems=FirstElems.astype(float)
- mytype=float
- except:
- raise Exception('Cannot convert line to float or int: {}'.format(lines[iStart]))
- M = np.zeros((n,len(FirstElems)), mytype)
- if len(FirstElems)>0:
- for i in np.arange(iStart,iEnd+1):
- elem = cleanComment(lines[i])[1:].replace(']','').replace('[','').split(',')
- M[i-iStart,:] = np.array([v.strip() for v in elem if len(v)>0]).astype(mytype)
- return M, iEnd+1
-
- i=0
- while i=2 and sp[1].strip()[0]=='{':
+ key = sp[0]
+ i1=l.find('{')
+ i2=l.find('}')
+ LL = l[i1:i2+1]
+ D=_str2dict(LL)
+ d[key] = D
+ elif len(sp)>=2:
+ key = sp[0]
+ value = ':'.join(sp[1:])
+ d[key] = value
+ print('mini_yaml: Setting {}={}'.format(key, value))
+
+ else:
+ raise Exception('Line {:d} has colon, number of splits is {}, which is not supported'.format(i,len(sp)))
+ return d
+
+def _cleanComment(l):
+ """ remove comments from a line"""
+ return l.split('#')[0].strip()
+
+
+def _str2dict(string):
+ string = string.strip('{}')
+ pairs = string.split(', ')
+ d={}
+ for pair in pairs:
+ print(pair)
+ sp = pair.split(':')
+ key = sp[0].strip()
+ value = sp[1].strip()
+ try:
+ d[key] = int(value)
+ continue
+ except ValueError:
+ pass
+ try:
+ d[key] = float(value)
+ continue
+ except ValueError:
+ pass
+ d[key] = value
+ return d
+
+def _readDashList(iStart, lines):
+ """ """
+ i=iStart
+ # Count number of lines that starts with dash
+ while i0:
+ for i in np.arange(iStart,iEnd+1):
+ L,_ = _readInlineList(lines[i].lstrip()[1:], mytype=mytype)
+ M[i-iStart,:] = L
+ return M, iEnd+1
+
+def _readInlineList(line, mytype=None):
+ """
+ Parse a simple list of int, float or string
+ [a, b, c]
+ [a, b, c,]
+ """
+ L = _cleanComment(line.replace(']','').replace('[','')).strip().rstrip(',').strip()
+ if len(L)==0:
+ return np.array([]), float
+ L=L.split(',')
+ L = np.asarray(L)
+ if mytype is None:
+ ## try to detect type
+ try:
+ L=L.astype(int)
+ mytype=int
+ except:
+ try:
+ L=L.astype(float)
+ mytype=float
+ except:
+ try:
+ L=L.astype(str)
+ mytype=str
+ L=np.array([c.strip() for c in L])
+ except:
+ raise Exception('Cannot parse list from string: >{}<'.format(line))
+ else:
+ try:
+ L = L.astype(mytype)
+ if mytype==str:
+ L=np.array([c.strip() for c in L])
+ except:
+ raise Exception('Cannot parse list of type {} from string: >{}<'.format(mytype, line))
+
+ #
+ return L, mytype
+
+
+if __name__=='__main__':
+# #d=yaml_read('test.SD.sum.yaml')
+ text = """
+# Comment
+IS1: 40567 # int scalar
+FS1: 40567.32 # float scalar
+FA1: # Array1
+ - [ 3.97887E+07, 0.00000E+00, 0.00000E+00]
+ - [ 0.00000E+00, 3.97887E+07, 0.00000E+00]
+ - [ 0.00000E+00, 0.00000E+00, 0.00000E+00]
+FA2: # Array2
+ - [ 1.E+00, 0.E+00, 0.E+00,]
+ - [ 0.E+00, 1.E+00, 0.E+00,]
+ - [ 0.E+00, 0.E+00, 1.E+00,]
+FL1: [ 0.0, 0.0, 1.0 ] # FloatList1
+FL2: [ 0.0, 0.0, 1.0,] # FloatList2
+SL2: [ aa, bb , cc, dd, ] #string list
+"""
+ text="""
+EL1: [ ] #empty list
+EL2: #empty list2
+ - [ ] # empty list
+"""
+ d=yaml_read(text=text)
+ print(d)
+ for k,v, in d.items():
+ if hasattr(v,'__len__'):
+ if len(v)>0:
+ print('{:12s} {:20s} {}'.format(k, str(type(v[0]))[6:], v[0]))
+ else:
+ print('{:12s} {:20s} {}'.format(k, str(type(v))[6:], v))
+ else:
+ print('{:12s} {:20s} {}'.format(k, str(type(v))[6:], v))
+
+
+
diff --git a/weio/netcdf_file.py b/weio/netcdf_file.py
index e54e412..0f73f6e 100644
--- a/weio/netcdf_file.py
+++ b/weio/netcdf_file.py
@@ -1,50 +1,40 @@
-from __future__ import division
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import absolute_import
-from io import open
-from builtins import map
-from builtins import range
-from builtins import chr
-from builtins import str
-from future import standard_library
-standard_library.install_aliases()
-
-from .file import File, WrongFormatError
-import pandas as pd
-
-#import xarray as xr #
-
-class NetCDFFile(File):
-
- @staticmethod
- def defaultExtensions():
- return ['.nc']
-
- @staticmethod
- def formatName():
- return 'NetCDF file (<=2D)'
-
- def _read(self):
- try:
- import xarray as xr
- except:
- raise Exception('Python module `xarray` not installed')
-
- self.data=xr.open_dataset(self.filename)
-
- def _write(self):
- self.data.to_netcdf(self.filename)
-
- def _toDataFrame(self):
- dfs={}
- for k in self.data.keys():
- # Not pretty...
- if len(self.data[k].shape)==2:
- dfs[k]=pd.DataFrame(data=self.data[k].values)
- elif len(self.data[k].shape)==1:
- dfs[k]=pd.DataFrame(data=self.data[k].values)
- #import pdb
- #pdb.set_trace()
- return dfs
-
+import pandas as pd
+
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File = dict
+ class WrongFormatError(Exception): pass
+ class BrokenFormatError(Exception): pass
+
+class NetCDFFile(File):
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.nc']
+
+ @staticmethod
+ def formatName():
+ return 'NetCDF file (<=2D)'
+
+ def _read(self):
+ try:
+ import xarray as xr
+ except:
+ raise Exception('Python module `xarray` not installed')
+
+ self.data=xr.open_dataset(self.filename)
+
+ def _write(self):
+ self.data.to_netcdf(self.filename)
+
+ def _toDataFrame(self):
+ dfs={}
+ for k in self.data.keys():
+ # Not pretty...
+ if len(self.data[k].shape)==2:
+ dfs[k]=pd.DataFrame(data=self.data[k].values)
+ elif len(self.data[k].shape)==1:
+ dfs[k]=pd.DataFrame(data=self.data[k].values)
+ return dfs
+
diff --git a/weio/parquet_file.py b/weio/parquet_file.py
index 9c9feba..9621868 100644
--- a/weio/parquet_file.py
+++ b/weio/parquet_file.py
@@ -1,45 +1,48 @@
-import pandas as pd
-
-from .file import File
-
-
-class ParquetFile(File):
-
- @staticmethod
- def defaultExtensions():
- return ['.parquet']
-
- @staticmethod
- def formatName():
- return 'Parquet file'
-
- def __init__(self,filename=None,**kwargs):
- self.filename = filename
- if filename:
- self.read(**kwargs)
-
-
- def _read(self):
- """ use pandas read_parquet function to read parquet file"""
- self.data=pd.read_parquet(self.filename)
-
- def _write(self):
- """ use pandas DataFrame.to_parquet method to write parquet file """
- self.data.to_parquet(path=self.filename)
-
- def toDataFrame(self):
- #already stored as a data frame in self.data
- #just return self.data
- return self.data
-
-
- def toString(self):
- """ use pandas DataFrame.to_string method to convert to a string """
- s=self.data.to_string()
- return s
-
- def __repr__(self):
- s ='Class Parquet (attributes: data)\n'
- return s
-
-
+import pandas as pd
+
+from .file import File
+
+
+class ParquetFile(File):
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.parquet']
+
+ @staticmethod
+ def formatName():
+ return 'Parquet file'
+
+ def __init__(self,filename=None,**kwargs):
+ self.filename = filename
+ if filename:
+ self.read(**kwargs)
+
+
+ def _read(self):
+ """ use pandas read_parquet function to read parquet file"""
+ self.data=pd.read_parquet(self.filename)
+
+ def _write(self):
+ """ use pandas DataFrame.to_parquet method to write parquet file """
+ self.data.to_parquet(path=self.filename)
+
+ def toDataFrame(self):
+ #already stored as a data frame in self.data
+ #just return self.data
+ return self.data
+
+ def fromDataFrame(self, df):
+ #data already in dataframe
+ self.data = df
+
+ def toString(self):
+ """ use pandas DataFrame.to_string method to convert to a string """
+ s=self.data.to_string()
+ return s
+
+ def __repr__(self):
+ s ='Class Parquet (attributes: data)\n'
+ return s
+
+
diff --git a/weio/pickle_file.py b/weio/pickle_file.py
new file mode 100644
index 0000000..1f3e470
--- /dev/null
+++ b/weio/pickle_file.py
@@ -0,0 +1,174 @@
+"""
+Input/output class for the pickle fileformats
+"""
+import numpy as np
+import pandas as pd
+import os
+import pickle
+import builtins
+
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File=dict
+ EmptyFileError = type('EmptyFileError', (Exception,),{})
+ WrongFormatError = type('WrongFormatError', (Exception,),{})
+ BrokenFormatError = type('BrokenFormatError', (Exception,),{})
+
+class PickleFile(File):
+ """
+ Read/write a pickle file. The object behaves as a dictionary.
+
+ Main methods
+ ------------
+ - read, write, toDataFrame, keys
+
+ Examples
+ --------
+ f = PickleFile('file.pkl')
+ print(f.keys())
+ print(f.toDataFrame().columns)
+
+ """
+
+ @staticmethod
+ def defaultExtensions():
+ """ List of file extensions expected for this fileformat"""
+ return ['.pkl']
+
+ @staticmethod
+ def formatName():
+ """ Short string (~100 char) identifying the file format"""
+ return 'Pickle file'
+
+ @staticmethod
+ def priority(): return 60 # Priority in weio.read fileformat list between 0=high and 100:low
+
+
+ def __init__(self, filename=None, data=None, **kwargs):
+ """ Class constructor. If a `filename` is given, the file is read. """
+ self.filename = filename
+ if filename and not data:
+ self.read(**kwargs)
+ if data is not None:
+ self._setData(data)
+ if filename:
+ self.write()
+
+ def _setData(self, data):
+ if isinstance(data, dict):
+ for k,v in data.items():
+ self[k] = v
+ else:
+ if hasattr(data, '__dict__'):
+ self.update(data.__dict__)
+ else:
+ self['data'] = data
+
+ def addDict(self, data):
+ self._setData(data)
+
+ def additem(self, key, data):
+ self[key]=data
+
+ 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)
+ # Reads self.filename and stores data into self. Self is (or behaves like) a dictionary
+ # If pickle data is a dict we store its keys in self, otherwise with store the pickle in the "data" key
+ d = pickle.load(open(self.filename, 'rb'))
+ self._setData(d)
+
+ 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')
+ with open(self.filename, 'wb') as fid:
+ pickle.dump(dict(self), fid)
+
+ def toDataFrame(self):
+ """ Returns object into one DataFrame, or a dictionary of DataFrames"""
+ dfs={}
+ for k,v in self.items():
+ if isinstance(v, pd.DataFrame):
+ dfs[k] = v
+ elif isinstance(v, np.ndarray):
+ if len(v.shape)==2:
+ dfs[k] = pd.DataFrame(data=v, columns=['C{}'.format(i) for i in range(v.shape[1])])
+ elif len(v.shape)==1:
+ dfs[k] = pd.DataFrame(data=v, columns=[k])
+ if len(dfs)==1:
+ dfs=dfs[list(dfs.keys())[0]]
+ return dfs
+
+ # --- 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'
+ for k,v in self.items():
+ try:
+ s+='| - {}: type:{} shape:{}\n'.format(k,type(v),v.shape)
+ except:
+ try:
+ s+='| - {}: type:{} len:{}\n'.format(k,type(v), len(v))
+ except:
+ s+='| - {}: type:{}\n'.format(k,type(v))
+ s+='|Main methods:\n'
+ s+='| - read, write, toDataFrame, keys'
+ return s
+
+
+ # --- Functions speficic to filetype
+ def toGlobal(self, namespace=None, overwrite=True, verbose=False, force=False) :
+ #def toGlobal(self, **kwargs):
+ """
+ NOTE: very dangerous, mostly works for global, but then might infect everything
+
+ Inject variables (keys of read dict) into namespace (e.g. globals()).
+ By default, the namespace of the caller is used
+ To use the global namespace, use namespace=globals()
+ """
+ import inspect
+ st = inspect.stack()
+ if len(st)>2:
+ if not force:
+ raise Exception('toGlobal is very dangerous, only use in isolated script. use `force=True` if you really know what you are doing')
+ else:
+ print('[WARN] toGlobal is very dangerous, only use in isolated script')
+ if namespace is None:
+ # Using parent local namespace
+ namespace = inspect.currentframe().f_back.f_globals
+ #namespace = inspect.currentframe().f_back.f_locals # could use f_globals
+ # Using global (difficult, overwriting won't work) It's best if the user sets namespace=globals()
+ # import builtins as _builtins
+ # namespace = _builtins # NOTE: globals() is for the package globals only, we need "builtins"
+
+ gl_keys = list(namespace.keys())
+ for k,v in self.items():
+ if k in gl_keys:
+ if not overwrite:
+ print('[INFO] not overwritting variable {}, already present in global namespace'.format(k))
+ continue
+ else:
+ print('[WARN] overwritting variable {}, already present in global namespace'.format(k))
+
+ if verbose:
+ print('[INFO] inserting in namespace: {}'.format(k))
+ namespace[k] = v # OR do: builtins.__setattr__(k,v)
+
diff --git a/weio/plot3d_file.py b/weio/plot3d_file.py
new file mode 100644
index 0000000..bd576f2
--- /dev/null
+++ b/weio/plot3d_file.py
@@ -0,0 +1,237 @@
+"""
+Input/output class for the Plot3D file format (formatted, ASCII)
+"""
+import numpy as np
+import pandas as pd
+import os
+
+try:
+ from .file import File, WrongFormatError, BrokenFormatError, EmptyFileError
+except:
+ File = dict
+ EmptyFileError = type('EmptyFileError', (Exception,),{})
+ WrongFormatError = type('WrongFormatError', (Exception,),{})
+ BrokenFormatError = type('BrokenFormatError', (Exception,),{})
+
+class Plot3DFile(File):
+ """
+ Read/write a Plot3D file (formatted, ASCII). The object behaves as a dictionary.
+
+ Main methods
+ ------------
+ - read, write, toDataFrame, keys
+
+ Examples
+ --------
+ f = Plot3DFile('file.fmt')
+ print(f.keys())
+ print(f.toDataFrame().columns)
+ """
+
+ @staticmethod
+ def defaultExtensions():
+ """ List of file extensions expected for this fileformat"""
+ return ['.g', '.x', '.y', '.xy', '.xyz', '.fmt']
+
+ @staticmethod
+ def formatName():
+ return 'Plot3D formatted ASCII file'
+
+ @staticmethod
+ def priority(): return 60 # Priority in weio.read fileformat list between 0=high and 100:low
+
+ 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, verbose=False, method='numpy'):
+ """ Reads the file self.filename, or `filename` if provided """
+ 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)
+
+ coords_list, dims = read_plot3d(self.filename, verbose=verbose, method=method, singleblock=False)
+ self['coords'] = coords_list
+ self['dims'] = dims
+
+ 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')
+
+ write_plot3d(self.filename, self['coords'], self['dims'], singleblock=False)
+
+ def toDataFrame(self):
+ """ Returns one DataFrame (single block) or a dict of DataFrames (multi-block) """
+ coords_list = self.get('coords', None)
+ if coords_list is None:
+ raise Exception("No coordinates loaded.")
+ if len(coords_list) == 1:
+ coords = coords_list[0]
+ df = pd.DataFrame(coords, columns=['x', 'y', 'z'])
+ return df
+ else:
+ dfs={}
+ for i, coords in enumerate(coords_list):
+ df = pd.DataFrame(coords, columns=['x', 'y', 'z'])
+ dfs[f'block_{i}'] = df
+ return dfs
+
+ def __repr__(self):
+ s = '<{} object>:\n'.format(type(self).__name__)
+ s += '|Main attributes:\n'
+ s += '| - filename: {}\n'.format(self.filename)
+ if 'dims' in self:
+ for i, dims in enumerate(self['dims']):
+ s += '| - dims[{}]: shape {}\n'.format(i, dims)
+ if 'coords' in self:
+ for i, coords in enumerate(self['coords']):
+ s += '| - coords[{}]: shape {}\n'.format(i, coords.shape)
+ s += '|Main methods:\n'
+ s += '| - read, write, toDataFrame, keys'
+ return s
+
+ def toString(self):
+ """ """
+ s = ''
+ return s
+
+ def plot(self, ax=None, **kwargs):
+ """
+ Plots the x, y coordinates as a scatter plot.
+
+ Parameters
+ ----------
+ ax : matplotlib.axes.Axes, optional
+ Existing matplotlib axes to plot on. If None, a new figure and axes are created.
+ **kwargs : dict
+ Additional keyword arguments passed to plt.scatter.
+ """
+ import matplotlib.pyplot as plt
+ dfs = self.toDataFrame()
+ if isinstance(dfs, dict):
+ for key, df in dfs.items():
+ if ax is None:
+ fig, ax = plt.subplots()
+ ax.scatter(df['x'], df['y'], label=key, **kwargs)
+ ax.legend()
+ else:
+ df = dfs
+ if ax is None:
+ fig, ax = plt.subplots()
+ ax.scatter(df['x'], df['y'], **kwargs)
+ ax.set_xlabel('x')
+ ax.set_ylabel('y')
+ ax.set_title('Plot3D x-y Scatter')
+ ax.axis('equal')
+ ax.grid(True)
+ return ax
+
+
+
+
+# --------------------------------------------------------------------------------}
+# --- Low level functions
+# --------------------------------------------------------------------------------{
+def read_plot3d(filename, verbose=False, method='numpy', singleblock=False):
+ """
+ Reads a simple multi-block Plot3D file (formatted, ASCII).
+ Returns:
+ coords_list: list of (n_points, 3) arrays, one per block
+ dims: (n_blocks, 3) list of (ni, nj, nk)
+ """
+ coords_list = []
+ if method == 'numpy':
+ dims = np.loadtxt(filename, skiprows=1, max_rows=1, dtype=int)
+ coords = np.loadtxt(filename, skiprows=2)
+ coords = coords.reshape((3, dims[0]*dims[1]*dims[2])).transpose()
+ dims = [dims]
+ coords_list = [coords]
+ else:
+ with open(filename, "r") as f:
+ nblocks = int(f.readline())
+ dims = []
+ for _ in range(nblocks):
+ dims.append(tuple(int(x) for x in f.readline().split()))
+ for block in range(nblocks):
+ ni, nj, nk = dims[block]
+ npts = ni * nj * nk
+ block_coords = np.zeros((npts, 3))
+ for idim in range(3):
+ for k in range(nk):
+ for j in range(nj):
+ for i in range(ni):
+ idx = i + j * ni + k * ni * nj
+ val = float(f.readline())
+ block_coords[idx, idim] = val
+ coords_list.append(block_coords)
+ if singleblock and len(coords_list) == 1:
+ return coords_list[0], dims[0]
+ if verbose:
+ for i, arr in enumerate(coords_list):
+ print(f"Block {i}: shape {arr.shape}, dims {dims[i]}")
+ x_min, x_max = arr[:, 0].min(), arr[:, 0].max()
+ y_min, y_max = arr[:, 1].min(), arr[:, 1].max()
+ z_min, z_max = arr[:, 2].min(), arr[:, 2].max()
+ print(f" x range: [{x_min:.6f}, {x_max:.6f}]")
+ print(f" y range: [{y_min:.6f}, {y_max:.6f}]")
+ print(f" z range: [{z_min:.6f}, {z_max:.6f}]")
+ return coords_list, dims
+
+def write_plot3d(filename, coords_list, dims, singleblock=False):
+ """
+ Writes a simple multi-block Plot3D file (formatted, ASCII).
+ Args:
+ filename: Output file name
+ coords_list: list of (n_points, 3) arrays, one per block
+ dims: (n_blocks, 3) list of (ni, nj, nk) or a single (ni, nj, nk)
+ singleblock: If True, write as single block (no nblocks header)
+ """
+ if singleblock:
+ coords_list = [coords_list] # Ensure coords_list is a list with one block
+ dims = [dims] # Ensure dims is a list with one block
+ # Ensure coords_list is a list
+ if not isinstance(coords_list, list):
+ coords_list = [coords_list]
+ with open(filename, "w") as f:
+ nblocks = len(coords_list)
+ f.write(f"{nblocks}\n")
+
+ if nblocks == 1:
+ ni, nj, nk = dims if isinstance(dims, (list, tuple)) and len(dims) == 3 else dims[0]
+ f.write(f"{ni} {nj} {nk}\n")
+ coords = coords_list[0]
+ for idim in range(3):
+ for idx in range(coords.shape[0]):
+ f.write(f"{coords[idx, idim]}\n")
+ else:
+ for block, (coords, (ni, nj, nk)) in enumerate(zip(coords_list, dims)):
+ f.write(f"{ni} {nj} {nk}\n")
+ for idim in range(3):
+ for idx in range(coords.shape[0]):
+ f.write(f"{coords[idx, idim]}\n")
+ #for k in range(nk):
+ # for j in range(nj):
+ # for i in range(ni):
+ # idx = i + j * ni + k * ni * nj
+ # f.write(f"{coords[idx, idim]}\n")
+
+
+if __name__ == '__main__':
+ import matplotlib.pyplot as plt
+ p3d = Plot3DFile('C:/Work/cfd/nalulib/examples/airfoils/naca0012_blunt.fmt')
+ print(p3d)
+ p3d.plot()
+ p3d.write('C:/Work/cfd/nalulib/examples/airfoils/_naca0012_blunt_out.fmt')
+ plt.show()
+
+
diff --git a/weio/rosco_discon_file.py b/weio/rosco_discon_file.py
new file mode 100644
index 0000000..5e5bc16
--- /dev/null
+++ b/weio/rosco_discon_file.py
@@ -0,0 +1,265 @@
+"""
+Input/output class for the fileformat ROSCO DISCON file
+"""
+import numpy as np
+import pandas as pd
+import os
+from collections import OrderedDict
+
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File=OrderedDict
+ EmptyFileError = type('EmptyFileError', (Exception,),{})
+ WrongFormatError = type('WrongFormatError', (Exception,),{})
+ BrokenFormatError = type('BrokenFormatError', (Exception,),{})
+
+class ROSCODISCONFile(File):
+ """
+ Read/write a ROSCO DISCON file. The object behaves as a dictionary.
+
+ Main methods
+ ------------
+ - read, write, toDataFrame, keys
+
+ Examples
+ --------
+ f = ROSCODISCONFile('DISCON.IN')
+ print(f.keys())
+ print(f.toDataFrame().columns)
+
+ """
+
+ @staticmethod
+ def defaultExtensions():
+ """ List of file extensions expected for this fileformat"""
+ return ['.in']
+
+ @staticmethod
+ def formatName():
+ """ Short string (~100 char) identifying the file format"""
+ return 'ROSCO DISCON file'
+
+ @staticmethod
+ def priority(): return 60 # Priority in weio.read fileformat list between 0=high and 100:low
+
+
+ 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
+ _, comments, lineKeys = read_DISCON(self.filename, self)
+ self.comments=comments
+ self.lineKeys=lineKeys
+
+ 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')
+ with open(self.filename, 'w') as f:
+ f.write(self.toString())
+
+
+ def toDataFrame(self):
+ """ Returns object into one DataFrame, or a dictionary of DataFrames"""
+ dfs={}
+ low_keys = [s.lower() for s in self.keys()]
+ if 'pc_gs_n' in low_keys:
+ M = np.column_stack([self['PC_GS_angles']*180/np.pi, self['PC_GS_KP'], self['PC_GS_KI'], self['PC_GS_KD'], self['PC_GS_TF']] )
+ cols = ['Pitch_[deg]', 'KP_[-]', 'KI_[s]', 'KD_[1/s]', 'TF_[-]']
+ dfs['PitchSchedule'] = pd.DataFrame(data=M, columns=cols)
+ if 'ps_bldpitchmin_n' in low_keys:
+ M = np.column_stack([self['PS_WindSpeeds'], self['PS_BldPitchMin']])
+ cols = ['WindSpeed_[m/s]', 'Pitch_[deg]']
+ dfs['PitchSaturation'] = pd.DataFrame(data=M, columns=cols)
+ if 'prc_n' in low_keys:
+ M = np.column_stack([self['PRC_WindSpeeds'], self['PRC_RotorSpeeds']*30/np.pi])
+ cols = ['WindSpeed_[m/s]', 'RotorSpeed_[rpm]']
+ dfs['PowerTracking'] = pd.DataFrame(data=M, columns=cols)
+
+ return dfs
+
+ # --- 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, write, toDataFrame, keys'
+ return s
+
+ def toString(self):
+ """ """
+ maxKeyLengh = np.max([len(k) for k in self.keys()])
+ maxKeyLengh = max(maxKeyLengh, 18)
+ fmtKey = '{:' +str(maxKeyLengh)+'s}'
+ s=''
+ for l in self.lineKeys:
+ if len(l)==0:
+ s+='\n'
+ elif l.startswith('!'):
+ s+=l+'\n'
+ else:
+ param = l
+ comment = self.comments[param]
+ v = self[param]
+ sparam = '! '+fmtKey.format(param)
+ # NOTE: could to "param" specific outputs here
+ FMTs = {}
+ FMTs['{:<4.6f}']=['F_NotchBetaNumDen', 'F_FlCornerFreq', 'F_FlpCornerFreq', 'PC_GS_angles', 'PC_GS_KP', 'PC_GS_KI', 'PC_GS_KD', 'PC_GS_TF', 'IPC_Vramp', 'IPC_aziOffset']
+ FMTs['{:<4.3e}']=['IPC_KP','IPC_KI']
+ FMTs['{:<4.3f}']=['PRC_WindSpeeds', 'PRC_RotorSpeed','PS_WindSpeeds']
+ FMTs['{:<4.4f}']=['WE_FOPoles_v']
+ FMTs['{:<10.8f}']=['WE_FOPoles']
+ FMTs['{:<10.3f}']=['PS_BldPitchMin']
+ fmtFloat='{:<014.5f}'
+ for fmt,keys in FMTs.items():
+ if param in keys:
+ fmtFloat=fmt
+ break
+ if type(v) is str:
+ sval='"{:15s}" '.format(v)
+ elif hasattr(v, '__len__'):
+ if isinstance(v[0], (np.floating, float)):
+ sval=' '.join([fmtFloat.format(vi) for vi in v] )+' '
+ else:
+ sval=' '.join(['{}'.format(vi) for vi in v] )+' '
+ elif type(v) is int:
+ sval='{:<14d} '.format(v)
+ elif isinstance(v, (np.floating, float)):
+ sval=fmtFloat.format(v) + ' '
+ else:
+ sval='{} '.format(v)
+ s+='{}{}{}\n'.format(sval, sparam, comment)
+ return s
+
+
+
+
+
+
+# Some useful constants
+pi = np.pi
+rad2deg = np.rad2deg(1)
+deg2rad = np.deg2rad(1)
+rpm2RadSec = 2.0*(np.pi)/60.0
+RadSec2rpm = 60/(2.0 * np.pi)
+
+def write_DISCON(turbine, controller, param_file='DISCON.IN', txt_filename='Cp_Ct_Cq.txt', rosco_vt = {}):
+ """
+ Print the controller parameters to the DISCON.IN input file for the generic controller
+
+ Parameters:
+ -----------
+ turbine: class
+ Turbine class containing turbine operation information (ref speeds, etc...)
+ controller: class
+ Controller class containing controller operation information (gains, etc...)
+ param_file: str, optional
+ filename for parameter input file, should be DISCON.IN
+ txt_filename: str, optional
+ filename of rotor performance file
+ """
+
+ # Get ROSCO var tree if not provided
+ if not rosco_vt:
+ rosco_vt = DISCON_dict(turbine, controller, txt_filename)
+
+ print('Writing new controller parameter file parameter file: %s.' % param_file)
+ # Should be obvious what's going on here...
+ file = open(param_file,'w')
+
+ # Write Open loop input
+ if rosco_vt['OL_Mode'] and hasattr(controller, 'OpenLoop'):
+ write_ol_control(controller)
+
+def read_DISCON(DISCON_filename, DISCON_in = None):
+ '''
+ Read the DISCON input file.
+ Adapted from ROSCO_Toolbox, https:github.com/NREL/ROSCO
+
+ Parameters:
+ ----------
+ DISCON_filename: string
+ Name of DISCON input file to read
+
+ Returns:
+ --------
+ DISCON_in: Dict
+ Dictionary containing input parameters from DISCON_in, organized by parameter name
+ '''
+
+ if DISCON_in is None:
+ DISCON_in = OrderedDict()
+ comments={}
+ lineKeys=[]
+ with open(DISCON_filename) as discon:
+ for line in discon:
+ line=line.strip()
+ # empty lines
+ if len(line)==0:
+ lineKeys.append('')
+ continue
+ # Pure comments
+ if line[0] == '!':
+ lineKeys.append(line)
+ continue
+
+ if (line.split()[1] != '!'): # Array valued entries
+ sps = line.split()
+ array_length = sps.index('!')
+ param = sps[array_length+1]
+ values = np.array( [float(x) for x in sps[:array_length]] )
+ else: # All other entries
+ param = line.split()[2]
+ value = line.split()[0]
+ # Remove printed quotations if string is in quotes
+ if (value[0] == '"') or (value[0] == "'"):
+ values = value[1:-1]
+ else:
+ if value.find('.')>0:
+ values = float(value)
+ else:
+ values = int(value)
+ DISCON_in[param] = values
+ lineKeys.append(param)
+
+ sp = line.split('!')
+ comment = sp[1].strip()
+ comment = comment[len(param):].strip()
+ comments [param] = comment
+
+ return DISCON_in, comments, lineKeys
+
+
+if __name__ == '__main__':
+ filename = 'DISCON.in'
+ rd = ROSCODISCONFile(filename)
+ #print(rd.keys())
+# print(rd.toString())
+ rd.write(filename+'_WEIO')
+ print(rd.toDataFrame())
diff --git a/weio/rosco_performance_file.py b/weio/rosco_performance_file.py
index 18deea1..402d18e 100644
--- a/weio/rosco_performance_file.py
+++ b/weio/rosco_performance_file.py
@@ -6,7 +6,7 @@
import os
try:
- from .file import File, WrongFormatError, BrokenFormatError
+ from .file import File, EmptyFileError, WrongFormatError, BrokenFormatError
except:
EmptyFileError = type('EmptyFileError', (Exception,),{})
WrongFormatError = type('WrongFormatError', (Exception,),{})
@@ -19,13 +19,17 @@ class ROSCOPerformanceFile(File):
Main methods
------------
- - read, write, toDataFrame, keys
+ - read, write, toDataFrame, to2DFields, keys
Examples
--------
f = ROSCOPerformanceFile('Cp_Ct_Cq.txt')
print(f.keys())
print(f.toDataFrame().columns)
+ fig = f.plotCP3D()
+ CPmax, tsr_max, pitch_max = f.CPmax()
+ CP = fCP([0, 1], [5, 5])
+ CT = fCT([0, 1], [5, 5])
"""
@@ -39,15 +43,38 @@ def formatName():
""" Short string (~100 char) identifying the file format"""
return 'ROSCO Performance file'
- def __init__(self, filename=None, **kwargs):
- """ Class constructor. If a `filename` is given, the file is read. """
+ def __init__(self, filename=None, pitch=None, tsr=None, WS=None, CP=None, CT=None, CQ=None, name='',**kwargs):
+ """ Class constructor. If a `filename` is given, the file is read.
+ Otherwise values may be provided directly
+
+ INPUTS:
+ - filename: input file for ROSCO Performance file
+ OR
+ - pitch: pitch angle [deg], array of length nPitch
+ - tsr: tip-speed ratio [-], array of length nTSR
+ - CP,CT,CQ: aerodynamic coefficients, arrays of shape nTSR x nPitch
+ CQ is optional since CP = tsr*CQ
+ - name: wind turbine name
+ """
self.filename = filename
+ self.name = name # Turbine name
+ self['pitch'] = pitch
+ self['TSR'] = tsr
+ self['WS'] = WS
+ self['CP'] = CP
+ self['CT'] = CT
+ self['CQ'] = CQ
+
if filename:
self.read(**kwargs)
- def read(self, filename=None, **kwargs):
- """ Reads the file self.filename, or `filename` if provided """
+ if self['pitch'] is not None:
+ self.checkConsistency()
+ def read(self, filename=None, **kwargs):
+ """ Reads the file self.filename, or `filename` if provided
+ stores data into self.
+ self is (or behaves like) a dictionary"""
# --- Standard tests and exceptions (generic code)
if filename:
self.filename = filename
@@ -57,8 +84,14 @@ def read(self, filename=None, **kwargs):
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)
+
+ pitch, TSR, WS, CP, CT, CQ = load_from_txt(self.filename)
+ self['pitch'] = pitch
+ self['TSR'] = TSR
+ self['WS'] = WS
+ self['CP'] = CP
+ self['CT'] = CT
+ self['CQ'] = CQ
def write(self, filename=None):
""" Rewrite object to file, or write object to `filename` if provided """
@@ -66,24 +99,43 @@ def write(self, filename=None):
self.filename = filename
if not self.filename:
raise Exception('No filename provided')
- # Calling (children) function to write
- self._write()
+ # Sanity
+ self.checkConsistency()
+ # Write
+ write_rotor_performance(self.filename, self['pitch'], self['TSR'], self['CP'],self['CT'], self['CQ'], self['WS'], TurbineName=self.name)
- def _read(self):
- """ Reads self.filename and stores data into self. Self is (or behaves like) a dictionary"""
- # --- Example:
- pitch, TSR, WS, Cp, Ct, Cq = load_from_txt(self.filename)
- self['pitch'] = pitch
- self['TSR'] = TSR
- self['WS'] = WS
- self['CP'] = Cp
- self['CT'] = Ct
- self['CQ'] = Cq
+ def checkConsistency(self, verbose=False):
+ """
+ Check that data makes sense.
+ in particular, check if CP=lambda CQ
+ """
+ if self['WS'] is not None:
+ if not hasattr(self['WS'],'__len__' ):
+ self['WS'] = np.array([self['WS']]).astype(float)
- def _write(self):
- """ Writes to self.filename"""
- # --- Example:
- write_rotor_performance(self.filename, self['pitch'], self['TSR'], self['CP'],self['CT'], self['CQ'], self['WS'], TurbineName='')
+ CQ = self['CQ']
+ CP = self['CP']
+ tsr = np.asarray(self['TSR'])
+ TSR = np.tile(tsr.flatten(), (len(self['pitch']),1)).T
+ if CQ is None and CP is not None:
+ CQ = CP/TSR
+ if verbose:
+ print('[INFO] Computing CQ from CP')
+ elif CQ is not None and CP is None:
+ CP = CQ*TSR
+ if verbose:
+ print('[INFO] Computing CP from CQ')
+ elif CQ is not None and CP is not None:
+ pass
+ else:
+ raise Exception('CP and CQ cannot be None')
+ # Check consistency
+ CP2 = CQ*TSR
+ deltaCP = np.abs(CP-CP2)/0.5*100 # relative difference in %, for a mean CP of 0.5
+ if np.max(deltaCP)>5: # more than 5%
+ raise Exception('Inconsitency between power coefficient and torque coefficient')
+ self['CP'] = CP
+ self['CQ'] = CQ
def toDataFrame(self):
""" Returns object into dictionary of DataFrames"""
@@ -94,7 +146,147 @@ def toDataFrame(self):
dfs['CQ'] = pd.DataFrame(np.column_stack((self['TSR'], self['CQ'])), columns=columns)
return dfs
+ def to2DFields(self, **kwargs):
+ import xarray as xr
+ if len(kwargs.keys())>0:
+ print('[WARN] RoscoPerformance_CpCtCq: to2DFields: ignored keys: ',kwargs.keys())
+ s1 = 'TSR'
+ s2 = 'pitch'
+ ds = xr.Dataset(coords={s1: self['TSR'], s2: self['pitch']})
+ ds[s1].attrs['unit'] = '-'
+ ds[s2].attrs['unit'] = 'deg'
+ for var in ['CP','CT','CQ']:
+ M = self[var].copy()
+ M[M<0] = 0
+ ds[var] = ([s1, s2], M)
+ ds[var].attrs['unit'] = '-'
+ return ds
+
# --- Optional functions
+ def toAeroDisc(self, filename, R, csv=False, WS=None, omegaRPM=10):
+ """ Convert to AeroDisc Format
+ INPUTS:
+ - filename: filename to be written
+ - R: rotor radius [m]
+ - csv: if True write to CSV format, else, use OpenFAST .dat format
+ either:
+ - WS: wind speed [m/s]
+ or
+ - omegaRPM: rotational speed [rpm]
+
+ Logic to determine wind speed or rotational speed:
+ - If user provide a wind speed, we use it. Omega is determined from TSR and WS
+ - If user provide a rotational speed, we use it. WS is determined from TSR and omega
+ - If ROSCO file contain one wind speed, we use it.
+ - Otherwise, we don't know what to do so we raise an exception
+ """
+ # --- Logic to determine wind speed or rotational speed
+ if WS is not None:
+ WS = WS
+ elif omegaRPM is not None:
+ WS = None
+ omega = omegaRPM*(2*np.pi)/60
+ elif self['WS'] is not None and len(self['WS'])==1:
+ WS = self['WS'][0]
+ else:
+ raise Exception('Provide either a wind speed (`WS`) or a rotational speed (`omegaRPM`)')
+
+ with open(filename,'w') as fid:
+ # Header
+ if csv:
+ fid.write("TSR_(-), RtSpd_(rpm) , VRel_(m/s) , Skew_(deg) , Pitch_(deg) , C_Fx_(-) , C_Fy_(-) , C_Fz_(-) , C_Mx_(-) , C_My_(-) , C_Mz_(-)\n")
+ else:
+ fid.write(' TSR RtSpd VRel Skew Pitch C_Fx C_Fy C_Fz C_Mx C_My C_Mz\n')
+ fid.write(' (-) (rpm) (m/s) (deg) (deg) (-) (-) (-) (-) (-) (-)\n')
+ if csv:
+ FMT='{:10.4f},{:10.4f},{:10.4f},{:10.4f},{:10.4f},{:10.4f},{:10.4f},{:10.4f},{:10.4f},{:10.4f},{:10.4f}\n'
+ else:
+ FMT='{:10.4f} {:10.4f} {:10.4f} {:10.4f} {:10.4f} {:10.4f} {:10.4f} {:10.4f} {:10.4f} {:10.4f} {:10.4f}\n'
+ # Loop on oper
+ for j,tsr in enumerate(self['TSR']):
+ if WS is None:
+ U0 = omega*R/tsr
+ else:
+ U0 = WS
+ omega = tsr*U0/R
+ omegaRPM = omega*60/(2*np.pi)
+ for i,p in enumerate(self['pitch']):
+ CP=self['CP'][j,i]
+ CT=self['CT'][j,i]
+ CQ=self['CQ'][j,i]
+ skew=0
+ cfx=CT
+ cfy=0
+ cfz=0
+ cmx=CQ
+ cmy=0
+ cmz=0
+ fid.write(FMT.format(tsr, omegaRPM, U0, skew, p, cfx,cfy,cfz,cmx,cmy,cmz))
+
+ def computeWeights(self):
+ """ Compute interpolant weights for fast evaluation of CP and CT at intermediate values"""
+ CP = self['CP'].copy()
+ CT = self['CT'].copy()
+ CP = CP[CP<0]=0
+ CT = CT[CT<0]=0
+ self._fCP = interp2d_pairs(self['pitch'], self['TSR'], CP, kind='cubic')
+ self._fCT = interp2d_pairs(self['pitch'], self['TSR'], CT, kind='cubic')
+
+ def fCP(self, pitch, tsr):
+ """ Compute CP for given pitch and tsr, where inputs can be scalar, arrays or matrices"""
+ if self._fCP is None:
+ self.computeWeights()
+ return self.fCP(pitch, tsr)
+
+ def fCT(self, pitch, tsr):
+ """ Compute CT for given pitch and tsr, where inputs can be scalar, arrays or matrices"""
+ if self._fCT is None:
+ self.computeWeights()
+ return self.fCT(pitch, tsr)
+
+ def CPmax(self):
+ """ return values at CPmax
+ TODO: interpolation instead of nearest value..
+ """
+ CP = self['CP']
+ i,j = np.unravel_index(CP.argmax(), CP.shape)
+ CPmax, tsr_max, pitch_max = CP[i,j], self['TSR'][i], self['pitch'][j]
+
+ return CPmax, tsr_max, pitch_max
+
+ def plotCP3D(self, plotMax=True, trajectory=None):
+ """
+ Plot 3D surface of CP
+ Optionally plot the maximum and a controller trajectory
+ """
+ import matplotlib.pyplot as plt
+ from mpl_toolkits.mplot3d import Axes3D
+ from matplotlib import cm
+ # Data
+ LAMBDA, PITCH = np.meshgrid(self['TSR'], self['pitch'])
+ CP = self['CP'].copy()
+ CP[CP<0]=0 #
+ CP_max, tsr_max, pitch_max = self.CPmax()
+ # plot
+ fig, ax = plt.subplots(subplot_kw={'projection': '3d'})
+ surf = ax.plot_surface(LAMBDA, PITCH, np.transpose(CP), cmap=cm.coolwarm, linewidth=0, antialiased=True,alpha=0.8, label='$C_p$')
+ if plotMax:
+ ax.scatter(tsr_max, pitch_max, CP_max, c='k', marker='o', s=50, label=r'$C_{p,max}$')
+ if trajectory is not None:
+ if len(trajectory)==3:
+ tsr_, pitch_, CP_ = trajectory
+ else:
+ tsr_, pitch_ = trajectory
+ CP_ = self.fCP(tsr_, pitch_)
+ ax.plot_surface(tsr_, pitch_, CP_, 'k-', linewidth=1 )
+ #fig.tight_layout()
+ #fig.colorbar(surf, shrink=0.5, aspect=15)
+ ax.view_init(elev=20., azim=26)
+ ax.set_xlabel('TSR [-]')
+ ax.set_ylabel('Pitch [deg]')
+ ax.set_zlabel(r'Power coefficient [-]')
+ return fig
+
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.
@@ -104,12 +296,14 @@ def __repr__(self):
s+='| - filename: {}\n'.format(self.filename)
# --- Example printing some relevant information for user
s+='|Main keys:\n'
- s+='| - pitch: {}\n'.format(self['pitch'])
- s+='| - TSR: {}\n'.format(self['TSR'])
- s+='| - WS: {}\n'.format(self['WS'])
- s+='| - CP,CT,CQ : shape {}\n'.format(self['CP'].shape)
+ s+='| - pitch: {} values: {}\n'.format(len(self['pitch']) if self['pitch'] is not None else 0, self['pitch'])
+ s+='| - TSR: {} values: {}\n'.format(len(self['TSR'] ) if self['TSR'] is not None else 0, self['TSR'] )
+ s+='| - WS: {} values: {}\n'.format(len(self['WS'] ) if self['WS'] is not None else 0, self['WS'] )
+ if self['CP'] is not None:
+ s+='| - CP,CT,CQ : shape {}\n'.format(self['CP'].shape)
s+='|Main methods:\n'
- s+='| - read, write, toDataFrame, keys'
+ s+='| - read, write, toDataFrame, keys\n'
+ s+='| - CPmax, plotCP3d, fCP, fCT, toAeroDisc'
return s
@@ -117,7 +311,7 @@ def __repr__(self):
def load_from_txt(txt_filename):
'''
- Taken from ROSCO_toolbox/utitities.py by Nikhar Abbas
+ Adapted from ROSCO_toolbox/utitities.py by Nikhar Abbas
https://github.com/NREL/ROSCO
Apache 2.0 License
@@ -175,7 +369,7 @@ def load_from_txt(txt_filename):
def write_rotor_performance(txt_filename, pitch, TSR, CP, CT, CQ, WS=None, TurbineName=''):
'''
- Taken from ROSCO_toolbox/utitities.py by Nikhar Abbas
+ Adapted from ROSCO_toolbox/utitities.py by Nikhar Abbas
https://github.com/NREL/ROSCO
Apache 2.0 License
@@ -187,8 +381,8 @@ def write_rotor_performance(txt_filename, pitch, TSR, CP, CT, CQ, WS=None, Turbi
'''
file = open(txt_filename,'w')
# Headerlines
- file.write('# ----- Rotor performance tables for the {} wind turbine ----- \n'.format(TurbineName))
- file.write('# ------------ Written on {} using the ROSCO toolbox ------------ \n\n'.format(now.strftime('%b-%d-%y')))
+ file.write('# ----- Rotor performance tables for the wind turbine: {} ----- \n'.format(TurbineName))
+ file.write('# ------------ Written using weio\n\n')
# Pitch angles, TSR, and wind speed
file.write('# Pitch angle vector, {} entries - x axis (matrix columns) (deg)\n'.format(len(pitch)))
@@ -200,7 +394,7 @@ def write_rotor_performance(txt_filename, pitch, TSR, CP, CT, CQ, WS=None, Turbi
if WS is not None:
file.write('\n# Wind speed vector - z axis (m/s)\n')
for i in range(len(WS)):
- file.write('{:0.4} '.format(WS[i]))
+ file.write('{:0.4f} '.format(WS[i]))
file.write('\n')
# Cp
@@ -229,6 +423,36 @@ def write_rotor_performance(txt_filename, pitch, TSR, CP, CT, CQ, WS=None, Turbi
file.close()
+def interp2d_pairs(X,Y,Z,**kwargs):
+ """ Same interface as interp2d but the returned interpolant will evaluate its inputs as pairs of values.
+ Inputs can therefore be arrays
+
+ example:
+ f = interp2d_pairs(vx, vy, M, kind='cubic')
+
+ vx: array of length nx
+ vy: array of length ny
+ M : array of shape nx x ny
+ f : interpolant function
+ v = f(x,y) : if x,y are array of length n, v is of length n
+ with v_i = f(x_i, y_i)
+ author: E. Branlard
+ """
+ import scipy.interpolate as si
+ # Wrapping the scipy interp2 function to call out interpolant instead
+ # --- OLD
+ # Internal function, that evaluates pairs of values, output has the same shape as input
+ #def interpolant(x,y,f):
+ # x,y = np.asarray(x), np.asarray(y)
+ # return (si.dfitpack.bispeu(f.tck[0], f.tck[1], f.tck[2], f.tck[3], f.tck[4], x.ravel(), y.ravel())[0]).reshape(x.shape)
+ #return lambda x,y: interpolant(x,y,si.interp2d(*args,**kwargs))
+ # --- NEW
+ Finterp = si.RegularGridInterpolator((X,Y), Z.T, **kwargs)
+ #r = si.RectBivariateSpline(X, Y, Z.T)
+ def interpolant(x,y):
+ x,y = np.asarray(x), np.asarray(y)
+ return Finterp((x,y))
+ return interpolant
if __name__ == '__main__':
diff --git a/weio/tdms_file.py b/weio/tdms_file.py
index d05e1a9..8bffff9 100644
--- a/weio/tdms_file.py
+++ b/weio/tdms_file.py
@@ -1,65 +1,225 @@
-from .file import File, WrongFormatError, BrokenFormatError
-import numpy as np
-import pandas as pd
-
-class TDMSFile(File):
-
- @staticmethod
- def defaultExtensions():
- return ['.tdms']
-
- @staticmethod
- def formatName():
- return 'TDMS file'
-
- def _read(self):
- try:
- from nptdms import TdmsFile
- except:
- raise Exception('Install the library nptdms to read this file')
-
- fh = TdmsFile(self.filename, read_metadata_only=False)
- channels_address = list(fh.objects.keys())
- channels_address = [ s.replace("'",'') for s in channels_address]
- channel_keys= [ s.split('/')[1:] for s in channels_address if len(s.split('/'))==3]
- # --- Setting up list of signals and times
- signals=[]
- times=[]
- for i,ck in enumerate(channel_keys):
- channel = fh.object(ck[0],ck[1])
- signals.append(channel.data)
- times.append (channel.time_track())
-
- lenTimes = [len(time) for time in times]
- minTimes = [np.min(time) for time in times]
- maxTimes = [np.max(time) for time in times]
- if len(np.unique(lenTimes))>1:
- print(lenTimes)
- raise NotImplementedError('Different time length')
- # NOTE: could use fh.as_dataframe
- if len(np.unique(minTimes))>1:
- print(minTimes)
- raise NotImplementedError('Different time span')
- if len(np.unique(maxTimes))>1:
- print(maxTimes)
- raise NotImplementedError('Different time span')
- # --- Gathering into a data frame with time
- time =times[0]
- signals = [time]+signals
- M = np.column_stack(signals)
- colnames = ['Time_[s]'] + [ck[1] for ck in channel_keys]
- self['data'] = pd.DataFrame(data = M, columns=colnames)
-
-# def toString(self):
-# s=''
-# return s
-# def _write(self):
-# pass
-
- def __repr__(self):
- s ='Class TDMS (key: data)\n'
- return s
-
- def _toDataFrame(self):
- return self['data']
-
+import numpy as np
+import pandas as pd
+import os
+
+try:
+ from .file import File, WrongFormatError, BrokenFormatError, OptionalImportError
+except:
+ File = dict
+ class WrongFormatError(Exception): pass
+ class BrokenFormatError(Exception): pass
+ class OptionalImportError(Exception): pass
+
+class TDMSFile(File):
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.tdms']
+
+ @staticmethod
+ def formatName():
+ return 'TDMS 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)
+ try:
+ from nptdms import TdmsFile
+ except:
+ raise OptionalImportError('Install the library nptdms to read this file')
+
+ fh = TdmsFile(self.filename, read_metadata_only=False)
+ # --- OLD, using some kind of old version of tdms and probably specific to one file
+ # channels_address = list(fh.objects.keys())
+ # channels_address = [ s.replace("'",'') for s in channels_address]
+ # channel_keys= [ s.split('/')[1:] for s in channels_address if len(s.split('/'))==3]
+ # # --- Setting up list of signals and times
+ # signals=[]
+ # times=[]
+ # for i,ck in enumerate(channel_keys):
+ # channel = fh.object(ck[0],ck[1])
+ # signals.append(channel.data)
+ # times.append (channel.time_track())
+
+ # lenTimes = [len(time) for time in times]
+ # minTimes = [np.min(time) for time in times]
+ # maxTimes = [np.max(time) for time in times]
+ # if len(np.unique(lenTimes))>1:
+ # print(lenTimes)
+ # raise NotImplementedError('Different time length')
+ # # NOTE: could use fh.as_dataframe
+ # if len(np.unique(minTimes))>1:
+ # print(minTimes)
+ # raise NotImplementedError('Different time span')
+ # if len(np.unique(maxTimes))>1:
+ # print(maxTimes)
+ # raise NotImplementedError('Different time span')
+ # # --- Gathering into a data frame with time
+ # time =times[0]
+ # signals = [time]+signals
+ # M = np.column_stack(signals)
+ # colnames = ['Time_[s]'] + [ck[1] for ck in channel_keys]
+ # self['data'] = pd.DataFrame(data = M, columns=colnames)
+ # --- NEW
+ self['data'] = fh
+
+ #for group in fh.groups():
+ # for channel in group.channels():
+ # #channel = group['channel name']
+ # print('Group:',group.name , 'Chan:',channel.name)
+ # channel_data = channel[:]
+ # if len(channel_data)>0:
+ # print(' ', type(channel_data))
+ # #print(' ', len(channel_data))
+ # print(' ', channel_data)
+ # print(' ', channel_data[0])
+ # try:
+ # print(channel.time_track())
+ # except KeyError:
+ # print('>>> No time track')
+
+ def write(self, filename=None, df=None):
+ """"
+ Write to TDMS file.
+ NOTE: for now only using a conversion from dataframe...
+ """
+ if filename is None:
+ filename = self.filename
+ if df is None:
+ df = self.toDataFrame(split=False)
+ writeTDMSFromDataFrame(filename, df)
+
+
+ def groups(self):
+ return self['data'].groups()
+
+ @property
+ def groupNames(self):
+ return [group.name for group in self['data'].groups()]
+
+ def __repr__(self):
+ s ='Class TDMS (key: data)\n'
+ s +=' - data: TdmsFile\n'
+ s +=' * groupNames: {}\n'.format(self.groupNames)
+ #for group in fh.groups():
+ # for channel in group.channels():
+ # print(group.name)
+ # print(channel.name)
+ return s
+
+ def toDataFrame(self, split=True):
+ """ Export to one (split=False) or several dataframes (split=True)
+ Splitting on the group
+ """
+
+ def cleanColumns(df):
+ # Cleanup columns
+ colnames = df.columns
+ colnames=[c.replace('\'','') for c in colnames]
+ colnames=[c[1:] if c.startswith('/') else c for c in colnames]
+ # If there is only one group, we remove the group key
+ groupNames = self.groupNames
+ if len(groupNames)==1:
+ nChar = len(groupNames[0])
+ colnames=[c[nChar+1:] for c in colnames] # +1 for the "/"
+ df.columns = colnames
+
+ fh = self['data']
+ if split:
+ # --- One dataframe per group. We skip group that have empty data
+ dfs={}
+ for group in fh.groups():
+ try:
+ df = group.as_dataframe(time_index=True)
+ df.insert(0,'Time_[s]', df.index.values)
+ df.index=np.arange(0,len(df))
+ except KeyError:
+ df = group.as_dataframe(time_index=False)
+ if len(df)>0:
+ dfs[group.name] = df
+ if len(dfs)==1:
+ dfs=dfs[group.name]
+ return dfs
+ else:
+ # --- One dataframe with all data
+ try:
+ df = fh.as_dataframe(time_index=True)
+ cleanColumns(df)
+ df.insert(0,'Time_[s]', df.index.values)
+ df.index=np.arange(0,len(df))
+ except KeyError:
+ df = fh.as_dataframe(time_index=False)
+ return df
+
+def writeTDMSFromDataFrame(filename, df, defaultGroupName='default'):
+ """
+ Write a TDMS file from a pandas dataframe
+
+ Example:
+ # --- Create a TDMS file - One group two channels with time track
+ time = np.linspace(0,1,20)
+ colA = np.random.normal(0,1,20)
+ colB = np.random.normal(0,1,20)
+ df = pd.DataFrame(data={'Time_[s]':time ,'ColA':colA,'ColB':colB})
+ writeTDMSFromDataFrame('out12.tdms', df, defaultGroupName = 'myGroup')
+
+ #--- Create a TDMS file - Two groups, two channels without time track but with timestamp
+ TS = np.arange('2010-02', '2010-02-21', dtype='datetime64[D]')
+ df = pd.DataFrame(data={'GroupA/ColTime':time,'GroupA/ColA':colA,'GroupB/ColTimestamp': TS,'GroupB/ColA':colB)})
+ writeTDMSFromDataFrame('out22.tdms', df)
+
+ """
+ from nptdms import TdmsWriter, ChannelObject
+
+ defaultGroupName = 'default'
+
+ columns =df.columns
+
+ # Check if first column is time
+ if columns[0].lower().find('time')==0:
+ t = df.iloc[:,0].values
+ n = len(t)
+ dt1 = (np.max(t)-np.min(t))/(n-1)
+ if n>1:
+ dt2 = t[1]-t[0]
+ timeProperties = {'wf_increment':dt1, 'wf_start_offset':t[0]}
+ columns = columns[1:] # We remove the time column
+ else:
+ timeProperties = {}
+
+ with TdmsWriter(filename) as tdms_writer:
+
+ channels=[]
+ for iCol, col in enumerate(columns):
+ sp = col.split('/')
+ if len(sp)==2:
+ groupName = sp[0]
+ channelName = sp[1]
+ else:
+ groupName = defaultGroupName
+ channelName = col
+ data_array = df[col].values
+ channels.append(ChannelObject(groupName, channelName, data_array, timeProperties))
+ tdms_writer.write_segment(channels)
+
+if __name__ == '__main__':
+ pass
+# f = TDMSFile('TDMS_.tdms')
+# dfs = f.toDataFrame(split=True)
+# print(f)
+
diff --git a/weio/tests/example_files/FASTIn_HD2.dat b/weio/tests/example_files/FASTIn_HD2.dat
new file mode 100644
index 0000000..b913a27
--- /dev/null
+++ b/weio/tests/example_files/FASTIn_HD2.dat
@@ -0,0 +1,269 @@
+------- HydroDyn Input File ----------------------------------------------------
+NREL 5.0 MW offshore baseline floating platform HydroDyn input properties for the OC3 Hywind.
+True Echo - Echo the input file data (flag)
+---------------------- ENVIRONMENTAL CONDITIONS --------------------------------
+ default WtrDens - Water density (kg/m^3)
+ default WtrDpth - Water depth (meters)
+ default MSL2SWL - Offset between still-water level and mean sea level (meters) [positive upward; unused when WaveMod = 6; must be zero if PotMod=1 or 2]
+---------------------- FLOATING PLATFORM --------------------------------------- [unused with WaveMod=6]
+ 0 PotMod - Potential-flow model {0: none=no potential flow, 1: frequency-to-time-domain transforms based on WAMIT output, 2: fluid-impulse theory (FIT)} (switch)
+ 0 ExctnMod - Wave-excitation model {0: no wave-excitation calculation, 1: DFT, 2: state-space} (switch) [only used when PotMod=1; STATE-SPACE REQUIRES *.ssexctn INPUT FILE]
+ 1 ExctnDisp - Method of computing Wave Excitation {0: use undisplaced position, 1: use displaced position, 2: use low-pass filtered displaced position) [only used when PotMod=1 and ExctnMod>0 and SeaState's WaveMod>0]} (switch)
+ 10 ExctnCutOff - Cutoff (corner) frequency of the low-pass time-filtered displaced position (Hz) [>0.0] [used only when PotMod=1, ExctnMod>0, and ExctnDisp=2]) [only used when PotMod=1 and ExctnMod>0 and SeaState's WaveMod>0]} (switch)
+ 0 RdtnMod - Radiation memory-effect model {0: no memory-effect calculation, 1: convolution, 2: state-space} (switch) [only used when PotMod=1; STATE-SPACE REQUIRES *.ss INPUT FILE]
+ 0 RdtnTMax - Analysis time for wave radiation kernel calculations (sec) [only used when PotMod=1 and RdtnMod=1; determines RdtnDOmega=Pi/RdtnTMax in the cosine transform; MAKE SURE THIS IS LONG ENOUGH FOR THE RADIATION IMPULSE RESPONSE FUNCTIONS TO DECAY TO NEAR-ZERO FOR THE GIVEN PLATFORM!]
+ 0.01 RdtnDT - Time step for wave radiation kernel calculations (sec) [only used when PotMod=1 and ExctnMod>1 or RdtnMod>0; DT<=RdtnDT<=0.1 recommended; determines RdtnOmegaMax=Pi/RdtnDT in the cosine transform]
+ 1 NBody - Number of WAMIT bodies to be used (-) [>=1; only used when PotMod=1. If NBodyMod=1, the WAMIT data contains a vector of size 6*NBody x 1 and matrices of size 6*NBody x 6*NBody; if NBodyMod>1, there are NBody sets of WAMIT data each with a vector of size 6 x 1 and matrices of size 6 x 6]
+ 2 NBodyMod - Body coupling model {1: include coupling terms between each body and NBody in HydroDyn equals NBODY in WAMIT, 2: neglect coupling terms between each body and NBODY=1 with XBODY=0 in WAMIT, 3: Neglect coupling terms between each body and NBODY=1 with XBODY=/0 in WAMIT} (switch) [only used when PotMod=1]
+ "zeros" PotFile - Root name of potential-flow model data; WAMIT output files containing the linear, nondimensionalized, hydrostatic restoring matrix (.hst), frequency-dependent hydrodynamic added mass matrix and damping matrix (.1), and frequency- and direction-dependent wave excitation force vector per unit wave amplitude (.3) (quoted string) [1 to NBody if NBodyMod>1] [only used when PotMod=1 and ExctnMod>0 or RdtnMod>0] [MAKE SURE THE FREQUENCIES INHERENT IN THESE WAMIT FILES SPAN THE PHYSICALLY-SIGNIFICANT RANGE OF FREQUENCIES FOR THE GIVEN PLATFORM; THEY MUST CONTAIN THE ZERO- AND INFINITE-FREQUENCY LIMITS!]
+ 1 WAMITULEN - Characteristic body length scale used to redimensionalize WAMIT output (meters) [1 to NBody if NBodyMod>1] [only used when PotMod=1 and ExctnMod=1 or RdtnMod=1]
+ 0 PtfmRefxt - The xt offset of the body reference point(s) from (0,0,0) (meters) [1 to NBody] [only used when PotMod=1]
+ 0 PtfmRefyt - The yt offset of the body reference point(s) from (0,0,0) (meters) [1 to NBody] [only used when PotMod=1]
+ 0 PtfmRefzt - The zt offset of the body reference point(s) from (0,0,0) (meters) [1 to NBody] [only used when PotMod=1. If NBodyMod=2,PtfmRefzt=0.0]
+ 0 PtfmRefztRot - The rotation about zt of the body reference frame(s) from xt/yt (degrees) [1 to NBody] [only used when PotMod=1]
+ 0 PtfmVol0 - Displaced volume of water when the body is in its undisplaced position (m^3) [1 to NBody] [only used when PotMod=1; USE THE SAME VALUE COMPUTED BY WAMIT AS OUTPUT IN THE .OUT FILE!]
+ 0 PtfmCOBxt - The xt offset of the center of buoyancy (COB) from (0,0) (meters) [1 to NBody] [only used when PotMod=1]
+ 0 PtfmCOByt - The yt offset of the center of buoyancy (COB) from (0,0) (meters) [1 to NBody] [only used when PotMod=1]
+---------------------- 2ND-ORDER FLOATING PLATFORM FORCES ---------------------- [unused with WaveMod=0 or 6 or PotMod=0 or 2]
+ 0 MnDrift - Mean-drift 2nd-order forces computed {0: None; [7, 8, 9, 10, 11, or 12]: WAMIT file to use} [Only one of MnDrift, NewmanApp, or DiffQTF can be non-zero. If NBody>1, MnDrift /=8]
+ 0 NewmanApp - Mean- and slow-drift 2nd-order forces computed with Newman's approximation {0: None; [7, 8, 9, 10, 11, or 12]: WAMIT file to use} [Only one of MnDrift, NewmanApp, or DiffQTF can be non-zero. If NBody>1, NewmanApp/=8. Used only when WaveDirMod=0]
+ 0 DiffQTF - Full difference-frequency 2nd-order forces computed with full QTF {0: None; [10, 11, or 12]: WAMIT file to use} [Only one of MnDrift, NewmanApp, or DiffQTF can be non-zero]
+ 0 SumQTF - Full summation -frequency 2nd-order forces computed with full QTF {0: None; [10, 11, or 12]: WAMIT file to use}
+---------------------- PLATFORM ADDITIONAL STIFFNESS AND DAMPING -------------- [unused with PotMod=0 or 2]
+ 0 AddF0 - Additional preload (N, N-m) [If NBodyMod=1, one size 6*NBody x 1 vector; if NBodyMod>1, NBody size 6 x 1 vectors]
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0 0 0 0 0 0 AddCLin - Additional linear stiffness (N/m, N/rad, N-m/m, N-m/rad) [If NBodyMod=1, one size 6*NBody x 6*NBody matrix; if NBodyMod>1, NBody size 6 x 6 matrices]
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 98340000
+ 100000 0 0 0 0 0 AddBLin - Additional linear damping(N/(m/s), N/(rad/s), N-m/(m/s), N-m/(rad/s)) [If NBodyMod=1, one size 6*NBody x 6*NBody matrix; if NBodyMod>1, NBody size 6 x 6 matrices]
+ 0 100000 0 0 0 0
+ 0 0 130000 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 13000000
+ 0 0 0 0 0 0 AddBQuad - Additional quadratic drag(N/(m/s)^2, N/(rad/s)^2, N-m(m/s)^2, N-m/(rad/s)^2) [If NBodyMod=1, one size 6*NBody x 6*NBody matrix; if NBodyMod>1, NBody size 6 x 6 matrices]
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+---------------------- STRIP THEORY OPTIONS --------------------------------------
+ 0 WaveDisp - Method of computing Wave Kinematics {0: use undisplaced position, 1: use displaced position) } (switch)
+ 0 AMMod - Method of computing distributed added-mass force. (0: Only and always on nodes below SWL at the undisplaced position. 2: Up to the instantaneous free surface) [overwrite to 0 when WaveMod = 0 or 6 or when WaveStMod = 0 in SeaState]
+---------------------- AXIAL COEFFICIENTS --------------------------------------
+ 2 NAxCoef - Number of axial coefficients (-)
+AxCoefID AxCd AxCa AxCp AxFDMod AxVnCOff AxFDLoFSc
+ (-) (-) (-) (-) (-) (-) (-)
+ 1 0.00 0.00 0.00 0 0.00 1.00
+ 2 0.60 0.00 1.00 0 0.00 1.00
+---------------------- MEMBER JOINTS -------------------------------------------
+ 55 NJoints - Number of joints (-) [must be exactly 0 or at least 2]
+JointID Jointxi Jointyi Jointzi JointAxID JointOvrlp [JointOvrlp= 0: do nothing at joint, 1: eliminate overlaps by calculating super member]
+ (-) (m) (m) (m) (-) (switch)
+ 101 0.0000000 0.000000 16.00000 1 0
+ 102 0.0000000 0.000000 14.87000 1 0
+ 103 0.0000000 0.000000 -14.03000 1 0
+ 104 0.0000000 0.000000 -14.83000 2 0
+ 211 2.7600000 0.000000 -13.97000 1 0
+ 212 33.3000000 0.000000 -13.97000 1 0
+ 213 36.0950000 0.000000 -13.97000 1 0
+ 221 -1.3800000 -2.390000 -13.97000 1 0
+ 222 -16.6500000 -28.838420 -13.97000 1 0
+ 223 -18.0475000 -31.258960 -13.97000 1 0
+ 231 -1.3800000 2.390000 -13.97000 1 0
+ 232 -16.6500000 28.838420 -13.97000 1 0
+ 233 -18.0475000 31.258960 -13.97000 1 0
+ 311 3.1700000 0.000000 13.98000 1 0
+ 312 4.2777890 0.000000 13.01372 1 0
+ 313 30.9250100 0.000000 -10.22968 1 0
+ 314 32.0328000 0.000000 -11.19596 1 0
+ 321 -1.5850000 -2.745000 13.98000 1 0
+ 322 -2.1388944 -3.704373 13.01372 1 0
+ 323 -15.4625000 -26.781540 -10.22968 1 0
+ 324 -16.0164000 -27.740910 -11.19596 1 0
+ 331 -1.5850000 2.745000 13.98000 1 0
+ 332 -2.1388944 3.704373 13.01372 1 0
+ 333 -15.4625000 26.781540 -10.22968 1 0
+ 334 -16.0164000 27.740910 -11.19596 1 0
+ 411 32.3960000 -2.410000 -13.97000 1 0
+ 412 27.7800800 -5.075000 -13.97000 1 0
+ 413 -9.48585500 -26.590500 -13.97000 1 0
+ 414 -14.10090000 -29.255000 -13.97000 1 0
+ 421 -18.28512000 -26.850530 -13.97000 1 0
+ 422 -18.28512000 -21.520530 -13.97000 1 0
+ 423 -18.28512000 21.510470 -13.97000 1 0
+ 424 -18.28512000 26.839470 -13.97000 1 0
+ 431 -14.11088000 29.260530 -13.97000 1 0
+ 432 -9.49496300 26.595530 -13.97000 1 0
+ 433 27.77098000 5.080029 -13.97000 1 0
+ 434 32.38603000 2.415529 -13.97000 1 0
+ 511 20.50000000 33.492000 -61.46800 1 0
+ 512 20.50000000 25.417000 -61.46800 1 0
+ 513 20.50000000 18.721000 -61.46800 1 0
+ 514 20.50000000 -18.719000 -61.46800 1 0
+ 515 20.50000000 -25.419000 -61.46800 1 0
+ 516 20.50000000 -33.489000 -61.46800 1 0
+ 521 18.76795000 -34.489000 -61.46800 1 0
+ 522 11.77479000 -30.451500 -61.46800 1 0
+ 523 5.97588800 -27.103500 -61.46800 1 0
+ 524 -26.44810000 -8.383500 -61.46800 1 0
+ 525 -32.25047000 -5.033500 -61.46800 1 0
+ 526 -39.23930000 -0.998500 -61.46800 1 0
+ 531 -39.23930000 1.001500 -61.46800 1 0
+ 532 -32.24614000 5.039000 -61.46800 1 0
+ 533 -26.44724000 8.387000 -61.46800 1 0
+ 534 5.97675400 27.107000 -61.46800 1 0
+ 535 11.77912000 30.457000 -61.46800 1 0
+ 536 18.76795000 34.492000 -61.46800 1 0
+---------------------- MEMBER CROSS-SECTION PROPERTIES -------------------------
+ 12 NPropSets - Number of member property sets (-)
+PropSetID PropD PropThck
+ (-) (m) (m)
+ 1 4.3 0.06000
+ 2 3.462 0.02200
+ 3 0.3462 0.00220
+ 4 2.125 0.02500
+ 5 2.16 0.02000
+ 6 2.2 0.03000
+ 7 4 0.02200
+ 8 2 0.10000
+ 9 4.1 0.02800
+ 10 4.1 0.02800
+ 11 4.1 0.02800
+ 13 0.10762 0.05381
+---------------------- SIMPLE HYDRODYNAMIC COEFFICIENTS (model 1) --------------
+ SimplCd SimplCdMG SimplCa SimplCaMG SimplCp SimplCpMG SimplAxCd SimplAxCdMG SimplAxCa SimplAxCaMG SimplAxCp SimplAxCpMG
+ (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-)
+ 0.60 0.60 0.00 0.00 1.00 1.00 0.60 0.60 0.00 0.00 1.00 1.00
+---------------------- DEPTH-BASED HYDRODYNAMIC COEFFICIENTS (model 2) ---------
+ 0 NCoefDpth - Number of depth-dependent coefficients (-)
+Dpth DpthCd DpthCdMG DpthCa DpthCaMG DpthCp DpthCpMG DpthAxCd DpthAxCdMG DpthAxCa DpthAxCaMG DpthAxCp DpthAxCpMG
+(m) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-)
+---------------------- MEMBER-BASED HYDRODYNAMIC COEFFICIENTS (model 3) --------
+ 0 NCoefMembers - Number of member-based coefficients (-)
+MemberID MemberCd1 MemberCd2 MemberCdMG1 MemberCdMG2 MemberCa1 MemberCa2 MemberCaMG1 MemberCaMG2 MemberCp1 MemberCp2 MemberCpMG1 MemberCpMG2 MemberAxCd1 MemberAxCd2 MemberAxCdMG1 MemberAxCdMG2 MemberAxCa1 MemberAxCa2 MemberAxCaMG1 MemberAxCaMG2 MemberAxCp1 MemberAxCp2 MemberAxCpMG1 MemberAxCpMG2
+ (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-) (-)
+-------------------- MEMBERS -------------------------------------------------
+ 48 NMembers - Number of members (-)
+MemberID MJointID1 MJointID2 MPropSetID1 MPropSetID2 MDivSize MCoefMod PropPot [MCoefMod=1: use simple coeff table, 2: use depth-based coeff table, 3: use member-based coeff table] [ PropPot/=0 if member is modeled with potential-flow theory]
+ (-) (-) (-) (-) (-) (m) (switch) (flag) R. Bergua comments: Dummy beams removed from the hydro. It is supposed that those beams are underneath other beams.
+ 1 101 102 1 1 5 1 FALSE
+ 2 102 103 1 1 1 1 FALSE
+ 3 103 104 1 1 5 1 FALSE
+ 4 211 212 2 2 5 1 FALSE
+ 5 212 213 2 3 5 1 FALSE
+ 6 221 222 2 2 5 1 FALSE
+ 7 222 223 2 3 5 1 FALSE
+ 8 231 232 2 2 5 1 FALSE
+ 9 232 233 2 3 5 1 FALSE
+ 10 311 312 4 5 5 1 FALSE
+ 11 312 313 5 5 1 1 FALSE
+ 12 313 314 5 4 5 1 FALSE
+ 13 321 322 4 5 5 1 FALSE
+ 14 322 323 5 5 1 1 FALSE
+ 15 323 324 5 4 5 1 FALSE
+ 16 331 332 4 5 5 1 FALSE
+ 17 332 333 5 5 1 1 FALSE
+ 18 333 334 5 4 5 1 FALSE
+ 19 411 412 6 7 5 1 FALSE
+ 20 412 413 7 7 5 1 FALSE
+ 21 413 414 7 6 5 1 FALSE
+ 22 421 422 6 7 5 1 FALSE
+ 23 422 423 7 7 5 1 FALSE
+ 24 423 424 7 6 5 1 FALSE
+ 25 431 432 6 7 5 1 FALSE
+ 26 432 433 7 7 5 1 FALSE
+ 27 433 434 7 6 5 1 FALSE
+ 28 511 512 8 9 5 1 FALSE
+ 29 512 513 10 10 5 1 FALSE
+ 30 513 514 11 11 5 1 FALSE
+ 31 514 515 10 10 5 1 FALSE
+ 32 515 516 9 8 5 1 FALSE
+ 33 521 522 8 9 5 1 FALSE
+ 34 522 523 10 10 5 1 FALSE
+ 35 523 524 11 11 5 1 FALSE
+ 36 524 525 10 10 5 1 FALSE
+ 37 525 526 9 8 5 1 FALSE
+ 38 531 532 8 9 5 1 FALSE
+ 39 532 533 10 10 5 1 FALSE
+ 40 533 534 11 11 5 1 FALSE
+ 41 534 535 10 10 5 1 FALSE
+ 42 535 536 9 8 5 1 FALSE
+ 61 213 511 13 13 5 1 FALSE
+ 62 213 516 13 13 5 1 FALSE
+ 63 223 521 13 13 5 1 FALSE
+ 64 223 526 13 13 5 1 FALSE
+ 65 233 531 13 13 5 1 FALSE
+ 66 233 536 13 13 5 1 FALSE
+---------------------- FILLED MEMBERS ------------------------------------------
+ 0 NFillGroups - Number of filled member groups (-) [If FillDens = DEFAULT, then FillDens = WtrDens; FillFSLoc is related to MSL2SWL]
+FillNumM FillMList FillFSLoc FillDens
+(-) (-) (m) (kg/m^3)
+---------------------- MARINE GROWTH -------------------------------------------
+ 0 NMGDepths - Number of marine-growth depths specified (-)
+MGDpth MGThck MGDens
+(m) (m) (kg/m^3)
+---------------------- MEMBER OUTPUT LIST --------------------------------------
+ 0 NMOutputs - Number of member outputs (-) [must be < 10]
+MemberID NOutLoc NodeLocs [NOutLoc < 10; node locations are normalized distance from the start of the member, and must be >=0 and <= 1] [unused if NMOutputs=0]
+ (-) (-) (-)
+---------------------- JOINT OUTPUT LIST ---------------------------------------
+ 0 NJOutputs - Number of joint outputs [Must be < 10]
+ 0 JOutLst - List of JointIDs which are to be output (-)[unused if NJOutputs=0]
+---------------------- OUTPUT --------------------------------------------------
+False HDSum - Output a summary file [flag]
+False OutAll - Output all user-specified member and joint loads (only at each member end, not interior locations) [flag]
+ 2 OutSwtch - Output requested channels to: [1=Hydrodyn.out, 2=GlueCode.out, 3=both files]
+"ES11.4e2" OutFmt - Output format for numerical results (quoted string) [not checked for validity!]
+"A11" OutSFmt - Output format for header strings (quoted string) [not checked for validity!]
+---------------------- OUTPUT CHANNELS -----------------------------------------
+HydroFxi
+HydroFyi
+HydroFzi
+HydroMxi
+HydroMyi
+HydroMzi
+B1Surge
+B1Sway
+B1Heave
+B1Roll
+B1Pitch
+B1Yaw
+B1TVxi
+B1TVyi
+B1TVzi
+B1RVxi
+B1RVyi
+B1RVzi
+B1TAxi
+B1TAyi
+B1TAzi
+B1RAxi
+B1RAyi
+B1RAzi
+B1WvsFxi
+B1WvsFyi
+B1WvsFzi
+B1WvsMxi
+B1WvsMyi
+B1WvsMzi
+B1HDSFxi
+B1HDSFyi
+B1HDSFzi
+B1HDSMxi
+B1HDSMyi
+B1HDSMzi
+B1RdtFxi
+B1RdtFyi
+B1RdtFzi
+B1RdtMxi
+B1RdtMyi
+B1RdtMzi
+END of output channels and end of file. (the word "END" must appear in the first 3 columns of this line)
diff --git a/weio/tests/example_files/FASTIn_HD_SeaState.dat b/weio/tests/example_files/FASTIn_HD_SeaState.dat
new file mode 100644
index 0000000..d53f296
--- /dev/null
+++ b/weio/tests/example_files/FASTIn_HD_SeaState.dat
@@ -0,0 +1,96 @@
+------- SeaState Input File ----------------------------------------------------
+Updated HydroDyn input file for the TCF project. Full scale model to verify OpenFAST against OrcaFlex for the TetraSpar project.
+False Echo - Echo the input file data (flag)
+---------------------- ENVIRONMENTAL CONDITIONS --------------------------------
+ default WtrDens - Water density (kg/m^3)
+ default WtrDpth - Water depth (meters)
+ default MSL2SWL - Offset between still-water level and mean sea level (meters) [positive upward; unused when WaveMod = 6; must be zero if PotMod=1 or 2]
+---------------------- SPATIAL DISCRETIZATION ---------------------------------------------------
+ 30 X_HalfWidth - Half-width of the domain in the X direction (m) [>0, NOTE: X[nX] = nX*dX, where nX = {-NX+1,-NX+2,…,NX-1} and dX = X_HalfWidth/(NX-1)]
+ 30 Y_HalfWidth - Half-width of the domain in the Y direction (m) [>0, NOTE: Y[nY] = nY*dY, where nY = {-NY+1,-NY+2,…,NY-1} and dY = Y_HalfWidth/(NY-1)]
+ default Z_Depth - Depth of the domain the Z direction (m) relative to SWL [0 < Z_Depth <= WtrDpth+MSL2SWL; "default": Z_Depth = WtrDpth+MSL2SWL; Z[nZ] = ( COS( nZ*dthetaZ ) – 1 )*Z_Depth, where nZ = {0,1,…NZ-1} and dthetaZ = pi/( 2*(NZ-1) )]
+ 3 NX - Number of nodes in half of the X-direction domain (-) [>=2]
+ 3 NY - Number of nodes in half of the Y-direction domain (-) [>=2]
+ 40 NZ - Number of nodes in the Z direction (-) [>=2]
+---------------------- WAVES ---------------------------------------------------
+ 0 WaveMod - Incident wave kinematics model {0: none=still water, 1: regular (periodic), 1P#: regular with user-specified phase, 2: JONSWAP/Pierson-Moskowitz spectrum (irregular), 3: White noise spectrum (irregular), 4: user-defined spectrum from routine UserWaveSpctrm (irregular), 5: Externally generated wave-elevation time series, 6: Externally generated full wave-kinematics time series [option 6 is invalid for PotMod/=0]} (switch)
+ 0 WaveStMod - Model for stretching incident wave kinematics to instantaneous free surface {0: none=no stretching, 1: vertical stretching, 2: extrapolation stretching, 3: Wheeler stretching} (switch) [unused when WaveMod=0 or when PotMod/=0]
+ 20000 WaveTMax - Analysis time for incident wave calculations (sec) [unused when WaveMod=0; determines WaveDOmega=2Pi/WaveTMax in the IFFT]
+ 0.2 WaveDT - Time step for incident wave calculations (sec) [unused when WaveMod=0; 0.1<=WaveDT<=1.0 recommended; determines WaveOmegaMax=Pi/WaveDT in the IFFT]
+ 9.41 WaveHs - Significant wave height of incident waves (meters) [used only when WaveMod=1, 2, or 3]
+ 14.3 WaveTp - Peak-spectral period of incident waves (sec) [used only when WaveMod=1 or 2]
+"DEFAULT" WavePkShp - Peak-shape parameter of incident wave spectrum (-) or DEFAULT (string) [used only when WaveMod=2; use 1.0 for Pierson-Moskowitz]
+ 0.15708 WvLowCOff - Low cut-off frequency or lower frequency limit of the wave spectrum beyond which the wave spectrum is zeroed (rad/s) [unused when WaveMod=0, 1, or 6]
+ 3.1416 WvHiCOff - High cut-off frequency or upper frequency limit of the wave spectrum beyond which the wave spectrum is zeroed (rad/s) [unused when WaveMod=0, 1, or 6]
+ 0 WaveDir - Incident wave propagation heading direction (degrees) [unused when WaveMod=0 or 6]
+ 0 WaveDirMod - Directional spreading function {0: none, 1: COS2S} (-) [only used when WaveMod=2,3, or 4]
+ 1 WaveDirSpread - Wave direction spreading coefficient ( > 0 ) (-) [only used when WaveMod=2,3, or 4 and WaveDirMod=1]
+ 1 WaveNDir - Number of wave directions (-) [only used when WaveMod=2,3, or 4 and WaveDirMod=1; odd number only]
+ 0 WaveDirRange - Range of wave directions (full range: WaveDir +/- 1/2*WaveDirRange) (degrees) [only used when WaveMod=2,3,or 4 and WaveDirMod=1]
+ 123456789 WaveSeed(1) - First random seed of incident waves [-2147483648 to 2147483647] (-) [unused when WaveMod=0, 5, or 6]
+ 1011121314 WaveSeed(2) - Second random seed of incident waves [-2147483648 to 2147483647] (-) [unused when WaveMod=0, 5, or 6]
+FALSE WaveNDAmp - Flag for normally distributed amplitudes (flag) [only used when WaveMod=2, 3, or 4]
+"" WvKinFile - Root name of externally generated wave data file(s) (quoted string) [used only when WaveMod=5 or 6]
+---------------------- 2ND-ORDER WAVES ----------------------------------------- [unused with WaveMod=0 or 6]
+FALSE WvDiffQTF - Full difference-frequency 2nd-order wave kinematics (flag)
+FALSE WvSumQTF - Full summation-frequency 2nd-order wave kinematics (flag)
+ 0 WvLowCOffD - Low frequency cutoff used in the difference-frequencies (rad/s) [Only used with a difference-frequency method]
+ 0.1 WvHiCOffD - High frequency cutoff used in the difference-frequencies (rad/s) [Only used with a difference-frequency method]
+ 0 WvLowCOffS - Low frequency cutoff used in the summation-frequencies (rad/s) [Only used with a summation-frequency method]
+ 0.1 WvHiCOffS - High frequency cutoff used in the summation-frequencies (rad/s) [Only used with a summation-frequency method]
+---------------------- CONSTRAINED WAVES ---------------------------------------
+ 0 ConstWaveMod - Constrained wave model: 0=none; 1=Constrained wave with specified crest elevation, alpha; 2=Constrained wave with guaranteed peak-to-trough crest height, HCrest (flag)
+ 1 CrestHmax - Crest height (2*alpha for ConstWaveMod=1 or HCrest for ConstWaveMod=2), must be larger than WaveHs (m) [unused when ConstWaveMod=0]
+ 60 CrestTime - Time at which the crest appears (s) [unused when ConstWaveMod=0]
+ 0 CrestXi - X-position of the crest (m) [unused when ConstWaveMod=0]
+ 0 CrestYi - Y-position of the crest (m) [unused when ConstWaveMod=0]
+---------------------- CURRENT ------------------------------------------------- [unused with WaveMod=6]
+ 0 CurrMod - Current profile model {0: none=no current, 1: standard, 2: user-defined from routine UserCurrent} (switch)
+ 0 CurrSSV0 - Sub-surface current velocity at still water level (m/s) [used only when CurrMod=1]
+"DEFAULT" CurrSSDir - Sub-surface current heading direction (degrees) or DEFAULT (string) [used only when CurrMod=1]
+ 0 CurrNSRef - Near-surface current reference depth (meters) [used only when CurrMod=1]
+ 0 CurrNSV0 - Near-surface current velocity at still water level (m/s) [used only when CurrMod=1]
+ 0 CurrNSDir - Near-surface current heading direction (degrees) [used only when CurrMod=1]
+ 0 CurrDIV - Depth-independent current velocity (m/s) [used only when CurrMod=1]
+ 0 CurrDIDir - Depth-independent current heading direction (degrees) [used only when CurrMod=1]
+---------------------- MacCamy-Fuchs diffraction model -------------------------
+ 0 MCFD - MacCamy-Fuchs member radius (ignored if radius <= 0) [must be 0 when WaveMod 0 or 6]
+---------------------- OUTPUT --------------------------------------------------
+False SeaStSum - Output a summary file [flag]
+ 2 OutSwtch - Output requested channels to: [1=SeaState.out, 2=GlueCode.out, 3=both files]
+"ES11.4e2" OutFmt - Output format for numerical results (quoted string) [not checked for validity!]
+"A11" OutSFmt - Output format for header strings (quoted string) [not checked for validity!]
+ 1 NWaveElev - Number of points where the incident wave elevations can be computed (-) [maximum of 9 output locations]
+ 0 WaveElevxi - List of xi-coordinates for points where the incident wave elevations can be output (meters) [NWaveElev points, separated by commas or white space; usused if NWaveElev = 0]
+ 0 WaveElevyi - List of yi-coordinates for points where the incident wave elevations can be output (meters) [NWaveElev points, separated by commas or white space; usused if NWaveElev = 0]
+ 0 NWaveKin - Number of points where the wave kinematics can be output (-) [maximum of 9 output locations]
+ 0, WaveKinxi - List of xi-coordinates for points where the wave kinematics can be output (meters) [NWaveKin points, separated by commas or white space; usused if NWaveKin = 0]
+ 0, WaveKinyi - List of yi-coordinates for points where the wave kinematics can be output (meters) [NWaveKin points, separated by commas or white space; usused if NWaveKin = 0]
+ 0, WaveKinzi - List of zi-coordinates for points where the wave kinematics can be output (meters) [NWaveKin points, separated by commas or white space; usused if NWaveKin = 0]
+---------------------- OUTPUT CHANNELS -----------------------------------------
+"Wave1Elev" - Wave elevation at the platform reference point (0, 0)
+"HydroFxi" - Hydro force [N] in the X direction.
+"HydroFyi" - Hydro force [N] in the Y direction.
+"HydroFzi" - Hydro force [N] in the vertical direction (Z).
+"HydroMxi" - Hydro moment [N] in the X direction.
+"HydroMyi" - Hydro moment [N] in the Y direction.
+"HydroMzi" - Hydro moment [N] in the vertical direction (Z).
+"PRPSurge"
+"PRPSway"
+"PRPHeave"
+"PRPRoll"
+"PRPPitch"
+"PRPYaw"
+"PRPTVxi"
+"PRPTVyi"
+"PRPTVzi"
+"PRPRVxi"
+"PRPRVyi"
+"PRPRVzi"
+"PRPTAxi"
+"PRPTAyi"
+"PRPTAzi"
+"PRPRAxi"
+"PRPRAyi"
+"PRPRAzi"
+END of output channels and end of file. (the word "END" must appear in the first 3 columns of this line)
diff --git a/weio/tests/example_files/FASTIn_HD_driver.dvr b/weio/tests/example_files/FASTIn_HD_driver.dvr
new file mode 100644
index 0000000..a0e93c8
--- /dev/null
+++ b/weio/tests/example_files/FASTIn_HD_driver.dvr
@@ -0,0 +1,27 @@
+------- HydroDyn Driver file -------------------------------------------------
+Compatible with HydroDyn v3.00
+FALSE Echo - Echo the input file data (flag)
+---------------------- ENVIRONMENTAL CONDITIONS -------------------------------
+9.80665 Gravity - Gravity (m/s^2)
+1025 WtrDens - Water density (kg/m^3)
+220 WtrDpth - Water depth (m)
+0 MSL2SWL - Offset between still-water level and mean sea level (m) [positive upward]
+---------------------- HYDRODYN -----------------------------------------------
+"./HD_NoCa.dat" HDInputFile - Primary HydroDyn input file name (quoted string)
+"./hd_driver_NoCa" OutRootName - The name which prefixes all HydroDyn generated files (quoted string)
+True Linearize - Flag to enable linearization
+1001 NSteps - Number of time steps in the simulations (-)
+0.0125 TimeInterval - TimeInterval for the simulation (sec)
+---------------------- PRP INPUTS (Platform Reference Point) ------------------
+1 PRPInputsMod - Model for the PRP (principal reference point) inputs {0: all inputs are zero for every timestep, 1: steadystate inputs, 2: read inputs from a file (InputsFile)} (switch)
+16 PtfmRefzt - Vertical distance from the ground level [onshore] or MSL [offshore] to the platform reference point (meters)
+"OpenFAST_DisplacementTimeseries.dat" PRPInputsFile - Filename for the PRP HydroDyn input InputsMod = 2 (quoted string)
+---------------------- PRP STEADY STATE INPUTS -------------------------------
+0.0 0.0 0.0 0.0 0.0 0.0 uPRPInSteady - PRP Steady-state displacements and rotations at the platform reference point (m, rads)
+0.0 0.0 0.0 0.0 0.0 0.0 uDotPRPInSteady - PRP Steady-state translational and rotational velocities at the platform reference point (m/s, rads/s)
+0.0 0.0 0.0 0.0 0.0 0.0 uDotDotPRPInSteady - PRP Steady-state translational and rotational accelerations at the platform reference point (m/s^2, rads/s^2)
+---------------------- Waves multipoint elevation output ----------------------
+FALSE WaveElevSeriesFlag - T/F flag to calculate the wave elevation field (for movies)
+5.0 5.0 WaveElevDX WaveElevDY - WaveElevSeries spacing -- WaveElevDX WaveElevDY
+3 3 WaveElevNX WaveElevNY - WaveElevSeries points -- WaveElevNX WaveElevNY
+END of driver input file
diff --git a/weio/tests/example_files/FASTIn_MD.dat b/weio/tests/example_files/FASTIn_MD-v1.dat
similarity index 100%
rename from weio/tests/example_files/FASTIn_MD.dat
rename to weio/tests/example_files/FASTIn_MD-v1.dat
diff --git a/weio/tests/example_files/FASTIn_MD-v2.dat b/weio/tests/example_files/FASTIn_MD-v2.dat
new file mode 100644
index 0000000..226bc44
--- /dev/null
+++ b/weio/tests/example_files/FASTIn_MD-v2.dat
@@ -0,0 +1,39 @@
+--------------------- MoorDyn Input File ------------------------------------
+Mooring system for OC4-DeepCwind Semi
+FALSE Echo - echo the input file data (flag)
+----------------------- LINE TYPES ------------------------------------------
+Name Diam MassDen EA BA/-zeta EI Cd Ca CdAx CaAx
+(-) (m) (kg/m) (N) (N-s/-) (-) (-) (-) (-) (-)
+main 0.0766 113.35 7.536E8 -1.0 0 2.0 0.8 0.4 0.25
+---------------------- POINTS --------------------------------
+ID Attachment X Y Z M V CdA CA
+(-) (-) (m) (m) (m) (kg) (m^3) (m^2) (-)
+1 Fixed 418.8 725.383 -200.0 0 0 0 0
+2 Fixed -837.6 0.0 -200.0 0 0 0 0
+3 Fixed 418.8 -725.383 -200.0 0 0 0 0
+4 Vessel 20.434 35.393 -14.0 0 0 0 0
+5 Vessel -40.868 0.0 -14.0 0 0 0 0
+6 Vessel 20.434 -35.393 -14.0 0 0 0 0
+---------------------- LINES --------------------------------------
+ID LineType AttachA AttachB UnstrLen NumSegs Outputs
+(-) (-) (-) (-) (m) (-) (-)
+1 main 1 4 835.35 20 -
+2 main 2 5 835.35 20 -
+3 main 3 6 835.35 20 -
+---------------------- SOLVER OPTIONS ---------------------------------------
+0.001 dtM - time step to use in mooring integration (s)
+3.0e6 kbot - bottom stiffness (Pa/m)
+3.0e5 cbot - bottom damping (Pa-s/m)
+2.0 dtIC - time interval for analyzing convergence during IC gen (s)
+60.0 TmaxIC - max time for ic gen (s)
+4.0 CdScaleIC - factor by which to scale drag coefficients during dynamic relaxation (-)
+0.01 threshIC - threshold for IC convergence (-)
+------------------------ OUTPUTS --------------------------------------------
+FairTen1
+FairTen2
+FairTen3
+AnchTen1
+AnchTen2
+AnchTen3
+END
+------------------------- need this line --------------------------------------
diff --git a/weio/tests/example_files/MannBox_2x4x8.bin b/weio/tests/example_files/MannBox_2x4x8.bin
new file mode 100644
index 0000000..b5f9c40
--- /dev/null
+++ b/weio/tests/example_files/MannBox_2x4x8.bin
@@ -0,0 +1 @@
+.•À½
ÀÜÀhÜÀ1À"i^À’§\À”žÀ²¯Ö¿ÀMG¿êQ~¿'—?*î@w™&@6¥>yà¶¿¯Ž?»Þ?-ø
@¡Ö²@E=A’ AÉT„@%@#@`¢:À‘× Àd’ñ¿Âv#¿{QO@Fä:@µù>Ò=Àž–À“wÀŠ{À¼i‰ÀË(À€—jÀÚ\À‘ä›ÀÞì¿ h.¿kP«¿©s±?x…@-E@Ïý|?YE˜¿Î0?'6Â?§ª@ë-³@—‡Ak4ò@Ó*”@ËÑ"@<§-ÀGGÀ¶íÀ^ô-¿«„`@Êk@Ì/7>úÀ
\ No newline at end of file
diff --git a/weio/tests/example_files/RoscoDISCON_PowerTracking.in b/weio/tests/example_files/RoscoDISCON_PowerTracking.in
new file mode 100644
index 0000000..464741c
--- /dev/null
+++ b/weio/tests/example_files/RoscoDISCON_PowerTracking.in
@@ -0,0 +1,151 @@
+! Controller parameter input file for the Main_ED wind turbine
+! - File written using ROSCO version 2.6.0 controller tuning logic on 01/21/23
+
+!------- DEBUG ------------------------------------------------------------
+1 ! LoggingLevel - {0: write no debug files, 1: write standard output .dbg-file, 2: LoggingLevel 1 + ROSCO LocalVars (.dbg2) 3: LoggingLevel 2 + complete avrSWAP-array (.dbg3)}
+
+!------- CONTROLLER FLAGS -------------------------------------------------
+1 ! F_LPFType - {1: first-order low-pass filter, 2: second-order low-pass filter}, [rad/s] (currently filters generator speed and pitch control signals
+0 ! F_NotchType - Notch on the measured generator speed and/or tower fore-aft motion (for floating) {0: disable, 1: generator speed, 2: tower-top fore-aft motion, 3: generator speed and tower-top fore-aft motion}
+0 ! IPC_ControlMode - Turn Individual Pitch Control (IPC) for fatigue load reductions (pitch contribution) {0: off, 1: 1P reductions, 2: 1P+2P reductions}
+3 ! VS_ControlMode - Generator torque control mode in above rated conditions {0: constant torque, 1: constant power, 2: TSR tracking PI control with constant torque, 3: TSR tracking PI control with constant power}
+1 ! PC_ControlMode - Blade pitch control mode {0: No pitch, fix to fine pitch, 1: active PI blade pitch control}
+0 ! Y_ControlMode - Yaw control mode {0: no yaw control, 1: yaw rate control, 2: yaw-by-IPC}
+1 ! SS_Mode - Setpoint Smoother mode {0: no setpoint smoothing, 1: introduce setpoint smoothing}
+1 ! PRC_Mode - Power reference tracking mode{0: use standard rotor speed set points, 1: use PRC rotor speed setpoints}
+2 ! WE_Mode - Wind speed estimator mode {0: One-second low pass filtered hub height wind speed, 1: Immersion and Invariance Estimator, 2: Extended Kalman Filter}
+1 ! PS_Mode - Pitch saturation mode {0: no pitch saturation, 1: implement pitch saturation}
+0 ! SD_Mode - Shutdown mode {0: no shutdown procedure, 1: pitch to max pitch at shutdown}
+0 ! Fl_Mode - Floating specific feedback mode {0: no nacelle velocity feedback, 1: feed back translational velocity, 2: feed back rotational veloicty}
+0 ! TD_Mode - Tower damper mode {0: no tower damper, 1: feed back translational nacelle accelleration to pitch angle}
+0 ! Flp_Mode - Flap control mode {0: no flap control, 1: steady state flap angle, 2: Proportional flap control, 2: Cyclic (1P) flap control}
+0 ! OL_Mode - Open loop control mode {0: no open loop control, 1: open loop control vs. time}
+0 ! PA_Mode - Pitch actuator mode {0 - not used, 1 - first order filter, 2 - second order filter}
+0 ! Ext_Mode - External control mode {0 - not used, 1 - call external dynamic library}
+0 ! ZMQ_Mode - Fuse ZeroMQ interface {0: unused, 1: Yaw Control}
+
+!------- FILTERS ----------------------------------------------------------
+0.61500 ! F_LPFCornerFreq >>> Tunedvalue was: 0.31500 - Corner frequency (-3dB point) in the low-pass filters, [rad/s]
+0.00000 ! F_LPFDamping - Damping coefficient {used only when F_FilterType = 2} [-]
+0.00000 ! F_NotchCornerFreq - Natural frequency of the notch filter, [rad/s]
+0.000000 0.250000 ! F_NotchBetaNumDen - Two notch damping values (numerator and denominator, resp) - determines the width and depth of the notch, [-]
+0.62830 ! F_SSCornerFreq - Corner frequency (-3dB point) in the first order low pass filter for the setpoint smoother, [rad/s].
+0.20944 ! F_WECornerFreq - Corner frequency (-3dB point) in the first order low pass filter for the wind speed estimate [rad/s].
+0.17952 ! F_YawErr - Low pass filter corner frequency for yaw controller [rad/s].
+0.000000 1.000000 ! F_FlCornerFreq - Natural frequency and damping in the second order low pass filter of the tower-top fore-aft motion for floating feedback control [rad/s, -].
+0.01042 ! F_FlHighPassFreq - Natural frequency of first-order high-pass filter for nacelle fore-aft motion [rad/s].
+3.780000 1.000000 ! F_FlpCornerFreq - Corner frequency and damping in the second order low pass filter of the blade root bending moment for flap control [rad/s, -].
+
+!------- BLADE PITCH CONTROL ----------------------------------------------
+30 ! PC_GS_n - Amount of gain-scheduling table entries
+0.150511 0.170047 0.187567 0.203638 0.218616 0.232742 0.246187 0.258963 0.271200 0.283059 0.294514 0.305550 0.316371 0.326840 0.337077 0.347108 0.356849 0.366502 0.375849 0.385148 0.394189 0.403154 0.411886 0.420555 0.429027 0.437457 0.445707 0.453937 0.461993 0.470003 ! PC_GS_angles - Gain-schedule table: pitch angles [rad].
+-1.802655 -1.644720 -1.508878 -1.390797 -1.287208 -1.195596 -1.113999 -1.040858 -0.974925 -0.915183 -0.860801 -0.811087 -0.765465 -0.723450 -0.684631 -0.648656 -0.615223 -0.584073 -0.554979 -0.527745 -0.502197 -0.478183 -0.455570 -0.434238 -0.414082 -0.395006 -0.376927 -0.359768 -0.343460 -0.327942 ! PC_GS_KP - Gain-schedule table: pitch controller kp gains [s].
+-0.747085 -0.690899 -0.642574 -0.600566 -0.563714 -0.531124 -0.502095 -0.476076 -0.452620 -0.431367 -0.412020 -0.394335 -0.378105 -0.363158 -0.349348 -0.336550 -0.324656 -0.313574 -0.303224 -0.293536 -0.284447 -0.275904 -0.267860 -0.260271 -0.253100 -0.246314 -0.239882 -0.233778 -0.227977 -0.222456 ! PC_GS_KI - Gain-schedule table: pitch controller ki gains [-].
+0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ! PC_GS_KD - Gain-schedule table: pitch controller kd gains
+0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ! PC_GS_TF - Gain-schedule table: pitch controller tf gains (derivative filter)
+1.570000000000 ! PC_MaxPit - Maximum physical pitch limit, [rad].
+0.036320000000 ! PC_MinPit - Minimum physical pitch limit, [rad].
+0.174500000000 ! PC_MaxRat - Maximum pitch rate (in absolute value) in pitch controller, [rad/s].
+-0.17450000000 ! PC_MinRat - Minimum pitch rate (in absolute value) in pitch controller, [rad/s].
+1.395100000000 ! PC_RefSpd - Desired (reference) HSS speed for pitch controller, [rad/s].
+0.036320000000 ! PC_FinePit - Record 5: Below-rated pitch angle set-point, [rad]
+0.017450000000 ! PC_Switch - Angle above lowest minimum pitch angle for switch, [rad]
+
+!------- INDIVIDUAL PITCH CONTROL -----------------------------------------
+9.600000 12.000000 ! IPC_Vramp - Start and end wind speeds for cut-in ramp function. First entry: IPC inactive, second entry: IPC fully active. [m/s]
+0.3 ! IPC_IntSat - Integrator saturation (maximum signal amplitude contribution to pitch from IPC), [rad]
+0.000e+00 0.000e+00 ! IPC_KP - Proportional gain for the individual pitch controller: first parameter for 1P reductions, second for 2P reductions, [-]
+0.000e+00 0.000e+00 ! IPC_KI - Integral gain for the individual pitch controller: first parameter for 1P reductions, second for 2P reductions, [-]
+0.000000 0.000000 ! IPC_aziOffset - Phase offset added to the azimuth angle for the individual pitch controller, [rad].
+0.0 ! IPC_CornerFreqAct - Corner frequency of the first-order actuators model, to induce a phase lag in the IPC signal {0: Disable}, [rad/s]
+
+!------- VS TORQUE CONTROL ------------------------------------------------
+100.0000000000 ! VS_GenEff - Generator efficiency mechanical power -> electrical power, [should match the efficiency defined in the generator properties!], [%]
+2580460.182070 ! VS_ArSatTq - Above rated generator torque PI control saturation, [Nm]
+650000.0000000 ! VS_MaxRat >>> TunedValued:650000.0000000 - Maximum torque rate (in absolute value) in torque controller, [Nm/s].
+2838506.200270 ! VS_MaxTq - Maximum generator torque in Region 3 (HSS side), [Nm].
+0.000000000000 ! VS_MinTq - Minimum generator torque (HSS side), [Nm].
+0.680000000000 ! VS_MinOMSpd >>> TunedValue: - Minimum generator speed [rad/s]
+1452489.929660 ! VS_Rgn2K - Generator torque constant in Region 2 (HSS side), [Nm/(rad/s)^2]
+3600000.000000 ! VS_RtPwr - Wind turbine rated power [W]
+2580460.182070 ! VS_RtTq - Rated torque, [Nm].
+1.395100000000 ! VS_RefSpd - Rated generator speed [rad/s]
+1 ! VS_n - Number of generator PI torque controller gains
+-6954512.69764 ! VS_KP - Proportional gain for generator PI torque controller [-]. (Only used in the transitional 2.5 region if VS_ControlMode =/ 2)
+-781263.090000 ! VS_KI - Integral gain for generator PI torque controller [s]. (Only used in the transitional 2.5 region if VS_ControlMode =/ 2)
+9.595 ! VS_TSRopt >>> TunedValue:8.82 - Power-maximizing region 2 tip-speed-ratio [rad].
+
+!------- SETPOINT SMOOTHER ---------------------------------------------
+1.00000 ! SS_VSGain - Variable speed torque controller setpoint smoother gain, [-].
+0.00100 ! SS_PCGain - Collective pitch controller setpoint smoother gain, [-].
+
+!------- POWER REFERENCE TRACKING --------------------------------------
+45 ! PRC_n - Number of elements in PRC_WindSpeeds and PRC_RotorSpeeds array
+3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 12.5 13.0 13.5 14.0 14.5 15.0 15.5 16.0 16.5 17.0 17.5 18.0 18.5 19.0 19.5 20.0 20.5 21.0 21.5 22.0 22.5 23.0 23.5 24.0 24.5 25.0 ! PRC_WindSpeeds - Array of wind speeds used in rotor speed vs. wind speed lookup table [m/s].
+0.6910268144787133 0.6960401593650649 0.7165597438704334 0.738185676421178 0.7608053103653986 0.7776758882514259 0.9275392403488596 0.9519480597035652 1.0186573938021806 1.0857272198939956 1.1535923474943748 1.205354257912827 1.2447250825053235 1.265512130479433 1.2731046664774792 1.2768229123691115 1.3035008107787789 1.3644268867738227 1.3950661379469709 1.3949948761209916 1.394923614295013 1.3948523524690342 1.394781090643055 1.3947098288170765 1.394638566991098 1.394567305165119 1.39449604333914 1.3944247815131616 1.3943535196871826 1.3942822578612035 1.3942109960352247 1.3941397342092463 1.3940684723832677 1.3939972105572886 1.3939259487313096 1.3938546869053312 1.3937834250793522 1.3937121632533735 1.3936409014273947 1.275861750264016 1.2764425597166416 1.2770233691692672 1.2776041786218928 1.2781849880745186 1.2787657975271445 ! PRC_RotorSpeeds - Array of rotor speeds corresponding to PRC_WindSpeeds [rad/s].
+
+!------- WIND SPEED ESTIMATOR ---------------------------------------------
+65.085 ! WE_BladeRadius - Blade length (distance from hub center to blade tip), [m]
+1 ! WE_CP_n - Amount of parameters in the Cp array
+0.0 ! WE_CP - Parameters that define the parameterized CP(lambda) function
+0.0 ! WE_Gamma - Adaption gain of the wind speed estimator algorithm [m/rad]
+1.0 ! WE_GearboxRatio - Gearbox ratio [>=1], [-]
+34722804.00000 ! WE_Jtot - Total drivetrain inertia, including blades, hub and casted generator inertia to LSS, [kg m^2]
+1.225 ! WE_RhoAir - Air density, [kg m^-3]
+"SWT-3p6-130_Cp_Ct_Cq.txt" ! PerfFileName - File containing rotor performance tables (Cp,Ct,Cq) (absolute path or relative to this file)
+36 26 ! PerfTableSize - Size of rotor performance tables, first number refers to number of blade pitch angles, second number referse to number of tip-speed ratios
+60 ! WE_FOPoles_N - Number of first-order system poles used in EKF
+3.0000 3.3103 3.6207 3.9310 4.2414 4.5517 4.8621 5.1724 5.4828 5.7931 6.1034 6.4138 6.7241 7.0345 7.3448 7.6552 7.9655 8.2759 8.5862 8.8966 9.2069 9.5172 9.8276 10.1379 10.4483 10.7586 11.0690 11.3793 11.6897 12.0000 12.4333 12.8667 13.3000 13.7333 14.1667 14.6000 15.0333 15.4667 15.9000 16.3333 16.7667 17.2000 17.6333 18.0667 18.5000 18.9333 19.3667 19.8000 20.2333 20.6667 21.1000 21.5333 21.9667 22.4000 22.8333 23.2667 23.7000 24.1333 24.5667 25.0000 ! WE_FOPoles_v - Wind speeds corresponding to first-order system poles [m/s]
+-0.01675237 -0.01848537 -0.02021837 -0.02195138 -0.02368438 -0.02541738 -0.02715039 -0.02888339 -0.03061639 -0.03234940 -0.03408240 -0.03581540 -0.03754841 -0.03928141 -0.04101441 -0.04274742 -0.04448042 -0.04621342 -0.04794643 -0.04967943 -0.05141243 -0.05314544 -0.05487844 -0.05661144 -0.05572739 -0.05017938 -0.04026019 -0.02871709 -0.00971339 -0.04067865 0.00818207 0.00211342 -0.00482802 -0.01262516 -0.02096731 -0.02974463 -0.03916053 -0.04874708 -0.05875276 -0.06931562 -0.07999290 -0.09081923 -0.10219998 -0.11405921 -0.12562480 -0.13746588 -0.14961465 -0.16249907 -0.17543461 -0.18782154 -0.20027790 -0.21343756 -0.22678626 -0.24101883 -0.25473108 -0.26767005 -0.28052026 -0.29438557 -0.30826452 -0.32314303 ! WE_FOPoles - First order system poles [1/s]
+
+!------- YAW CONTROL ------------------------------------------------------
+0.00000 ! Y_uSwitch - Wind speed to switch between Y_ErrThresh. If zero, only the second value of Y_ErrThresh is used [m/s]
+4.000000 8.000000 ! Y_ErrThresh - Yaw error threshold/deadbands. Turbine begins to yaw when it passes this. If Y_uSwitch is zero, only the second value is used. [deg].
+0.00870 ! Y_Rate - Yaw rate [rad/s]
+0.00000 ! Y_MErrSet - Integrator saturation (maximum signal amplitude contribution to pitch from yaw-by-IPC), [rad]
+0.00000 ! Y_IPC_IntSat - Integrator saturation (maximum signal amplitude contribution to pitch from yaw-by-IPC), [rad]
+0.00000 ! Y_IPC_KP - Yaw-by-IPC proportional controller gain Kp
+0.00000 ! Y_IPC_KI - Yaw-by-IPC integral controller gain Ki
+
+!------- TOWER FORE-AFT DAMPING -------------------------------------------
+-1.00000 ! FA_KI - Integral gain for the fore-aft tower damper controller [rad s/m]
+0.0 ! FA_HPFCornerFreq - Corner frequency (-3dB point) in the high-pass filter on the fore-aft acceleration signal [rad/s]
+0.0 ! FA_IntSat - Integrator saturation (maximum signal amplitude contribution to pitch from FA damper), [rad]
+
+!------- MINIMUM PITCH SATURATION -------------------------------------------
+45 ! PS_BldPitchMin_N - Number of values in minimum blade pitch lookup table (should equal number of values in PS_WindSpeeds and PS_BldPitchMin)
+3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 12.5 13.0 13.5 14.0 14.5 15.0 15.5 16.0 16.5 17.0 17.5 18.0 18.5 19.0 19.5 20.0 20.5 21.0 21.5 22.0 22.5 23.0 23.5 24.0 24.5 25.0 ! PS_WindSpeeds - Wind speeds corresponding to minimum blade pitch angles [m/s]
+0.03433429157815764 0.031810319257858856 0.0290411053542571 0.026159059732509628 0.023197536519278562 0.02023601330604749 0.016539572859770497 0.012812807649260424 0.009178197567017179 0.0055435874847739285 0.0019089774025306792 0.00035367821989023234 -0.0001847841217967057 -0.0006880495172071447 0.0011581050975086446 0.07313877536508506 0.10080437650314836 0.12350678083692652 0.143559308829277 0.16319259532494113 0.18282588182060522 0.19751809368049394 0.2121360790842221 0.2267540644879502 0.24148455945770236 0.2562873503246761 0.27109014119164987 0.2851632439462169 0.2986736364561117 0.3121840289660065 0.3256944214759013 0.337480946474891 0.3490998890057371 0.360718831536583 0.37233777406742913 0.3839567165982751 0.39557565912912107 0.40673217388403315 0.41782455324483364 0.42891693260563396 0.44000931196643445 0.45110169132723493 0.4621940706880354 0.47314377493350046 0.483632110622263 ! PS_BldPitchMin - Minimum blade pitch angles [rad]
+
+!------- SHUTDOWN -----------------------------------------------------------
+0.436300000000 ! SD_MaxPit - Maximum blade pitch angle to initiate shutdown, [rad]
+0.418880000000 ! SD_CornerFreq - Cutoff Frequency for first order low-pass filter for blade pitch angle, [rad/s]
+
+!------- Floating -----------------------------------------------------------
+0.000000000000 ! Fl_Kp - Nacelle velocity proportional feedback gain [s]
+
+!------- FLAP ACTUATION -----------------------------------------------------
+0.000000000000 ! Flp_Angle - Initial or steady state flap angle [rad]
+0.00000000e+00 ! Flp_Kp - Blade root bending moment proportional gain for flap control [s]
+0.00000000e+00 ! Flp_Ki - Flap displacement integral gain for flap control [-]
+0.174500000000 ! Flp_MaxPit - Maximum (and minimum) flap pitch angle [rad]
+
+!------- Open Loop Control -----------------------------------------------------
+"unused" ! OL_Filename - Input file with open loop timeseries (absolute path or relative to this file)
+0 ! Ind_Breakpoint - The column in OL_Filename that contains the breakpoint (time if OL_Mode = 1)
+0 ! Ind_BldPitch - The column in OL_Filename that contains the blade pitch input in rad
+0 ! Ind_GenTq - The column in OL_Filename that contains the generator torque in Nm
+0 ! Ind_YawRate - The column in OL_Filename that contains the generator torque in Nm
+
+!------- Pitch Actuator Model -----------------------------------------------------
+3.140000000000 ! PA_CornerFreq - Pitch actuator bandwidth/cut-off frequency [rad/s]
+0.707000000000 ! PA_Damping - Pitch actuator damping ratio [-, unused if PA_Mode = 1]
+
+!------- External Controller Interface -----------------------------------------------------
+"unused" ! DLL_FileName - Name/location of the dynamic library in the Bladed-DLL format
+"unused" ! DLL_InFile - Name of input file sent to the DLL (-)
+"DISCON" ! DLL_ProcName - Name of procedure in DLL to be called (-)
+
+!------- ZeroMQ Interface ---------------------------------------------------------
+"tcp://localhost:5555" ! ZMQ_CommAddress - Communication address for ZMQ server, (e.g. "tcp://localhost:5555")
+2 ! ZMQ_UpdatePeriod - Call ZeroMQ every [x] seconds, [s]
diff --git a/weio/tests/example_files/TDMS_1Grp2Chan_TimeTrack.tdms b/weio/tests/example_files/TDMS_1Grp2Chan_TimeTrack.tdms
new file mode 100644
index 0000000..b09be01
Binary files /dev/null and b/weio/tests/example_files/TDMS_1Grp2Chan_TimeTrack.tdms differ
diff --git a/weio/tests/example_files/TDMS_2Grp2Chan.tdms b/weio/tests/example_files/TDMS_2Grp2Chan.tdms
new file mode 100644
index 0000000..cf9b2e9
Binary files /dev/null and b/weio/tests/example_files/TDMS_2Grp2Chan.tdms differ
diff --git a/weio/tests/test_all.py b/weio/tests/test_all.py
index cb76d0c..6dd4e53 100644
--- a/weio/tests/test_all.py
+++ b/weio/tests/test_all.py
@@ -3,10 +3,7 @@
import weio
import os
import numpy as np
-try:
- from .helpers_for_test import MyDir, reading_test
-except:
- from helpers_for_test import MyDir, reading_test
+from weio.tests.helpers_for_test import MyDir, reading_test
class Test(unittest.TestCase):
def test_000_debug(self):
@@ -53,11 +50,15 @@ def test_001_read_all(self):
nError += 1
if DEBUG:
print('[FAIL] {:30s}\tFormat not detected'.format(os.path.basename(f)[:30]))
+ except weio.OptionalImportError:
+ nError += 0 # Optional..
+ if DEBUG:
+ print('[FAIL] {:30s}\tOptional package missing'.format(os.path.basename(f)[:30]))
except:
nError += 1
if DEBUG:
print('[FAIL] {:30s}\tException occurred'.format(os.path.basename(f)[:30]))
- raise
+ #raise
if nError>0:
raise Exception('Some tests failed')
diff --git a/weio/tests/test_csv.py b/weio/tests/test_csv.py
index 63c413b..86359e8 100644
--- a/weio/tests/test_csv.py
+++ b/weio/tests/test_csv.py
@@ -1,56 +1,53 @@
-import unittest
-import os
-import numpy as np
-from .helpers_for_test import MyDir, reading_test
-try:
- from weio.csv_file import CSVFile
-except:
- from weio.weio.csv_file import CSVFile
-
-class Test(unittest.TestCase):
-
- def test_001_read_all(self, DEBUG=True):
- reading_test('CSV*.*', CSVFile)
-
- def DF(self,FN):
- """ Reads a file and return a dataframe """
- return CSVFile(os.path.join(MyDir,FN)).toDataFrame()
-
- def test_CSV(self):
- self.assertEqual(self.DF('CSVAutoCommentChar.txt').shape,(11,6))
-
- DF=self.DF('CSVColInHeader.csv')
- self.assertTrue(all(DF.columns.values==['ColA','ColB','ColC']))
- self.assertEqual(DF.shape,(2,3))
-
- DF=self.DF('CSVColInHeader2.csv')
- self.assertTrue(all(DF.columns.values==['ColA','ColB','ColC']))
- self.assertEqual(DF.shape,(2,3))
-
- DF=self.DF('CSVColInHeader3.csv')
- self.assertTrue(all(DF.columns.values==['ColA','ColB','ColC']))
- self.assertEqual(DF.shape,(2,3))
-
- DF=self.DF('CSVComma_UTF16.csv')
- self.assertEqual(DF.shape,(4,3))
-
- self.assertEqual(self.DF('CSVComma.csv').shape,(4,2))
- self.assertEqual(self.DF('CSVDateNaN.csv').shape,(11,2))
- self.assertEqual(self.DF('CSVNoHeader.csv').shape,(4,2))
- self.assertEqual(self.DF('CSVSemi.csv').shape,(3,2))
- self.assertEqual(self.DF('CSVSpace_ExtraCol.csv').shape,(5,4))
- self.assertEqual(self.DF('CSVTab.csv').shape,(5,2))
-
- DF = self.DF('CSVTwoLinesHeaders.txt')
- self.assertEqual(DF.columns.values[-1],'GenTq_(kN m)')
- self.assertEqual(DF.shape,(9,6))
-
- def test_CSV_string(self):
- DF=self.DF('CSVxIsString.csv')
- self.assertEqual(DF.shape,(7,2))
- self.assertEqual(DF.columns.values[0],'Label_[-]')
-
-
-if __name__ == '__main__':
- #Test().test_CSV()
- unittest.main()
+import unittest
+import os
+import numpy as np
+from weio.tests.helpers_for_test import MyDir, reading_test
+from weio.csv_file import CSVFile
+
+class Test(unittest.TestCase):
+
+ def test_001_read_all(self, DEBUG=True):
+ reading_test('CSV*.*', CSVFile)
+
+ def DF(self,FN):
+ """ Reads a file and return a dataframe """
+ return CSVFile(os.path.join(MyDir,FN)).toDataFrame()
+
+ def test_CSV(self):
+ self.assertEqual(self.DF('CSVAutoCommentChar.txt').shape,(11,6))
+
+ DF=self.DF('CSVColInHeader.csv')
+ self.assertTrue(all(DF.columns.values==['ColA','ColB','ColC']))
+ self.assertEqual(DF.shape,(2,3))
+
+ DF=self.DF('CSVColInHeader2.csv')
+ self.assertTrue(all(DF.columns.values==['ColA','ColB','ColC']))
+ self.assertEqual(DF.shape,(2,3))
+
+ DF=self.DF('CSVColInHeader3.csv')
+ self.assertTrue(all(DF.columns.values==['ColA','ColB','ColC']))
+ self.assertEqual(DF.shape,(2,3))
+
+ DF=self.DF('CSVComma_UTF16.csv')
+ self.assertEqual(DF.shape,(4,3))
+
+ self.assertEqual(self.DF('CSVComma.csv').shape,(4,2))
+ self.assertEqual(self.DF('CSVDateNaN.csv').shape,(11,2))
+ self.assertEqual(self.DF('CSVNoHeader.csv').shape,(4,2))
+ self.assertEqual(self.DF('CSVSemi.csv').shape,(3,2))
+ self.assertEqual(self.DF('CSVSpace_ExtraCol.csv').shape,(5,4))
+ self.assertEqual(self.DF('CSVTab.csv').shape,(5,2))
+
+ DF = self.DF('CSVTwoLinesHeaders.txt')
+ self.assertEqual(DF.columns.values[-1],'GenTq_(kN m)')
+ self.assertEqual(DF.shape,(9,6))
+
+ def test_CSV_string(self):
+ DF=self.DF('CSVxIsString.csv')
+ self.assertEqual(DF.shape,(7,2))
+ self.assertEqual(DF.columns.values[0],'Label_[-]')
+
+
+if __name__ == '__main__':
+ #Test().test_CSV()
+ unittest.main()
diff --git a/weio/tests/test_fast_input.py b/weio/tests/test_fast_input.py
index 5875925..b1ee958 100644
--- a/weio/tests/test_fast_input.py
+++ b/weio/tests/test_fast_input.py
@@ -1,13 +1,13 @@
import unittest
import os
import numpy as np
-import weio
from weio.tests.helpers_for_test import MyDir, reading_test
-
from weio.fast_input_file import FASTInputFile
from weio.fast_input_file import ExtPtfmFile
from weio.fast_input_file import ADPolarFile
from weio.fast_input_file import EDBladeFile
+from weio.fast_wind_file import FASTWndFile
+
class Test(unittest.TestCase):
@@ -33,6 +33,11 @@ def test_FASTIn(self):
F.test_ascii(bCompareWritesOnly=True,bDelete=True)
self.assertEqual(F['RotSpeed'],0.2)
+ F=FASTInputFile(os.path.join(MyDir,'FASTIn_ED_bld.dat'))
+ F.test_ascii(bCompareWritesOnly=True,bDelete=True)
+ self.assertEqual(F['BldEdgSh(6)'],-0.6952)
+ F.comment = 'ElastoDyn file'
+
F=FASTInputFile(os.path.join(MyDir,'FASTIn_ED_twr.dat'))
F.test_ascii(bCompareWritesOnly=True,bDelete=True)
self.assertEqual(F['AdjFASt'],1)
@@ -41,6 +46,10 @@ def test_FASTIn(self):
F.test_ascii(bCompareWritesOnly=True,bDelete=True)
self.assertTrue(F['TipLoss'])
+ F=FASTInputFile(os.path.join(MyDir,'FASTIn_ExtPtfm_SubSef.dat'))
+ F.test_ascii(bCompareWritesOnly=True,bDelete=True)
+ self.assertEqual(F['StiffnessMatrix'][2,2],1.96653266e+09)
+
F=FASTInputFile(os.path.join(MyDir,'FASTIn_HD.dat'))
#F.test_ascii(bCompareWritesOnly=True,bDelete=True) # TODO
self.assertAlmostEqual(F['RdtnDT'],0.0125)
@@ -59,11 +68,6 @@ def test_FASTIn(self):
F.test_ascii(bCompareWritesOnly=True,bDelete=True)
self.assertEqual(F['PitManRat(1)'],2)
- F=FASTInputFile(os.path.join(MyDir,'FASTIn_MD.dat'))
- F.test_ascii(bCompareWritesOnly=True,bDelete=True)
- self.assertEqual(float(F['LineTypes'][0,1]),0.02)
-
-
def test_FASTADBld(self):
F=FASTInputFile(os.path.join(MyDir,'FASTIn_AD15_bld.dat'))
F.test_ascii(bCompareWritesOnly=True,bDelete=True)
@@ -106,10 +110,10 @@ def test_FASTADPolMulti(self):
F.test_ascii(bCompareWritesOnly=False,bDelete=True)
dfs = F.toDataFrame()
- self.assertTrue('AFCoeff_2' in dfs.keys())
+ self.assertTrue('AFCoeff_Re0.06' in dfs.keys())
- df1 = dfs['AFCoeff_1']
- df2 = dfs['AFCoeff_2']
+ df1 = dfs['AFCoeff_Re0.05']
+ df2 = dfs['AFCoeff_Re0.06']
self.assertTrue('Cn_pot_[-]' in df2.keys())
self.assertEqual(df1.shape[0],23)
@@ -140,7 +144,7 @@ def test_FASTExt(self):
self.assertAlmostEqual(df['InpF_Fx_[N]'].values[-1], 1660.749680)
def test_FASTWnd(self):
- F=weio.read(os.path.join(MyDir,'FASTWnd.wnd'))
+ F=FASTWndFile(os.path.join(MyDir,'FASTWnd.wnd'))
F.test_ascii(bCompareWritesOnly=True,bDelete=True)
def test_FASTInGraph(self):
@@ -155,12 +159,39 @@ def test_FASTInGraph(self):
graph = F.toGraph()
# self.assertEqual(len(graph.Nodes), 2)
# self.assertEqual(len(graph.Elements), 1)
+ def test_FASTInMoorDyn(self):
+ # MoorDyn version 1
+ F=FASTInputFile(os.path.join(MyDir,'FASTIn_MD-v1.dat'))
+ F.test_ascii(bCompareWritesOnly=True,bDelete=True)
+ self.assertEqual(float(F['LineTypes'][0,1]),0.02)
+
+ # MoorDyn version 2
+ F=FASTInputFile(os.path.join(MyDir,'FASTIn_MD-v2.dat'))
+ #F.write(os.path.join(MyDir,'FASTIn_MD-v2.dat---OUT'))
+ self.assertTrue('Points' in F.keys())
+ self.assertTrue('LineTypes' in F.keys())
+ self.assertTrue('LineProp' in F.keys())
+ self.assertEqual(F['LineProp'].shape , (3,7))
+ self.assertEqual(F['LineTypes'].shape , (1,10))
+ self.assertEqual(F['Points'].shape , (6,9))
+ self.assertEqual(len(F['Outlist']) , 6)
+ self.assertEqual(F['Outlist'][0] , 'FairTen1')
+ self.assertEqual(F['LineProp'][0,0] , '1')
+ self.assertEqual(F['LineProp'][0,1] , 'main')
+ self.assertEqual(F['LineProp'][0,6] , '-')
+
+ def test_FASTInAirfoil(self):
+ F=FASTInputFile(os.path.join(MyDir,'FASTIn_AD15_arfl.dat'))
+ F.test_ascii(bCompareWritesOnly=True,bDelete=True)
+ self.assertTrue('InterpOrd' in F.keys())
+ self.assertTrue('AFCoeff' in F.keys())
+ self.assertEqual(F['AFCoeff'].shape, (30,4))
if __name__ == '__main__':
#Test().test_FASTEDBld()
#Test().test_FASTADBld()
#Test().test_FASTADPol()
- Test().test_FASTADPolMulti()
+ #Test().test_FASTADPolMulti()
#Test().test_FASTExt()
#Test().test_FASTIn()
- #unittest.main()
+ unittest.main()
diff --git a/weio/tests/test_fast_input_deck.py b/weio/tests/test_fast_input_deck.py
index 00fef9d..a0c3937 100644
--- a/weio/tests/test_fast_input_deck.py
+++ b/weio/tests/test_fast_input_deck.py
@@ -1,33 +1,25 @@
-import unittest
-import os
-import numpy as np
-import weio
-try:
- from .helpers_for_test import MyDir, reading_test
-except ImportError:
- from helpers_for_test import MyDir, reading_test
-
-try:
- from weio.fast_input_deck import FASTInputDeck
-except:
- from weio.weio.fast_input_deck import FASTInputDeck
-
-class Test(unittest.TestCase):
-
- def test_deck_driver(self):
- F=FASTInputDeck(os.path.join(MyDir,'input_decks/Main_EllipticalWingInf_OLAF.dvr'))
- #F.test_ascii(bCompareWritesOnly=True,bDelete=True)
- self.assertEqual(F.fst['NumTurbines'],1)
-
- self.assertEqual(F.version,'AD_driver')
- self.assertEqual(F.ADversion,'AD15')
- self.assertTrue(F.fst is not None)
- self.assertTrue(F.IW is not None)
- self.assertTrue(F.AD is not None)
- self.assertTrue(F.AD.Bld1 is not None)
-
-
-
-if __name__ == '__main__':
- #Test().test_FASTIn()
- unittest.main()
+import unittest
+import os
+import numpy as np
+from weio.tests.helpers_for_test import MyDir, reading_test
+from weio.fast_input_deck import FASTInputDeck
+
+class Test(unittest.TestCase):
+
+ def test_deck_driver(self):
+ F=FASTInputDeck(os.path.join(MyDir,'input_decks/Main_EllipticalWingInf_OLAF.dvr'))
+ #F.test_ascii(bCompareWritesOnly=True,bDelete=True)
+ self.assertEqual(F.fst['NumTurbines'],1)
+
+ self.assertEqual(F.version,'AD_driver')
+ self.assertEqual(F.ADversion,'AD15')
+ self.assertTrue(F.fst is not None)
+ self.assertTrue(F.IW is not None)
+ self.assertTrue(F.AD is not None)
+ self.assertTrue(F.AD.Bld1 is not None)
+
+
+
+if __name__ == '__main__':
+ #Test().test_FASTIn()
+ unittest.main()
diff --git a/weio/tests/test_fast_linearization.py b/weio/tests/test_fast_linearization.py
index 84bfdc9..8e530ef 100644
--- a/weio/tests/test_fast_linearization.py
+++ b/weio/tests/test_fast_linearization.py
@@ -1,28 +1,25 @@
-import unittest
-import os
-import numpy as np
-from .helpers_for_test import MyDir, reading_test
-try:
- from weio.fast_linearization_file import FASTLinearizationFile
-except:
- from weio.weio.fast_linearization_file import FASTLinearizationFile
-
-class Test(unittest.TestCase):
-
- def test_001_read_all(self, DEBUG=True):
- reading_test('FASTLin*.*', FASTLinearizationFile)
-
- def test_FASTLin(self):
- F=FASTLinearizationFile(os.path.join(MyDir,'FASTLin.lin'))
- self.assertAlmostEqual(F['A'][3,1], 3.91159454E-04 )
- self.assertAlmostEqual(F['u'][7] ,4.00176055E+04)
-
- F=FASTLinearizationFile(os.path.join(MyDir,'FASTLin_EDM.lin'))
- dfs=F.toDataFrame()
- M=dfs['M']
- self.assertAlmostEqual(M['7_TwFADOF1']['7_TwFADOF1'],0.436753E+06)
- self.assertAlmostEqual(M['13_GeAz']['13_GeAz'] , 0.437026E+08)
-
-if __name__ == '__main__':
-# Test().test_000_debug()
- unittest.main()
+import unittest
+import os
+import numpy as np
+from weio.tests.helpers_for_test import MyDir, reading_test
+from weio.fast_linearization_file import FASTLinearizationFile
+
+class Test(unittest.TestCase):
+
+ def test_001_read_all(self, DEBUG=True):
+ reading_test('FASTLin*.*', FASTLinearizationFile)
+
+ def test_FASTLin(self):
+ F=FASTLinearizationFile(os.path.join(MyDir,'FASTLin.lin'))
+ self.assertAlmostEqual(F['A'][3,1], 3.91159454E-04 )
+ self.assertAlmostEqual(F['u'][7] ,4.00176055E+04)
+
+ F=FASTLinearizationFile(os.path.join(MyDir,'FASTLin_EDM.lin'))
+ dfs=F.toDataFrame()
+ M=dfs['M']
+ self.assertAlmostEqual(M['7_TwFADOF1']['7_TwFADOF1'],0.436753E+06)
+ self.assertAlmostEqual(M['13_GeAz']['13_GeAz'] , 0.437026E+08)
+
+if __name__ == '__main__':
+# Test().test_000_debug()
+ unittest.main()
diff --git a/weio/tests/test_fast_output.py b/weio/tests/test_fast_output.py
index 838244e..f19e598 100644
--- a/weio/tests/test_fast_output.py
+++ b/weio/tests/test_fast_output.py
@@ -1,47 +1,44 @@
-import unittest
-import os
-import numpy as np
-from .helpers_for_test import MyDir, reading_test
-try:
- from weio.fast_output_file import FASTOutputFile
-except:
- from weio.weio.fast_output_file import FASTOutputFile
-
-class Test(unittest.TestCase):
-
- def test_001_read_all(self, DEBUG=True):
- reading_test('FASTOut*.*', FASTOutputFile)
-
- def DF(self,FN):
- """ Reads a file and return a dataframe """
- return FASTOutputFile(os.path.join(MyDir,FN)).toDataFrame()
-
- def test_FASTOut(self):
- self.assertEqual(self.DF('FASTOut.out').values[-1,1],1036)
-
- def test_FASTOutBin(self):
- # --- Test reading
- F = FASTOutputFile(os.path.join(MyDir,'FASTOutBin.outb'))
- M = F.toDataFrame()
- self.assertAlmostEqual(M['GenPwr_[kW]'].values[-1],40.57663190807828)
- # --- Test writing
- tempFilename = '_FASTOutBin_out.outb'
- # Write to tempfile
- F.write(tempFilename)
- # Read written file
- F2= FASTOutputFile(tempFilename)
- # Test that read data match
- np.testing.assert_almost_equal(F.data,F2.data, 4)
- np.testing.assert_almost_equal(F.data[-1,-1] ,40.57663190807828, 10)
- np.testing.assert_almost_equal(F2.data[-1,-1],40.57663190807828, 10)
- self.assertEqual(F2.info['attribute_names'][-1],'GenPwr')
- self.assertEqual(F2.info['attribute_units'][-1],'kW')
- # cleanup
- try:
- os.remove(tempFilename)
- except:
- pass
-
-if __name__ == '__main__':
-# Test().test_000_debug()
- unittest.main()
+import unittest
+import os
+import numpy as np
+from weio.tests.helpers_for_test import MyDir, reading_test
+from weio.fast_output_file import FASTOutputFile
+
+class Test(unittest.TestCase):
+
+ def test_001_read_all(self, DEBUG=True):
+ reading_test('FASTOut*.*', FASTOutputFile)
+
+ def DF(self,FN):
+ """ Reads a file and return a dataframe """
+ return FASTOutputFile(os.path.join(MyDir,FN)).toDataFrame()
+
+ def test_FASTOut(self):
+ self.assertEqual(self.DF('FASTOut.out').values[-1,1],1036)
+
+ def test_FASTOutBin(self):
+ # --- Test reading
+ F = FASTOutputFile(os.path.join(MyDir,'FASTOutBin.outb'))
+ M = F.toDataFrame()
+ self.assertAlmostEqual(M['GenPwr_[kW]'].values[-1],40.57663190807828)
+ # --- Test writing
+ tempFilename = '_FASTOutBin_out.outb'
+ # Write to tempfile
+ F.write(tempFilename)
+ # Read written file
+ F2= FASTOutputFile(tempFilename)
+ # Test that read data match
+ np.testing.assert_almost_equal(F.data.values,F2.data.values, 4)
+ np.testing.assert_almost_equal(F.data.values[-1,-1] ,40.57663190807828, 10)
+ np.testing.assert_almost_equal(F2.data.values[-1,-1],40.57663190807828, 10)
+ self.assertEqual(F2.channels[-1],'GenPwr')
+ self.assertEqual(F2.units[-1],'kW')
+ # cleanup
+ try:
+ os.remove(tempFilename)
+ except:
+ pass
+
+if __name__ == '__main__':
+# Test().test_000_debug()
+ unittest.main()
diff --git a/weio/tests/test_fast_summary.py b/weio/tests/test_fast_summary.py
index 8c04da0..ca482a2 100644
--- a/weio/tests/test_fast_summary.py
+++ b/weio/tests/test_fast_summary.py
@@ -1,49 +1,42 @@
-import unittest
-import os
-import numpy as np
-try:
- from .helpers_for_test import MyDir, reading_test
-except ImportError:
- from helpers_for_test import MyDir, reading_test
-try:
- from weio.fast_summary_file import FASTSummaryFile
-except ImportError:
- from weio.weio.fast_summary_file import FASTSummaryFile
-
-
-class Test(unittest.TestCase):
-
- def test_001_read_all(self, DEBUG=True):
- reading_test('FASTSum*.*', FASTSummaryFile)
-
- def test_FASTSum(self):
- f = FASTSummaryFile(os.path.join(MyDir, 'FASTSum_Pendulum.SD.sum.yaml'))
- np.testing.assert_almost_equal(f['CB_frequencies'].ravel(),[2.571561E-02,5.154897E+00,3.448768E+01,3.639185E+01,9.826435E+01], 5)
-
- # Test toDataFrame
- df=f.toDataFrame()
- np.testing.assert_almost_equal(df['z_[m]'].values,[-6,-1,0])
- np.testing.assert_almost_equal(df['GuyanMode1x_[m]'].values[0],0.6)
-
- # Test toJSON
- dJSON=f.toJSON('_test.json')
- np.testing.assert_almost_equal(dJSON['Connectivity'], [[0,1],[1,2]])
- try:
- os.remove('_test.json')
- except:
- pass
-
-
- def test_FASTSumGraph(self):
- f = FASTSummaryFile(os.path.join(MyDir, 'FASTSum_Pendulum.SD.sum.yaml'))
- graph = f.toGraph()
- # print(graph)
- self.assertEqual(len(graph.Nodes), 3)
- self.assertEqual(len(graph.Elements), 2)
- self.assertEqual(len(graph.Modes), 11)
- np.testing.assert_almost_equal(graph.Modes[10]['freq'], 98.26435)
-
-
-if __name__ == '__main__':
-# Test().test_000_debug()
- unittest.main()
+import unittest
+import os
+import numpy as np
+from weio.tests.helpers_for_test import MyDir, reading_test
+from weio.fast_summary_file import FASTSummaryFile
+
+class Test(unittest.TestCase):
+
+ def test_001_read_all(self, DEBUG=True):
+ reading_test('FASTSum*.*', FASTSummaryFile)
+
+ def test_FASTSum(self):
+ f = FASTSummaryFile(os.path.join(MyDir, 'FASTSum_Pendulum.SD.sum.yaml'))
+ np.testing.assert_almost_equal(f['CB_frequencies'].ravel(),[2.571561E-02,5.154897E+00,3.448768E+01,3.639185E+01,9.826435E+01], 5)
+
+ # Test toDataFrame
+ df=f.toDataFrame(sortDim=2)
+ np.testing.assert_almost_equal(df['z_[m]'].values,[-6,-1,0])
+ np.testing.assert_almost_equal(df['GuyanMode1x_[m]'].values[0],0.6)
+
+ # Test toJSON
+ dJSON=f.toJSON('_test.json')
+ np.testing.assert_almost_equal(dJSON['Connectivity'], [[0,1],[1,2]])
+ try:
+ os.remove('_test.json')
+ except:
+ pass
+
+
+ def test_FASTSumGraph(self):
+ f = FASTSummaryFile(os.path.join(MyDir, 'FASTSum_Pendulum.SD.sum.yaml'))
+ graph = f.toGraph()
+ # print(graph)
+ self.assertEqual(len(graph.Nodes), 3)
+ self.assertEqual(len(graph.Elements), 2)
+ self.assertEqual(len(graph.Modes), 11)
+ np.testing.assert_almost_equal(graph.Modes[10]['freq'], 98.26435)
+
+
+if __name__ == '__main__':
+# Test().test_000_debug()
+ unittest.main()
diff --git a/weio/tests/test_hawc.py b/weio/tests/test_hawc.py
index c7c61f9..697ac7c 100644
--- a/weio/tests/test_hawc.py
+++ b/weio/tests/test_hawc.py
@@ -1,27 +1,15 @@
import unittest
import os
import numpy as np
-try:
- from .helpers_for_test import MyDir, reading_test
-except ImportError:
- from helpers_for_test import MyDir, reading_test
+import weio
+from weio.tests.helpers_for_test import MyDir, reading_test
+from weio.hawc2_dat_file import HAWC2DatFile
+from weio.hawc2_ae_file import HAWC2AEFile
+from weio.hawc2_pc_file import HAWC2PCFile
+from weio.hawc2_st_file import HAWC2StFile
+from weio.hawcstab2_ind_file import HAWCStab2IndFile
+from weio.hawcstab2_pwr_file import HAWCStab2PwrFile
-try:
- import weio
- from weio.hawc2_dat_file import HAWC2DatFile
- from weio.hawc2_ae_file import HAWC2AEFile
- from weio.hawc2_pc_file import HAWC2PCFile
- from weio.hawc2_st_file import HAWC2StFile
- from weio.hawcstab2_ind_file import HAWCStab2IndFile
- from weio.hawcstab2_pwr_file import HAWCStab2PwrFile
-except:
- import weio.weio as weio
- from weio.weio.hawc2_dat_file import HAWC2DatFile
- from weio.weio.hawc2_ae_file import HAWC2AEFile
- from weio.weio.hawc2_pc_file import HAWC2PCFile
- from weio.weio.hawc2_st_file import HAWC2StFile
- from weio.weio.hawcstab2_ind_file import HAWCStab2IndFile
- from weio.weio.hawcstab2_pwr_file import HAWCStab2PwrFile
class Test(unittest.TestCase):
diff --git a/weio/tests/test_mannbox.py b/weio/tests/test_mannbox.py
new file mode 100644
index 0000000..500c8c8
--- /dev/null
+++ b/weio/tests/test_mannbox.py
@@ -0,0 +1,24 @@
+import unittest
+import os
+import numpy as np
+from weio.tests.helpers_for_test import MyDir, reading_test
+from weio.mannbox_file import MannBoxFile
+
+
+class Test(unittest.TestCase):
+
+ def test_001_read_all(self, DEBUG=True):
+ reading_test('MannBox_*.*', MannBoxFile)
+
+ def test_MannBox(self):
+ # --- Test read/write
+ F = MannBoxFile(os.path.join(MyDir,'MannBox_2x4x8.bin'))
+ F.write( os.path.join(MyDir,'MannBox_2x4x8_TMP.bin'))
+ F2= MannBoxFile(os.path.join(MyDir,'MannBox_2x4x8_TMP.bin'))
+ os.remove( os.path.join(MyDir,'MannBox_2x4x8_TMP.bin'))
+ np.testing.assert_almost_equal(F['field'].shape ,[2,4,8])
+ np.testing.assert_almost_equal(F['field'][:,:,:],F2['field'][:,:,:],8)
+ np.testing.assert_almost_equal(F['field'][1,3,5], -3.6654968, 6)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/weio/tests/test_parquet.py b/weio/tests/test_parquet.py
index 0c063af..c92d7d1 100644
--- a/weio/tests/test_parquet.py
+++ b/weio/tests/test_parquet.py
@@ -1,30 +1,26 @@
-import unittest
-import os
-
-from .helpers_for_test import MyDir, reading_test
-try:
- from weio.parquet_file import ParquetFile
-except:
- from weio.weio.parquet_file import ParquetFile
-
-class Test(unittest.TestCase):
-
- def test_001_read_all(self, DEBUG=True):
- reading_test('ParquetFile*.*', ParquetFile)
-
- def DF(self,FN):
- """ Reads a file and return a dataframe """
- return ParquetFile(os.path.join(MyDir,FN)).toDataFrame()
-
- def test_ParquetFile(self):
- df=self.DF('ParquetFile_test.parquet')
- self.assertListEqual(list(df.columns),["Column1","Column 2","Column Str"])
- self.assertEqual(df.shape,(3,3))
- self.assertEqual(df.loc[0,"Column Str"],'abc')
- self.assertEqual(df.loc[0, "Column1"], 1)
-
-
-
-if __name__ == '__main__':
-# Test().test_000_debug()
- unittest.main()
+import unittest
+import os
+from weio.tests.helpers_for_test import MyDir, reading_test
+from weio.parquet_file import ParquetFile
+
+class Test(unittest.TestCase):
+
+ def test_001_read_all(self, DEBUG=True):
+ reading_test('ParquetFile*.*', ParquetFile)
+
+ def DF(self,FN):
+ """ Reads a file and return a dataframe """
+ return ParquetFile(os.path.join(MyDir,FN)).toDataFrame()
+
+ def test_ParquetFile(self):
+ df=self.DF('ParquetFile_test.parquet')
+ self.assertListEqual(list(df.columns),["Column1","Column 2","Column Str"])
+ self.assertEqual(df.shape,(3,3))
+ self.assertEqual(df.loc[0,"Column Str"],'abc')
+ self.assertEqual(df.loc[0, "Column1"], 1)
+
+
+
+if __name__ == '__main__':
+# Test().test_000_debug()
+ unittest.main()
diff --git a/weio/tests/test_tdms.py b/weio/tests/test_tdms.py
new file mode 100644
index 0000000..58b3150
--- /dev/null
+++ b/weio/tests/test_tdms.py
@@ -0,0 +1,45 @@
+import unittest
+import os
+import numpy as np
+from weio.tests.helpers_for_test import MyDir, reading_test
+
+try:
+ from nptdms import TdmsFile
+ HasNPTDMS=True
+except:
+ HasNPTDMS=False
+from weio.tdms_file import TDMSFile
+
+class Test(unittest.TestCase):
+
+ def test_001_read_all(self, DEBUG=True):
+ # read all TDMS present in the example folder, using the TDMSFile class
+ if HasNPTDMS:
+ reading_test('TDMS*.*', TDMSFile)
+
+ def DF(self, FN):
+ """ Reads a file and return a dataframe """
+ return TDMSFile(os.path.join(MyDir,FN)).toDataFrame()
+
+ def test_TDMS(self):
+ # One group two channels with time track
+ if HasNPTDMS:
+ df = self.DF('TDMS_1Grp2Chan_TimeTrack.tdms')
+ self.assertEqual(df.shape,(20,3))
+ self.assertEqual(df.columns[0], 'Time_[s]')
+ self.assertAlmostEqual(df['ColB'].values[-1], 1.41852580388, 4)
+ self.assertAlmostEqual(df['Time_[s]'].values[1], 1/19, 4)
+ self.assertAlmostEqual(df['Time_[s]'].values[-1], 1, 4)
+
+ # Two groups, two channels, no time track but timestamps
+ dfs = self.DF('TDMS_2Grp2Chan.tdms')
+ np.testing.assert_array_equal(list(dfs.keys()),('GroupA','GroupB'))
+
+ df = dfs['GroupA']
+ self.assertAlmostEqual(df['ColA'].values[-1], -0.35676837, 4)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
diff --git a/weio/tests/test_turbsim.py b/weio/tests/test_turbsim.py
index 2afe6ac..3c7334c 100644
--- a/weio/tests/test_turbsim.py
+++ b/weio/tests/test_turbsim.py
@@ -1,38 +1,35 @@
-import unittest
-import os
-import numpy as np
-from .helpers_for_test import MyDir, reading_test
-try:
- from weio.turbsim_file import TurbSimFile
-except:
- from weio.weio.turbsim_file import TurbSimFile
-
-class Test(unittest.TestCase):
-
- def test_001_read_all(self, DEBUG=True):
- reading_test('TurbSim_*.*', TurbSimFile)
-
- def test_TurbSim(self):
- # --- Test without tower
- F = TurbSimFile(os.path.join(MyDir,'TurbSim_NoTwr.bts'))
- F.write( os.path.join(MyDir,'TurbSim_NoTwr_TMP.bts'))
- F2= TurbSimFile(os.path.join(MyDir,'TurbSim_NoTwr_TMP.bts'))
- os.remove( os.path.join(MyDir,'TurbSim_NoTwr_TMP.bts'))
- np.testing.assert_almost_equal(F['u'][0,:,:,:],F2['u'][0,:,:,:],4)
- np.testing.assert_almost_equal(F['u'][1,:,:,:],F2['u'][1,:,:,:],4)
- np.testing.assert_almost_equal(F['u'][2,:,:,:],F2['u'][2,:,:,:],4)
- # --- Test with tower
- F = TurbSimFile(os.path.join(MyDir,'TurbSim_WithTwr.bts'))
- np.testing.assert_almost_equal(F['u'][2,-1,1,3], 0.508036, 5)
- np.testing.assert_almost_equal(F['u'][0, 4,2,0], 7.4867466, 5)
- np.testing.assert_almost_equal(F['uTwr'][0, 4, :], [6.1509, 6.4063, 8.9555, 7.6943], 4)
- F.write( os.path.join(MyDir,'TurbSim_WithTwr_TMP.bts'))
- F2= TurbSimFile(os.path.join(MyDir,'TurbSim_WithTwr_TMP.bts'))
- os.remove( os.path.join(MyDir,'TurbSim_WithTwr_TMP.bts'))
- np.testing.assert_almost_equal(F['u'][0,:,:,:],F2['u'][0,:,:,:],3)
- np.testing.assert_almost_equal(F['u'][1,:,:,:],F2['u'][1,:,:,:],3)
- np.testing.assert_almost_equal(F['u'][2,:,:,:],F2['u'][2,:,:,:],3)
-
-if __name__ == '__main__':
-# Test().test_000_debug()
- unittest.main()
+import unittest
+import os
+import numpy as np
+from weio.tests.helpers_for_test import MyDir, reading_test
+from weio.turbsim_file import TurbSimFile
+
+class Test(unittest.TestCase):
+
+ def test_001_read_all(self, DEBUG=True):
+ reading_test('TurbSim_*.*', TurbSimFile)
+
+ def test_TurbSim(self):
+ # --- Test without tower
+ F = TurbSimFile(os.path.join(MyDir,'TurbSim_NoTwr.bts'))
+ F.write( os.path.join(MyDir,'TurbSim_NoTwr_TMP.bts'))
+ F2= TurbSimFile(os.path.join(MyDir,'TurbSim_NoTwr_TMP.bts'))
+ os.remove( os.path.join(MyDir,'TurbSim_NoTwr_TMP.bts'))
+ np.testing.assert_almost_equal(F['u'][0,:,:,:],F2['u'][0,:,:,:],4)
+ np.testing.assert_almost_equal(F['u'][1,:,:,:],F2['u'][1,:,:,:],4)
+ np.testing.assert_almost_equal(F['u'][2,:,:,:],F2['u'][2,:,:,:],4)
+ # --- Test with tower
+ F = TurbSimFile(os.path.join(MyDir,'TurbSim_WithTwr.bts'))
+ np.testing.assert_almost_equal(F['u'][2,-1,1,3], 0.508036, 5)
+ np.testing.assert_almost_equal(F['u'][0, 4,2,0], 7.4867466, 5)
+ np.testing.assert_almost_equal(F['uTwr'][0, 4, :], [6.1509, 6.4063, 8.9555, 7.6943], 4)
+ F.write( os.path.join(MyDir,'TurbSim_WithTwr_TMP.bts'))
+ F2= TurbSimFile(os.path.join(MyDir,'TurbSim_WithTwr_TMP.bts'))
+ os.remove( os.path.join(MyDir,'TurbSim_WithTwr_TMP.bts'))
+ np.testing.assert_almost_equal(F['u'][0,:,:,:],F2['u'][0,:,:,:],3)
+ np.testing.assert_almost_equal(F['u'][1,:,:,:],F2['u'][1,:,:,:],3)
+ np.testing.assert_almost_equal(F['u'][2,:,:,:],F2['u'][2,:,:,:],3)
+
+if __name__ == '__main__':
+# Test().test_000_debug()
+ unittest.main()
diff --git a/weio/tests/test_vtk.py b/weio/tests/test_vtk.py
index 78e39b7..b70a6a7 100644
--- a/weio/tests/test_vtk.py
+++ b/weio/tests/test_vtk.py
@@ -1,29 +1,24 @@
-import unittest
-import os
-import numpy as np
-try:
- from .helpers_for_test import MyDir, reading_test
-except ImportError:
- from helpers_for_test import MyDir, reading_test
-try:
- from weio.vtk_file import VTKFile
-except ImportError:
- from weio.weio.vtk_file import VTKFile
-
-
-class Test(unittest.TestCase):
-
- def test_001_read_all(self, DEBUG=True):
- reading_test('VTK*.*', VTKFile)
-
- def test_VTKStruct(self):
- f = VTKFile(os.path.join(MyDir, 'VTKStructuredPointsPointData.vtk'))
- np.testing.assert_almost_equal(f.points,f.point_data['DisXY'])
-
- np.testing.assert_almost_equal(f.xp_grid,[0,20,40])
- np.testing.assert_almost_equal(f.point_data_grid['DisXY'][:,0,0,0],[0,20,40])
-
-
-if __name__ == '__main__':
-# Test().test_000_debug()
- unittest.main()
+import unittest
+import os
+import numpy as np
+from weio.tests.helpers_for_test import MyDir, reading_test
+from weio.vtk_file import VTKFile
+
+
+
+class Test(unittest.TestCase):
+
+ def test_001_read_all(self, DEBUG=True):
+ reading_test('VTK*.*', VTKFile)
+
+ def test_VTKStruct(self):
+ f = VTKFile(os.path.join(MyDir, 'VTKStructuredPointsPointData.vtk'))
+ np.testing.assert_almost_equal(f.points,f.point_data['DisXY'])
+
+ np.testing.assert_almost_equal(f.xp_grid,[0,20,40])
+ np.testing.assert_almost_equal(f.point_data_grid['DisXY'][:,0,0,0],[0,20,40])
+
+
+if __name__ == '__main__':
+# Test().test_000_debug()
+ unittest.main()
diff --git a/weio/tests/test_yaml.py b/weio/tests/test_yaml.py
new file mode 100644
index 0000000..f3013c8
--- /dev/null
+++ b/weio/tests/test_yaml.py
@@ -0,0 +1,107 @@
+import unittest
+import numpy as np
+import os as os
+from weio.mini_yaml import *
+
+
+class Test(unittest.TestCase):
+ def test_scalar(self):
+ # Scalar integer
+ D = yaml_read(text='k: 10 # comment')
+ self.assertDictEqual(D, {'k':10})
+ self.assertIsInstance(D['k'], int)
+ # Scalar float
+ D = yaml_read(text='k: 10.# comment')
+ self.assertDictEqual(D, {'k':10})
+ self.assertIsInstance(D['k'], float)
+ # Scalar string
+ D = yaml_read(text='k:s# comment')
+ self.assertDictEqual(D, {'k':'s'})
+ self.assertIsInstance(D['k'], str)
+
+ def test_lists(self):
+ # list integer
+ D = yaml_read(text='k:[ 10 ,] # comment')
+ self.assertDictEqual(D, {'k':[10]})
+ self.assertTrue(np.issubdtype(D['k'][0], np.integer))
+ #self.assertIsInstance(D['k'][0], np.int32)
+ # list float
+ D = yaml_read(text='k: [ 10., ]# comment')
+ self.assertDictEqual(D, {'k':[10]})
+ self.assertIsInstance(D['k'][0], float)
+ # list string
+ D = yaml_read(text='k: [ s ,] #comment')
+ self.assertDictEqual(D, {'k': ['s']})
+ self.assertIsInstance(D['k'][0], str)
+ # empty
+ D = yaml_read(text='k:[ ] # comment')
+ self.assertIsInstance(D['k'], np.ndarray)
+ self.assertTrue(len(D['k'])==0)
+
+ # list integers
+ D = yaml_read(text='k: [ 10, 20, 30,] # comment')
+ np.testing.assert_array_equal(D['k'], [10,20,30])
+ self.assertTrue(np.issubdtype(D['k'].dtype, np.integer))
+ # list float
+ D = yaml_read(text='k: [ 10., 20.5, 30,] # comment')
+ np.testing.assert_array_equal(D['k'], [10,20.5,30])
+ self.assertTrue(np.issubdtype(D['k'].dtype, float))
+ # list str
+ D = yaml_read(text='k: [ a , b , c,] # comment')
+ np.testing.assert_array_equal(D['k'], ['a','b','c'])
+ self.assertTrue(np.issubdtype(D['k'].dtype, str))
+
+ # list mixed, for now all as strings
+ D = yaml_read(text='k: [ 1 , b , 0,] # comment')
+ np.testing.assert_array_equal(D['k'], ['1','b','0'])
+ self.assertTrue(np.issubdtype(D['k'].dtype, str))
+
+ def test_arrays(self):
+ # Empty array
+ D = yaml_read(text="""k: # comment
+ - [ ] """)
+ self.assertIsInstance(D['k'], np.ndarray)
+ self.assertEqual(D['k'].shape, (1,0)) # that's a choice...
+
+ # Float array
+ D = yaml_read(text="""k:# comment
+ - [ 1, 3.5 ]# comment """)
+ self.assertIsInstance(D['k'], np.ndarray)
+ self.assertEqual(D['k'].shape, (1,2))
+ self.assertTrue(np.issubdtype(D['k'].dtype, float))
+ np.testing.assert_array_equal(D['k'], [[1,3.5]])
+
+ # int array
+ D = yaml_read(text="""k:# comment
+ - [ 1 , ]# comment
+ - [2]
+
+ """)
+ self.assertIsInstance(D['k'], np.ndarray)
+ self.assertEqual(D['k'].shape, (2,1))
+ self.assertTrue(np.issubdtype(D['k'].dtype, np.integer))
+ np.testing.assert_array_equal(D['k'], [[1],[2]])
+
+ # string array
+ D = yaml_read(text="""k:# comment
+ - [ a , b ,c ]
+ - [d, e ,f , ]
+ # comment
+ """)
+ self.assertIsInstance(D['k'], np.ndarray)
+ self.assertEqual(D['k'].shape, (2,3))
+ self.assertTrue(np.issubdtype(D['k'].dtype, str))
+ np.testing.assert_array_equal(D['k'], [['a','b','c'],['d','e','f']])
+
+ # Mixed array
+ D = yaml_read(text="""k:# comment
+ - [ a, 3.5 ]# comment """)
+ self.assertIsInstance(D['k'], np.ndarray)
+ self.assertEqual(D['k'].shape, (1,2))
+ self.assertTrue(np.issubdtype(D['k'].dtype, str))
+ np.testing.assert_array_equal(D['k'], [['a','3']])
+
+
+if __name__=='__main__':
+ #Test().test_arrays()
+ unittest.main()
diff --git a/weio/tools/graph.py b/weio/tools/graph.py
index e924ef1..ec98297 100644
--- a/weio/tools/graph.py
+++ b/weio/tools/graph.py
@@ -1,672 +1,689 @@
-"""
-Basics Classes for a "geometrical" graph model:
- - nodes have a position (x,y,z), and some data (taken from a list of properties)
- - elements (links) connect nodes, they contain some data (taken from a list of properties)
-
-An ordering of Elements, Nodes, and Properties is present, but whenever possible,
-the "ID" is used to identify them, instead of their index.
-
-
-Nodes:
- Node.ID: unique ID (int) of the node. IDs never change.
- Node.x,y,z: coordinate of the nodes
- Node.data : dictionary of data stored at the node
-
-Elements:
- Elem.ID: unique ID (int) of the element. IDs never change.
- Elem.nodeIDs: list of node IDs making up the element
- Elem.nodes : list of nodes making the element (by reference) # NOTE: this has cross reference!
- Elem.nodeProps : properties # Nodal properties. NOTE: cannot be transfered to node because of how SubDyn handles it..
- Elem.data : dictionary of data stored at the element
- # Optional
- Elem.propset: string referring to the property set in the dictionary of properties
- Elem.propIDs: IDs used for the properties of this element at each node
-
-NodePropertySets: dictionary of NodeProperties
- Node Property:
- NProp.ID: unique ID of the node proprety
- NProp.data: dictionary of data
-
-
-ElemPropertySets: dictionary of ElemProperties
-
-"""
-
-import numpy as np
-import pandas as pd
-
-
-# --------------------------------------------------------------------------------}
-# --- Node
-# --------------------------------------------------------------------------------{
-class Node(object):
- def __init__(self, ID, x, y, z=0, **kwargs):
- self.ID = int(ID)
- self.x = x
- self.y = y
- self.z = z
- self.data = kwargs
-
- def setData(self, data_dict):
- """ set or add data"""
- for k,v in data_dict.items():
- #if k in self.data.keys():
- # print('Warning overriding key {} for node {}'.format(k,self.ID))
- self.data[k]=v
-
- def __repr__(self):
- s=' x:{:7.2f} y:{:7.2f} z:{:7.2f} {:}'.format(self.ID, self.x, self.y, self.z, self.data)
- return s
-
-# --------------------------------------------------------------------------------}
-# --- Properties
-# --------------------------------------------------------------------------------{
-class Property(dict):
- def __init__(self, ID, data=None, **kwargs):
- """
- data is a dictionary
- """
- dict.__init__(self)
- self.ID= int(ID)
- self.update(kwargs)
- if data is not None:
- self.update(data)
-
- @property
- def data(self):
- return {k:v for k,v in self.items() if k!='ID'}
-
- def __repr__(self):
- s=' {:}'.format(self.ID, self.data)
- return s
-
-class NodeProperty(Property):
- def __init__(self, ID, data=None, **kwargs):
- Property.__init__(self, ID, data, **kwargs)
- def __repr__(self):
- s=' {:}'.format(self.ID, self.data)
- return s
-
-class ElemProperty(Property):
- def __init__(self, ID, data=None, **kwargs):
- Property.__init__(self, ID, data, **kwargs)
- def __repr__(self):
- s=' {:}'.format(self.ID, self.data)
- return s
-
-
-# --------------------------------------------------------------------------------}
-# --- Elements
-# --------------------------------------------------------------------------------{
-class Element(dict):
- def __init__(self, ID, nodeIDs, nodes=None, propset=None, propIDs=None, properties=None, **kwargs):
- """
-
- """
- self.ID = int(ID)
- self.nodeIDs = nodeIDs
- self.propset = propset
- self.propIDs = propIDs
- self.data = kwargs # Nodal data
- self.nodes = nodes # Typically a trigger based on nodeIDs
- self.nodeProps= properties # Typically a trigger based on propIDs. Otherwise list of dictionaries
- if (self.propIDs is not None) and (self.propset is None):
- raise Exception('`propset` should be provided if `propIDs` are provided')
- if (self.propIDs is not None) and (self.propset is not None) and properties is not None:
- raise Exception('When providing `propset` & `propIDs`, properties should not be provided')
- if nodes is not None:
- if len(nodes)!=len(nodeIDs):
- raise Exception('List of nodes has different length than list of nodeIDs')
- for i, (ID,n) in enumerate(zip(nodeIDs,nodes)):
- if n.ID!=ID:
- raise Exception('Node ID do not match {}/={} for node index {}'.format(n.ID,ID,i))
-
- @property
- def length(self):
- n1=self.nodes[0]
- n2=self.nodes[1]
- return np.sqrt((n1.x-n2.x)**2+(n1.y-n2.y)**2+(n1.z-n2.z)**2)
-
- def __repr__(self):
- s=' NodeIDs: {} {}'.format(self.ID, self.nodeIDs, self.data)
- if self.propIDs is not None:
- s+=' {'+'propIDs:{} propset:{}'.format(self.propIDs, self.propset)+'}'
- if self.nodes is not None:
- s+=' l={:.2f}'.format(self.length)
- return s
-
-
-# --------------------------------------------------------------------------------}
-# --- Mode
-# --------------------------------------------------------------------------------{
-class Mode(dict):
- def __init__(self, data, name, freq=1, **kwargs):
- dict.__init__(self)
-
- self['name']=name
- self['freq']=freq
- self['data']=data # displacements nNodes x 3 assuming a given sorting of nodes
-
- def __repr__(self):
- s=' name:{:4s} freq:{:} '.format(self['name'], self['freq'])
- return s
-
- def reSort(self,I):
- self['data']=self['data'][I,:]
-
-# --------------------------------------------------------------------------------}
-# --- Graph
-# --------------------------------------------------------------------------------{
-class GraphModel(object):
- def __init__(self, Elements=None, Nodes=None, NodePropertySets=None, ElemPropertySets=None, MiscPropertySets=None ):
- self.Elements = Elements if Elements is not None else []
- self.Nodes = Nodes if Nodes is not None else []
- self.NodePropertySets = NodePropertySets if NodePropertySets is not None else {}
- self.ElemPropertySets = ElemPropertySets if ElemPropertySets is not None else {}
- self.MiscPropertySets = MiscPropertySets if MiscPropertySets is not None else {}
- # Dynamics
- self.Modes = []
- self.Motions = []
- # Optimization variables
- self._nodeIDs2Elements = {} # dictionary with key NodeID and value list of ElementID
- self._nodeIDs2Elements = {} # dictionary with key NodeID and value list of elements
- self._elementIDs2NodeIDs = {} # dictionary with key ElemID and value list of nodes IDs
- self._connectivity =[]#
-
- def addNode(self,node):
- self.Nodes.append(node)
-
- def addElement(self,elem):
- # Giving nodes to element if these were not provided
- elem.nodes=[self.getNode(i) for i in elem.nodeIDs]
- # Giving props to element if these were not provided
- if elem.propIDs is not None:
- elem.nodeProps=[self.getNodeProperty(elem.propset, i) for i in elem.propIDs]
- self.Elements.append(elem)
-
- # --- Getters
- def getNode(self, nodeID):
- for n in self.Nodes:
- if n.ID==nodeID:
- return n
- raise KeyError('NodeID {} not found in Nodes'.format(nodeID))
-
- def getElement(self, elemID):
- for e in self.Elements:
- if e.ID==elemID:
- return e
- raise KeyError('ElemID {} not found in Elements'.format(elemID))
-
- def getNodeProperty(self, setname, propID):
- for p in self.NodePropertySets[setname]:
- if p.ID==propID:
- return p
- raise KeyError('PropID {} not found for Node propset {}'.format(propID,setname))
-
- def getElementProperty(self, setname, propID):
- for p in self.ElemPropertySets[setname]:
- if p.ID==propID:
- return p
- raise KeyError('PropID {} not found for Element propset {}'.format(propID,setname))
-
- def getMiscProperty(self, setname, propID):
- for p in self.MiscPropertySets[setname]:
- if p.ID==propID:
- return p
- raise KeyError('PropID {} not found for Misc propset {}'.format(propID,setname))
-
- # ---
- @property
- def nodeIDs2ElementIDs(self):
- """ Return list of elements IDs connected to each node"""
- if len(self._nodeIDs2ElementIDs) == 0:
- # Compute list of connected elements for each node
- self._nodeIDs2ElementIDs=dict()
- for i,n in enumerate(self.Nodes):
- self._nodeIDs2ElementIDs[n.ID] = [e.ID for e in self.Elements if n.ID in e.nodeIDs]
- return self._nodeIDs2ElementIDs
-
- @property
- def nodeIDs2Elements(self):
- """ Return list of elements connected to each node"""
- if len(self._nodeIDs2Elements) == 0:
- # Compute list of connected elements for each node
- self._nodeIDs2Elements
- for i,n in enumerate(self.Nodes):
- self._nodeIDs2Elements[n.ID] = [e for e in self.Elements if n.ID in e.nodeIDs]
- return self._nodeIDs2Elements
-
-
- @property
- def elementIDs2NodeIDs(self):
- """ returns """
- if len(self._elementIDs2NodeIDs) ==0:
- self._elementIDs2NodeIDs =dict()
- for e in self.Elements:
- self._elementIDs2NodeIDs[e.ID] = [n.ID for n in e.nodes]
- return self._elementIDs2NodeIDs
-
-
- @property
- def connectivity(self):
- """ returns connectivity, assuming points are indexed starting at 0
- NOTE: this is basically element2Nodes but reindexed
- """
- if len(self._connectivity) ==0:
- self._connectivity = [[self.Nodes.index(n) for n in e.nodes] for e in self.Elements]
- return self._connectivity
-
-
- # --- Handling of (element/material) Properties
- def addElementPropertySet(self, setname):
- self.ElemPropertySets[setname]= []
-
- def addNodePropertySet(self, setname):
- self.NodePropertySets[setname]= []
-
- def addMiscPropertySet(self, setname):
- self.MiscPropertySets[setname]= []
-
- def addNodeProperty(self, setname, prop):
- if not isinstance(prop, NodeProperty):
- print(type(prop))
- raise Exception('Property needs to inherit from NodeProperty')
- self.PropertySets[setname].append(prop)
-
- def addNodeProperty(self, setname, prop):
- if not isinstance(prop, NodeProperty):
- print(type(prop))
- raise Exception('Property needs to inherit from NodeProperty')
- self.NodePropertySets[setname].append(prop)
-
- def addElementProperty(self, setname, prop):
- if not isinstance(prop, ElemProperty):
- print(type(prop))
- raise Exception('Property needs to inherit from ElementProperty')
- self.ElemPropertySets[setname].append(prop)
-
- def addMiscProperty(self, setname, prop):
- if not isinstance(prop, ElemProperty):
- print(type(prop))
- raise Exception('Property needs to inherit from Property')
- self.MiscPropertySets[setname].append(prop)
-
- # --- Data and node and element prop setters
- def setElementNodalProp(self, elem, propset, propIDs):
- """
- Set Nodal Properties to each node of an element
- """
- for node, pID in zip(elem.nodes, propIDs):
- node.setData(self.getNodeProperty(propset, pID).data)
-
- def setNodeNodalProp(self, node, propset, propID):
- """
- Set Nodal Properties to a node
- """
- node.setData(self.getNodeProperty(propset, propID).data)
-
- def setNodalData(self, nodeID, **data_dict):
- self.getNode(nodeID).setData(data_dict)
-
- def __repr__(self):
- s='<{} object> with keys:\n'.format(type(self).__name__)
- s+='- Nodes ({}):\n'.format(len(self.Nodes))
- s+='\n'.join(str(n) for n in self.Nodes)
- s+='\n- Elements ({}):\n'.format(len(self.Elements))
- s+='\n'.join(str(n) for n in self.Elements)
- s+='\n- NodePropertySets ({}):'.format(len(self.NodePropertySets))
- for k,v in self.NodePropertySets.items():
- s+='\n> {} ({}):\n'.format(k, len(v))
- s+='\n'.join(str(p) for p in v)
- s+='\n- ElementPropertySets ({}):'.format(len(self.ElemPropertySets))
- for k,v in self.ElemPropertySets.items():
- s+='\n> {} ({}):\n'.format(k, len(v))
- s+='\n'.join(str(p) for p in v)
- s+='\n- MiscPropertySets ({}):'.format(len(self.MiscPropertySets))
- for k,v in self.MiscPropertySets.items():
- s+='\n> {} ({}):\n'.format(k, len(v))
- s+='\n'.join(str(p) for p in v)
- s+='\n- Modes ({}):\n'.format(len(self.Modes))
- s+='\n'.join(str(m) for m in self.Modes)
- s+='\n- Motions ({}):'.format(len(self.Motions))
- for m in self.Motions:
- s+='\n> {}\n'.format({k:v for k,v in m.items() if not isintance(v,np.ndarray)})
- return s
-
- # --------------------------------------------------------------------------------}
- # --- Geometrical properties
- # --------------------------------------------------------------------------------{
- @property
- def extent(self):
- xmax=np.max([node.x for node in self.Nodes])
- ymax=np.max([node.y for node in self.Nodes])
- zmax=np.max([node.z for node in self.Nodes])
- xmin=np.min([node.x for node in self.Nodes])
- ymin=np.min([node.y for node in self.Nodes])
- zmin=np.min([node.z for node in self.Nodes])
- return [xmin,ymin,zmin],[xmax,ymax,zmax],[xmax-xmin,ymax-ymin,zmax-zmin]
-
- @property
- def maxDimension(self):
- _,_,D=self.extent
- return np.max(D)
-
- @property
- def points(self):
- nNodes = len(self.Nodes)
- Points = np.zeros((nNodes,3))
- for i,n in enumerate(self.Nodes):
- Points[i,:]=(n.x, n.y, n.z)
- return Points
-
- def toLines(self, output='coord'):
- if output=='coord':
- lines = np.zeros((len(self.Elements), 2, 3)) #
- for ie, e in enumerate(self.Elements):
- n1=e.nodes[0]
- n2=e.nodes[-1]
- lines[ie, 0, : ] = (n1.x, n1.y, n1.z)
- lines[ie, 1, : ] = (n2.x, n2.y, n2.z)
- elif output=='lines3d':
- import mpl_toolkits.mplot3d as plt3d
- lines=[]
- for ie, e in enumerate(self.Elements):
- n1=e.nodes[0]
- n2=e.nodes[-1]
- line = plt3d.art3d.Line3D((n1.x,n2.x), (n1.y,n2.y), (n1.z,n2.z))
- lines.append(line)
- else:
- raise NotImplementedError()
-
- return lines
-
- # --------------------------------------------------------------------------------}
- # --- Change of connectivity
- # --------------------------------------------------------------------------------{
- def connecticityHasChanged(self):
- self._nodeIDs2ElementIDs = dict()
- self._nodeIDs2Elements = dict()
- self._elementIDs2NodeIDs = dict()
- self._connectivity=[]
-
- def updateConnectivity(self):
- for e in self.Elements:
- e.nodes=[self.getNode(i) for i in e.nodeIDs]
-
- for e in self.Elements:
- e.nodeProps = [self.getNodeProperty(e.propset, ID) for ID in e.propIDs]
-
- # Potentially call nodeIDs2ElementIDs etc
-
-
- def _divideElement(self, elemID, nPerElement, maxElemId, keysNotToCopy=[]):
- """ divide a given element by nPerElement (add nodes and elements to graph) """
- if len(self.Modes)>0:
- raise Exception('Cannot divide graph when mode data is present')
- if len(self.Motions)>0:
- raise Exception('Cannot divide graph when motion data is present')
-
-
- maxNodeId=np.max([n.ID for n in self.Nodes])
- e = self.getElement(elemID)
- newElems = []
- if len(e.nodes)==2:
- n1=e.nodes[0]
- n2=e.nodes[1]
- subNodes=[n1]
- for iSub in range(1,nPerElement):
- maxNodeId += 1
- #data_dict = n1.data.copy()
- data_dict = dict()
- fact = float(iSub)/nPerElement
- # Interpolating position
- x = n1.x*(1-fact)+n2.x*fact
- y = n1.y*(1-fact)+n2.y*fact
- z = n1.z*(1-fact)+n2.z*fact
- # Interpolating data (only if floats)
- for k,v in n1.data.items():
- if k not in keysNotToCopy:
- try:
- data_dict[k] = n1.data[k]*(1-fact) + n2.data[k]*fact
- except:
- data_dict[k] = n1.data[k]
- ni = Node(maxNodeId, x, y, z, **data_dict)
- subNodes.append(ni)
- self.addNode(ni)
- subNodes+=[n2]
- e.nodes =subNodes[0:2]
- e.nodeIDs=[e.ID for e in e.nodes]
- for i in range(1,nPerElement):
- maxElemId+=1
- elem_dict = e.data.copy()
- # Creating extra properties if necessary
- if e.propIDs is not None:
- if all(e.propIDs==e.propIDs[0]):
- # No need to create a new property
- propIDs=e.propIDs
- propset=e.propset
- else:
- raise NotImplementedError('Division of element with different properties on both ends. TODO add new property.')
- elem= Element(maxElemId, [subNodes[i].ID, subNodes[i+1].ID], propset=propset, propIDs=propIDs, **elem_dict )
- newElems.append(elem)
- return newElems
-
-
- def sortNodesBy(self,key):
- """ Sort nodes, will affect the connectivity, but node IDs remain the same"""
-
- # TODO, that's quite doable
- if len(self.Modes)>0:
- raise Exception('Cannot sort nodes when mode data is present')
- if len(self.Motions)>0:
- raise Exception('Cannot sort nodes when motion data is present')
-
- nNodes = len(self.Nodes)
- if key=='x':
- values=[n.x for n in self.Nodes]
- elif key=='y':
- values=[n.y for n in self.Nodes]
- elif key=='z':
- values=[n.z for n in self.Nodes]
- elif key=='ID':
- values=[n.ID for n in self.Nodes]
- else:
- values=[n[key] for n in self.Nodes]
- I= np.argsort(values)
- self.Nodes=[self.Nodes[i] for i in I]
-
- # Trigger, remove precomputed values related to connectivity:
- self.connecticityHasChanged()
-
- return self
-
- def divideElements(self, nPerElement, excludeDataKey='', excludeDataList=[], method='append', keysNotToCopy=[]):
- """ divide all elements by nPerElement (add nodes and elements to graph)
-
- - excludeDataKey: is provided, will exclude elements such that e.data[key] in `excludeDataList`
-
- - method: append or insert
-
- - keysNotToCopy: when duplicating node and element data, make sure not to duplicate data with these keys
- For instance if a node that has a boundary condition, it should not be passed to the
- node that is created when dividing an element.
-
- Example:
- to avoid dividing elements of `Type` 'Cable' or `Rigid`, call as follows:
- self.divideElements(3, excludeDataKey='Type', excludeDataList=['Cable','Rigid'] )
-
- """
- maxNodeId=np.max([n.ID for n in self.Nodes])
- maxElemId=np.max([e.ID for e in self.Elements])
-
- if nPerElement<=0:
- raise Exception('nPerElement should be more than 0')
-
- newElements=[]
- for ie in np.arange(len(self.Elements)): # cannot enumerate since length increases
- elemID = self.Elements[ie].ID
- if method=='insert':
- newElements+=[self.getElement(elemID)] # newElements contains
- if (len(excludeDataKey)>0 and self.Elements[ie].data[excludeDataKey] not in excludeDataList) or len(excludeDataKey)==0:
- elems = self._divideElement(elemID, nPerElement, maxElemId, keysNotToCopy)
- maxElemId+=len(elems)
- newElements+=elems
- else:
- print('Not dividing element with ID {}, based on key `{}` with value `{}`'.format(elemID, excludeDataKey,self.Elements[ie].data[excludeDataKey]))
- # Adding elements at the end
- if method=='append':
- pass
- elif method=='insert':
- self.Elements=[] # We clear all elements
- else:
- raise NotImplementedError('Element Insertions')
-
- for e in newElements:
- self.addElement(e)
-
- # Trigger, remove precomputed values related to connectivity:
- self.connecticityHasChanged()
-
- return self
-
- # --------------------------------------------------------------------------------}
- # --- Dynamics
- # --------------------------------------------------------------------------------{
- def addMode(self,displ,name=None,freq=1):
- if name is None:
- name='Mode '+str(len(self.Modes))
- mode = Mode(data=displ, name=name, freq=freq)
- self.Modes.append(mode)
-
-
- # --------------------------------------------------------------------------------}
- # --- Ouputs / converters
- # --------------------------------------------------------------------------------{
- def nodalDataFrame(self, sortBy=None):
- """ return a DataFrame of all the nodal data """
- data=dict()
- nNodes=len(self.Nodes)
- for i,n in enumerate(self.Nodes):
- if i==0:
- data['ID'] = np.zeros(nNodes).astype(int)
- data['x'] = np.zeros(nNodes)
- data['y'] = np.zeros(nNodes)
- data['z'] = np.zeros(nNodes)
-
- data['ID'][i] = n.ID
- data['x'][i] = n.x
- data['y'][i] = n.y
- data['z'][i] = n.z
- for k,v in n.data.items():
- if k not in data:
- data[k] = np.zeros(nNodes)
- try:
- data[k][i]=v
- except:
- pass
- df = pd.DataFrame(data)
- # Sorting
- if sortBy is not None:
- df.sort_values([sortBy],inplace=True,ascending=True)
- df.reset_index(drop=True,inplace=True)
- return df
-
-
- def toJSON(self,outfile=None):
- d=dict();
- Points=self.points
- d['Connectivity'] = self.connectivity
- d['Nodes'] = Points.tolist()
-
- d['ElemProps']=list()
- for iElem,elem in enumerate(self.Elements):
- Shape = elem.data['shape'] if 'shape' in elem.data.keys() else 'cylinder'
- Type = elem.data['Type'] if 'Type' in elem.data.keys() else 1
- try:
- Diam = elem.D
- except:
- Diam = elem.data['D'] if 'D' in elem.data.keys() else 1
- if Shape=='cylinder':
- d['ElemProps'].append({'shape':'cylinder','type':Type, 'Diam':Diam})
- else:
- raise NotImplementedError()
-
-
- d['Modes']=[
- {
- 'name': self.Modes[iMode]['name'],
- 'omega':self.Modes[iMode]['freq']*2*np.pi, #in [rad/s]
- 'Displ':self.Modes[iMode]['data'].tolist()
- } for iMode,mode in enumerate(self.Modes)]
- d['groundLevel']=np.min(Points[:,2]) # TODO
-
- if outfile is not None:
- import json
- from io import open
- jsonFile=outfile
- with open(jsonFile, 'w', encoding='utf-8') as f:
- #f.write(to_json(d))
- try:
- #f.write(unicode(json.dumps(d, ensure_ascii=False))) #, indent=2)
- #f.write(json.dumps(d, ensure_ascii=False)) #, indent=2)
- f.write(json.dumps(d, ensure_ascii=False))
- except:
- print('>>> FAILED')
- json.dump(d, f, indent=0)
- return d
-
-#
-
-
-
-INDENT = 3
-SPACE = " "
-NEWLINE = "\n"
-# Changed basestring to str, and dict uses items() instead of iteritems().
-
-def to_json(o, level=0):
- ret = ""
- if isinstance(o, dict):
- if level==0:
- ret += "{" + NEWLINE
- comma = ""
- for k, v in o.items():
- ret += comma
- comma = ",\n"
- ret += SPACE * INDENT * (level + 1)
- ret += '"' + str(k) + '":' + SPACE
- ret += to_json(v, level + 1)
- ret += NEWLINE + SPACE * INDENT * level + "}"
- else:
- ret += "{"
- comma = ""
- for k, v in o.items():
- ret += comma
- comma = ",\n"
- ret += SPACE
- ret += '"' + str(k) + '":' + SPACE
- ret += to_json(v, level + 1)
- ret += "}"
-
- elif isinstance(o, str):
- ret += '"' + o + '"'
- elif isinstance(o, list):
- ret += "[" + ",".join([to_json(e, level + 1) for e in o]) + "]"
- # Tuples are interpreted as lists
- elif isinstance(o, tuple):
- ret += "[" + ",".join(to_json(e, level + 1) for e in o) + "]"
- elif isinstance(o, bool):
- ret += "true" if o else "false"
- elif isinstance(o, int):
- ret += str(o)
- elif isinstance(o, float):
- ret += '%.7g' % o
- elif isinstance(o, numpy.ndarray) and numpy.issubdtype(o.dtype, numpy.integer):
- ret += "[" + ','.join(map(str, o.flatten().tolist())) + "]"
- elif isinstance(o, numpy.ndarray) and numpy.issubdtype(o.dtype, numpy.inexact):
- ret += "[" + ','.join(map(lambda x: '%.7g' % x, o.flatten().tolist())) + "]"
- elif o is None:
- ret += 'null'
- else:
- raise TypeError("Unknown type '%s' for json serialization" % str(type(o)))
- return ret
+"""
+Basics Classes for a "geometrical" graph model:
+ - nodes have a position (x,y,z), and some data (taken from a list of properties)
+ - elements (links) connect nodes, they contain some data (taken from a list of properties)
+
+An ordering of Elements, Nodes, and Properties is present, but whenever possible,
+the "ID" is used to identify them, instead of their index.
+
+
+Nodes:
+ Node.ID: unique ID (int) of the node. IDs never change.
+ Node.x,y,z: coordinate of the nodes
+ Node.data : dictionary of data stored at the node
+
+Elements:
+ Elem.ID: unique ID (int) of the element. IDs never change.
+ Elem.nodeIDs: list of node IDs making up the element
+ Elem.nodes : list of nodes making the element (by reference) # NOTE: this has cross reference!
+ Elem.nodeProps : properties # Nodal properties. NOTE: cannot be transfered to node because of how SubDyn handles it..
+ Elem.data : dictionary of data stored at the element
+ # Optional
+ Elem.propset: string referring to the property set in the dictionary of properties
+ Elem.propIDs: IDs used for the properties of this element at each node
+
+NodePropertySets: dictionary of NodeProperties
+ Node Property:
+ NProp.ID: unique ID of the node proprety
+ NProp.data: dictionary of data
+
+
+ElemPropertySets: dictionary of ElemProperties
+
+"""
+
+import numpy as np
+import pandas as pd
+
+
+# --------------------------------------------------------------------------------}
+# --- Node
+# --------------------------------------------------------------------------------{
+class Node(object):
+ def __init__(self, ID, x, y, z=0, **kwargs):
+ self.ID = int(ID)
+ self.x = x
+ self.y = y
+ self.z = z
+ self.data = kwargs
+
+ def setData(self, data_dict):
+ """ set or add data"""
+ for k,v in data_dict.items():
+ #if k in self.data.keys():
+ # print('Warning overriding key {} for node {}'.format(k,self.ID))
+ self.data[k]=v
+
+ def __repr__(self):
+ s=' x:{:7.2f} y:{:7.2f} z:{:7.2f} {:}'.format(self.ID, self.x, self.y, self.z, self.data)
+ return s
+
+# --------------------------------------------------------------------------------}
+# --- Properties
+# --------------------------------------------------------------------------------{
+class Property(dict):
+ def __init__(self, ID, data=None, **kwargs):
+ """
+ data is a dictionary
+ """
+ dict.__init__(self)
+ self.ID= int(ID)
+ self.update(kwargs)
+ if data is not None:
+ self.update(data)
+
+ @property
+ def data(self):
+ return {k:v for k,v in self.items() if k!='ID'}
+
+ def __repr__(self):
+ s=' {:}'.format(self.ID, self.data)
+ return s
+
+class NodeProperty(Property):
+ def __init__(self, ID, data=None, **kwargs):
+ Property.__init__(self, ID, data, **kwargs)
+ def __repr__(self):
+ s=' {:}'.format(self.ID, self.data)
+ return s
+
+class ElemProperty(Property):
+ def __init__(self, ID, data=None, **kwargs):
+ Property.__init__(self, ID, data, **kwargs)
+ def __repr__(self):
+ s=' {:}'.format(self.ID, self.data)
+ return s
+
+
+# --------------------------------------------------------------------------------}
+# --- Elements
+# --------------------------------------------------------------------------------{
+class Element(dict):
+ def __init__(self, ID, nodeIDs, nodes=None, propset=None, propIDs=None, properties=None, **kwargs):
+ """
+
+ """
+ self.ID = int(ID)
+ self.nodeIDs = nodeIDs
+ self.propset = propset
+ self.propIDs = propIDs
+ self.data = kwargs # Nodal data
+ self.nodes = nodes # Typically a trigger based on nodeIDs
+ self.nodeProps= properties # Typically a trigger based on propIDs. Otherwise list of dictionaries
+ if (self.propIDs is not None) and (self.propset is None):
+ raise Exception('`propset` should be provided if `propIDs` are provided')
+ if (self.propIDs is not None) and (self.propset is not None) and properties is not None:
+ raise Exception('When providing `propset` & `propIDs`, properties should not be provided')
+ if nodes is not None:
+ if len(nodes)!=len(nodeIDs):
+ raise Exception('List of nodes has different length than list of nodeIDs')
+ for i, (ID,n) in enumerate(zip(nodeIDs,nodes)):
+ if n.ID!=ID:
+ raise Exception('Node ID do not match {}/={} for node index {}'.format(n.ID,ID,i))
+
+ @property
+ def length(self):
+ n1=self.nodes[0]
+ n2=self.nodes[1]
+ return np.sqrt((n1.x-n2.x)**2+(n1.y-n2.y)**2+(n1.z-n2.z)**2)
+
+ def setData(self, data_dict):
+ """ set or add data"""
+ for k,v in data_dict.items():
+ #if k in self.data.keys():
+ # print('Warning overriding key {} for node {}'.format(k,self.ID))
+ self.data[k]=v
+
+ def __repr__(self):
+ s=' NodeIDs: {} {}'.format(self.ID, self.nodeIDs, self.data)
+ if self.propIDs is not None:
+ s+=' {'+'propIDs:{} propset:{}'.format(self.propIDs, self.propset)+'}'
+ if self.nodes is not None:
+ s+=' l={:.2f}'.format(self.length)
+ return s
+
+
+# --------------------------------------------------------------------------------}
+# --- Mode
+# --------------------------------------------------------------------------------{
+class Mode(dict):
+ def __init__(self, data, name, freq=1, **kwargs):
+ dict.__init__(self)
+
+ self['name']=name
+ self['freq']=freq
+ self['data']=data # displacements nNodes x 3 assuming a given sorting of nodes
+
+ def __repr__(self):
+ s=' name:{:4s} freq:{:} '.format(self['name'], self['freq'])
+ return s
+
+ def reSort(self,I):
+ self['data']=self['data'][I,:]
+
+# --------------------------------------------------------------------------------}
+# --- Graph
+# --------------------------------------------------------------------------------{
+class GraphModel(object):
+ def __init__(self, Elements=None, Nodes=None, NodePropertySets=None, ElemPropertySets=None, MiscPropertySets=None ):
+ self.Elements = Elements if Elements is not None else []
+ self.Nodes = Nodes if Nodes is not None else []
+ self.NodePropertySets = NodePropertySets if NodePropertySets is not None else {}
+ self.ElemPropertySets = ElemPropertySets if ElemPropertySets is not None else {}
+ self.MiscPropertySets = MiscPropertySets if MiscPropertySets is not None else {}
+ # Dynamics
+ self.Modes = []
+ self.Motions = []
+ # Optimization variables
+ self._nodeIDs2Elements = {} # dictionary with key NodeID and value list of ElementID
+ self._nodeIDs2Elements = {} # dictionary with key NodeID and value list of elements
+ self._elementIDs2NodeIDs = {} # dictionary with key ElemID and value list of nodes IDs
+ self._connectivity =[]#
+
+ def addNode(self,node):
+ self.Nodes.append(node)
+
+ def addElement(self,elem):
+ # Giving nodes to element if these were not provided
+ elem.nodes=[self.getNode(i) for i in elem.nodeIDs]
+ # Giving props to element if these were not provided
+ if elem.propIDs is not None:
+ elem.nodeProps=[self.getNodeProperty(elem.propset, i) for i in elem.propIDs]
+ self.Elements.append(elem)
+
+ # --- Getters
+ def getNode(self, nodeID):
+ for n in self.Nodes:
+ if n.ID==nodeID:
+ return n
+ raise KeyError('NodeID {} not found in Nodes'.format(nodeID))
+
+ def getElement(self, elemID):
+ for e in self.Elements:
+ if e.ID==elemID:
+ return e
+ raise KeyError('ElemID {} not found in Elements'.format(elemID))
+
+ def getNodeProperty(self, setname, propID):
+ for p in self.NodePropertySets[setname]:
+ if p.ID==propID:
+ return p
+ raise KeyError('PropID {} not found for Node propset {}'.format(propID,setname))
+
+ def getElementProperty(self, setname, propID):
+ for p in self.ElemPropertySets[setname]:
+ if p.ID==propID:
+ return p
+ raise KeyError('PropID {} not found for Element propset {}'.format(propID,setname))
+
+ def getMiscProperty(self, setname, propID):
+ for p in self.MiscPropertySets[setname]:
+ if p.ID==propID:
+ return p
+ raise KeyError('PropID {} not found for Misc propset {}'.format(propID,setname))
+
+ # ---
+ @property
+ def nodeIDs2ElementIDs(self):
+ """ Return list of elements IDs connected to each node"""
+ if len(self._nodeIDs2ElementIDs) == 0:
+ # Compute list of connected elements for each node
+ self._nodeIDs2ElementIDs=dict()
+ for i,n in enumerate(self.Nodes):
+ self._nodeIDs2ElementIDs[n.ID] = [e.ID for e in self.Elements if n.ID in e.nodeIDs]
+ return self._nodeIDs2ElementIDs
+
+ @property
+ def nodeIDs2Elements(self):
+ """ Return list of elements connected to each node"""
+ if len(self._nodeIDs2Elements) == 0:
+ # Compute list of connected elements for each node
+ self._nodeIDs2Elements
+ for i,n in enumerate(self.Nodes):
+ self._nodeIDs2Elements[n.ID] = [e for e in self.Elements if n.ID in e.nodeIDs]
+ return self._nodeIDs2Elements
+
+
+ @property
+ def elementIDs2NodeIDs(self):
+ """ returns """
+ if len(self._elementIDs2NodeIDs) ==0:
+ self._elementIDs2NodeIDs =dict()
+ for e in self.Elements:
+ self._elementIDs2NodeIDs[e.ID] = [n.ID for n in e.nodes]
+ return self._elementIDs2NodeIDs
+
+
+ @property
+ def connectivity(self):
+ """ returns connectivity, assuming points are indexed starting at 0
+ NOTE: this is basically element2Nodes but reindexed
+ """
+ if len(self._connectivity) ==0:
+ self._connectivity = [[self.Nodes.index(n) for n in e.nodes] for e in self.Elements]
+ return self._connectivity
+
+
+ # --- Handling of (element/material) Properties
+ def addElementPropertySet(self, setname):
+ self.ElemPropertySets[setname]= []
+
+ def addNodePropertySet(self, setname):
+ self.NodePropertySets[setname]= []
+
+ def addMiscPropertySet(self, setname):
+ self.MiscPropertySets[setname]= []
+
+ def addNodeProperty(self, setname, prop):
+ if not isinstance(prop, NodeProperty):
+ print(type(prop))
+ raise Exception('Property needs to inherit from NodeProperty')
+ self.PropertySets[setname].append(prop)
+
+ def addNodeProperty(self, setname, prop):
+ if not isinstance(prop, NodeProperty):
+ print(type(prop))
+ raise Exception('Property needs to inherit from NodeProperty')
+ self.NodePropertySets[setname].append(prop)
+
+ def addElementProperty(self, setname, prop):
+ if not isinstance(prop, ElemProperty):
+ print(type(prop))
+ raise Exception('Property needs to inherit from ElementProperty')
+ self.ElemPropertySets[setname].append(prop)
+
+ def addMiscProperty(self, setname, prop):
+ if not isinstance(prop, ElemProperty):
+ print(type(prop))
+ raise Exception('Property needs to inherit from Property')
+ self.MiscPropertySets[setname].append(prop)
+
+ # --- Data and node and element prop setters
+ def setElementNodalProp(self, elem, propset, propIDs):
+ """
+ Set Nodal Properties to each node of an element
+ """
+ for node, pID in zip(elem.nodes, propIDs):
+ node.setData(self.getNodeProperty(propset, pID).data)
+
+ def setElementNodalPropToElem(self, elem):
+ """
+ Set Element Properties to an element
+ TODO: this seems to be a hack. It should be automatic I think...
+ """
+ propset=elem.propset
+ propIDs=elem.propIDs
+ # USING PROPID 0!!!
+ elem.setData(self.getNodeProperty(propset, propIDs[0]).data)
+ # TODO average the two maybe..
+
+ def setNodeNodalProp(self, node, propset, propID):
+ """
+ Set Nodal Properties to a node
+ """
+ node.setData(self.getNodeProperty(propset, propID).data)
+
+ def setNodalData(self, nodeID, **data_dict):
+ self.getNode(nodeID).setData(data_dict)
+
+ def __repr__(self):
+ s='<{} object> with keys:\n'.format(type(self).__name__)
+ s+='- Nodes ({}):\n'.format(len(self.Nodes))
+ s+='\n'.join(str(n) for n in self.Nodes)
+ s+='\n- Elements ({}):\n'.format(len(self.Elements))
+ s+='\n'.join(str(n) for n in self.Elements)
+ s+='\n- NodePropertySets ({}):'.format(len(self.NodePropertySets))
+ for k,v in self.NodePropertySets.items():
+ s+='\n> {} ({}):\n'.format(k, len(v))
+ s+='\n'.join(str(p) for p in v)
+ s+='\n- ElementPropertySets ({}):'.format(len(self.ElemPropertySets))
+ for k,v in self.ElemPropertySets.items():
+ s+='\n> {} ({}):\n'.format(k, len(v))
+ s+='\n'.join(str(p) for p in v)
+ s+='\n- MiscPropertySets ({}):'.format(len(self.MiscPropertySets))
+ for k,v in self.MiscPropertySets.items():
+ s+='\n> {} ({}):\n'.format(k, len(v))
+ s+='\n'.join(str(p) for p in v)
+ s+='\n- Modes ({}):\n'.format(len(self.Modes))
+ s+='\n'.join(str(m) for m in self.Modes)
+ s+='\n- Motions ({}):'.format(len(self.Motions))
+ for m in self.Motions:
+ s+='\n> {}\n'.format({k:v for k,v in m.items() if not isintance(v,np.ndarray)})
+ return s
+
+ # --------------------------------------------------------------------------------}
+ # --- Geometrical properties
+ # --------------------------------------------------------------------------------{
+ @property
+ def extent(self):
+ xmax=np.max([node.x for node in self.Nodes])
+ ymax=np.max([node.y for node in self.Nodes])
+ zmax=np.max([node.z for node in self.Nodes])
+ xmin=np.min([node.x for node in self.Nodes])
+ ymin=np.min([node.y for node in self.Nodes])
+ zmin=np.min([node.z for node in self.Nodes])
+ return [xmin,ymin,zmin],[xmax,ymax,zmax],[xmax-xmin,ymax-ymin,zmax-zmin]
+
+ @property
+ def maxDimension(self):
+ _,_,D=self.extent
+ return np.max(D)
+
+ @property
+ def points(self):
+ nNodes = len(self.Nodes)
+ Points = np.zeros((nNodes,3))
+ for i,n in enumerate(self.Nodes):
+ Points[i,:]=(n.x, n.y, n.z)
+ return Points
+
+ def toLines(self, output='coord'):
+ if output=='coord':
+ lines = np.zeros((len(self.Elements), 2, 3)) #
+ for ie, e in enumerate(self.Elements):
+ n1=e.nodes[0]
+ n2=e.nodes[-1]
+ lines[ie, 0, : ] = (n1.x, n1.y, n1.z)
+ lines[ie, 1, : ] = (n2.x, n2.y, n2.z)
+ elif output=='lines3d':
+ import mpl_toolkits.mplot3d as plt3d
+ lines=[]
+ for ie, e in enumerate(self.Elements):
+ n1=e.nodes[0]
+ n2=e.nodes[-1]
+ line = plt3d.art3d.Line3D((n1.x,n2.x), (n1.y,n2.y), (n1.z,n2.z))
+ lines.append(line)
+ else:
+ raise NotImplementedError()
+
+ return lines
+
+ # --------------------------------------------------------------------------------}
+ # --- Change of connectivity
+ # --------------------------------------------------------------------------------{
+ def connecticityHasChanged(self):
+ self._nodeIDs2ElementIDs = dict()
+ self._nodeIDs2Elements = dict()
+ self._elementIDs2NodeIDs = dict()
+ self._connectivity=[]
+
+ def updateConnectivity(self):
+ for e in self.Elements:
+ e.nodes=[self.getNode(i) for i in e.nodeIDs]
+
+ for e in self.Elements:
+ e.nodeProps = [self.getNodeProperty(e.propset, ID) for ID in e.propIDs]
+
+ # Potentially call nodeIDs2ElementIDs etc
+
+
+ def _divideElement(self, elemID, nPerElement, maxElemId, keysNotToCopy=[]):
+ """ divide a given element by nPerElement (add nodes and elements to graph) """
+ if len(self.Modes)>0:
+ raise Exception('Cannot divide graph when mode data is present')
+ if len(self.Motions)>0:
+ raise Exception('Cannot divide graph when motion data is present')
+
+
+ maxNodeId=np.max([n.ID for n in self.Nodes])
+ e = self.getElement(elemID)
+ newElems = []
+ if len(e.nodes)==2:
+ n1=e.nodes[0]
+ n2=e.nodes[1]
+ subNodes=[n1]
+ for iSub in range(1,nPerElement):
+ maxNodeId += 1
+ #data_dict = n1.data.copy()
+ data_dict = dict()
+ fact = float(iSub)/nPerElement
+ # Interpolating position
+ x = n1.x*(1-fact)+n2.x*fact
+ y = n1.y*(1-fact)+n2.y*fact
+ z = n1.z*(1-fact)+n2.z*fact
+ # Interpolating data (only if floats)
+ for k,v in n1.data.items():
+ if k not in keysNotToCopy:
+ try:
+ data_dict[k] = n1.data[k]*(1-fact) + n2.data[k]*fact
+ except:
+ data_dict[k] = n1.data[k]
+ ni = Node(maxNodeId, x, y, z, **data_dict)
+ subNodes.append(ni)
+ self.addNode(ni)
+ subNodes+=[n2]
+ e.nodes =subNodes[0:2]
+ e.nodeIDs=[e.ID for e in e.nodes]
+ for i in range(1,nPerElement):
+ maxElemId+=1
+ elem_dict = e.data.copy()
+ # Creating extra properties if necessary
+ if e.propIDs is not None:
+ if all(e.propIDs==e.propIDs[0]):
+ # No need to create a new property
+ propIDs=e.propIDs
+ propset=e.propset
+ else:
+ raise NotImplementedError('Division of element with different properties on both ends. TODO add new property.')
+ elem= Element(maxElemId, [subNodes[i].ID, subNodes[i+1].ID], propset=propset, propIDs=propIDs, **elem_dict )
+ newElems.append(elem)
+ return newElems
+
+
+ def sortNodesBy(self,key):
+ """ Sort nodes, will affect the connectivity, but node IDs remain the same"""
+
+ # TODO, that's quite doable
+ if len(self.Modes)>0:
+ raise Exception('Cannot sort nodes when mode data is present')
+ if len(self.Motions)>0:
+ raise Exception('Cannot sort nodes when motion data is present')
+
+ nNodes = len(self.Nodes)
+ if key=='x':
+ values=[n.x for n in self.Nodes]
+ elif key=='y':
+ values=[n.y for n in self.Nodes]
+ elif key=='z':
+ values=[n.z for n in self.Nodes]
+ elif key=='ID':
+ values=[n.ID for n in self.Nodes]
+ else:
+ values=[n[key] for n in self.Nodes]
+ I= np.argsort(values)
+ self.Nodes=[self.Nodes[i] for i in I]
+
+ # Trigger, remove precomputed values related to connectivity:
+ self.connecticityHasChanged()
+
+ return self
+
+ def divideElements(self, nPerElement, excludeDataKey='', excludeDataList=[], method='append', keysNotToCopy=[]):
+ """ divide all elements by nPerElement (add nodes and elements to graph)
+
+ - excludeDataKey: is provided, will exclude elements such that e.data[key] in `excludeDataList`
+
+ - method: append or insert
+
+ - keysNotToCopy: when duplicating node and element data, make sure not to duplicate data with these keys
+ For instance if a node that has a boundary condition, it should not be passed to the
+ node that is created when dividing an element.
+
+ Example:
+ to avoid dividing elements of `Type` 'Cable' or `Rigid`, call as follows:
+ self.divideElements(3, excludeDataKey='Type', excludeDataList=['Cable','Rigid'] )
+
+ """
+ maxNodeId=np.max([n.ID for n in self.Nodes])
+ maxElemId=np.max([e.ID for e in self.Elements])
+
+ if nPerElement<=0:
+ raise Exception('nPerElement should be more than 0')
+
+ newElements=[]
+ for ie in np.arange(len(self.Elements)): # cannot enumerate since length increases
+ elemID = self.Elements[ie].ID
+ if method=='insert':
+ newElements+=[self.getElement(elemID)] # newElements contains
+ if (len(excludeDataKey)>0 and self.Elements[ie].data[excludeDataKey] not in excludeDataList) or len(excludeDataKey)==0:
+ elems = self._divideElement(elemID, nPerElement, maxElemId, keysNotToCopy)
+ maxElemId+=len(elems)
+ newElements+=elems
+ else:
+ print('Not dividing element with ID {}, based on key `{}` with value `{}`'.format(elemID, excludeDataKey,self.Elements[ie].data[excludeDataKey]))
+ # Adding elements at the end
+ if method=='append':
+ pass
+ elif method=='insert':
+ self.Elements=[] # We clear all elements
+ else:
+ raise NotImplementedError('Element Insertions')
+
+ for e in newElements:
+ self.addElement(e)
+
+ # Trigger, remove precomputed values related to connectivity:
+ self.connecticityHasChanged()
+
+ return self
+
+ # --------------------------------------------------------------------------------}
+ # --- Dynamics
+ # --------------------------------------------------------------------------------{
+ def addMode(self,displ,name=None,freq=1):
+ if name is None:
+ name='Mode '+str(len(self.Modes))
+ mode = Mode(data=displ, name=name, freq=freq)
+ self.Modes.append(mode)
+
+
+ # --------------------------------------------------------------------------------}
+ # --- Ouputs / converters
+ # --------------------------------------------------------------------------------{
+ def nodalDataFrame(self, sortBy=None):
+ """ return a DataFrame of all the nodal data """
+ data=dict()
+ nNodes=len(self.Nodes)
+ for i,n in enumerate(self.Nodes):
+ if i==0:
+ data['ID'] = np.zeros(nNodes).astype(int)
+ data['x'] = np.zeros(nNodes)
+ data['y'] = np.zeros(nNodes)
+ data['z'] = np.zeros(nNodes)
+
+ data['ID'][i] = n.ID
+ data['x'][i] = n.x
+ data['y'][i] = n.y
+ data['z'][i] = n.z
+ for k,v in n.data.items():
+ if k not in data:
+ data[k] = np.zeros(nNodes)
+ try:
+ data[k][i]=v
+ except:
+ pass
+ df = pd.DataFrame(data)
+ # Sorting
+ if sortBy is not None:
+ df.sort_values([sortBy],inplace=True,ascending=True)
+ df.reset_index(drop=True,inplace=True)
+ return df
+
+
+ def toJSON(self,outfile=None):
+ d=dict();
+ Points=self.points
+ d['Connectivity'] = self.connectivity
+ d['Nodes'] = Points.tolist()
+
+ d['ElemProps']=list()
+ for iElem,elem in enumerate(self.Elements):
+ Shape = elem.data['shape'] if 'shape' in elem.data.keys() else 'cylinder'
+ Type = elem.data['Type'] if 'Type' in elem.data.keys() else 1
+ try:
+ Diam = elem.D
+ except:
+ Diam = elem.data['D'] if 'D' in elem.data.keys() else 1
+ if Shape=='cylinder':
+ d['ElemProps'].append({'shape':'cylinder','type':Type, 'Diam':Diam})
+ else:
+ raise NotImplementedError()
+
+
+ d['Modes']=[
+ {
+ 'name': self.Modes[iMode]['name'],
+ 'omega':self.Modes[iMode]['freq']*2*np.pi, #in [rad/s]
+ 'Displ':self.Modes[iMode]['data'].tolist()
+ } for iMode,mode in enumerate(self.Modes)]
+ d['groundLevel']=np.min(Points[:,2]) # TODO
+
+ if outfile is not None:
+ import json
+ jsonFile=outfile
+ with open(jsonFile, 'w', encoding='utf-8') as f:
+ #f.write(to_json(d))
+ try:
+ #f.write(unicode(json.dumps(d, ensure_ascii=False))) #, indent=2)
+ #f.write(json.dumps(d, ensure_ascii=False)) #, indent=2)
+ f.write(json.dumps(d, ensure_ascii=False))
+ except:
+ print('>>> FAILED')
+ json.dump(d, f, indent=0)
+ return d
+
+#
+
+
+
+INDENT = 3
+SPACE = " "
+NEWLINE = "\n"
+# Changed basestring to str, and dict uses items() instead of iteritems().
+
+def to_json(o, level=0):
+ ret = ""
+ if isinstance(o, dict):
+ if level==0:
+ ret += "{" + NEWLINE
+ comma = ""
+ for k, v in o.items():
+ ret += comma
+ comma = ",\n"
+ ret += SPACE * INDENT * (level + 1)
+ ret += '"' + str(k) + '":' + SPACE
+ ret += to_json(v, level + 1)
+ ret += NEWLINE + SPACE * INDENT * level + "}"
+ else:
+ ret += "{"
+ comma = ""
+ for k, v in o.items():
+ ret += comma
+ comma = ",\n"
+ ret += SPACE
+ ret += '"' + str(k) + '":' + SPACE
+ ret += to_json(v, level + 1)
+ ret += "}"
+
+ elif isinstance(o, str):
+ ret += '"' + o + '"'
+ elif isinstance(o, list):
+ ret += "[" + ",".join([to_json(e, level + 1) for e in o]) + "]"
+ # Tuples are interpreted as lists
+ elif isinstance(o, tuple):
+ ret += "[" + ",".join(to_json(e, level + 1) for e in o) + "]"
+ elif isinstance(o, bool):
+ ret += "true" if o else "false"
+ elif isinstance(o, int):
+ ret += str(o)
+ elif isinstance(o, float):
+ ret += '%.7g' % o
+ elif isinstance(o, numpy.ndarray) and numpy.issubdtype(o.dtype, numpy.integer):
+ ret += "[" + ','.join(map(str, o.flatten().tolist())) + "]"
+ elif isinstance(o, numpy.ndarray) and numpy.issubdtype(o.dtype, numpy.inexact):
+ ret += "[" + ','.join(map(lambda x: '%.7g' % x, o.flatten().tolist())) + "]"
+ elif o is None:
+ ret += 'null'
+ else:
+ raise TypeError("Unknown type '%s' for json serialization" % str(type(o)))
+ return ret
diff --git a/weio/turbsim_file.py b/weio/turbsim_file.py
index 5cee26a..916e2f6 100644
--- a/weio/turbsim_file.py
+++ b/weio/turbsim_file.py
@@ -53,12 +53,12 @@ def defaultExtensions():
def formatName():
return 'TurbSim binary'
- def __init__(self,filename=None, **kwargs):
+ def __init__(self, filename=None, **kwargs):
self.filename = None
if filename:
self.read(filename, **kwargs)
- def read(self, filename=None, header_only=False):
+ 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)
@@ -98,11 +98,11 @@ def read(self, filename=None, header_only=False):
self['uTwr'] = uTwr
self['info'] = info
self['ID'] = ID
- self['dt'] = dt
+ 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.arange(nt)*dt
+ self['t'] = np.round(np.arange(nt)*dt, tdecimals)
self['zTwr'] =-np.arange(nTwr)*dz + zBottom
self['zRef'] = zHub
self['uRef'] = uHub
@@ -174,13 +174,32 @@ def write(self, filename=None):
# --- Convenient properties (matching Mann Box interface as well)
# --------------------------------------------------------------------------------{
@property
- def z(self): return self['z']
+ def z(self): return self['z'] # np.arange(nz)*dz +zBottom
@property
- def y(self): return self['y']
+ def y(self): return self['y'] # np.arange(ny)*dy - np.mean( np.arange(ny)*dy )
+
+ @property
+ def t(self): return self['t'] # np.arange(nt)*dt
+
+ # NOTE: it would be best to use dz and dy as given in the file to avoid numerical issues
+ @property
+ def dz(self): return self['z'][1]-self['z'][0]
+
+ @property
+ def dy(self): return self['y'][1]-self['y'][0]
@property
- def t(self): return self['t']
+ def dt(self): return self['t'][1]-self['t'][0]
+
+ @property
+ def nz(self): return len(self.z)
+
+ @property
+ def ny(self): return len(self.y)
+
+ @property
+ def nt(self): return len(self.t)
# --------------------------------------------------------------------------------}
# --- Extracting relevant "Line" data at one point
@@ -244,9 +263,9 @@ def _longiline(ts, iy0=None, iz0=None, removeMean=False):
"""
if iy0 is None:
iy0,iz0 = ts.iMid
- u = ts['u'][0,:,iy0,iz0]
- v = ts['u'][1,:,iy0,iz0]
- w = ts['u'][2,:,iy0,iz0]
+ u = ts['u'][0,:,iy0,iz0].copy()
+ v = ts['u'][1,:,iy0,iz0].copy()
+ w = ts['u'][2,:,iy0,iz0].copy()
if removeMean:
u -= np.mean(u)
v -= np.mean(v)
@@ -260,9 +279,9 @@ def _latline(ts, ix0=None, iz0=None, removeMean=False):
if ix0 is None:
iy0,iz0 = ts.iMid
ix0=int(len(ts['t'])/2)
- u = ts['u'][0,ix0,:,iz0]
- v = ts['u'][1,ix0,:,iz0]
- w = ts['u'][2,ix0,:,iz0]
+ u = ts['u'][0,ix0,:,iz0].copy()
+ v = ts['u'][1,ix0,:,iz0].copy()
+ w = ts['u'][2,ix0,:,iz0].copy()
if removeMean:
u -= np.mean(u)
v -= np.mean(v)
@@ -276,9 +295,9 @@ def _vertline(ts, ix0=None, iy0=None, removeMean=False):
if ix0 is None:
iy0,iz0 = ts.iMid
ix0=int(len(ts['t'])/2)
- u = ts['u'][0,ix0,iy0,:]
- v = ts['u'][1,ix0,iy0,:]
- w = ts['u'][2,ix0,iy0,:]
+ u = ts['u'][0,ix0,iy0,:].copy()
+ v = ts['u'][1,ix0,iy0,:].copy()
+ w = ts['u'][2,ix0,iy0,:].copy()
if removeMean:
u -= np.mean(u)
v -= np.mean(v)
@@ -289,7 +308,7 @@ def _vertline(ts, ix0=None, iy0=None, removeMean=False):
# --- Extracting plane data at one point
# --------------------------------------------------------------------------------{
def horizontalPlane(ts, z=None, iz0=None, removeMean=False):
- """ return velocity components on a horizontal plane
+ """ return velocity components on a horizontal plane z=cst, (time x ny)
If no z value is provided, returned at mid box
"""
if z is None and iz0 is None:
@@ -297,9 +316,9 @@ def horizontalPlane(ts, z=None, iz0=None, removeMean=False):
elif z is not None:
_, iz0 = ts.closestPoint(ts.y[0], z)
- u = ts['u'][0,:,:,iz0]
- v = ts['u'][1,:,:,iz0]
- w = ts['u'][2,:,:,iz0]
+ u = ts['u'][0,:,:,iz0].copy()
+ v = ts['u'][1,:,:,iz0].copy()
+ w = ts['u'][2,:,:,iz0].copy()
if removeMean:
u -= np.mean(u)
v -= np.mean(v)
@@ -307,7 +326,7 @@ def horizontalPlane(ts, z=None, iz0=None, removeMean=False):
return u, v, w
def verticalPlane(ts, y=None, iy0=None, removeMean=False):
- """ return velocity components on a vertical plane
+ """ return velocity components on a vertical plane y=cst, (time x nz)
If no y value is provided, returned at mid box
"""
if y is None and iy0 is None:
@@ -315,15 +334,33 @@ def verticalPlane(ts, y=None, iy0=None, removeMean=False):
elif y is not None:
iy0, _ = ts.closestPoint(y, ts.z[0])
- u = ts['u'][0,:,iy0,:]
- v = ts['u'][1,:,iy0,:]
- w = ts['u'][2,:,iy0,:]
+ u = ts['u'][0,:,iy0,:].copy()
+ v = ts['u'][1,:,iy0,:].copy()
+ w = ts['u'][2,:,iy0,:].copy()
if removeMean:
u -= np.mean(u)
v -= np.mean(v)
w -= np.mean(w)
return u, v, w
+ def normalPlane(ts, t=None, it0=None, removeMean=False):
+ """ return velocity components on a normal plane t=cst, (ny x nz)
+ """
+ if t is None and it0 is None:
+ it0 = 0
+ elif t is not None:
+ it0 = np.argmin(np.abs(self['t']-t))
+
+ u = ts['u'][0,it0,:,:].copy()
+ v = ts['u'][1,it0,:,:].copy()
+ w = ts['u'][2,it0,:,:].copy()
+ if removeMean:
+ u -= np.mean(u)
+ v -= np.mean(v)
+ w -= np.mean(w)
+ return u, v, w
+
+
# --------------------------------------------------------------------------------}
# --- Extracting average data
# --------------------------------------------------------------------------------{
@@ -334,8 +371,9 @@ def vertProfile(self, y_span='full'):
if 'mid', average the vertical profile at the middle y value
"""
if y_span=='full':
+ # Compute statistics with respect to time first, then average over "y"
m = np.mean(np.mean(self['u'][:,:,:,:], axis=1), axis=1)
- s = np.std( np.std( self['u'][:,:,:,:], axis=1), axis=1)
+ s = np.mean(np.std( self['u'][:,:,:,:], axis=1), axis=1)
elif y_span=='mid':
iy, iz = self.iMid
m = np.mean(self['u'][:,:,iy,:], axis=1)
@@ -404,10 +442,7 @@ def csd_lat(ts, ix0=None, iz0=None):
""" Compute lateral cross spectral density
If no index is provided, computed at mid box
"""
- try:
- import scipy.signal as sig
- except:
- import pydatview.tools.spectral as sig
+ import scipy.signal as sig
u, v, w = ts._latline(ix0=ix0, iz0=iz0, removeMean=True)
t = ts['t']
dt = t[1]-t[0]
@@ -421,10 +456,7 @@ def csd_vert(ts, ix0=None, iy0=None):
""" Compute vertical cross spectral density
If no index is provided, computed at mid box
"""
- try:
- import scipy.signal as sig
- except:
- import pydatview.tools.spectral as sig
+ import scipy.signal as sig
t = ts['t']
dt = t[1]-t[0]
fs = 1/dt
@@ -442,10 +474,7 @@ def coherence_longi(ts, iy0=None, iz0=None):
""" Coherence on a longitudinal line for different delta y and delta z
compared to a given point with index iy0,iz0
"""
- try:
- import scipy.signal as sig
- except:
- import pydatview.tools.spectral as sig
+ import scipy.signal as sig
if iy0 is None:
iy0,iz0 = ts.iMid
u, v, w = ts._longiline(iy0=iy0, iz0=iz0, removeMean=True)
@@ -573,7 +602,11 @@ def __repr__(self):
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):
@@ -624,6 +657,9 @@ def toDataFrame(self):
# Mid csd
try:
+ import warnings
+ with warnings.catch_warnings():
+ warnings.filterwarnings('ignore') #, category=DeprecationWarning)
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))
@@ -658,8 +694,246 @@ def toDataFrame(self):
# pass
return dfs
+ def to2DFields(self, nTOut=10, nYOut=3, nZOut=3, **kwargs):
+ import xarray as xr
+ if len(kwargs.keys())>0:
+ print('[WARN] TurbSimFile: to2DFields: ignored keys: ',kwargs.keys())
+ # Sanity check
+ nTOut = int(nTOut)
+ nYOut = int(nYOut)
+ nZOut = int(nZOut)
+ # 'u': velocity field, shape (3 x nt x ny x nz)
+ IT = list(np.arange(nTOut)) + [len(self.t)-1]
+ IT = np.unique(IT)
+
+ IY = np.unique(np.linspace(0,len(self['y'])-1, nYOut).astype(int))
+ IZ = np.unique(np.linspace(0,len(self['z'])-1, nZOut).astype(int))
+ Fields=[]
+ # --- YZ planes
+ s1 = 'TSR'
+ s2 = 'pitch'
+ ds = xr.Dataset(coords={'t': self.t, 'y': self.y, 'z': self.z})
+ ds['t'].attrs['unit'] = 's'
+ ds['y'].attrs['unit'] = 'm'
+ ds['z'].attrs['unit'] = 'm'
+
+ ds['(y,z)_u_avg_[m/s]']= (['y','z'], np.squeeze(np.mean(self['u'][0,:,:,:], axis=0)))
+ ds['(y,z)_v_avg_[m/s]']= (['y','z'], np.squeeze(np.mean(self['u'][1,:,:,:], axis=0)))
+ ds['(y,z)_w_avg_[m/s]']= (['y','z'], np.squeeze(np.mean(self['u'][2,:,:,:], axis=0)))
+
+ ds['(t,y)_u_avg_[m/s]']= (['t','y'], np.squeeze(np.mean(self['u'][0,:,:,:], axis=2)))
+ ds['(t,y)_v_avg_[m/s]']= (['t','y'], np.squeeze(np.mean(self['u'][1,:,:,:], axis=2)))
+ ds['(t,y)_w_avg_[m/s]']= (['t','y'], np.squeeze(np.mean(self['u'][2,:,:,:], axis=2)))
+
+ ds['(t,z)_u_avg_[m/s]']= (['t','z'], np.squeeze(np.mean(self['u'][0,:,:,:], axis=1)))
+ ds['(t,z)_v_avg_[m/s]']= (['t','z'], np.squeeze(np.mean(self['u'][1,:,:,:], axis=1)))
+ ds['(t,z)_w_avg_[m/s]']= (['t','z'], np.squeeze(np.mean(self['u'][2,:,:,:], axis=1)))
+
+ for it in IT:
+ ds['(y,z)_u_t={:.2f}_[m/s]'.format(self.t[it])] = (['y','z'], np.squeeze(self['u'][0,it,:,:]))
+ for it in IT:
+ ds['(y,z)_v_t={:.2f}_[m/s]'.format(self.t[it])] = (['y','z'], np.squeeze(self['u'][1,it,:,:]))
+ for it in IT:
+ ds['(y,z)_w_t={:.2f}_[m/s]'.format(self.t[it])] = (['y','z'], np.squeeze(self['u'][2,it,:,:]))
+ # --- TZ planes
+ for iy in IY:
+ ds['(t,z)_u_y={:.2f}_[m/s]'.format(self['y'][iy])] = (['t','z'], np.squeeze(self['u'][0,:,iy,:]))
+ for iy in IY:
+ ds['(t,z)_v_y={:.2f}_[m/s]'.format(self['y'][iy])] = (['t','z'], np.squeeze(self['u'][1,:,iy,:]))
+ for iy in IY:
+ ds['(t,z)_w_y={:.2f}_[m/s]'.format(self['y'][iy])] = (['t','z'], np.squeeze(self['u'][2,:,iy,:]))
+ # --- TY planes
+ for iz in IZ:
+ ds['(t,y)_u_z={:.2f}_[m/s]'.format(self['z'][iz])] = (['t','y'], np.squeeze(self['u'][0,:,:,iz]))
+ for iz in IZ:
+ ds['(t,y)_v_z={:.2f}_[m/s]'.format(self['z'][iz])] = (['t','y'], np.squeeze(self['u'][1,:,:,iz]))
+ for iz in IZ:
+ ds['(t,y)_w_z={:.2f}_[m/s]'.format(self['z'][iz])] = (['t','y'], np.squeeze(self['u'][2,:,:,iz]))
+ return ds
+
+ 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] 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] turbsim_file.toDataSet: should be discontinued')
+ print('[TODO] 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 weio.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
@@ -782,6 +1056,91 @@ def fitPowerLaw(ts, z_ref=None, y_span='full', U_guess=10, alpha_guess=0.1):
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
@@ -797,5 +1156,3 @@ def fit_powerlaw_u_alpha(x, y, z_ref=100, p0=(10,0.1)):
if __name__=='__main__':
ts = TurbSimFile('../_tests/TurbSim.bts')
-
-
diff --git a/weio/turbsim_ts_file.py b/weio/turbsim_ts_file.py
index b9383bb..f590c35 100644
--- a/weio/turbsim_ts_file.py
+++ b/weio/turbsim_ts_file.py
@@ -1,11 +1,12 @@
-from __future__ import division
-from __future__ import print_function
-from __future__ import absolute_import
-from io import open
-from .file import File, isBinary, WrongFormatError, BrokenFormatError
+from itertools import takewhile
import pandas as pd
import numpy as np
-from itertools import takewhile
+try:
+ from .file import File, WrongFormatError, BrokenFormatError
+except:
+ File = dict
+ class WrongFormatError(Exception): pass
+ class BrokenFormatError(Exception): pass
class TurbSimTSFile(File):
diff --git a/weio/vtk_file.py b/weio/vtk_file.py
index e3ed2f7..ec57ad0 100644
--- a/weio/vtk_file.py
+++ b/weio/vtk_file.py
@@ -35,6 +35,13 @@ class VTKFile(File):
- cells
- cell_data
+ Main attributes for polydata:
+ ---------
+ - points
+ - point_data
+ - polygons
+ - cell_data
+
Main methods
------------
- read, write
@@ -42,8 +49,8 @@ class VTKFile(File):
Examples
--------
vtk = VTKFile('DisXZ1.vtk')
- x = vtk.x_grid
- z = vtk.z_grid
+ x = vtk.xp_grid
+ z = vtk.zp_grid
Ux = vtk.point_data_grid['DisXZ'][:,0,:,0]
"""
@@ -61,13 +68,18 @@ def __init__(self,filename=None,**kwargs):
self.xp_grid=None # location of points
self.yp_grid=None
self.zp_grid=None
- self.point_data_grid = None
+ self.point_data_grid = {}
+
# Main Data
- self.points = None
+ self.header = ''
+ self.points = None
+ self.polygons = None
+ self.cells = None
+ self.cell_data = None
self.field_data = {}
- self.point_data = {}
- self.dataset = {}
+ self.point_data = {}
+ self.dataset = {'type':None}
@@ -86,7 +98,7 @@ def __init__(self,filename=None,**kwargs):
self.read(filename=filename,**kwargs)
- def read(self, filename=None):
+ def read(self, filename=None, verbose=False):
""" read a VTK file """
if filename:
self.filename = filename
@@ -97,47 +109,10 @@ def read(self, filename=None):
if os.stat(self.filename).st_size == 0:
raise EmptyFileError('File is empty:',self.filename)
- with open(filename, "rb") as f:
- # initialize output data
- # skip header and title
- f.readline()
- f.readline()
-
- data_type = f.readline().decode("utf-8").strip().upper()
- if data_type not in ["ASCII", "BINARY"]:
- raise ReadError('Unknown VTK data type ',data_type)
- self.is_ascii = data_type == "ASCII"
-
- while True:
- line = f.readline().decode("utf-8")
- if not line:
- # EOF
- break
-
- line = line.strip()
- if len(line) == 0:
- continue
-
- self.split = line.split()
- self.section = self.split[0].upper()
-
- if self.section in vtk_sections:
- _read_section(f, self)
- else:
- _read_subsection(f, self)
-
- # --- Postpro
- _check_mesh(self) # generate points if needed
- cells, cell_data = translate_cells(self.c, self.ct, self.cell_data_raw)
- self.cells = cells
- self.cell_data = cell_data
-
- if self.dataset['type']=='STRUCTURED_POINTS':
- self.point_data_grid = {}
- # We provide point_data_grid, corresponds to point_data but reshaped
- for k,PD in self.point_data.items():
- # NOTE: tested foe len(y)=1, len(z)=1
- self.point_data_grid[k]=PD.reshape(len(self.xp_grid), len(self.yp_grid), len(self.zp_grid),PD.shape[1], order='F')
+ # Calling wrapped function
+ read_vtk(filename, self, verbose=verbose)
+ if verbose:
+ print('Dataset',self.dataset)
def write(self, filename=None, binary=True):
@@ -151,54 +126,7 @@ def write(self, filename=None, binary=True):
if not self.filename:
raise Exception('No filename provided')
-
- def pad(array):
- return np.pad(array, ((0, 0), (0, 1)), "constant")
-
- if self.points.shape[1] == 2:
- points = pad(self.points)
- else:
- points = self.points
-
- if self.point_data:
- for name, values in self.point_data.items():
- if len(values.shape) == 2 and values.shape[1] == 2:
- self.point_data[name] = pad(values)
-
- for name, data in self.cell_data.items():
- for k, values in enumerate(data):
- if len(values.shape) == 2 and values.shape[1] == 2:
- data[k] = pad(data[k])
-
- with open(filename, "wb") as f:
- f.write(b"# vtk DataFile Version 4.2\n")
- f.write("written \n".encode("utf-8"))
- f.write(("BINARY\n" if binary else "ASCII\n").encode("utf-8"))
- f.write(b"DATASET UNSTRUCTURED_GRID\n")
-
- # write points and cells
- _write_points(f, points, binary)
- _write_cells(f, self.cells, binary)
-
- # write point data
- if self.point_data:
- num_points = self.points.shape[0]
- f.write("POINT_DATA {}\n".format(num_points).encode("utf-8"))
- _write_field_data(f, self.point_data, binary)
-
- # write cell data
- if self.cell_data:
- total_num_cells = sum(len(c.data) for c in self.cells)
- f.write("CELL_DATA {}\n".format(total_num_cells).encode("utf-8"))
- _write_field_data(f, self.cell_data, binary)
-
-
- def __repr__(self):
- s='<{} object> with keys:\n'.format(type(self).__name__)
- for k,v in self.items():
- s+=' - {}: {}\n'.format(k,v)
- return s
-
+ write_vtk(self.filename, self)
def __repr__(self):
""" print function """
@@ -213,255 +141,246 @@ def show_grid(v,s):
lines.append('- {}: [{} ... {}], dx: {}, n: {}'.format(s,v[0],v[-1],v[1]-v[0],len(v)))
lines = ['<{} object> with attributes:'.format(type(self).__name__)]
- show_grid(self.xp_grid, 'xp_grid')
- show_grid(self.yp_grid, 'yp_grid')
- show_grid(self.zp_grid, 'zp_grid')
- if self.point_data_grid:
- lines.append('- point_data_grid:')
- for k,v in self.point_data_grid.items():
- lines.append(' "{}" : {}'.format(k,v.shape))
+ try:
+ lines.append('- dataset: {}'.format(self.dataset))
+ except:
+ pass
+
+ # grid
+ try:
+ show_grid(self.xp_grid, 'xp_grid')
+ show_grid(self.yp_grid, 'yp_grid')
+ show_grid(self.zp_grid, 'zp_grid')
+ except:
+ pass
+
+ lines.append('- point_data_grid:')
+ for k,v in self.point_data_grid.items():
+ lines.append(' "{}" : {}'.format(k,v.shape))
lines.append('- points {}'.format(len(self.points)))
- if len(self.cells) > 0:
+ if self.cells is not None and len(self.cells) > 0:
lines.append("- cells:")
for tpe, elems in self.cells:
lines.append(" {}: {}".format(tpe,len(elems)))
else:
- lines.append(" No cells.")
+ lines.append("- cells: None")
+ if self.polygons is not None:
+ lines.append("- polygons: {}, consiting of {} points".format(len(self.polygons), len(self.polygons[0])))
+ else:
+ lines.append("- polygons: None")
- if self.point_data:
- lines.append('- point_data:')
- for k,v in self.point_data.items():
- lines.append(' "{}" : {}'.format(k,v.shape))
+ lines.append('- point_data:')
+ for k,v in self.point_data.items():
+ lines.append(' "{}" : {}'.format(k,v.shape))
- if self.cell_data:
- names = ", ".join(self.cell_data.keys())
- lines.append(" Cell data: {}".format(names))
+ names = ", ".join(self.cell_data.keys())
+ lines.append("- cell_data: {}".format(names))
return "\n".join(lines)
def toDataFrame(self):
- return None
+ if self.dataset['type'] == 'STRUCTURED_POINTS':
+ data = np.zeros((2,3))
+ data[:,0] = [np.min(self.xp_grid), np.max(self.xp_grid)]
+ data[:,1] = [np.min(self.yp_grid), np.max(self.yp_grid)]
+ data[:,2] = [np.min(self.zp_grid), np.max(self.zp_grid)]
+ return pd.DataFrame(data=data, columns=['xRange', 'yRange', 'zRange'])
+ #if self.point_data_grid is not None:
+ # for k,v in self.point_data_grid.items():
+
+ else:
+ print('[WARN] vtk_file: toDataFrame not implemented for dataset type: {}'.format(self.dataset['type']))
+ data = np.zeros((2,2))
+ data[:,0] = [0,1]
+ return pd.DataFrame(data=data, columns=['Dummy1', 'Dummy2'])
+ return None
+
+ def to2DFields(self, **kwargs):
+ import xarray as xr
+ if len(kwargs.keys())>0:
+ print('[WARN] VTKFile: to2DFields: ignored keys: ',kwargs.keys())
+
+ ds = None
+ if self.dataset['type'] == 'STRUCTURED_POINTS':
+ I = np.where(np.asarray(self.dataset['DIMENSIONS'])==1)[0]
+ if len(I) == 1:
+ icst = I[0]
+ # --- 2D velocity fields
+ ds = xr.Dataset(coords={'x': self.xp_grid, 'y': self.yp_grid, 'z': self.zp_grid})
+ dims = {0:['y','z'], 1:['x','z'], 2:['x','y']}[icst]
+ for k,v in self.point_data_grid.items():
+ if v.shape[-1]==3:
+ ds[k + '_x'] = (dims, np.squeeze(v[:,:,:,0]))
+ ds[k + '_y'] = (dims, np.squeeze(v[:,:,:,1]))
+ ds[k + '_z'] = (dims, np.squeeze(v[:,:,:,2]))
+ else:
+ ds[k] = (dims, np.squeeze(v[:,:,:,0]))
+ else:
+ print('[WARN] VTKFile: field dimension not supported: {}'.format(self.dataset['DIMENSIONS']))
+ return None
+ else:
+ print('[WARN] VTKFile: datatype not implemented/suitable for 2D fields: {}'.format(self.dataset['type']))
+ return None
+ return ds
+
+# --------------------------------------------------------------------------------
+# --- Simple/dedicated reader and writers
+# --------------------------------------------------------------------------------
+def write_dataset_unstructured_grid(filename, points, cells, point_data=None, cell_data=None, header='', binary=False):
+ def pad(array):
+ return np.pad(array, ((0, 0), (0, 1)), "constant")
+
+ if points.shape[1] == 2:
+ points = pad(points)
+ else:
+ points = points
+
+ if point_data:
+ for name, values in point_data.items():
+ if len(values.shape) == 2 and values.shape[1] == 2:
+ point_data[name] = pad(values)
+
+ for name, data in cell_data.items():
+ for k, values in enumerate(data):
+ if len(values.shape) == 2 and values.shape[1] == 2:
+ data[k] = pad(data[k])
+
+ with open(filename, "wb") as f:
+ f.write(b"# vtk DataFile Version 4.2\n")
+ f.write((header.strip()+"\n").encode("utf-8"))
+ f.write(("BINARY\n" if binary else "ASCII\n").encode("utf-8"))
+ f.write(b"DATASET UNSTRUCTURED_GRID\n")
+
+ # write points and cells
+ _write_points(f, points, binary)
+ _write_cells(f, cells, binary)
+
+ # write point data
+ if point_data is not None:
+ num_points = points.shape[0]
+ f.write("POINT_DATA {}\n".format(num_points).encode("utf-8"))
+ _write_field_data(f, point_data, binary)
+
+ # write cell data
+ if cell_data is not None:
+ total_num_cells = sum(len(c.data) for c in cells)
+ f.write("CELL_DATA {}\n".format(total_num_cells).encode("utf-8"))
+ _write_field_data(f, cell_data, binary)
+def write_dataset_polydata(filename, points, polygons, point_data=None, cell_data=None, uniquePoints=False, header='', binary=False):
+ if uniquePoints:
+ # NOTE: this will completed change the order
+ # Create list of points per polygon
+ polygons_points = [ [tuple(points[i,:]) for i in polygon] for polygon in polygons]
+
+ # Create a unique list of points
+ points_list = [tuple(row) for row in points]
+ points = list(set(points_list))
+ # Convert the array to a view with a structured dtype
+ #pview = np.ascontiguousarray(points).view(np.dtype((np.void, points.dtype.itemsize * points.shape[1])))
+ #points = np.unique(pview).view(points.dtype).reshape(-1, points.shape[1])
+
+ # Update indices in the connectivity table
+ polygons = [[points.index(point) for point in poly_points] for poly_points in polygons_points]
+ with open(filename, "wb") as f:
+ f.write(b"# vtk DataFile Version 2.0\n")
+ f.write((header.strip()+"\n").encode("utf-8"))
+ f.write(("BINARY\n" if binary else "ASCII\n").encode("utf-8"))
+ f.write(b"DATASET POLYDATA\n")
+ _write_points(f, points, binary)
+ _write_polygons(f, polygons, binary)
+ if point_data:
+ num_points = points.shape[0]
+ f.write("POINT_DATA {}\n".format(num_points).encode("utf-8"))
+ _write_field_data(f, point_data, binary)
-# Save FlowData Object to vtk
-# """
-# n_points = self.dimensions.x1 * self.dimensions.x2 * self.dimensions.x3
-# vtk_file = Output(filename)
- #self.file = open(self.filename, "w")
- #self.ln = "\n"
-# vtk_file.write_line('# vtk DataFile Version 3.0')
-# vtk_file.write_line('array.mean0D')
-# vtk_file.write_line('ASCII')
-# vtk_file.write_line('DATASET STRUCTURED_POINTS')
-# vtk_file.write_line('DIMENSIONS {}'.format(self.dimensions))
-# vtk_file.write_line('ORIGIN {}'.format(self.origin))
-# vtk_file.write_line('SPACING {}'.format(self.spacing))
-# vtk_file.write_line('POINT_DATA {}'.format(n_points))
-# vtk_file.write_line('FIELD attributes 1')
-# vtk_file.write_line('UAvg 3 {} float'.format(n_points))
-# for u, v, w in zip(self.u, self.v, self.w):
-# vtk_file.write_line('{}'.format(Vec3(u, v, w)))
-
-# --- Paraview
-# except: from paraview.simple import *
-# sliceFile = sliceDir + '/' + tStartStr + '/' + sliceName
-# print ' Slice file 1a: ' + sliceFile
-# slice_1a_vtk = LegacyVTKReader( FileNames=[sliceFile] )
-# sliceFile = sliceDir + '/' + tEndStr + '/' + sliceName
-# DataRepresentation3 = GetDisplayProperties(slice_1a_vtk)
-# DataRepresentation3.Visibility = 0
-# SetActiveSource(slice_1a_vtk)
-
-# --- VTK
-# import np
-# from vtk import vtkStructuredPointsReader
-# from vtk.util import np as VN
-#
-# reader = vtkStructuredPointsReader()
-# reader.SetFileName(filename)
-# reader.ReadAllVectorsOn()
-# reader.ReadAllScalarsOn()
-# reader.Update()
-#
-# data = reader.GetOutput()
-#
-# dim = data.GetDimensions()
-# vec = list(dim)
-# vec = [i-1 for i in dim]
-# vec.append(3)
-#
-# u = VN.vtk_to_np(data.GetCellData().GetArray('velocity'))
-# b = VN.vtk_to_numpy(data.GetCellData().GetArray('cell_centered_B'))
-#
-# u = u.reshape(vec,order='F')
-# b = b.reshape(vec,order='F')
-#
-# x = zeros(data.GetNumberOfPoints())
-# y = zeros(data.GetNumberOfPoints())
-# z = zeros(data.GetNumberOfPoints())
-#
-# for i in range(data.GetNumberOfPoints()):
-# x[i],y[i],z[i] = data.GetPoint(i)
-#
-# x = x.reshape(dim,order='F')
-# y = y.reshape(dim,order='F')
-# z = z.reshape(dim,order='F')
-
-# --- vtk
-# import vtk
-# import numpy
-# import vtk_demo.version as version
-#
-#
-# def main():
-# """
-# :return: The render window interactor.
-# """
-#
-# chessboard_resolution = 5
-# n_lut_colors = 256
-# data_max_value = 1
-#
-# # Provide some geometry
-# chessboard = vtk.vtkPlaneSource()
-# chessboard.SetXResolution(chessboard_resolution)
-# chessboard.SetYResolution(chessboard_resolution)
-# num_squares = chessboard_resolution * chessboard_resolution
-# # Force an update so we can set cell data
-# chessboard.Update()
-#
-# # Make some arbitrary data to show on the chessboard geometry
-# data = vtk.vtkFloatArray()
-# for i in range(num_squares):
-# if i == 4:
-# # This square should in principle light up with color given by SetNanColor below
-# data.InsertNextTuple1(numpy.nan)
-# else:
-# thing = (i * data_max_value) / (num_squares - 1)
-# data.InsertNextTuple1(thing)
-#
-# # Make a LookupTable
-# lut = vtk.vtkLookupTable()
-# lut.SetNumberOfColors(n_lut_colors)
-# lut.Build()
-# lut.SetTableRange(0, data_max_value)
-# lut.SetNanColor(.1, .5, .99, 1.0) # <------ This color gets used
-# for i in range(n_lut_colors):
-# # Fill it with arbitrary colors, e.g. grayscale
-# x = data_max_value*i/(n_lut_colors-1)
-# lut.SetTableValue(i, x, x, x, 1.0)
-# lut.SetNanColor(.99, .99, .1, 1.0) # <----- This color gets ignored! ...except by GetNanColor
-#
-# print(lut.GetNanColor()) # <-- Prints the color set by the last SetNanColor call above!
-#
-# chessboard.GetOutput().GetCellData().SetScalars(data)
-#
-# mapper = vtk.vtkPolyDataMapper()
-# mapper.SetInputConnection(chessboard.GetOutputPort())
-# mapper.SetScalarRange(0, data_max_value)
-# mapper.SetLookupTable(lut)
-# mapper.Update()
-#
-# actor = vtk.vtkActor()
-# actor.SetMapper(mapper)
-#
-# renderer = vtk.vtkRenderer()
-# ren_win = vtk.vtkRenderWindow()
-# ren_win.AddRenderer(renderer)
-# renderer.SetBackground(vtk.vtkNamedColors().GetColor3d('MidnightBlue'))
-# renderer.AddActor(actor)
-#
-# iren = vtk.vtkRenderWindowInteractor()
-
-# --- pyvtk
-# """Read vtk-file stored previously with tovtk."""
-# p = pyvtk.VtkData(filename)
-# xn = array(p.structure.points)
-# dims = p.structure.dimensions
-# try:
-# N = eval(p.header.split(" ")[-1])
-
- # --- Extract
- # # Convert the center of the turbine coordinate in m to High-Resolution domains left most corner in (i,j)
- # xe_index = int(origin_at_precusr[0]/10)
- # ye_index = int(origin_at_precusr[1]/10)
- #
- # # Read the full domain from VTK
- # reader = vtk.vtkStructuredPointsReader()
- # reader.SetFileName(in_vtk)
- # reader.Update()
- #
- # # Extract the High Resolution domain at same spacial spacing by specifying the (i,i+14),(j,j+14),(k,k+20) tuples
- # extract = vtk.vtkExtractVOI()
- # extract.SetInputConnection(reader.GetOutputPort())
- # extract.SetVOI(xe_index, xe_index+14, ye_index, ye_index+14, 0, 26)
- # extract.SetSampleRate(1, 1, 1)
- # extract.Update()
- #
- # # Write the extract as VTK
- # points = extract.GetOutput()
- # vec = points.GetPointData().GetVectors('Amb')
- #
- # with open(out_vtk, 'a') as the_file:
- # the_file.write('# vtk DataFile Version 3.0\n')
- # the_file.write('High\n')
- # the_file.write('ASCII\n')
- # the_file.write('DATASET STRUCTURED_POINTS\n')
- # the_file.write('DIMENSIONS %d %d %d\n' % points.GetDimensions())
- # the_file.write('ORIGIN %f %f %f\n' % origin_at_stitch)
- # the_file.write('SPACING %f %f %f\n' % points.GetSpacing())
- # the_file.write('POINT_DATA %d\n' % points.GetNumberOfPoints())
- # the_file.write('VECTORS Amb float\n')
- # for i in range(points.GetNumberOfPoints()):
- # the_file.write('%f %f %f\n' % vec.GetTuple(i) )
-
- # --- Stitch
- # reader = vtk.vtkStructuredPointsReader()
- # reader.SetFileName(in_vtk)
- # reader.Update()
- #
- # hAppend = vtk.vtkImageAppend()
- # hAppend.SetAppendAxis(0)
- # for i in range(nx):
- # hAppend.AddInputData(reader.GetOutput())
- # hAppend.Update()
- #
- # vAppend = vtk.vtkImageAppend()
- # vAppend.SetAppendAxis(1)
- # for i in range(ny):
- # vAppend.AddInputData(hAppend.GetOutput())
- # vAppend.Update()
- #
- # points = vAppend.GetOutput()
- # vec = points.GetPointData().GetVectors('Amb')
- #
- # with open(out_vtk, 'a') as the_file:
- # the_file.write('# vtk DataFile Version 3.0\n')
- # the_file.write('Low\n')
- # the_file.write('ASCII\n')
- # the_file.write('DATASET STRUCTURED_POINTS\n')
- # the_file.write('DIMENSIONS %d %d %d\n' % points.GetDimensions())
- # the_file.write('ORIGIN %f %f %f\n' % points.GetOrigin())
- # the_file.write('SPACING %f %f %f\n' % points.GetSpacing())
- # the_file.write('POINT_DATA %d\n' % points.GetNumberOfPoints())
- # the_file.write('VECTORS Amb float\n')
- # for i in range(points.GetNumberOfPoints()):
- # the_file.write('%f %f %f\n' % vec.GetTuple(i) )
-
-
- #
+ ## write cell data
+ if cell_data:
+ total_num_cells = len(polygons)
+ f.write("CELL_DATA {}\n".format(total_num_cells).encode("utf-8"))
+ _write_field_data(f, cell_data, binary)
+def read_dataset_polydata(file_path):
+ """ Simple dedicated function to read a ASCII VTK file with polygons and field data"""
+ points = []
+ polygons = []
+ cell_data = {}
+ point_data = {}
+
+ with open(file_path, 'r') as file:
+ lines = file.readlines()
+
+ data_section = 'header'
+ header=[]
+
+ for line in lines:
+ if len(line.strip())==0:
+ continue
+ if line.startswith("POINTS"):
+ data_section = "points"
+ continue
+ elif line.startswith("POLYGONS"):
+ data_section = "polygons"
+ continue
+ elif line.startswith("POINT_DATA") or line.startswith("CELL_DATA"):
+ data_section = "cell_data"
+ continue
+ elif line.startswith("SCALARS") :
+ sp = line.split()
+ field_name = sp[1]
+ field_type = sp[2]
+ data_section = "cell_data_scalar"
+ cell_data[field_name] = []
+ continue
+ elif line.startswith("VECTORS"):
+ sp = line.split()
+ field_name = sp[1]
+ field_type = sp[2]
+ data_section = "cell_data_vector"
+ cell_data[field_name] = []
+ continue
+ elif line.startswith("LOOKUP_TABLE"):
+ continue
+
+ if data_section == "points":
+ # Extract points
+ point = list(map(float, line.split()[:]))
+ points.append(point)
+ elif data_section == "polygons":
+ # Extract polygons
+ polygon_data = list(map(int, line.split()[1:]))
+ polygons.append(polygon_data[:]) # Ignore the first value (number of vertices)
+ elif data_section == "cell_data_scalar":
+ field_values = list(map(float, line.split()))
+ cell_data[field_name].extend(field_values)
+ elif data_section == "cell_data_vector":
+ if line.startswith("LOOKUP_TABLE"):
+ continue
+ field_values = list(map(float, line.split()))
+ cell_data[field_name].append(field_values)
+ elif data_section == "header":
+ header.append(line)
+ else:
+ print('>>> Unknown data_section', data_section)
+ raise NotImplementedError()
+ points = np.asarray(points)
+ polygons = np.asarray(polygons)
+ for k,v in cell_data.items():
+ cell_data[k] = np.asarray(v)
-# --------------------------------------------------------------------------------}
-# --- The code below is taken from meshio
+ return header, points, polygons, point_data, cell_data
+
+# --------------------------------------------------------------------------------
+# --- The code below was taken from meshio
# https://github.com/nschloe/meshio
# The MIT License (MIT)
# Copyright (c) 2015-2020 meshio developers
-# --------------------------------------------------------------------------------{
+# Adapted by E. Branlard
+# --------------------------------------------------------------------------------
ReadError = BrokenFormatError
WriteError = BrokenFormatError
@@ -622,6 +541,7 @@ def __repr__(self):
# supported vtk dataset types
vtk_dataset_types = [
+ "POLYDATA",
"UNSTRUCTURED_GRID",
"STRUCTURED_POINTS",
"STRUCTURED_GRID",
@@ -629,6 +549,7 @@ def __repr__(self):
]
# additional infos per dataset type
vtk_dataset_infos = {
+ "POLYDATA": [],
"UNSTRUCTURED_GRID": [],
"STRUCTURED_POINTS": [
"DIMENSIONS",
@@ -650,6 +571,7 @@ def __repr__(self):
"METADATA",
"DATASET",
"POINTS",
+ "POLYGONS",
"CELLS",
"CELL_TYPES",
"POINT_DATA",
@@ -659,23 +581,31 @@ def __repr__(self):
]
-
-
-
-def read(filename):
+def read_vtk(filename, info, **kwargs):
"""Reads a VTK vtk file."""
+ # initialize output data
+ if info is None:
+ info = VTKFile()
+
with open(filename, "rb") as f:
- out = read_buffer(f)
- return out
+ out = read_buffer(f, info, **kwargs)
+ # --- Postpro
+ _check_mesh(info) # generate points if needed
+ if info.polygons is not None:
+ info.cell_data = info.cell_data_raw
+ else:
+ cells, cell_data = translate_cells(info.c, info.ct, info.cell_data_raw)
+ info.cells = cells
+ info.cell_data = cell_data
-def read_buffer(f):
- # initialize output data
- info = VTKFile()
+ return out
+
+def read_buffer(f, info, verbose=False):
# skip header and title
f.readline()
- f.readline()
+ info.header = f.readline().decode("utf-8").strip()
data_type = f.readline().decode("utf-8").strip().upper()
if data_type not in ["ASCII", "BINARY"]:
@@ -694,18 +624,14 @@ def read_buffer(f):
info.split = line.split()
info.section = info.split[0].upper()
-
+ if verbose:
+ print('Section', info.section, info.section in vtk_sections)
if info.section in vtk_sections:
+ # Sections: METADATA, DATASET, POINTS, POLYGONS, CELLS, CELL_TYPES, POINT_DATA, CELL_DATA, LOOKUP_TABLE, COLOR_SCALARS
_read_section(f, info)
else:
+ # SubSections: POINT_DATA, CELL_DATA, DATASET, SCALARS, VECTORS, TENSORS, FIELD
_read_subsection(f, info)
-
- _check_mesh(info)
- cells, cell_data = translate_cells(info.c, info.ct, info.cell_data_raw)
-
- info.cells = cells
- info.cell_data = cell_data
-
return info
@@ -715,6 +641,7 @@ def _read_section(f, info):
elif info.section == "DATASET":
info.active = "DATASET"
+ # Dataset types: POLYDATA, UNSTRUCTURED_GRID, STRUCTURED_POINTS, STRUCTURED_GRID, RECTILINEAR_GRID
info.dataset["type"] = info.split[1].upper()
if info.dataset["type"] not in vtk_dataset_types:
raise BrokenFormatError(
@@ -770,7 +697,7 @@ def _read_section(f, info):
elif info.section == "LOOKUP_TABLE":
info.num_items = int(info.split[2])
- numpy.fromfile(f, count=info.num_items * 4, sep=" ", dtype=float)
+ c = numpy.fromfile(f, count=info.num_items * 4, sep=" ", dtype=float)
# rgba = data.reshape((info.num_items, 4))
elif info.section == "COLOR_SCALARS":
@@ -780,7 +707,25 @@ def _read_section(f, info):
dtype = numpy.ubyte
if info.is_ascii:
dtype = float
- numpy.fromfile(f, count=num_items * nValues, dtype=dtype)
+ c = numpy.fromfile(f, count=num_items * nValues, dtype=dtype)
+
+ elif info.section == "POLYGONS":
+ info.num_items = int(info.split[1])
+ num_floats = int(info.split[2])
+ nPerLine = int(num_floats/info.num_items)
+ if np.mod(num_floats, info.num_items):
+ raise NotImplementedError('Polygons with varying number of edges')
+ if info.is_ascii:
+ dtype = int
+ poly = numpy.fromfile(f, count=num_floats, sep=" ", dtype=float)
+ poly = poly.reshape(-1,nPerLine).astype(int)
+ poly = poly[:,1:] # We remove Ns. Put it back if it's an important data
+ info.polygons = poly
+ else:
+ raise NotImplementedError('Polygons binary')
+
+ else:
+ raise NotImplementedError('Section not implemented `{}`'.format(info.section))
def _read_subsection(f, info):
@@ -847,6 +792,13 @@ def _check_mesh(info):
info.points = _generate_points(axis)
info.c, info.ct = _generate_cells(dim=info.dataset["DIMENSIONS"])
+ # --- point_data_grid added for convenience, TODO, make it a property
+ info.point_data_grid = {}
+ # We provide point_data_grid, corresponds to point_data but reshaped
+ for k,PD in info.point_data.items():
+ # NOTE: tested for len(y)=1, len(z)=1 only
+ info.point_data_grid[k]=PD.reshape(len(info.xp_grid), len(info.yp_grid), len(info.zp_grid),PD.shape[1], order='F')
+
elif info.dataset["type"] == "RECTILINEAR_GRID":
axis = [
info.dataset["X_COORDINATES"],
@@ -1159,50 +1111,16 @@ def translate_cells(data, types, cell_data_raw):
return cells, cell_data
-
-def write(filename, mesh, binary=True):
- def pad(array):
- return numpy.pad(array, ((0, 0), (0, 1)), "constant")
-
- if mesh.points.shape[1] == 2:
- points = pad(mesh.points)
+def write_vtk(filename, info, binary=False):
+ if info.dataset['type']=='POLYDATA':
+ write_dataset_polydata(info.filename, points=info.points, polygons=info.polygons, point_data=info.point_data, cell_data=info.cell_data, binary=binary, header=info.header)
+ elif info.dataset['type']=='UNSTRUCTURED_GRID':
+ write_dataset_unstructured_grid(info.filename, points=info.points, cells=info.cells, point_data=info.point_data, cell_data=info.cell_data, binary=binary)
else:
- points = mesh.points
-
- if mesh.point_data:
- for name, values in mesh.point_data.items():
- if len(values.shape) == 2 and values.shape[1] == 2:
- mesh.point_data[name] = pad(values)
-
- for name, data in mesh.cell_data.items():
- for k, values in enumerate(data):
- if len(values.shape) == 2 and values.shape[1] == 2:
- data[k] = pad(data[k])
-
- with open(filename, "wb") as f:
- f.write(b"# vtk DataFile Version 4.2\n")
- f.write("written \n".encode("utf-8"))
- f.write(("BINARY\n" if binary else "ASCII\n").encode("utf-8"))
- f.write(b"DATASET UNSTRUCTURED_GRID\n")
-
- # write points and cells
- _write_points(f, points, binary)
- _write_cells(f, mesh.cells, binary)
-
- # write point data
- if mesh.point_data:
- num_points = mesh.points.shape[0]
- f.write("POINT_DATA {}\n".format(num_points).encode("utf-8"))
- _write_field_data(f, mesh.point_data, binary)
-
- # write cell data
- if mesh.cell_data:
- total_num_cells = sum(len(c.data) for c in mesh.cells)
- f.write("CELL_DATA {}\n".format(total_num_cells).encode("utf-8"))
- _write_field_data(f, mesh.cell_data, binary)
-
+ raise NotImplementedError('Write function for DATASET TYPE: `{}`'.format(info.dataset['type']))
def _write_points(f, points, binary):
+ points = np.asarray(points)
f.write(
"POINTS {} {}\n".format(
len(points), numpy_to_vtk_dtype[points.dtype.name]
@@ -1218,7 +1136,10 @@ def _write_points(f, points, binary):
points.astype(points.dtype.newbyteorder(">")).tofile(f, sep="")
else:
# ascii
- points.tofile(f, sep=" ")
+ #points.tofile(f, sep=" ")
+ for point in points:
+ np.asarray(point).tofile(f, sep=" ")
+ f.write(b"\n")
f.write(b"\n")
@@ -1274,6 +1195,24 @@ def _write_cells(f, cells, binary):
f.write(b"\n")
+def _write_polygons(f, polygons, binary):
+
+ polyN = [len(p) for p in polygons]
+ nFloats = np.sum(polyN) + len(polygons)
+ f.write(("POLYGONS {} {}\n".format(len(polygons), nFloats)).encode("utf-8"))
+ poly_list = [ [pn]+ list(polygon) for pn,polygon in zip(polyN, polygons)]
+ if binary:
+ print('>>>')
+#> endian
+# points.astype(points.dtype.newbyteorder(">")).tofile(f, sep="")
+ else:
+ # ascii
+ for poly in poly_list:
+ np.asarray(poly).tofile(f, sep=" ")
+ f.write(b"\n")
+ f.write(b"\n")
+
+
def _write_field_data(f, data, binary):
f.write(("FIELD FieldData {}\n".format(len(data))).encode("utf-8"))
for name, values in data.items():
diff --git a/weio/wetb/hawc2/Hawc2io.py b/weio/wetb/hawc2/Hawc2io.py
index 192ab50..4484bae 100644
--- a/weio/wetb/hawc2/Hawc2io.py
+++ b/weio/wetb/hawc2/Hawc2io.py
@@ -27,17 +27,6 @@
* add error handling for allmost every thing
"""
-from __future__ import division
-from __future__ import print_function
-from __future__ import unicode_literals
-from __future__ import absolute_import
-from builtins import int
-from builtins import range
-from io import open as opent
-from builtins import str
-from future import standard_library
-standard_library.install_aliases()
-from builtins import object
import numpy as np
import os
@@ -80,7 +69,7 @@ def _ReadSelFile(self):
# read *.sel hawc2 output file for result info
if self.FileName.lower().endswith('.sel'):
self.FileName = self.FileName[:-4]
- fid = opent(self.FileName + '.sel', 'r')
+ fid = open(self.FileName + '.sel', 'r')
Lines = fid.readlines()
fid.close()
if Lines[0].lower().find('bhawc')>=0:
diff --git a/weio/wetb/hawc2/__init__.py b/weio/wetb/hawc2/__init__.py
index 1cd84f9..fde8868 100644
--- a/weio/wetb/hawc2/__init__.py
+++ b/weio/wetb/hawc2/__init__.py
@@ -1,18 +1,12 @@
-# from __future__ import unicode_literals
-# from __future__ import print_function
-# from __future__ import division
-# from __future__ import absolute_import
-# from future import standard_library
-# standard_library.install_aliases()
-# d = None
-# d = dir()
-#
-# from .htc_file import HTCFile
-# from .log_file import LogFile
-# from .ae_file import AEFile
-# from .at_time_file import AtTimeFile
-# from .pc_file import PCFile
-# from . import shear_file
-# from .st_file import StFile
-#
-# __all__ = sorted([m for m in set(dir()) - set(d)])
+# d = None
+# d = dir()
+#
+# from .htc_file import HTCFile
+# from .log_file import LogFile
+# from .ae_file import AEFile
+# from .at_time_file import AtTimeFile
+# from .pc_file import PCFile
+# from . import shear_file
+# from .st_file import StFile
+#
+# __all__ = sorted([m for m in set(dir()) - set(d)])
diff --git a/weio/wetb/hawc2/htc_file.py b/weio/wetb/hawc2/htc_file.py
index 6848a7e..ca3237c 100644
--- a/weio/wetb/hawc2/htc_file.py
+++ b/weio/wetb/hawc2/htc_file.py
@@ -1,592 +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:
- raise ValueError(
- "Modelpath cannot be autodetected for '%s'.\nInput files not found near htc file" % self.filename)
-
- 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)
- 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)
- else:
- filename = os.path.join(self.modelpath, 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())
- import pdb; pdb.set_trace()
-# 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/weio/yaml_file.py b/weio/yaml_file.py
new file mode 100644
index 0000000..bd24b0c
--- /dev/null
+++ b/weio/yaml_file.py
@@ -0,0 +1,211 @@
+"""
+Input/output class for YAML files, with special logic for wind turbine and polar data.
+"""
+import numpy as np
+import pandas as pd
+import os
+import yaml
+
+try:
+ from .file import File, WrongFormatError, BrokenFormatError, EmptyFileError
+except:
+ File = dict
+ EmptyFileError = type('EmptyFileError', (Exception,),{})
+ WrongFormatError = type('WrongFormatError', (Exception,),{})
+ BrokenFormatError = type('BrokenFormatError', (Exception,),{})
+
+class YAMLFile(File):
+ """
+ Read/write a YAML file. The object behaves as a dictionary.
+
+ Main methods
+ ------------
+ - read, write, toDataFrame, keys
+
+ Examples
+ --------
+ f = YAMLFile('file.yaml')
+ print(f.keys())
+ dfs = f.toDataFrame()
+ print(dfs)
+ """
+
+ @staticmethod
+ def defaultExtensions():
+ return ['.yaml', '.yml']
+
+ @staticmethod
+ def formatName():
+ return 'YAML file'
+
+ @staticmethod
+ def priority(): return 60
+
+ def __init__(self, filename=None, **kwargs):
+ self.filename = filename
+ if filename:
+ self.read(**kwargs)
+
+ 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)
+
+ with open(self.filename, 'r', encoding='utf-8') as f:
+ data = yaml.safe_load(f)
+ self.clear()
+ self.update(data) # We are a dictionary-like object
+
+ def write(self, filename=None):
+ if filename:
+ self.filename = filename
+ if not self.filename:
+ raise Exception('No filename provided')
+
+ with open(self.filename, 'w', encoding='utf-8') as f:
+ yaml.dump(dict(self), f, default_flow_style=False)
+
+
+
+ def toDataFrame(self):
+ """
+ Loops through all keys in the YAML file, detects 1D and 2D arrays,
+ and combines all 1D arrays with the same length into a DataFrame.
+ Column names are made unique using the minimal chain of keys necessary.
+ The DataFrame key is based on the common parent of the arrays being combined.
+ Returns a dictionary of DataFrames.
+ """
+ import collections
+
+ def is_1d_array(val):
+ return isinstance(val, (list, np.ndarray)) and all(isinstance(x, (int, float)) for x in val)
+
+ def is_2d_array(val):
+ return (
+ isinstance(val, (list, np.ndarray))
+ and len(val) > 0
+ and isinstance(val[0], (list, np.ndarray))
+ and all(isinstance(row, (list, np.ndarray)) for row in val)
+ )
+
+ # Recursively walk the dict, collecting arrays and their key chains
+ arrays_1d = []
+ arrays_2d = []
+
+ def walk(d, chain=None):
+ if chain is None:
+ chain = []
+ if isinstance(d, dict):
+ for k, v in d.items():
+ walk(v, chain + [k])
+ elif is_1d_array(d):
+ arrays_1d.append((tuple(chain), d))
+ elif is_2d_array(d):
+ arrays_2d.append((tuple(chain), d))
+
+ walk(self)
+
+ # Group 1D arrays by their length
+ length_map = collections.defaultdict(list)
+ for chain, arr in arrays_1d:
+ length_map[len(arr)].append((chain, arr))
+
+ dfs = {}
+ parent_counter = collections.defaultdict(int)
+ for arrlen, arrlist in length_map.items():
+ if arrlen == 0 or len(arrlist) < 2:
+ continue # skip empty or single columns
+
+ # Find minimal unique suffix for each chain (for column names)
+ all_chains = [chain for chain, arr in arrlist]
+ min_suffixes = {}
+ for i, chain in enumerate(all_chains):
+ for n in range(1, len(chain)+1):
+ suffix = chain[-n:]
+ if sum([other[-n:] == suffix for other in all_chains]) == 1:
+ min_suffixes[chain] = suffix
+ break
+ else:
+ min_suffixes[chain] = chain # fallback
+
+ # Find common parent for all chains
+ def common_prefix(chains):
+ if not chains:
+ return ()
+ min_len = min(len(c) for c in chains)
+ prefix = []
+ for i in range(min_len):
+ vals = set(c[i] for c in chains)
+ if len(vals) == 1:
+ prefix.append(chains[0][i])
+ else:
+ break
+ return tuple(prefix)
+ parent = common_prefix(all_chains)
+ parent_str = "_".join(str(k) for k in parent) if parent else "root"
+ parent_counter[parent_str] += 1
+ key = parent_str
+ if parent_counter[parent_str] > 1:
+ key = f"{parent_str}_{parent_counter[parent_str]}"
+
+ # Build DataFrame dict
+ df_dict = {}
+ for chain, arr in arrlist:
+ colname = "_".join(str(k) for k in min_suffixes[chain])
+ df_dict[colname] = arr
+ if df_dict:
+ # --- Special logic for "_grid" columns ---
+ grid_cols = [c for c in df_dict if c.endswith("_grid")]
+ if grid_cols and len(grid_cols) > 1:
+ # Check if all grid columns have the same content
+ first_grid = df_dict[grid_cols[0]]
+ if all(np.array_equal(df_dict[c], first_grid) for c in grid_cols[1:]):
+ # Remove all grid columns
+ for c in grid_cols:
+ df_dict.pop(c)
+ # Insert 'grid' as first column
+ df_dict = {'grid': first_grid, **df_dict}
+ # --- Remove "_values" suffix from columns ---
+ new_df_dict = {}
+ for c in df_dict:
+ if c.endswith("_values"):
+ new_c = c[:-7]
+ else:
+ new_c = c
+ new_df_dict[new_c] = df_dict[c]
+ df_dict = new_df_dict
+ dfs[key] = pd.DataFrame(df_dict)
+
+ # Add 2D arrays as their own DataFrames
+ for chain, arr in arrays_2d:
+ ncols = len(arr[0])
+ colnames = []
+ for i in range(ncols):
+ colname = "_".join(str(k) for k in chain + (f"col{i+1}",))
+ colnames.append(colname)
+ df = pd.DataFrame(arr, columns=colnames)
+ key = "_".join(str(k) for k in chain) + "_2d"
+ dfs[key] = df
+
+ if not dfs:
+ return None
+ return dfs
+
+ def __repr__(self):
+ s = '<{} object>:\n'.format(type(self).__name__)
+ s += '|Main attributes:\n'
+ s += '| - filename: {}\n'.format(self.filename)
+ s += '|Main keys:\n'
+ for k in self.keys():
+ s += '| - {}\n'.format(k)
+ s += '|Main methods:\n'
+ s += '| - read, write, toDataFrame, keys'
+ return s
+
+ def toString(self):
+ return yaml.dump(dict(self), default_flow_style=False)
\ No newline at end of file