diff --git a/.github/scripts/build_windows_executables.bat b/.github/scripts/build_windows_executables.bat new file mode 100644 index 0000000000..70257fcb69 --- /dev/null +++ b/.github/scripts/build_windows_executables.bat @@ -0,0 +1,42 @@ +@call "C:\Program Files (x86)\Intel\oneAPI\setvars-vcvarsall.bat" %VS_VER% + +for /f "tokens=* usebackq" %%f in (`dir /b "C:\Program Files (x86)\Intel\oneAPI\compiler\" ^| findstr /V latest ^| sort`) do @set "LATEST_VERSION=%%f" +@call "C:\Program Files (x86)\Intel\oneAPI\compiler\%LATEST_VERSION%\env\vars.bat" + +@REM Make the script that generates the git version description ignore dirty +@REM since building the Visual Studio projects modifies files +powershell -command "(Get-Content -Path '.\vs-build\CreateGitVersion.bat') -replace '--dirty', '' | Set-Content -Path '.\vs-build\CreateGitVersion.bat'" + +echo on + +@REM Build all solutions +devenv vs-build/AeroDisk/AeroDisk_Driver.sln /Build "Release|x64" +devenv vs-build/AeroDyn/AeroDyn_Driver.sln /Build "Release|x64" +devenv vs-build/AeroDyn/AeroDyn_Driver.sln /Build "Release_OpenMP|x64" +devenv vs-build/AeroDyn_Inflow_c_binding/AeroDyn_Inflow_c_binding.sln /Build "Release|x64" +devenv vs-build/AeroDyn_Inflow_c_binding/AeroDyn_Inflow_c_binding.sln /Build "Release_OpenMP|x64" +devenv vs-build/BeamDyn/BeamDyn-w-registry.sln /Build "Release|x64" +devenv vs-build/Discon/Discon.sln /Build "Release|x64" +devenv vs-build/FAST-farm/FAST-Farm.sln /Build "Release|x64" +devenv vs-build/FAST-farm/FAST-Farm.sln /Build "Release_OpenMP|x64" +devenv vs-build/HydroDyn/HydroDynDriver.sln /Build "Release|x64" +devenv vs-build/HydroDyn_c_binding/HydroDyn_c_binding.sln /Build "Release|x64" +devenv vs-build/InflowWind_c_binding/InflowWind_c_binding.sln /Build "Release|x64" +devenv vs-build/InflowWind/InflowWind_driver.sln /Build "Release|x64" +devenv vs-build/InflowWind/InflowWind_driver.sln /Build "Release_OpenMP|x64" +devenv vs-build/MoorDyn/MoorDynDriver.sln /Build "Release|x64" +devenv vs-build/MoorDyn_c_binding/MoorDyn_c_binding.sln /Build "Release|x64" +devenv vs-build/FAST/FAST.sln /Build "Release|x64" +devenv vs-build/SeaState/SeaStateDriver.sln /Build "Release|x64" +devenv vs-build/SimpleElastoDyn/SimpleElastoDyn_Driver.sln /Build "Release|x64" +devenv vs-build/SubDyn/SubDyn.sln /Build "Release|x64" +devenv vs-build/TurbSim/TurbSim.vfproj /Build "Release|x64" +devenv vs-build/UnsteadyAero/UnsteadyAero.sln /Build "Release|x64" + +@REM Build MATLAB solution last +devenv vs-build/FAST/FAST.sln /Build "Release_Matlab|x64" + +@REM Copy controllers to bin directory +xcopy .\reg_tests\r-test\glue-codes\openfast\5MW_Baseline\ServoData\*.dll .\build\bin\ /y + +exit /b %ERRORLEVEL% diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 87240dbce4..adf3513704 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,11 +11,20 @@ on: types: - released + push: + paths-ignore: + - 'LICENSE' + - 'README.rst' + - 'docs/**' + - 'share/**' jobs: + + # Disabled as publish-to-pypi is working correctly publish-to-pypi-test: runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' + if: false + # if: github.event_name == 'workflow_dispatch' steps: - uses: actions/checkout@v3 @@ -44,7 +53,7 @@ jobs: publish-to-pypi: runs-on: ubuntu-latest - if: github.event_name == 'release' + if: github.event_name == 'release' steps: - uses: actions/checkout@v3 @@ -71,9 +80,11 @@ jobs: run: hatch publish working-directory: openfast_io + # Disabled as it fails due to huge memory requirements docker-build-and-push: runs-on: ubuntu-latest - if: github.event_name == 'release' + if: false + # if: github.event_name == 'release' timeout-minutes: 500 env: DOCKERFILE_PATH: share/docker/Dockerfile @@ -123,3 +134,87 @@ jobs: push: true cache-from: type=gha cache-to: type=gha,mode=max + + build-windows-executables: + runs-on: windows-2022 + if: github.event_name == 'workflow_dispatch' || github.event_name == 'release' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 0 + + - name: Install Intel oneAPI BaseKit (Windows) + shell: cmd + env: + URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/7dff44ba-e3af-4448-841c-0d616c8da6e7/w_BaseKit_p_2024.1.0.595_offline.exe + COMPONENTS: intel.oneapi.win.mkl.devel + run: | + curl.exe --output %TEMP%\webimage.exe --url %URL% --retry 5 --retry-delay 5 + start /b /wait %TEMP%\webimage.exe -s -x -f webimage_extracted --log extract.log + del %TEMP%\webimage.exe + webimage_extracted\bootstrapper.exe -s --action install --components=%COMPONENTS% --eula=accept -p=NEED_VS2017_INTEGRATION=0 -p=NEED_VS2019_INTEGRATION=0 -p=NEED_VS2022_INTEGRATION=1 --log-dir=. + set installer_exit_code=%ERRORLEVEL% + rd /s/q "webimage_extracted" + exit /b %installer_exit_code% + + - name: Install Intel oneAPI HPCKit (Windows) + shell: cmd + env: + URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/c95a3b26-fc45-496c-833b-df08b10297b9/w_HPCKit_p_2024.1.0.561_offline.exe + COMPONENTS: intel.oneapi.win.ifort-compiler + run: | + curl.exe --output %TEMP%\webimage.exe --url %URL% --retry 5 --retry-delay 5 + start /b /wait %TEMP%\webimage.exe -s -x -f webimage_extracted --log extract.log + del %TEMP%\webimage.exe + webimage_extracted\bootstrapper.exe -s --action install --components=%COMPONENTS% --eula=accept -p=NEED_VS2017_INTEGRATION=0 -p=NEED_VS2019_INTEGRATION=0 -p=NEED_VS2022_INTEGRATION=1 --log-dir=. + set installer_exit_code=%ERRORLEVEL% + rd /s/q "webimage_extracted" + exit /b %installer_exit_code% + + - name: Set up MATLAB + id: setup-matlab + uses: matlab-actions/setup-matlab@v2 + with: + products: Simulink + + - name: Build Executables + env: + MATLAB_ROOT: ${{ steps.setup-matlab.outputs.matlabroot }} + run: .github/scripts/build_windows_executables.bat + + - name: Test version output + run: | + build/bin/openfast_x64.exe -h + build/bin/TurbSim_x64.exe -h + build/bin/FAST.Farm_x64.exe -h + + - name: Build MATLAB Mex File + uses: matlab-actions/run-command@v2 + with: + command: | + mexname = 'FAST_SFunc'; + mex('-largeArrayDims', ... + '-v', ... + ['-L' fullfile('build','bin')], ... + ['-I' fullfile('modules','openfast-library','src')], ... % "FAST_Library.h" + ['-I' fullfile('modules','supercontroller','src')], ... % "SuperController_Types.h" + ['-I' fullfile('modules','externalinflow','src')], ... % "ExternalInflow_Types.h" + ['-I' fullfile('modules','extloads','src')], ... % "ExtLoadsDX_Types.h" + ['-I' fullfile(matlabroot,'simulink','include')], ... + ['-I' fullfile(matlabroot,'extern','include')], ... + ['COMPFLAGS=$COMPFLAGS -MT -DS_FUNCTION_NAME=' mexname], ... + '-lOpenFAST-Simulink_x64', ... + '-outdir', fullfile('build','bin'), ... + '-output', mexname, ... + fullfile('glue-codes','simulink','src','FAST_SFunc.c')); + + - name: Upload executables + uses: actions/upload-artifact@v4 + with: + name: openfast-binaries + path: | + build/bin/*.exe + build/bin/*.dll + build/bin/*.mexw64 diff --git a/docs/changelogs/v4.0.4.md b/docs/changelogs/v4.0.4.md new file mode 100644 index 0000000000..6dcc91776b --- /dev/null +++ b/docs/changelogs/v4.0.4.md @@ -0,0 +1,94 @@ +**Feature or improvement description** +Pull request to merge `rc-4.0.4` into `main` and create a tagged release for v4.0.4 + +See the milestone and project pages for additional information + + https://github.com/OpenFAST/openfast/milestone/20 + +Test results, if applicable +See GitHub Actions + +### Release checklist: +- [ ] Update the documentation version in docs/conf.py +- [ ] Update the versions in docs/source/user/api\_change.rst +- [ ] Update version info in openfast\_io/pyproject.toml +- [ ] Verify readthedocs builds correctly +- [ ] Create an annotated tag in OpenFAST during merge (mark as most recent if necessary) +- [ ] Create a merge commit in r-test and add a corresponding annotated tag +- [ ] Upload Docker image +- [ ] Compile executables for Windows builds + - [ ] `AeroDisk_Driver_x64.exe` + - [ ] `AeroDyn_Driver_x64.exe` + - [ ] `AeroDyn_Driver_x64_OpenMP.exe` + - [ ] `AeroDyn_Inflow_c_binding_x64.dll` + - [ ] `AeroDyn_Inflow_c_binding_x64_OpenMP.dll` + - [ ] `BeamDyn_Driver_x64.exe` + - [ ] `DISCON.dll (x64)` + - [ ] `DISCON_ITIBarge.dll (x64)` + - [ ] `DISCON_OC3Hywind.dll (x64)` + - [ ] `DISCON_SC.dll (x64)` + - [ ] `FAST.Farm_x64.exe` + - [ ] `FAST.Farm_x64_OMP.exe` + - [ ] `FAST_SFunc.mexw64` + - [ ] `HydroDynDriver_x64.exe` + - [ ] `HydroDyn_C_Binding_x64.dll` + - [ ] `IinflowWind_c_binding_x64.dll` + - [ ] `InflowWind_Driver_x64.exe` + - [ ] `InflowWind_Driver_x64_OpenMP.exe` + - [ ] `MoorDyn_Driver_x64.exe` + - [ ] `MoorDyn_c_binding_x64.dll` + - [ ] `OpenFAST-Simulink_x64.dll` + - [ ] `openfast_x64.exe` + - [ ] `SeaStateDriver_x64.exe` + - [ ] `SimpleElastoDyn_x64.exe` + - [ ] `SubDyn_x64.exe` + - [ ] `Turbsim_x64.exe` + - [ ] `UnsteadyAero_x64.exe` + +# Changelog + +## Overview + +This release includes several bug fixes and improvements for _OpenFAST_, GitHub actions, and _openfast\_io_. + +## General + +### CMake build system + +### GitHub actions + +#2778 Backport of GitHub Action to build windows executables on release (@deslaughter) -- backport of #2636 + + +### openfast_io + +#2779 MD: Backport of PR #2658 -- openfast\_IO MoorDyn compatibility (@RyanDavies19) + +#2777 bug fix #2762 (@mayankchetan) + +#2658 openfast\_IO MoorDyn compatibilityModule (@RyanDavies19) + + + +## Solvers + +### Simulink + +#2785 Avoid ending program when called as a shared library (@bjonkman) -- backport of #2671 + + + +## Module changes + +### OpenFAST library + +#2780 backport of #2760: bug-fix: Adjust steady-state solver small angle assumptions (@bjonkman) + +#2776 Fix FAST\_ExtInfw\_Restart APIC++ API (@marchdf) + + + +## Input file changes + +No input file changes since v4.0.0 + diff --git a/docs/conf.py b/docs/conf.py index bb9d65f674..6e5c983e0f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -138,7 +138,7 @@ def runDoxygen(sourcfile, doxyfileIn, doxyfileOut): # The short X.Y version. version = u'4.0' # The full version, including alpha/beta/rc tags. -release = u'v4.0.3' +release = u'v4.0.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/user/api_change.rst b/docs/source/user/api_change.rst index 06492245a2..054ac176ea 100644 --- a/docs/source/user/api_change.rst +++ b/docs/source/user/api_change.rst @@ -10,6 +10,12 @@ The line number corresponds to the resulting line number after all changes are i Thus, be sure to implement each in order so that subsequent line numbers are correct. +OpenFAST v4.0.3 to OpenFAST v4.0.4 +---------------------------------- + +No input file changes were made. + + OpenFAST v4.0.2 to OpenFAST v4.0.3 ---------------------------------- diff --git a/glue-codes/openfast-cpp/src/OpenFAST.cpp b/glue-codes/openfast-cpp/src/OpenFAST.cpp index 33a534f8dc..b064c29bee 100644 --- a/glue-codes/openfast-cpp/src/OpenFAST.cpp +++ b/glue-codes/openfast-cpp/src/OpenFAST.cpp @@ -642,7 +642,6 @@ void fast::OpenFAST::init() { tmpRstFileRoot, &AbortErrLev, &turbineData[iTurb].dt, - &turbineData[iTurb].inflowType, &turbineData[iTurb].numBlades, &turbineData[iTurb].numVelPtsBlade, &turbineData[iTurb].numVelPtsTwr, diff --git a/modules/moordyn/src/MoorDyn.f90 b/modules/moordyn/src/MoorDyn.f90 index 80d21a7810..0e1f1ae812 100644 --- a/modules/moordyn/src/MoorDyn.f90 +++ b/modules/moordyn/src/MoorDyn.f90 @@ -34,7 +34,7 @@ MODULE MoorDyn PRIVATE - TYPE(ProgDesc), PARAMETER :: MD_ProgDesc = ProgDesc( 'MoorDyn', 'v2.2.2', '2024-01-16' ) + TYPE(ProgDesc), PARAMETER :: MD_ProgDesc = ProgDesc( 'MoorDyn', 'v2.3.8', '2025-02-27' ) INTEGER(IntKi), PARAMETER :: wordy = 0 ! verbosity level. >1 = more console output diff --git a/modules/moordyn/src/MoorDyn_IO.f90 b/modules/moordyn/src/MoorDyn_IO.f90 index 4eb8a6e2a0..4b32b2a898 100644 --- a/modules/moordyn/src/MoorDyn_IO.f90 +++ b/modules/moordyn/src/MoorDyn_IO.f90 @@ -1583,7 +1583,7 @@ SUBROUTINE MDIO_WriteOutputs( Time, p, m, y, ErrStat, ErrMsg ) if ( p%NumOuts > 0_IntKi .and. p%MDUnOut > 0 ) then ! Write the output parameters to the file - Frmt = '(F10.4,'//TRIM(Int2LStr(p%NumOuts))//'(A1,ES15.7E2))' ! should evenutally use user specified format? + Frmt = '(F10.4,'//TRIM(Int2LStr(p%NumOuts))//'(A1,ES15.7))' ! should evenutally use user specified format? WRITE(p%MDUnOut,Frmt) Time, ( p%Delim, y%WriteOutput(I), I=1,p%NumOuts ) END IF @@ -1605,9 +1605,9 @@ SUBROUTINE MDIO_WriteOutputs( Time, p, m, y, ErrStat, ErrMsg ) + m%LineList(I)%N*SUM(m%LineList(I)%OutFlagList(10:18)) if (m%LineList(I)%OutFlagList(2) == 1) THEN ! if node positions are included, make them using a float format for higher precision - Frmt = '(F10.4,'//TRIM(Int2LStr(3*(m%LineList(I)%N + 1)))//'(A1,ES15.7E2),'//TRIM(Int2LStr(LineNumOuts - 3*(m%LineList(I)%N - 1)))//'(A1,ES15.7E2))' + Frmt = '(F10.4,'//TRIM(Int2LStr(3*(m%LineList(I)%N + 1)))//'(A1,ES15.7),'//TRIM(Int2LStr(LineNumOuts - 3*(m%LineList(I)%N - 1)))//'(A1,ES15.7))' else - Frmt = '(F10.4,'//TRIM(Int2LStr(LineNumOuts))//'(A1,ES15.7E2))' ! should evenutally use user specified format? + Frmt = '(F10.4,'//TRIM(Int2LStr(LineNumOuts))//'(A1,ES15.7))' ! should evenutally use user specified format? end if L = 1 ! start of index of line output file at first entry 12345.7890 @@ -1755,7 +1755,7 @@ SUBROUTINE MDIO_WriteOutputs( Time, p, m, y, ErrStat, ErrMsg ) + m%RodList(I)%N*SUM(m%RodList(I)%OutFlagList(12:18)) - Frmt = '(F10.4,'//TRIM(Int2LStr(RodNumOuts))//'(A1,ES15.7E2))' ! should evenutally use user specified format? + Frmt = '(F10.4,'//TRIM(Int2LStr(RodNumOuts))//'(A1,ES15.7))' ! should evenutally use user specified format? L = 1 ! start of index of line output file at first entry diff --git a/modules/openfast-library/src/FAST_Library.h b/modules/openfast-library/src/FAST_Library.h index 4fc1e3124b..744d845063 100644 --- a/modules/openfast-library/src/FAST_Library.h +++ b/modules/openfast-library/src/FAST_Library.h @@ -16,7 +16,7 @@ EXTERNAL_ROUTINE void FAST_AllocateTurbines(int * iTurb, int *ErrStat, char *ErrMsg); EXTERNAL_ROUTINE void FAST_DeallocateTurbines(int *ErrStat, char *ErrMsg); -EXTERNAL_ROUTINE void FAST_ExtInfw_Restart(int * iTurb, const char *CheckpointRootName, int *AbortErrLev, double * dt, int * InflowType, +EXTERNAL_ROUTINE void FAST_ExtInfw_Restart(int * iTurb, const char *CheckpointRootName, int *AbortErrLev, double * dt, int * NumBl, int * NumBlElem, int * NumTwrElem, int * n_t_global, ExtInfw_InputType_t* ExtInfw_Input, ExtInfw_OutputType_t* ExtInfw_Output, SC_DX_InputType_t* SC_DX_Input, SC_DX_OutputType_t* SC_DX_Output, int *ErrStat, char *ErrMsg); diff --git a/modules/openfast-library/src/FAST_SS_Solver.f90 b/modules/openfast-library/src/FAST_SS_Solver.f90 index f4ea398e61..ffd64c979a 100644 --- a/modules/openfast-library/src/FAST_SS_Solver.f90 +++ b/modules/openfast-library/src/FAST_SS_Solver.f90 @@ -586,8 +586,11 @@ SUBROUTINE SteadyStateSolve_Residual(caseData, p_FAST, y_FAST, m_FAST, ED, BD, A INTEGER(IntKi) , INTENT( OUT) :: ErrStat !< Error status of the operation CHARACTER(*) , INTENT( OUT) :: ErrMsg !< Error message if ErrStat /= ErrID_None + REAL(R8Ki) :: Orientation(3,3) + INTEGER(IntKi) :: k, node INTEGER(IntKi) :: ErrStat2 INTEGER(IntKi) :: Indx_u_start + INTEGER(IntKi) :: Indx_u_angle_start(p_FAST%NumBl_Lin) CHARACTER(ErrMsgLen) :: ErrMsg2 CHARACTER(*), PARAMETER :: RoutineName = 'SteadyStateSolve_Residual' @@ -608,12 +611,23 @@ SUBROUTINE SteadyStateSolve_Residual(caseData, p_FAST, y_FAST, m_FAST, ED, BD, A !.................. ! Pack the output "residual vector" with these state derivatives and new inputs: !.................. - CALL Create_SS_Vector( p_FAST, y_FAST, U_Resid, AD, ED, BD, InputIndex, STATE_PRED ) + CALL Create_SS_Vector( p_FAST, y_FAST, U_Resid, AD, ED, BD, InputIndex, STATE_PRED, Indx_u_angle_start ) ! Make the inputs a residual (subtract from previous inputs) Indx_u_start = y_FAST%Lin%Glue%SizeLin(LIN_ContSTATE_COL) + 1 U_Resid(Indx_u_start : ) = u_in(Indx_u_start : ) - U_Resid(Indx_u_start : ) + ! we need to make a special case for the orientation matrices + do k=1,p_FAST%NumBl_Lin + Indx_u_start = Indx_u_angle_start(k) + do node=1, AD%Input(InputIndex)%rotors(1)%BladeMotion(k)%NNodes + Orientation = EulerConstruct( u_in( Indx_u_start:Indx_u_start+2 ) ) + Orientation = MATMUL(TRANSPOSE( AD%Input(InputIndex)%rotors(1)%BladeMotion(k)%Orientation(:,:,node)), Orientation) + U_Resid(Indx_u_start:Indx_u_start+2) = EulerExtract(Orientation) + Indx_u_start = Indx_u_start + 3 + end do + end do + END SUBROUTINE SteadyStateSolve_Residual !---------------------------------------------------------------------------------------------------------------------------------- !> This subroutine saves the current states so they can be used to compute the residual. @@ -794,7 +808,7 @@ END SUBROUTINE Precondition_Jmat !---------------------------------------------------------------------------------------------------------------------------------- !> This routine basically packs the relevant parts of the modules' inputs and states for use in the steady-state solver. -SUBROUTINE Create_SS_Vector( p_FAST, y_FAST, u, AD, ED, BD, InputIndex, StateIndex ) +SUBROUTINE Create_SS_Vector( p_FAST, y_FAST, u, AD, ED, BD, InputIndex, StateIndex, IndxOrientStart ) !.................................................................................................................................. TYPE(FAST_ParameterType) , INTENT(IN ) :: p_FAST !< Glue-code simulation parameters TYPE(FAST_OutputFileType), INTENT(IN ) :: y_FAST !< Output variables for the glue code @@ -804,6 +818,7 @@ SUBROUTINE Create_SS_Vector( p_FAST, y_FAST, u, AD, ED, BD, InputIndex, StateInd TYPE(AeroDyn_Data), INTENT(INOUT) :: AD !< AeroDyn data INTEGER(IntKi), INTENT(IN ) :: InputIndex INTEGER(IntKi), INTENT(IN ) :: StateIndex + INTEGER(IntKi), optional, INTENT( OUT) :: IndxOrientStart(p_FAST%NumBl_Lin) ! local variables: INTEGER :: n @@ -904,8 +919,9 @@ SUBROUTINE Create_SS_Vector( p_FAST, y_FAST, u, AD, ED, BD, InputIndex, StateInd end do end do + if (PRESENT(IndxOrientStart)) IndxOrientStart(k) = n ! keep track of index for AD orientation do node = 1, AD%Input(InputIndex)%rotors(1)%BladeMotion(k)%NNodes - CALL DCM_LogMap( AD%Input(InputIndex)%rotors(1)%BladeMotion(k)%Orientation(:,:,node), u(n:n+2), ErrStat2, ErrMsg2 ) + u(n:n+2) = EulerExtract( AD%Input(InputIndex)%rotors(1)%BladeMotion(k)%Orientation(:,:,node) ) n = n+3 end do diff --git a/modules/openfast-library/src/FAST_Subs.f90 b/modules/openfast-library/src/FAST_Subs.f90 index f8d11cb72a..1b0050734c 100644 --- a/modules/openfast-library/src/FAST_Subs.f90 +++ b/modules/openfast-library/src/FAST_Subs.f90 @@ -9615,12 +9615,16 @@ SUBROUTINE ExitThisProgram( p_FAST, y_FAST, m_FAST, ED, SED, BD, SrvD, AD, ADsk, SimMsg = TRIM(FAST_Ver%Name)//' encountered an error '//TRIM(SimMsg)//'.'//NewLine//' Simulation error level: '//TRIM(GetErrStr(ErrorLevel)) +#if (defined COMPILE_SIMULINK || defined COMPILE_LABVIEW) + ! When built as a shared library/dll, don't end the program. + CALL WrScr(trim(SimMsg)) +#else if (StopTheProgram) then CALL ProgAbort( trim(SimMsg), TrapErrors=.FALSE., TimeWait=3._ReKi ) ! wait 3 seconds (in case they double-clicked and got an error) else CALL WrScr(trim(SimMsg)) end if - +#endif END IF !............................................................................................................................ diff --git a/openfast_io/openfast_io/FAST_reader.py b/openfast_io/openfast_io/FAST_reader.py index c7a5db763c..4500610412 100644 --- a/openfast_io/openfast_io/FAST_reader.py +++ b/openfast_io/openfast_io/FAST_reader.py @@ -31,6 +31,22 @@ def readline_filterComments(f): read = False return line +def readline_ignoreComments(f, char = '#'): # see line 64 in NWTC_IO.f90 + """ + returns line before comment character + + Args: + f: file handle + char: comment character + + Returns: + line: content of next line in the file before comment character + """ + + line = f.readline().strip().split(char) + + return line[0] + def read_array(f,len,split_val=None,array_type=str): """ Read an array of values from a line in a file @@ -2879,13 +2895,8 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['Rod_Name'] = [] self.fst_vt['MoorDyn']['Body_ID'] = [] self.fst_vt['MoorDyn']['Rod_ID'] = [] - self.fst_vt['MoorDyn']['ChannelID'] = [] - # MoorDyn - f.readline() - f.readline() - self.fst_vt['MoorDyn']['Echo'] = bool_read(f.readline().split()[0]) data_line = f.readline() while data_line: @@ -2903,25 +2914,48 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['Diam'] = [] self.fst_vt['MoorDyn']['MassDen'] = [] self.fst_vt['MoorDyn']['EA'] = [] + self.fst_vt['MoorDyn']['NonLinearEA'] = [] self.fst_vt['MoorDyn']['BA_zeta'] = [] self.fst_vt['MoorDyn']['EI'] = [] self.fst_vt['MoorDyn']['Cd'] = [] self.fst_vt['MoorDyn']['Ca'] = [] self.fst_vt['MoorDyn']['CdAx'] = [] self.fst_vt['MoorDyn']['CaAx'] = [] - data_line = f.readline().strip().split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same + self.fst_vt['MoorDyn']['Cl'] = [] + self.fst_vt['MoorDyn']['dF'] = [] + self.fst_vt['MoorDyn']['cF'] = [] + data_line = readline_filterComments(f).split() + while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same. self.fst_vt['MoorDyn']['Name'].append(str(data_line[0])) self.fst_vt['MoorDyn']['Diam'].append(float(data_line[1])) self.fst_vt['MoorDyn']['MassDen'].append(float(data_line[2])) - self.fst_vt['MoorDyn']['EA'].append(float(data_line[3])) - self.fst_vt['MoorDyn']['BA_zeta'].append(float(data_line[4])) + self.fst_vt['MoorDyn']['EA'].append([float_read(dl) for dl in data_line[3].split('|')]) + self.fst_vt['MoorDyn']['BA_zeta'].append([float(dl) for dl in data_line[4].split('|')]) self.fst_vt['MoorDyn']['EI'].append(float(data_line[5])) self.fst_vt['MoorDyn']['Cd'].append(float(data_line[6])) self.fst_vt['MoorDyn']['Ca'].append(float(data_line[7])) self.fst_vt['MoorDyn']['CdAx'].append(float(data_line[8])) self.fst_vt['MoorDyn']['CaAx'].append(float(data_line[9])) - data_line = f.readline().strip().split() + if len(data_line) == 10: + self.fst_vt['MoorDyn']['Cl'].append(None) + self.fst_vt['MoorDyn']['dF'].append(None) + self.fst_vt['MoorDyn']['cF'].append(None) + elif (len(data_line) == 11): + self.fst_vt['MoorDyn']['Cl'].append(float(data_line[10])) + self.fst_vt['MoorDyn']['dF'].append(None) + self.fst_vt['MoorDyn']['cF'].append(None) + elif (len(data_line) == 13): + self.fst_vt['MoorDyn']['Cl'].append(float(data_line[10])) + self.fst_vt['MoorDyn']['dF'].append(float(data_line[11])) + self.fst_vt['MoorDyn']['cF'].append(float(data_line[12])) + + if type(self.fst_vt['MoorDyn']['EA'][0]) is str: + EA_file = os.path.normpath(os.path.join(os.path.dirname(moordyn_file), self.fst_vt['MoorDyn']['EA'][0])) + self.fst_vt['MoorDyn']['NonLinearEA'].append(self.read_NonLinearEA(EA_file)) + else: + self.fst_vt['MoorDyn']['NonLinearEA'].append(None) # Empty to keep track of which non-linear EA files go with what line + + data_line = readline_filterComments(f).split() data_line = ''.join(data_line) # re-join elif 'rodtypes' in data_line or 'roddictionary' in data_line: @@ -2935,7 +2969,7 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['Rod_CdEnd'] = [] self.fst_vt['MoorDyn']['Rod_CaEnd'] = [] - data_line = f.readline().strip().split() + data_line = readline_filterComments(f).split() while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same self.fst_vt['MoorDyn']['Rod_Name'].append(data_line[0]) self.fst_vt['MoorDyn']['Rod_Diam'].append(float(data_line[1])) @@ -2944,7 +2978,7 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['Rod_Ca'].append(float(data_line[4])) self.fst_vt['MoorDyn']['Rod_CdEnd'].append(float(data_line[5])) self.fst_vt['MoorDyn']['Rod_CaEnd'].append(float(data_line[6])) - data_line = f.readline().strip().split() + data_line = readline_filterComments(f).split() data_line = ''.join(data_line) # re-join for reading next section uniformly @@ -2967,7 +3001,7 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['Body_CdA'] = [] self.fst_vt['MoorDyn']['Body_Ca'] = [] - data_line = f.readline().strip().split() + data_line = readline_filterComments(f).split() while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same self.fst_vt['MoorDyn']['Body_ID'].append(int(data_line[0])) self.fst_vt['MoorDyn']['Body_Attachment'].append(data_line[1]) @@ -2983,7 +3017,7 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['Body_Volume'].append(float(data_line[11])) self.fst_vt['MoorDyn']['Body_CdA'].append([float(dl) for dl in data_line[12].split('|')]) self.fst_vt['MoorDyn']['Body_Ca'].append([float(dl) for dl in data_line[13].split('|')]) - data_line = f.readline().strip().split() + data_line = readline_filterComments(f).split() data_line = ''.join(data_line) # re-join for reading next section uniformly @@ -3002,7 +3036,7 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['Rod_NumSegs'] = [] self.fst_vt['MoorDyn']['RodOutputs'] = [] - data_line = f.readline().strip().split() + data_line = readline_filterComments(f).split() while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same self.fst_vt['MoorDyn']['Rod_ID'].append(data_line[0]) self.fst_vt['MoorDyn']['Rod_Type'].append(data_line[1]) @@ -3015,7 +3049,7 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['Zb'].append(float(data_line[8])) self.fst_vt['MoorDyn']['Rod_NumSegs'].append(int(data_line[9])) self.fst_vt['MoorDyn']['RodOutputs'].append(data_line[10]) - data_line = f.readline().strip().split() + data_line = readline_filterComments(f).split() data_line = ''.join(data_line) # re-join for reading next section uniformly elif 'points' in data_line or 'connectionproperties' in data_line or \ @@ -3033,7 +3067,7 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['V'] = [] self.fst_vt['MoorDyn']['CdA'] = [] self.fst_vt['MoorDyn']['CA'] = [] - data_line = f.readline().strip().split() + data_line = readline_filterComments(f).split() while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same self.fst_vt['MoorDyn']['Point_ID'].append(int(data_line[0])) self.fst_vt['MoorDyn']['Attachment'].append(str(data_line[1])) @@ -3044,7 +3078,7 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['V'].append(float(data_line[6])) self.fst_vt['MoorDyn']['CdA'].append(float(data_line[7])) self.fst_vt['MoorDyn']['CA'].append(float(data_line[8])) - data_line = f.readline().strip().split() + data_line = readline_filterComments(f).split() data_line = ''.join(data_line) # re-join for reading next section uniformly elif 'lines' in data_line or 'lineproperties' in data_line or 'linelist' in data_line: @@ -3059,7 +3093,7 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['UnstrLen'] = [] self.fst_vt['MoorDyn']['NumSegs'] = [] self.fst_vt['MoorDyn']['Outputs'] = [] - data_line = f.readline().strip().split() + data_line = readline_filterComments(f).split() while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same self.fst_vt['MoorDyn']['Line_ID'].append(int(data_line[0])) self.fst_vt['MoorDyn']['LineType'].append(str(data_line[1])) @@ -3068,8 +3102,27 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['UnstrLen'].append(float(data_line[4])) self.fst_vt['MoorDyn']['NumSegs'].append(int(data_line[5])) self.fst_vt['MoorDyn']['Outputs'].append(str(data_line[6])) - data_line = f.readline().strip().split() - data_line = ''.join(data_line) # re-join for reading next section uniformly + data_line = readline_filterComments(f).split() + data_line = ''.join(data_line) # re-join for reading next section uniformly + + elif 'failure' in data_line.lower(): + f.readline() + f.readline() + + self.fst_vt['MoorDyn']['Failure_ID'] = [] + self.fst_vt['MoorDyn']['Failure_Point'] = [] + self.fst_vt['MoorDyn']['Failure_Line(s)'] = [] + self.fst_vt['MoorDyn']['FailTime'] = [] + self.fst_vt['MoorDyn']['FailTen'] = [] + data_line = readline_filterComments(f).split() + while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same + self.fst_vt['MoorDyn']['Failure_ID'].append(int(data_line[0])) + self.fst_vt['MoorDyn']['Failure_Point'].append(data_line[1]) + self.fst_vt['MoorDyn']['Failure_Line(s)'].append([int(dl) for dl in data_line[2].split(',')]) + self.fst_vt['MoorDyn']['FailTime'].append(float(data_line[3])) + self.fst_vt['MoorDyn']['FailTen'].append(float(data_line[4])) + data_line = readline_filterComments(f).split() + data_line = ''.join(data_line) # re-join for reading next section uniformly elif 'control' in data_line.lower(): f.readline() @@ -3079,7 +3132,7 @@ def read_MoorDyn(self, moordyn_file): self.fst_vt['MoorDyn']['ChannelID'] = [] self.fst_vt['MoorDyn']['Lines_Control'] = [] - data_line = f.readline().strip().split() + data_line = readline_filterComments(f).split() while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same self.fst_vt['MoorDyn']['ChannelID'].append(int(data_line[0])) # Line(s) is a list of mooring lines, spaces are allowed between commas @@ -3093,46 +3146,87 @@ def read_MoorDyn(self, moordyn_file): control_lines.remove('') self.fst_vt['MoorDyn']['Lines_Control'].append(control_lines) - data_line = f.readline().strip().split() + data_line = readline_filterComments(f).split() data_line = ''.join(data_line) # re-join for reading next section uniformly + elif 'external' in data_line.lower(): + f.readline() + f.readline() + + self.fst_vt['MoorDyn']['External_ID'] = [] + self.fst_vt['MoorDyn']['Object'] = [] + self.fst_vt['MoorDyn']['Fext'] = [] + self.fst_vt['MoorDyn']['Blin'] = [] + self.fst_vt['MoorDyn']['Bquad'] = [] + self.fst_vt['MoorDyn']['CSys'] = [] + data_line = readline_filterComments(f).split() + while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same + self.fst_vt['MoorDyn']['External_ID'].append(int(data_line[0])) + self.fst_vt['MoorDyn']['Object'].append(data_line[1]) + self.fst_vt['MoorDyn']['Fext'].append([float(dl) for dl in data_line[2].split('|')]) + self.fst_vt['MoorDyn']['Blin'].append([float(dl) for dl in data_line[3].split('|')]) + self.fst_vt['MoorDyn']['Bquad'].append([float(dl) for dl in data_line[4].split('|')]) + self.fst_vt['MoorDyn']['CSys'].append(data_line[5]) + data_line = readline_filterComments(f).split() + data_line = ''.join(data_line) # re-join for reading next section uniformly elif 'options' in data_line: # MoorDyn lets options be written in any order # Solver options - self.fst_vt['MoorDyn']['options'] = [] # keep list of MoorDyn options - - string_options = ['WaterKin'] + self.fst_vt['MoorDyn']['option_values'] = [] + self.fst_vt['MoorDyn']['option_names'] = [] # keep list of MoorDyn options + self.fst_vt['MoorDyn']['option_descriptions'] = [] - data_line = f.readline().strip().split() + data_line = readline_filterComments(f).split() while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - raw_value = data_line[0] - option_name = data_line[1] - - self.fst_vt['MoorDyn']['options'].append(option_name) - if option_name in string_options: - self.fst_vt['MoorDyn'][option_name] = raw_value.strip('"') + option_value = data_line[0].upper() # MD is case insensitive + option_name = data_line[1].upper() # MD is case insensitive + if len(data_line) > 2: + option_description = ' '.join(data_line[2:]) else: - self.fst_vt['MoorDyn'][option_name] = float(raw_value) + option_description = '-' - data_line = f.readline().strip().split() + if option_name.upper() == 'WATERKIN': + self.fst_vt['MoorDyn']['WaterKin'] = option_value.strip('"') + WaterKin_file = os.path.normpath(os.path.join(os.path.dirname(moordyn_file), self.fst_vt['MoorDyn']['WaterKin'])) + self.read_WaterKin(WaterKin_file) + + self.fst_vt['MoorDyn']['option_values'].append(float_read(option_value.strip('"'))) # some options values can be strings or floats + self.fst_vt['MoorDyn']['option_names'].append(option_name) + self.fst_vt['MoorDyn']['option_descriptions'].append(option_description) + + data_line = readline_filterComments(f).split() data_line = ''.join(data_line) # re-join for reading next section uniformly elif 'outputs' in data_line: - self.read_outlist_freeForm(f,'MoorDyn') + data_line = readline_filterComments(f) + + while (data_line[0] and data_line[0][:3] != '---') and not ('END' in data_line): # OpenFAST searches for --- and END, so we'll do the same + + if '"' in data_line: + pattern = r'"?(.*?)"?' # grab only the text between quotes + data_line = re.findall(pattern, data_line)[0] + + channels = data_line.split(',') # split on commas + channels = [c.strip() for c in channels] # strip whitespace + for c in channels: + self.fst_vt['outlist']['MoorDyn'][c] = True + data_line = readline_filterComments(f) f.close() break - if 'WaterKin' in self.fst_vt['MoorDyn']['options']: - WaterKin_file = os.path.normpath(os.path.join(os.path.dirname(moordyn_file), self.fst_vt['MoorDyn']['WaterKin'])) - self.read_WaterKin(WaterKin_file) + else: # we must be in the header section, unlimited lines of text allowed here so skip until we hit the first line w/ keywords 'Line Types' or 'Line Dictionary' + data_line = f.readline() def read_WaterKin(self,WaterKin_file): - print('here') + + self.fst_vt['WaterKin']['z-depth'] = [] + self.fst_vt['WaterKin']['x-current'] = [] + self.fst_vt['WaterKin']['y-current'] = [] f = open(WaterKin_file) f.readline() @@ -3152,7 +3246,30 @@ def read_WaterKin(self,WaterKin_file): self.fst_vt['WaterKin']['Z_Grid'] = read_array(f,None,split_val='-',array_type=float) f.readline() self.fst_vt['WaterKin']['CurrentMod'] = int_read(f.readline().split()[0]) + f.readline() + f.readline() + data_line = readline_filterComments(f).split() + while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same + self.fst_vt['WaterKin']['z-depth'].append(float(data_line[0])) + self.fst_vt['WaterKin']['x-current'].append(float(data_line[1])) + self.fst_vt['WaterKin']['y-current'].append(float(data_line[2])) + data_line = readline_filterComments(f).split() + f.close() + + def read_NonLinearEA(self,Stiffness_file): # read and return the nonlinear line stiffness lookup table for a given line + + f = open(Stiffness_file) + f.readline() + f.readline() + f.readline() + NonLinearEA = {"Strain" : [], "Tension" : []} + data_line = readline_filterComments(f).split() + while data_line: + NonLinearEA['Strain'].append([float(data_line[0])]) + NonLinearEA['Tension'].append([float(data_line[1])]) + data_line = readline_filterComments(f).split() f.close() + return NonLinearEA def execute(self): @@ -3254,7 +3371,7 @@ def execute(self): bd_file1 = os.path.normpath(os.path.join(self.FAST_directory, self.fst_vt['Fst']['BDBldFile(1)'])) bd_file2 = os.path.normpath(os.path.join(self.FAST_directory, self.fst_vt['Fst']['BDBldFile(2)'])) bd_file3 = os.path.normpath(os.path.join(self.FAST_directory, self.fst_vt['Fst']['BDBldFile(3)'])) - if os.path.exists(bd_file1): + if os.path.isfile(bd_file1): # if the files are the same then we only need to read it once, need to handle the cases where we have a 2 or 1 bladed rotor # Check unique BeamDyn blade files and read only once if identical if bd_file1 == bd_file2 and bd_file1 == bd_file3: diff --git a/openfast_io/openfast_io/FAST_vars_out.py b/openfast_io/openfast_io/FAST_vars_out.py index f1c9fea3be..ea11689d3b 100644 --- a/openfast_io/openfast_io/FAST_vars_out.py +++ b/openfast_io/openfast_io/FAST_vars_out.py @@ -9577,10 +9577,12 @@ """ MoorDyn """ # THIS IS NOT A COMPLETE LIST! # the "flexible naming system" discussed on page 7-8 of the documentation is not included -# http://www.matt-hall.ca/files/MoorDyn-Users-Guide-2017-08-16.pdf +# https://moordyn.readthedocs.io/en/latest/inputs.html#id5 # also assuming that like other OpenFAST variables, it is limited to 9 output locations per veriable, i.e. FairTen1-FairTen9 # TODO: Handle the flexible outputs for moordyn. This will require a different approach than the current dictionary structure. +# Right now a hackish way is used in the reader +# Update the message in FAST_writer.write_MoorDyn() when this is finished MoorDyn = {} MoorDyn['FairTen1'] = False # (); ; diff --git a/openfast_io/openfast_io/FAST_writer.py b/openfast_io/openfast_io/FAST_writer.py index 98c6677ecd..89917a8af5 100644 --- a/openfast_io/openfast_io/FAST_writer.py +++ b/openfast_io/openfast_io/FAST_writer.py @@ -30,35 +30,49 @@ def auto_format(f, var): elif isinstance(var, float): f.write('{: 2.15e}\n'.format(var)) -def float_default_out(val): +def float_default_out(val, trim = False): """ Formatted float output when 'default' is an option args: val: value to be formatted + trim: trim the whitespace from the returned value returns: formatted value """ if type(val) is float: - return '{: 22f}'.format(val) + if trim: + return '{:.4f}'.format(val) + else: + return '{: 22f}'.format(val) else: - return '{:<22}'.format(val) + if trim: + return '{:}'.format(val) + else: + return '{:<22}'.format(val) -def int_default_out(val): +def int_default_out(val, trim = False): """ Formatted int output when 'default' is an option args: val: value to be formatted + trim: trim the whitespace from the returned value returns: formatted value """ if type(val) is float: - return '{:<22d}'.format(val) + if trim: + return '{:d}'.format(val) + else: + return '{:<22d}'.format(val) else: - return '{:<22}'.format(val) + if trim: + return '{:}'.format(val) + else: + return '{:<22}'.format(val) def get_dict(vartree, branch): """ @@ -236,7 +250,7 @@ def execute(self): self.write_MAP() elif self.fst_vt['Fst']['CompMooring'] == 3: self.write_MoorDyn() - if 'options' in self.fst_vt['MoorDyn'] and 'WaterKin' in self.fst_vt['MoorDyn']['options']: + if 'option_names' in self.fst_vt['MoorDyn'] and 'WATERKIN' in self.fst_vt['MoorDyn']['option_names']: self.write_WaterKin(os.path.join(self.FAST_runDirectory,self.fst_vt['MoorDyn']['WaterKin_file'])) # # look at if the the self.fst_vt['BeamDyn'] is an array, if so, loop through the array @@ -2334,30 +2348,35 @@ def write_MoorDyn(self): f.write('--------------------- MoorDyn Input File ------------------------------------\n') f.write('Generated with OpenFAST_IO\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['MoorDyn']['Echo'], 'Echo', '- echo the input file data (flag)\n')) + f.write('Comments from seed file have been dropped. Output Channels are not yet fully supported\n') + f.write('NOTE: MoorDyn does not use ECHO, instead use WriteLog of 0, 1, 2, or 3 in the options list \n') f.write('----------------------- LINE TYPES ------------------------------------------\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['Name', 'Diam', 'MassDen', 'EA', 'BA/-zeta', 'EI', 'Cd', 'Ca', 'CdAx', 'CaAx']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)', '(m)', '(kg/m)', '(N)', '(N-s/-)', '(-)', '(-)', '(-)', '(-)', '(-)']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['Name', 'Diam', 'MassDen', 'EA', 'BA/-zeta', 'EI', 'Cd', 'Ca', 'CdAx', 'CaAx', 'Cl (optional)', 'dF (optional)', 'cF (optional)']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['(-)', '(m)', '(kg/m)', '(N)', '(N-s/-)', '(N-m^2)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)']])+'\n') for i in range(len(self.fst_vt['MoorDyn']['Name'])): ln = [] - ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Name'][i])) + ln.append('{:<11}'.format(self.fst_vt['MoorDyn']['Name'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Diam'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['MassDen'][i])) - ln.append('{:^11.4e}'.format(self.fst_vt['MoorDyn']['EA'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['BA_zeta'][i])) + ln.append('|'.join([float_default_out(a, trim=True) for a in self.fst_vt['MoorDyn']['EA'][i]])) + ln.append('|'.join(['{:^.4f}'.format(a) for a in self.fst_vt['MoorDyn']['BA_zeta'][i]])) ln.append('{:^11.4e}'.format(self.fst_vt['MoorDyn']['EI'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Cd'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Ca'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['CdAx'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['CaAx'][i])) f.write(" ".join(ln) + '\n') + + if self.fst_vt['MoorDyn']['NonLinearEA'][i] != None: + self.write_NonLinearEA(self.fst_vt['MoorDyn']['EA'][i][0], self.fst_vt['MoorDyn']['Name'][i], self.fst_vt['MoorDyn']['NonLinearEA'][i]) + if 'Rod_Name' in self.fst_vt['MoorDyn'] and self.fst_vt['MoorDyn']['Rod_Name']: f.write('----------------------- ROD TYPES ------------------------------------------\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['TypeName', 'Diam', 'Mass/m', 'Cd', 'Ca', 'CdEnd', 'CaEnd']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(name)', '(m)', '(kg/m)', '(-)', '(-)', '(-)', '(-)']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['TypeName', 'Diam', 'Mass/m', 'Cd', 'Ca', 'CdEnd', 'CaEnd']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['(name)', '(m)', '(kg/m)', '(-)', '(-)', '(-)', '(-)']])+'\n') for i in range(len(self.fst_vt['MoorDyn']['Rod_Name'])): ln = [] - ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Rod_Name'][i])) + ln.append('{:<11}'.format(self.fst_vt['MoorDyn']['Rod_Name'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Rod_Diam'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Rod_MassDen'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Rod_Cd'][i])) @@ -2369,11 +2388,11 @@ def write_MoorDyn(self): if 'Body_ID' in self.fst_vt['MoorDyn'] and self.fst_vt['MoorDyn']['Body_ID']: f.write('----------------------- BODIES ------------------------------------------\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['ID', 'Attachement', 'X0', 'Y0', 'Z0', 'r0', 'p0','y0','Mass','CG*','I*','Volume','CdA*','Ca*']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(#)', '(word)', '(m)', '(m)', '(m)', '(deg)', '(deg)','(deg)','(kg)','(m)','(kg-m^2)','(m^3)','m^2','(kg/m^3)']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['ID', 'Attachement', 'X0', 'Y0', 'Z0', 'r0', 'p0','y0','Mass','CG*','I*','Volume','CdA*','Ca*']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['(#)', '(word)', '(m)', '(m)', '(m)', '(deg)', '(deg)','(deg)','(kg)','(m)','(kg-m^2)','(m^3)','m^2','(kg/m^3)']])+'\n') for i in range(len(self.fst_vt['MoorDyn']['Body_ID'])): ln = [] - ln.append('{:^11d}'.format(self.fst_vt['MoorDyn']['Body_ID'][i])) + ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['Body_ID'][i])) ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Body_Attachment'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['X0'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Y0'][i])) @@ -2391,11 +2410,11 @@ def write_MoorDyn(self): if 'Rod_ID' in self.fst_vt['MoorDyn'] and self.fst_vt['MoorDyn']['Rod_ID']: f.write('----------------------- RODS ------------------------------------------\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['ID', 'RodType', 'Attachment', 'Xa', 'Ya', 'Za', 'Xb','Yb','Zb','NumSegs','RodOutputs']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(#)', '(name)', '(word/ID)', '(m)', '(m)', '(m)', '(m)','(m)','(m)','(-)','(-)']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['ID', 'RodType', 'Attachment', 'Xa', 'Ya', 'Za', 'Xb','Yb','Zb','NumSegs','RodOutputs']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['(#)', '(name)', '(word/ID)', '(m)', '(m)', '(m)', '(m)','(m)','(m)','(-)','(-)']])+'\n') for i in range(len(self.fst_vt['MoorDyn']['Rod_ID'])): ln = [] - ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Rod_ID'][i])) + ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['Rod_ID'][i])) ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Rod_Type'][i])) ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Rod_Attachment'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Xa'][i])) @@ -2410,11 +2429,11 @@ def write_MoorDyn(self): f.write('---------------------- POINTS --------------------------------\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['ID', 'Attachment', 'X', 'Y', 'Z', 'M', 'V', 'CdA', 'CA']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)', '(-)', '(m)', '(m)', '(m)', '(kg)', '(m^3)', '(m^2)', '(-)']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['ID', 'Attachment', 'X', 'Y', 'Z', 'M', 'V', 'CdA', 'CA']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['(-)', '(-)', '(m)', '(m)', '(m)', '(kg)', '(m^3)', '(m^2)', '(-)']])+'\n') for i in range(len(self.fst_vt['MoorDyn']['Point_ID'])): ln = [] - ln.append('{:^11d}'.format(self.fst_vt['MoorDyn']['Point_ID'][i])) + ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['Point_ID'][i])) ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Attachment'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['X'][i])) ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Y'][i])) @@ -2425,11 +2444,11 @@ def write_MoorDyn(self): ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['CA'][i])) f.write(" ".join(ln) + '\n') f.write('---------------------- LINES --------------------------------------\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['Line', 'LineType', 'AttachA', 'AttachB', 'UnstrLen', 'NumSegs', 'Outputs']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)', '(-)', '(-)', '(-)', '(m)', '(-)', '(-)']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['Line', 'LineType', 'AttachA', 'AttachB', 'UnstrLen', 'NumSegs', 'Outputs']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['(-)', '(-)', '(-)', '(-)', '(m)', '(-)', '(-)']])+'\n') for i in range(len(self.fst_vt['MoorDyn']['Line_ID'])): ln = [] - ln.append('{:^11d}'.format(self.fst_vt['MoorDyn']['Line_ID'][i])) + ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['Line_ID'][i])) ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['LineType'][i])) ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['AttachA'][i])) ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['AttachB'][i])) @@ -2438,41 +2457,58 @@ def write_MoorDyn(self): ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Outputs'][i])) f.write(" ".join(ln) + '\n') + if 'Failure_ID' in self.fst_vt['MoorDyn'] and self.fst_vt['MoorDyn']['Failure_ID']: # there are failure inputs + f.write('---------------------- FAILURE ----------------------\n') + f.write('FailureID Point Line(s) FailTime FailTen\n') + f.write('() () (,) (s or 0) (N or 0)\n') + for i_line in range(len(self.fst_vt['MoorDyn']['Failure_ID'])): + ln = [] + ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['Failure_ID'][i_line])) + ln.append('{:^11s}'.format(self.fst_vt['MoorDyn']['Failure_Point'][i_line])) + ln.append('{:^11s}'.format(','.join(['{:^d}'.format(a) for a in self.fst_vt['MoorDyn']['Failure_Line(s)'][i_line]]))) + ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['FailTime'][i_line])) + ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['FailTen'][i_line])) + f.write(" ".join(ln) + '\n') + if 'ChannelID' in self.fst_vt['MoorDyn'] and self.fst_vt['MoorDyn']['ChannelID']: # There are control inputs f.write('---------------------- CONTROL ---------------------------------------\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['ChannelID', 'Line(s)']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['()', '(,)']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['ChannelID', 'Line(s)']])+'\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['()', '(,)']])+'\n') for i_line in range(len(self.fst_vt['MoorDyn']['ChannelID'])): ln = [] - ln.append('{:^11d}'.format(self.fst_vt['MoorDyn']['ChannelID'][i_line])) + ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['ChannelID'][i_line])) ln.append(','.join(self.fst_vt['MoorDyn']['Lines_Control'][i_line])) f.write(" ".join(ln) + '\n') - f.write('---------------------- SOLVER OPTIONS ---------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['MoorDyn']['dtM'], 'dtM', '- time step to use in mooring integration (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['MoorDyn']['kbot'], 'kbot', '- bottom stiffness (Pa/m)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['MoorDyn']['cbot'], 'cbot', '- bottom damping (Pa-s/m)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['MoorDyn']['dtIC'], 'dtIC', '- time interval for analyzing convergence during IC gen (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['MoorDyn']['TmaxIC'], 'TmaxIC', '- max time for ic gen (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['MoorDyn']['CdScaleIC'], 'CdScaleIC', '- factor by which to scale drag coefficients during dynamic relaxation (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['MoorDyn']['threshIC'], 'threshIC', '- threshold for IC convergence (-)\n')) - if 'options' in self.fst_vt['MoorDyn']: - if 'inertialF' in self.fst_vt['MoorDyn']['options']: - f.write('{:<22d} {:<11} {:}'.format(int(self.fst_vt['MoorDyn']['inertialF']), 'inertialF', '- Compute the inertial forces (0: no, 1: yes). Switch to 0 if you get: Warning: extreme pitch moment from body-attached Rod.\n')) - - if 'WaterKin' in self.fst_vt['MoorDyn']['options']: + if 'External_ID' in self.fst_vt['MoorDyn'] and self.fst_vt['MoorDyn']['External_ID']: # there are external load outputs + f.write('---------------------- EXTERNAL LOADS --------------------------------\n') + f.write('ID Object Fext Blin Bquad CSys\n') + f.write('(#) (name) (N) (Ns/m) (Ns^2/m^2) (-)\n') + for i_line in range(len(self.fst_vt['MoorDyn']['External_ID'])): + ln = [] + ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['External_ID'][i_line])) + ln.append('{:^11s}'.format(self.fst_vt['MoorDyn']['Object'][i_line])) + ln.append('|'.join(['{:^.4f}'.format(a) for a in self.fst_vt['MoorDyn']['Fext'][i_line]])) + ln.append('|'.join(['{:^.4f}'.format(a) for a in self.fst_vt['MoorDyn']['Blin'][i_line]])) + ln.append('|'.join(['{:^.4f}'.format(a) for a in self.fst_vt['MoorDyn']['Bquad'][i_line]])) + ln.append('{:^11s}'.format(self.fst_vt['MoorDyn']['CSys'][i_line])) + f.write(" ".join(ln) + '\n') + + f.write('---------------------- SOLVER OPTIONS ---------------------------------------\n') + for i in range(len(self.fst_vt['MoorDyn']['option_values'])): + + if 'WATERKIN' in self.fst_vt['MoorDyn']['option_names'][i]: self.fst_vt['MoorDyn']['WaterKin_file'] = self.FAST_namingOut + '_WaterKin.dat' - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['MoorDyn']['WaterKin_file']+'"', 'WaterKin', '- WaterKin input file\n')) + f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['MoorDyn']['WaterKin_file']+'"', self.fst_vt['MoorDyn']['option_names'][i], self.fst_vt['MoorDyn']['option_descriptions'][i]+'\n')) + else: # if not waterkin handle normally + f.write('{:<22} {:<11} {:}'.format(float_default_out(self.fst_vt['MoorDyn']['option_values'][i]), self.fst_vt['MoorDyn']['option_names'][i], self.fst_vt['MoorDyn']['option_descriptions'][i]+'\n')) - - # f.write('{:^11s} {:<11} {:}'.format(self.fst_vt['MoorDyn']['WaterKin'], 'WaterKin', 'Handling of water motion (0=off, 1=on)\n')) f.write('------------------------ OUTPUTS --------------------------------------------\n') outlist = self.get_outlist(self.fst_vt['outlist'], ['MoorDyn']) for channel_list in outlist: for i in range(len(channel_list)): - f.write('"' + channel_list[i] + '"\n') - f.write('END\n') - f.write('------------------------- need this line --------------------------------------\n') + f.write(channel_list[i] + '\n') + f.write('------------------------- END --------------------------------------\n') f.flush() os.fsync(f) @@ -2481,8 +2517,8 @@ def write_MoorDyn(self): def write_WaterKin(self,WaterKin_file): f = open(WaterKin_file, 'w') - f.write('MoorDyn v2 (Feb 2022) Waves and Currents input file set up for USFLOWT\n') - f.write('Wave kinematics that will have an impact over the cans and the mooring lines.\n') + f.write('MoorDyn v2 Waves and Currents input file set up\n') + f.write('This file was written by FAST_writer.py, comments from seed file have been dropped.\n') f.write('--------------------------- WAVES -------------------------------------\n') f.write('{:<22} {:<11} {:}'.format(self.fst_vt['WaterKin']['WaveKinMod'], 'WaveKinMod', '- type of wave input {0 no waves; 3 set up grid of wave data based on time series}\n')) f.write('{:<22} {:<11} {:}'.format(self.fst_vt['WaterKin']['WaveKinFile'], 'WaveKinFile', '- file containing wave elevation time series at 0,0,0\n')) @@ -2495,16 +2531,30 @@ def write_WaterKin(self,WaterKin_file): f.write('{:<22} {:}'.format(self.fst_vt['WaterKin']['Z_Type'], '- Z wave input type (0: not used; 1: list values in ascending order; 2: uniform specified by -Zlim, Zlim, num)\n')) f.write('{:<22} {:}'.format(', '.join(['{:.3f}'.format(i) for i in self.fst_vt['WaterKin']['Z_Grid']]), '- Z wave grid point data separated by commas\n')) f.write('--------------------------- CURRENT -------------------------------------\n') - f.write('0 CurrentMod - type of current input {0 no current; 1 steady current profile described below} \n') + f.write('{:} CurrentMod - type of current input (0 no current; 1 steady current profile described below) \n'.format(self.fst_vt['WaterKin']['CurrentMod'])) f.write('z-depth x-current y-current\n') f.write('(m) (m/s) (m/s)\n') + if self.fst_vt['WaterKin']['z-depth']: + for i in range(len(self.fst_vt['WaterKin']['z-depth'])): + f.write('{:.3f} {:.3f} {:.3f}'.format(self.fst_vt['WaterKin']['z-depth'][i], self.fst_vt['WaterKin']['x-current'][i], self.fst_vt['WaterKin']['y-current'][i])) f.write('--------------------- need this line ------------------\n') f.flush() os.fsync(f) f.close() + def write_NonLinearEA(self,Stiffness_file, Linetype, NonLinearEA): + f = open(Stiffness_file, 'w') + f.write('---{:}---\n'.format(Linetype)) + f.write('Strain Tension\n') + f.write('(-) (N)\n') + for i in range(len(NonLinearEA["Strain"])): + f.write('{:.3f} {:.3f}'.format(NonLinearEA["Strain"][i], NonLinearEA["Tension"][i])) + + f.flush() + os.fsync(f) + f.close() def write_StC(self,StC_vt,StC_filename): @@ -2631,7 +2681,7 @@ def write_StC(self,StC_vt,StC_filename): 'glue-codes', 'openfast', '5MW_Land_BD_DLL_WTurb') # Path to fst directory files - check_rtest_cloned(os.path.join(fast.FAST_directory, fast.FAST_InputFile)) + check_rtest_cloned(os.path.join(fast.FAST_directory)) fast.execute() diff --git a/openfast_io/pyproject.toml b/openfast_io/pyproject.toml index 7595361ef7..6eb3e86f8d 100644 --- a/openfast_io/pyproject.toml +++ b/openfast_io/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "openfast_io" # dynamic = ["version"] -version = "4.0.3" +version = "4.0.4" description = "Readers and writers for OpenFAST files." license = {file = "../LICENSE"} authors = [ diff --git a/vs-build/OpenFAST-Simulink/OpenFAST-Simulink.vfproj b/vs-build/OpenFAST-Simulink/OpenFAST-Simulink.vfproj index 1b7110e1d7..655b2435a6 100644 --- a/vs-build/OpenFAST-Simulink/OpenFAST-Simulink.vfproj +++ b/vs-build/OpenFAST-Simulink/OpenFAST-Simulink.vfproj @@ -16,7 +16,7 @@ - + @@ -36,7 +36,7 @@ - +