diff --git a/LICENSE b/LICENSE index 8dada3edaf..3b43f8bbe7 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + Copyright 2025 NREL Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/docs/source/working.rst b/docs/source/working.rst index f15320aa39..07436514b8 100644 --- a/docs/source/working.rst +++ b/docs/source/working.rst @@ -139,6 +139,68 @@ In general, if an error is displayed in the terminal, you can use the guidelines You can use relative and aboslute path to the OpenFAST executable and to the main OpenFAST input file. Input files of OpenFAST also contain filepaths that reference other input files. These filepaths are either relative to the current file, or, can be absolute paths. +Checking an input deck without running +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``-CheckInput`` flag fully initializes every module enabled by the input file -- +reading and validating all module input files, resolving file references, building +meshes and airfoil tables, and running the glue code's cross-module consistency checks +-- and then exits without any time marching: + +.. code-block:: bash + + ./openfast -CheckInput InputFile.fst + +Unlike a normal run, which stops at the first fatal error, ``-CheckInput`` attempts +every module even after one fails, so a single invocation reports as many independent +input problems as possible. Results are printed as a summary on the console and written +to a machine-readable report ``.verify.yaml`` next to the output files. The +process exit code is ``0`` when the deck is valid (warnings allowed) and ``1`` when any +fatal input error was found. + +The report file is append-only: readers must treat a file without a trailing +``overall_status:`` entry as a crashed check. Runtime-only problems (large-deflection +warnings, solver convergence, NaN blow-ups) are outside the scope of this check. + +.. note:: + ``-CheckInput`` initializes modules exactly as a real run does, so it needs all + referenced resources present -- wind files, airfoil tables, and (if ServoDyn uses a + DLL controller) the controller shared library. + + +Availability across executables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``-CheckInput`` flag is available on all user-facing OpenFAST executables: + +- **Full accumulation (attempt-everything across modules):** + + - ``openfast`` — checks all enabled modules in the input file and reports cross-module consistency + - ``FAST.Farm`` — checks farm input, initializes all wrapped turbines with per-turbine attribution in the report (T1, T2, etc. message prefixes); skips downstream steps if any turbine fails + +- **Init-only check with report (single-module drivers, fail-fast semantics):** + + - ``turbsim`` — validates wind input + - Module drivers: ``aerodyn_driver``, ``aeroacoustics_driver``, ``hydrodyn_driver``, ``seastate_driver``, ``moordyn_driver``, ``inflowwind_driver``, ``aerodisk_driver``, ``sed_driver`` (simple ElastoDyn), ``soildyn_driver``, ``orca_driver``, ``beamdyn_driver``, ``unsteadyaero_driver`` + +Each driver reads its input file(s), initializes its module, and exits before the time loop or compute phase. +All failures are accumulated and reported before exit. + +**Report files:** + +- ``.verify.yaml`` — for ``openfast``, ``FAST.Farm``, and ``turbsim`` +- ``.driver.verify.yaml`` — for single-module drivers (``*_driver`` executables) +- ``checkinput.verify.yaml`` (in the current working directory) — fallback when the root name cannot be determined before a failure + +**Excluded executables:** + +- ``servodyn_driver`` — hardcoded module-test harness; its input deck is a code literal embedded in the executable, not CLI-driven + +**DLL caveat:** + +SoilDyn (REDWIN) and OrcaFlex load their DLLs during initialization. +Under ``-CheckInput``, a missing DLL is reported as that component's failure. +This is deliberate: the DLL is part of the deck/environment, and its absence is a real problem the deck cannot solve around. diff --git a/glue-codes/fast-farm/src/FAST_Farm.f90 b/glue-codes/fast-farm/src/FAST_Farm.f90 index c3d63ed756..17d6a52ed6 100644 --- a/glue-codes/fast-farm/src/FAST_Farm.f90 +++ b/glue-codes/fast-farm/src/FAST_Farm.f90 @@ -80,6 +80,10 @@ PROGRAM FAST_Farm if (ErrStat/=0) then call ProgAbort('', TrapErrors=.FALSE., TimeWait=3._ReKi ) + else if ( TRIM(FlagArg) == 'CHECKINPUT' ) then ! Initialize every farm-level component + all wrapped turbines and report; no time-marching + ! Runs collect-and-continue initialization, writes the -CheckInput report, and ENDS the program + ! with a report-derived exit code; never returns. + call Farm_CheckInput( farm, InputFileName ) else if ( len( trim(FlagArg) ) > 0 ) then ! Any other flag (-v,-h) end normally call NormStop() endif diff --git a/glue-codes/fast-farm/src/FAST_Farm_Subs.f90 b/glue-codes/fast-farm/src/FAST_Farm_Subs.f90 index 5a2eabde5c..c680ec3dc8 100644 --- a/glue-codes/fast-farm/src/FAST_Farm_Subs.f90 +++ b/glue-codes/fast-farm/src/FAST_Farm_Subs.f90 @@ -27,6 +27,7 @@ MODULE FAST_Farm_Subs USE FAST_Farm_Types USE NWTC_Library + USE NWTC_CheckInput USE WakeDynamics USE AWAE USE FAST_Farm_IO @@ -134,28 +135,40 @@ end subroutine TrilinearInterpRegGrid !! - Open Output File !! - n=0 !! - t=0 -SUBROUTINE Farm_Initialize( farm, InputFile, ErrStat, ErrMsg ) +SUBROUTINE Farm_Initialize( farm, InputFile, ErrStat, ErrMsg, CkInCollector ) type(All_FastFarm_Data), INTENT(INOUT) :: farm !< FAST.Farm data - + INTEGER(IntKi), INTENT( OUT) :: ErrStat !< Error status of the operation CHARACTER(*), INTENT( OUT) :: ErrMsg !< Error message if ErrStat /= ErrID_None CHARACTER(*), INTENT(IN ) :: InputFile !< A CHARACTER string containing the name of the primary FAST.Farm input file - - - ! local variables + TYPE(CheckInputCollectorType), OPTIONAL, INTENT(INOUT) :: CkInCollector !< -CheckInput accumulator; its mere presence IS the + !! -CheckInput mode (there is no farm-level analog of + !! p_FAST%CheckInputMode) -- when present, the patched + !! Failed() below collects and continues instead of + !! aborting, and StepOK gates each downstream component. + + + ! local variables type(AWAE_InitInputType) :: AWAE_InitInput type(AWAE_InitOutputType) :: AWAE_InitOutput - - INTEGER(IntKi) :: ErrStat2 + + INTEGER(IntKi) :: ErrStat2 CHARACTER(ErrMsgLen) :: ErrMsg2 + INTEGER(IntKi) :: ErrStat3 ! -CheckInput: scratch status for CkIn_Report* calls (must not clobber ErrStat/ErrStat2) + CHARACTER(ErrMsgLen) :: ErrMsg3 ! -CheckInput: scratch message for CkIn_Report* calls TYPE(WD_InitInputType) :: WD_InitInput ! init-input data for WakeDynamics module - CHARACTER(*), PARAMETER :: RoutineName = 'Farm_Initialize' + CHARACTER(*), PARAMETER :: RoutineName = 'Farm_Initialize' CHARACTER(ChanLen) :: OutList(Farm_MaxOutPts) ! list of user-requested output channels INTEGER(IntKi) :: i + CHARACTER(64) :: CurrentComponent ! -CheckInput: component label used by Failed() when collecting + LOGICAL :: StepOK ! -CheckInput: sequential gate; once false, remaining components are marked 'skipped' rather than attempted + LOGICAL :: AWAE_OK ! -CheckInput: captures AWAE_Init's own success BEFORE the patched Failed() masks ErrStat, so IsInitialized is never set on a failed init !.......... ErrStat = ErrID_None - ErrMsg = "" + ErrMsg = "" + CurrentComponent = 'FAST.Farm' + StepOK = .true. AbortErrLev = ErrID_Fatal ! Until we read otherwise from the FAST input file, we abort only on FATAL errors @@ -168,8 +181,13 @@ SUBROUTINE Farm_Initialize( farm, InputFile, ErrStat, ErrMsg ) END IF ! Determine the root name of the primary file (will be used for output files) - CALL GetRoot( InputFile, farm%p%OutFileRoot ) - + CALL GetRoot( InputFile, farm%p%OutFileRoot ) + + IF ( PRESENT(CkInCollector) ) THEN + CALL CkIn_OpenReport( CkInCollector, TRIM(farm%p%OutFileRoot), ErrStat2, ErrMsg2 ) + IF ( ErrStat2 >= AbortErrLev ) CALL WrScr( 'Warning: could not open -CheckInput report file: '//TRIM(ErrMsg2) ) + END IF + DO i=1,NumFFModules farm%p%Module_Ver(i)%Date = 'unknown date' farm%p%Module_Ver(i)%Ver = 'unknown version' @@ -215,89 +233,191 @@ SUBROUTINE Farm_Initialize( farm, InputFile, ErrStat, ErrMsg ) farm%p%MaxNumPlanes(i) = max( 2, min( farm%p%MaxNumPlanes(i) , farm%p%n_TMax + 2 ) ) end do - !............................................................................................................................... + ! -CheckInput: 'FAST.Farm' covers the primary-file read, ValidateInput, and the DT/MaxNumPlanes setup above. + ! Report it now, before touching any downstream module, so a bad primary file (e.g. NumTurbines) never lets + ! WAT/AWAE/WD/Turbines/SharedMooring/FarmOutput run against garbage farm%p data (deliberate deviation from + ! FAST_Subs's attempt-everything: there is no stub infrastructure for farm-level parameters). + IF ( PRESENT(CkInCollector) ) THEN + CALL CkIn_Collect( CkInCollector, 'FAST.Farm', ErrID_None, '' ) + CALL CkIn_ReportComponent( CkInCollector, 'FAST.Farm', ErrStat3, ErrMsg3 ) + StepOK = ( CkIn_ComponentStatus( CkInCollector, 'FAST.Farm' ) /= CkIn_St_Failed ) + ErrStat = ErrID_None ! -CheckInput: don't let a benign accumulated message bleed into the next component + ErrMsg = '' + END IF + + !............................................................................................................................... ! step 3: initialize WAT, AWAE, and WD (b, c, and d can be done in parallel) - !............................................................................................................................... + !............................................................................................................................... !------------------- ! a. read WAT input files using InflowWind - if (farm%p%WAT /= Mod_WAT_None) then - call WAT_init( farm%p, farm%WAT_IfW, AWAE_InitInput, ErrStat2, ErrMsg2 ) - if(Failed()) return; - endif + CurrentComponent = 'WakeAddedTurbulence' + IF ( StepOK ) THEN + if (farm%p%WAT /= Mod_WAT_None) then + call WAT_init( farm%p, farm%WAT_IfW, AWAE_InitInput, ErrStat2, ErrMsg2 ) + if(Failed()) return; + IF ( PRESENT(CkInCollector) ) THEN + CALL CkIn_Collect( CkInCollector, 'WakeAddedTurbulence', ErrID_None, '' ) + CALL CkIn_ReportComponent( CkInCollector, 'WakeAddedTurbulence', ErrStat3, ErrMsg3 ) + StepOK = ( CkIn_ComponentStatus( CkInCollector, 'WakeAddedTurbulence' ) /= CkIn_St_Failed ) + ErrStat = ErrID_None ! -CheckInput: don't let a benign accumulated message bleed into the next component + ErrMsg = '' + END IF + else if ( PRESENT(CkInCollector) ) then + call CkIn_Collect( CkInCollector, 'WakeAddedTurbulence', ErrID_None, '', Status='not_used' ) + call CkIn_ReportComponent( CkInCollector, 'WakeAddedTurbulence', ErrStat3, ErrMsg3 ) + endif + ELSE IF ( PRESENT(CkInCollector) ) THEN + call CkIn_Collect( CkInCollector, 'WakeAddedTurbulence', ErrID_Info, 'blocked by upstream failure(s)', Status='skipped' ) + call CkIn_ReportComponent( CkInCollector, 'WakeAddedTurbulence', ErrStat3, ErrMsg3 ) + END IF !------------------- ! b. CALL AWAE_Init + CurrentComponent = 'AWAE' + IF ( StepOK ) THEN + if (farm%p%WAT /= Mod_WAT_None) AWAE_InitInput%WAT_Enabled = .true. + AWAE_InitInput%InputFileData%dr = WD_InitInput%InputFileData%dr + AWAE_InitInput%InputFileData%dt_low = farm%p%dt_low + AWAE_InitInput%InputFileData%NumTurbines = farm%p%NumTurbines + AWAE_InitInput%InputFileData%NumRadii = WD_InitInput%InputFileData%NumRadii + AWAE_InitInput%MaxPlanes = MAXVAL(farm%p%MaxNumPlanes) + AWAE_InitInput%InputFileData%WindFilePath = farm%p%WindFilePath + AWAE_InitInput%n_high_low = farm%p%n_high_low + AWAE_InitInput%NumDT = farm%p%n_TMax + AWAE_InitInput%OutFileRoot = farm%p%OutFileRoot + if (farm%p%WAT /= Mod_WAT_None .and. associated(farm%WAT_IfW%p%FlowField)) then + AWAE_InitInput%WAT_FlowField => farm%WAT_IfW%p%FlowField + endif + call AWAE_Init( AWAE_InitInput, farm%AWAE%u, farm%AWAE%p, farm%AWAE%x, farm%AWAE%xd, farm%AWAE%z, farm%AWAE%OtherSt, farm%AWAE%y, & + farm%AWAE%m, farm%p%DT_low, AWAE_InitOutput, ErrStat2, ErrMsg2 ) + ! Capture success BEFORE Failed() (patched, above) can mask ErrStat2 into a swallowed collector entry -- + ! under -CheckInput, Failed() returns .false. even when AWAE_Init failed, so IsInitialized must not be + ! gated on Failed()'s return value. + AWAE_OK = (ErrStat2 < AbortErrLev) + if(Failed()) return; + + if (AWAE_OK) farm%AWAE%IsInitialized = .true. + + farm%p%X0_Low = AWAE_InitOutput%oXYZ_Low(1) + farm%p%Y0_low = AWAE_InitOutput%oXYZ_Low(2) + farm%p%Z0_low = AWAE_InitOutput%oXYZ_Low(3) + farm%p%nX_Low = AWAE_InitOutput%nXYZ_Low(1) + farm%p%nY_low = AWAE_InitOutput%nXYZ_Low(2) + farm%p%nZ_low = AWAE_InitOutput%nXYZ_Low(3) + farm%p%dX_low = AWAE_InitOutput%dXYZ_Low(1) + farm%p%dY_low = AWAE_InitOutput%dXYZ_Low(2) + farm%p%dZ_low = AWAE_InitOutput%dXYZ_Low(3) + farm%p%Module_Ver( ModuleFF_AWAE ) = AWAE_InitOutput%Ver + + IF ( PRESENT(CkInCollector) ) THEN + CALL CkIn_Collect( CkInCollector, 'AWAE', ErrID_None, '' ) + CALL CkIn_ReportComponent( CkInCollector, 'AWAE', ErrStat3, ErrMsg3 ) + StepOK = ( CkIn_ComponentStatus( CkInCollector, 'AWAE' ) /= CkIn_St_Failed ) + ErrStat = ErrID_None ! -CheckInput: don't let a benign accumulated message bleed into the next component + ErrMsg = '' + END IF + ELSE IF ( PRESENT(CkInCollector) ) THEN + call CkIn_Collect( CkInCollector, 'AWAE', ErrID_Info, 'blocked by upstream failure(s)', Status='skipped' ) + call CkIn_ReportComponent( CkInCollector, 'AWAE', ErrStat3, ErrMsg3 ) + END IF - if (farm%p%WAT /= Mod_WAT_None) AWAE_InitInput%WAT_Enabled = .true. - AWAE_InitInput%InputFileData%dr = WD_InitInput%InputFileData%dr - AWAE_InitInput%InputFileData%dt_low = farm%p%dt_low - AWAE_InitInput%InputFileData%NumTurbines = farm%p%NumTurbines - AWAE_InitInput%InputFileData%NumRadii = WD_InitInput%InputFileData%NumRadii - AWAE_InitInput%MaxPlanes = MAXVAL(farm%p%MaxNumPlanes) - AWAE_InitInput%InputFileData%WindFilePath = farm%p%WindFilePath - AWAE_InitInput%n_high_low = farm%p%n_high_low - AWAE_InitInput%NumDT = farm%p%n_TMax - AWAE_InitInput%OutFileRoot = farm%p%OutFileRoot - if (farm%p%WAT /= Mod_WAT_None .and. associated(farm%WAT_IfW%p%FlowField)) then - AWAE_InitInput%WAT_FlowField => farm%WAT_IfW%p%FlowField - endif - call AWAE_Init( AWAE_InitInput, farm%AWAE%u, farm%AWAE%p, farm%AWAE%x, farm%AWAE%xd, farm%AWAE%z, farm%AWAE%OtherSt, farm%AWAE%y, & - farm%AWAE%m, farm%p%DT_low, AWAE_InitOutput, ErrStat2, ErrMsg2 ) - if(Failed()) return; - - farm%AWAE%IsInitialized = .true. - - farm%p%X0_Low = AWAE_InitOutput%oXYZ_Low(1) - farm%p%Y0_low = AWAE_InitOutput%oXYZ_Low(2) - farm%p%Z0_low = AWAE_InitOutput%oXYZ_Low(3) - farm%p%nX_Low = AWAE_InitOutput%nXYZ_Low(1) - farm%p%nY_low = AWAE_InitOutput%nXYZ_Low(2) - farm%p%nZ_low = AWAE_InitOutput%nXYZ_Low(3) - farm%p%dX_low = AWAE_InitOutput%dXYZ_Low(1) - farm%p%dY_low = AWAE_InitOutput%dXYZ_Low(2) - farm%p%dZ_low = AWAE_InitOutput%dXYZ_Low(3) - farm%p%Module_Ver( ModuleFF_AWAE ) = AWAE_InitOutput%Ver - !------------------- ! c. initialize WD (one instance per turbine, each can be done in parallel, too) - - call Farm_InitWD( farm, WD_InitInput, ErrStat2, ErrMsg2 ); if(Failed()) return; - - - !............................................................................................................................... + CurrentComponent = 'WakeDynamics' + IF ( StepOK ) THEN + call Farm_InitWD( farm, WD_InitInput, ErrStat2, ErrMsg2 ); if(Failed()) return; + IF ( PRESENT(CkInCollector) ) THEN + CALL CkIn_Collect( CkInCollector, 'WakeDynamics', ErrID_None, '' ) + CALL CkIn_ReportComponent( CkInCollector, 'WakeDynamics', ErrStat3, ErrMsg3 ) + StepOK = ( CkIn_ComponentStatus( CkInCollector, 'WakeDynamics' ) /= CkIn_St_Failed ) + ErrStat = ErrID_None ! -CheckInput: don't let a benign accumulated message bleed into the next component + ErrMsg = '' + END IF + ELSE IF ( PRESENT(CkInCollector) ) THEN + call CkIn_Collect( CkInCollector, 'WakeDynamics', ErrID_Info, 'blocked by upstream failure(s)', Status='skipped' ) + call CkIn_ReportComponent( CkInCollector, 'WakeDynamics', ErrStat3, ErrMsg3 ) + END IF + + !............................................................................................................................... ! step 4: initialize FAST (each instance of FAST can also be done in parallel) - !............................................................................................................................... + !............................................................................................................................... - CALL Farm_InitFAST( farm, WD_InitInput%InputFileData, AWAE_InitOutput, ErrStat2, ErrMsg2); if(Failed()) return; - - !............................................................................................................................... + ! -CheckInput: 'Turbines' -- Farm_InitFAST's per-turbine loop already runs every turbine even after one + ! fails (its ErrStat/ErrMsg only aborts the loop's *caller* after the loop, via 'T:'-prefixed messages + ! per turbine), so a single failing wrapped turbine still surfaces every other turbine's status here. + CurrentComponent = 'Turbines' + IF ( StepOK ) THEN + CALL Farm_InitFAST( farm, WD_InitInput%InputFileData, AWAE_InitOutput, ErrStat2, ErrMsg2); if(Failed()) return; + IF ( PRESENT(CkInCollector) ) THEN + CALL CkIn_Collect( CkInCollector, 'Turbines', ErrID_None, '' ) + CALL CkIn_ReportComponent( CkInCollector, 'Turbines', ErrStat3, ErrMsg3 ) + StepOK = ( CkIn_ComponentStatus( CkInCollector, 'Turbines' ) /= CkIn_St_Failed ) + ErrStat = ErrID_None ! -CheckInput: don't let a benign accumulated message bleed into the next component + ErrMsg = '' + END IF + ELSE IF ( PRESENT(CkInCollector) ) THEN + call CkIn_Collect( CkInCollector, 'Turbines', ErrID_Info, 'blocked by upstream failure(s)', Status='skipped' ) + call CkIn_ReportComponent( CkInCollector, 'Turbines', ErrStat3, ErrMsg3 ) + END IF + + !............................................................................................................................... ! step 4.5: initialize farm-level MoorDyn if applicable - !............................................................................................................................... - - if (farm%p%MooringMod == 3) then - CALL Farm_InitMD( farm, ErrStat2, ErrMsg2); if(Failed()) return; ! FAST instances must be initialized first so that turbine initial positions are known - end if + !............................................................................................................................... - !............................................................................................................................... - ! step 5: Open output file (or set up output file handling) - !............................................................................................................................... - - ! Set parameters for output channels: - CALL Farm_SetOutParam(OutList, farm, ErrStat2, ErrMsg2 ); if(Failed()) return; ! requires: p%NumOuts, sets: p%OutParam. - - call Farm_InitOutput( farm, ErrStat2, ErrMsg2 ); if(Failed()) return; + CurrentComponent = 'SharedMooring' + IF ( StepOK ) THEN + if (farm%p%MooringMod == 3) then + CALL Farm_InitMD( farm, ErrStat2, ErrMsg2); if(Failed()) return; ! FAST instances must be initialized first so that turbine initial positions are known + IF ( PRESENT(CkInCollector) ) THEN + CALL CkIn_Collect( CkInCollector, 'SharedMooring', ErrID_None, '' ) + CALL CkIn_ReportComponent( CkInCollector, 'SharedMooring', ErrStat3, ErrMsg3 ) + StepOK = ( CkIn_ComponentStatus( CkInCollector, 'SharedMooring' ) /= CkIn_St_Failed ) + ErrStat = ErrID_None ! -CheckInput: don't let a benign accumulated message bleed into the next component + ErrMsg = '' + END IF + else if ( PRESENT(CkInCollector) ) then + call CkIn_Collect( CkInCollector, 'SharedMooring', ErrID_None, '', Status='not_used' ) + call CkIn_ReportComponent( CkInCollector, 'SharedMooring', ErrStat3, ErrMsg3 ) + end if + ELSE IF ( PRESENT(CkInCollector) ) THEN + call CkIn_Collect( CkInCollector, 'SharedMooring', ErrID_Info, 'blocked by upstream failure(s)', Status='skipped' ) + call CkIn_ReportComponent( CkInCollector, 'SharedMooring', ErrStat3, ErrMsg3 ) + END IF + + !............................................................................................................................... + ! step 5: Open output file (or set up output file handling) + !............................................................................................................................... + + CurrentComponent = 'FarmOutput' + IF ( StepOK ) THEN + ! Set parameters for output channels: + CALL Farm_SetOutParam(OutList, farm, ErrStat2, ErrMsg2 ); if(Failed()) return; ! requires: p%NumOuts, sets: p%OutParam. + + call Farm_InitOutput( farm, ErrStat2, ErrMsg2 ); if(Failed()) return; + + ! Print the summary file if requested: + IF (farm%p%SumPrint) THEN + CALL Farm_PrintSum( farm, WD_InitInput%InputFileData, ErrStat2, ErrMsg2 ); if(Failed()) return; + END IF - ! Print the summary file if requested: - IF (farm%p%SumPrint) THEN - CALL Farm_PrintSum( farm, WD_InitInput%InputFileData, ErrStat2, ErrMsg2 ); if(Failed()) return; + IF ( PRESENT(CkInCollector) ) THEN + CALL CkIn_Collect( CkInCollector, 'FarmOutput', ErrID_None, '' ) + CALL CkIn_ReportComponent( CkInCollector, 'FarmOutput', ErrStat3, ErrMsg3 ) + StepOK = ( CkIn_ComponentStatus( CkInCollector, 'FarmOutput' ) /= CkIn_St_Failed ) + ErrStat = ErrID_None ! -CheckInput: don't let a benign accumulated message bleed into the next component + ErrMsg = '' + END IF + ELSE IF ( PRESENT(CkInCollector) ) THEN + call CkIn_Collect( CkInCollector, 'FarmOutput', ErrID_Info, 'blocked by upstream failure(s)', Status='skipped' ) + call CkIn_ReportComponent( CkInCollector, 'FarmOutput', ErrStat3, ErrMsg3 ) END IF - + !............................................................................................................................... ! Destroy initializion data - !............................................................................................................................... + !............................................................................................................................... CALL Cleanup() - + CONTAINS SUBROUTINE Cleanup() call WD_DestroyInitInput(WD_InitInput, ErrStat2, ErrMsg2) @@ -308,10 +428,65 @@ END SUBROUTINE Cleanup logical function Failed() call SetErrStat(errStat2, errMsg2, errStat, errMsg, RoutineName) Failed = errStat >= AbortErrLev - if (Failed) call cleanup() + if (Failed) then + if ( present(CkInCollector) ) then + ! -CheckInput: record under the current component and keep going; StepOK (set by the explicit + ! CkIn_Collect/CkIn_ReportComponent calls above, once this component's block finishes) is what + ! actually gates whether downstream components are attempted or marked 'skipped'. + call CkIn_Collect(CkInCollector, trim(CurrentComponent), errStat, errMsg) + errStat = ErrID_None + errMsg = '' + Failed = .false. + else + call cleanup() + end if + end if end function Failed END SUBROUTINE Farm_Initialize +!---------------------------------------------------------------------------------------------------------------------------------- +!> -CheckInput driver for FAST.Farm: attempts every farm-level component (and, through Farm_InitFAST, every +!! wrapped turbine) with collect-and-continue semantics, writes a console summary + .verify.yaml report, +!! and exits the process with a report-derived exit code (0 valid / 1 any fatal input error). Never returns to +!! the caller. Does NOT call FARM_InitialCO and does NOT enter the time-marching loop. +SUBROUTINE Farm_CheckInput( farm, InputFileName ) + + TYPE(All_FastFarm_Data), INTENT(INOUT) :: farm !< FAST.Farm data + CHARACTER(*), INTENT(IN ) :: InputFileName !< primary FAST.Farm input file + + TYPE(CheckInputCollectorType) :: Checker + INTEGER(IntKi) :: ErrStat, ErrStat2 + CHARACTER(ErrMsgLen) :: ErrMsg, ErrMsg2 + INTEGER(IntKi) :: ExitCode + + CALL Farm_Initialize( farm, InputFileName, ErrStat, ErrMsg, CkInCollector=Checker ) + IF (ErrStat >= AbortErrLev) THEN + ! Only a failure that bypasses the collect-and-continue path entirely lands here -- e.g. the + ! required-input-file-name check at the very top of Farm_Initialize, before the report is even open. + ! Every attempted component's own failure already took the collect path inside Farm_Initialize's + ! patched Failed() (see above), so this is a residual/last-resort catch-all under 'FAST.Farm'. + CALL CkIn_Collect( Checker, 'FAST.Farm', ErrStat, ErrMsg ) + END IF + + ! Finalize the report BEFORE teardown so a completed check's summary/yaml survive even if FARM_End + ! crashes on partially-initialized state -- mirrors FAST_CheckInput_T (FAST_Subs.f90): WrSummary + + ! CloseReport + capture the exit code, THEN tear down, THEN exit. Inlined (rather than + ! CkIn_DriverFinish) because FARM_End must run between CloseReport and ProgExit. + CALL CkIn_WrSummary( Checker ) + CALL CkIn_CloseReport( Checker, ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not finalize -CheckInput report: '//TRIM(ErrMsg2)) + ExitCode = CkIn_ExitCode( Checker ) + + ! Tear down whatever did get initialized -- mirrors the FARM_End call in FAST_Farm.f90's CheckError, minus + ! the abort. Farm_CheckInput never calls FARM_InitialCO or enters the time loop, so FARM_End must tolerate a + ! farm left partially initialized by an early failure; this has been verified against every seeded failure + ! mode exercised by the -CheckInput smoke tests (bad primary file, corrupted wrapped-turbine deck). The + ! report is already finalized above, so any fatal here does not need to (and cannot) be folded into it. + CALL FARM_End( farm, ErrStat2, ErrMsg2 ) + + CALL ProgExit( ExitCode ) + +END SUBROUTINE Farm_CheckInput !---------------------------------------------------------------------------------------------------------------------------------- @@ -624,8 +799,10 @@ SUBROUTINE Farm_InitWD( farm, WD_InitInp, ErrStat, ErrMsg ) ! note that WD_Init has Interval as INTENT(IN) so, we don't need to worry about overwriting farm%p%dt_low here: call WD_Init( WD_InitInp, farm%WD(nt)%u, farm%WD(nt)%p, farm%WD(nt)%x, farm%WD(nt)%xd, farm%WD(nt)%z, & farm%WD(nt)%OtherSt, farm%WD(nt)%y, farm%WD(nt)%m, farm%p%dt_low, WD_InitOut, ErrStat2, ErrMsg2 ) - - farm%WD(nt)%IsInitialized = .true. + + ! Only mark this turbine's WD instance initialized if WD_Init actually succeeded for it -- previously + ! set unconditionally, so a failed WD_Init still left FARM_End calling WD_End on an uninitialized instance. + IF (ErrStat2 < AbortErrLev) farm%WD(nt)%IsInitialized = .true. CALL SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, 'T'//trim(num2lstr(nt))//':'//RoutineName) if (ErrStat >= AbortErrLev) then call cleanup() @@ -728,9 +905,12 @@ SUBROUTINE Farm_InitFAST( farm, WD_InitInp, AWAE_InitOutput, ErrStat, ErrMsg ) ! NOTE: FWrap_interval, and FWrap_InitOut appear unused call FWrap_Init( FWrap_InitInp, farm%FWrap(nt)%u, farm%FWrap(nt)%p, farm%FWrap(nt)%x, farm%FWrap(nt)%xd, farm%FWrap(nt)%z, & farm%FWrap(nt)%OtherSt, farm%FWrap(nt)%y, farm%FWrap(nt)%m, FWrap_Interval, FWrap_InitOut, ErrStat2, ErrMsg2 ) - - farm%FWrap(nt)%IsInitialized = .true. - + + ! Only mark this turbine's FWrap instance initialized if FWrap_Init actually succeeded for it -- this + ! loop runs every turbine even after one fails (by design, for attempt-everything reporting), so a + ! failed turbine must not be left marked initialized or FARM_End will call FWrap_End on it. + if (ErrStat2 < AbortErrLev) farm%FWrap(nt)%IsInitialized = .true. + if (ErrStat2 >= AbortErrLev) then !OMP CRITICAL ! Needed to avoid data race on ErrStat and ErrMsg CALL SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, 'T'//trim(num2lstr(nt))//':'//RoutineName) diff --git a/glue-codes/openfast/src/FAST_Prog.f90 b/glue-codes/openfast/src/FAST_Prog.f90 index d41dd0e376..3e5528541e 100644 --- a/glue-codes/openfast/src/FAST_Prog.f90 +++ b/glue-codes/openfast/src/FAST_Prog.f90 @@ -82,7 +82,14 @@ PROGRAM FAST ! this runs the steady-state solver driver and ENDS the program: CALL FAST_RunSteadyStateDriver( Turbine(1) ) CALL ExitThisProgram_T( Turbine(1), ErrID_None, .true., SkipRunTimeMsg = .TRUE. ) - + + ELSE IF ( TRIM(FlagArg) == 'CHECKINPUT' ) THEN ! Initialize every enabled module and report; no time-marching (NumTurbines==1 only) + + ! Runs attempt-everything initialization, writes the -CheckInput report, and ENDS the program + ! with a report-derived exit code (it calls ProgExit itself and never returns; calling + ! ExitThisProgram_T here as the other branches do would double-free the already-destroyed turbine). + CALL FAST_CheckInput_T( Turbine(1) ) + ELSEIF ( LEN( TRIM(FlagArg) ) > 0 ) THEN ! Any other flag, end normally CALL NormStop() diff --git a/modules/aerodisk/src/driver/AeroDisk_Driver.f90 b/modules/aerodisk/src/driver/AeroDisk_Driver.f90 index 92cb218add..66017654c9 100644 --- a/modules/aerodisk/src/driver/AeroDisk_Driver.f90 +++ b/modules/aerodisk/src/driver/AeroDisk_Driver.f90 @@ -27,6 +27,7 @@ PROGRAM AeroDisk_Driver USE AeroDisk_Driver_Subs USE AeroDisk_Driver_Types USE IfW_FLowField + USE NWTC_CheckInput IMPLICIT NONE @@ -84,6 +85,13 @@ PROGRAM AeroDisk_Driver integer(IntKi) :: TmpIdx !< Index of last point accessed by dimension INTEGER(IntKi) :: ErrStat !< Status of error message CHARACTER(ErrMsgLen) :: ErrMsg !< Error message if ErrStat /= ErrID_None + INTEGER(IntKi) :: ErrStat2 !< -CheckInput: temp error status for calls + CHARACTER(ErrMsgLen) :: ErrMsg2 !< -CheckInput: temp error message for calls + + ! -CheckInput support (no initializers on these -- set as early executable statements below) + LOGICAL :: CheckInputMode !< true if -CheckInput was given on the command line + TYPE(CheckInputCollectorType) :: Checker !< -CheckInput result collector + CHARACTER(64) :: CkStage !< name of the -CheckInput stage/component currently executing CHARACTER(200) :: git_commit ! String containing the current git commit hash TYPE(ProgDesc), PARAMETER :: version = ProgDesc( 'AeroDisk Driver', '', '' ) ! The version number of this program. @@ -106,6 +114,10 @@ PROGRAM AeroDisk_Driver ! Start the timer call CPU_TIME( Timer(1) ) + ! -CheckInput: no initializers on these -- set as early executable statements + CheckInputMode = .FALSE. + CkStage = 'Driver' ! default stage label; overridden before the ADsk_Init call below + ! Initialize the driver settings to their default values (same as the CL -- command line -- values) call InitSettingsFlags( ProgInfo, CLSettings, CLSettingsFlags ) Settings = CLSettings @@ -120,6 +132,10 @@ PROGRAM AeroDisk_Driver ErrStat = ErrID_None ENDIF + ! -CheckInput is command-line only (mirrors how Verbose/VVerbose are handled below -- not + ! merged into SettingsFlags by UpdateSettingsWithCL, so read it straight off the CL flags). + CheckInputMode = CLSettingsFlags%CheckInput + ! Check if we are doing verbose error reporting IF ( CLSettingsFlags%VVerbose ) ADskDriver_Verbose = 10_IntKi IF ( CLSettingsFlags%Verbose ) ADskDriver_Verbose = 7_IntKi @@ -178,6 +194,17 @@ PROGRAM AeroDisk_Driver ELSE + ! -CheckInput: the direct "-adsk" input-file mode never populates CaseTime/CaseData (those + ! are only read by ParseDvrIptFile above, in the driver-input-file branch) -- the time-step + ! setup below dereferences them unconditionally and crashes if they were never allocated. + ! That is a pre-existing bug in this mode and is out of scope here; under -CheckInput we + ! must not route into it, so fail cleanly with a clear message instead. + IF ( CheckInputMode ) THEN + CALL CkIn_DriverFail( Checker, 'Driver', ErrID_Fatal, & + 'AeroDisk -CheckInput requires a driver input file (the direct AeroDisk-file mode, "'// & + SwChar//'adsk", is not supported under -CheckInput).' ) ! never returns + END IF + ! VVerbose error reporting IF ( ADskDriver_Verbose >= 10_IntKi ) CALL WrScr('No driver input file used. Updating driver settings with command line arguments') @@ -189,6 +216,14 @@ PROGRAM AeroDisk_Driver CALL UpdateSettingsWithCL( SettingsFlags, Settings, CLSettingsFlags, CLSettings, SettingsFlags%DvrIptFile, ErrStat, ErrMsg ) call CheckErr('') + ! -CheckInput: RootName is known now (pre-Init, driver-input-file mode only -- the direct + ! "-adsk" mode already exited above under CheckInputMode). Open the report before ADsk_Init + ! runs so a fatal from Init itself is caught. + IF ( CheckInputMode ) THEN + CALL CkIn_OpenReport( Checker, TRIM(Settings%OutRootName)//'.driver', ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) + END IF + ! Verbose error reporting IF ( ADskDriver_Verbose >= 10_IntKi ) THEN CALL WrScr(NewLine//'--- Driver settings after copying over CL settings: ---') @@ -266,15 +301,21 @@ PROGRAM AeroDisk_Driver ! Initialize the module + CkStage = 'AeroDisk' CALL ADsk_Init( InitInData, u(1), p, x, xd, z, OtherState, y, misc, TimeInterval, InitOutData, ErrStat, ErrMsg ) IF ( ErrStat /= ErrID_None ) THEN ! Check if there was an error and do something about it if necessary call CheckErr('After Init: ') END IF + CkStage = 'Driver' ! output-file setup below is attributed back to the Driver stage ! Set the output file - call GetRoot(Settings%OutRootName,OutputFileRootName) - call Dvr_InitializeOutputFile(DvrOut, InitOutData, OutputFileRootName, ErrStat, ErrMsg) - call CheckErr('Setting output file'); + ! -CheckInput: opening the output file is a real compute artifact -- skip it entirely in check + ! mode so a passing check run leaves no output file behind. + IF ( .NOT. CheckInputMode ) THEN + call GetRoot(Settings%OutRootName,OutputFileRootName) + call Dvr_InitializeOutputFile(DvrOut, InitOutData, OutputFileRootName, ErrStat, ErrMsg) + call CheckErr('Setting output file'); + END IF ! Destroy initialization data CALL ADsk_DestroyInitInput( InitInData, ErrStat, ErrMsg ) @@ -285,6 +326,17 @@ PROGRAM AeroDisk_Driver ! Routines called in loose coupling -- the glue code may implement this in various ways !............................................................................................................................... + IF ( CheckInputMode ) THEN + ! Reaching here means every stage above completed without a fatal error (a fatal one would have + ! routed through CheckErr's CkIn_DriverFail interception, or the -adsk-mode interception above, + ! and never returned). Record both stages as passed, in order, then finish -- this call never + ! returns, so the time-marching loop below is never reached in check mode. + CALL CkIn_Collect( Checker, 'Driver', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Driver', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'AeroDisk', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'AeroDisk', ErrStat2, ErrMsg2 ) + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns + END IF TmpIdx = 0_IntKi @@ -351,6 +403,9 @@ subroutine CheckErr(Text) IF ( ErrStat /= ErrID_None ) THEN ! Check if there was an error and do something about it if necessary CALL WrScr( Text//ErrMsg ) if ( ErrStat >= AbortErrLev ) then + ! -CheckInput: ProgEnd -> ProgAbort discards ErrMsg, so the collector call must fire + ! before Cleanup/ProgEnd, not after. + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns call Cleanup() call ProgEnd() endif diff --git a/modules/aerodisk/src/driver/AeroDisk_Driver_Subs.f90 b/modules/aerodisk/src/driver/AeroDisk_Driver_Subs.f90 index 16b1ebca91..044b6c7741 100644 --- a/modules/aerodisk/src/driver/AeroDisk_Driver_Subs.f90 +++ b/modules/aerodisk/src/driver/AeroDisk_Driver_Subs.f90 @@ -49,6 +49,7 @@ SUBROUTINE DispHelpText() CALL WrScr(" "//SwChar//"v -- verbose output ") CALL WrScr(" "//SwChar//"vv -- very verbose output ") CALL WrScr(" "//SwChar//"NonLinear -- only return non-linear portion of reaction force") + CALL WrScr(" "//SwChar//"CheckInput -- validate the input deck, write a report, and exit") CALL WrScr(" "//SwChar//"help -- print this help menu and exit") CALL WrScr("") CALL WrScr(" Notes:") @@ -83,6 +84,7 @@ subroutine InitSettingsFlags( ProgInfo, CLSettings, CLFlags ) CLFlags%DTDefault = .FALSE. ! specified 'DEFAULT' for resolution in time CLFlags%Verbose = .FALSE. ! Turn on verbose error reporting? CLFlags%VVerbose = .FALSE. ! Turn on very verbose error reporting? + CLFlags%CheckInput = .FALSE. ! -CheckInput mode requested on the command line end subroutine InitSettingsFlags @@ -285,6 +287,9 @@ SUBROUTINE ParseArg( CLSettings, CLFlags, ThisArgUC, ThisArg, adskFlagSet, ErrSt ELSEIF ( ThisArgUC(1:1) == "V" ) THEN CLFlags%Verbose = .TRUE. RETURN + ELSEIF ( TRIM(ThisArgUC) == "CHECKINPUT" ) THEN + CLFlags%CheckInput = .TRUE. + RETURN ELSE CALL SetErrStat( ErrID_Warn," Unrecognized option '"//SwChar//TRIM(ThisArg)//"'. Ignoring. Use option "//SwChar//"help for list of options.", & ErrStat,ErrMsg,'ParseArg') diff --git a/modules/aerodisk/src/driver/AeroDisk_Driver_Types.f90 b/modules/aerodisk/src/driver/AeroDisk_Driver_Types.f90 index 6378703b0f..1168f1b26b 100644 --- a/modules/aerodisk/src/driver/AeroDisk_Driver_Types.f90 +++ b/modules/aerodisk/src/driver/AeroDisk_Driver_Types.f90 @@ -52,6 +52,7 @@ module AeroDisk_Driver_Types logical :: DTDefault = .FALSE. !< specified a 'DEFAULT' for the time resolution logical :: Verbose = .FALSE. !< Verbose error reporting logical :: VVerbose = .FALSE. !< Very Verbose error reporting + logical :: CheckInput = .FALSE. !< specified -CheckInput mode on the command line end type ADskDriver_Flags diff --git a/modules/aerodyn/src/AeroAcoustics_Driver.f90 b/modules/aerodyn/src/AeroAcoustics_Driver.f90 index ea5bdbf6bd..3cf06809a6 100644 --- a/modules/aerodyn/src/AeroAcoustics_Driver.f90 +++ b/modules/aerodyn/src/AeroAcoustics_Driver.f90 @@ -31,8 +31,9 @@ program AeroAcoustics_Driver use AeroAcoustics_Driver_Subs use VersionInfo + use NWTC_CheckInput implicit none - + ! Program variables REAL(ReKi) :: PrevClockTime ! Clock time at start of simulation in seconds [(s)] REAL(ReKi) :: UsrTime1 ! User CPU time for simulation initialization [(s)] @@ -41,39 +42,73 @@ program AeroAcoustics_Driver INTEGER(IntKi) , DIMENSION(1:8) :: SimStrtTime ! Start time of simulation (after initialization) [-] REAL(DbKi) :: t_global ! global-loop time marker REAL(DbKi) :: TiLstPrn ! The simulation time of the last print (to file) [(s)] - + TYPE(Dvr_Data) :: DriverData - + character(1024) :: InputFile integer :: nt !< loop counter (for time step) character(20) :: FlagArg ! flag argument from command line integer(IntKi) :: ErrStat ! status of error message character(ErrMsgLen) :: ErrMsg !local error message if ErrStat /= ErrID_None - + LOGICAL :: CheckInputMode ! true if -CheckInput was given on the command line (no initializer -- set below) + TYPE(CheckInputCollectorType) :: Checker ! -CheckInput result collector + CHARACTER(64) :: CkStage ! name of the -CheckInput stage/component currently executing (no initializer -- set below) + INTEGER(IntKi) :: ErrStat2 ! secondary error status, used only for -CheckInput report calls + CHARACTER(ErrMsgLen) :: ErrMsg2 ! secondary error message, used only for -CheckInput report calls + CALL DATE_AND_TIME ( Values=StrtTime ) ! Let's time the whole simulation CALL CPU_TIME ( UsrTime1 ) ! Initial time (this zeros the start time when used as a MATLAB function) UsrTime1 = MAX( 0.0_ReKi, UsrTime1 ) ! CPU_TIME: If a meaningful time cannot be returned, a processor-dependent negative value is returned UsrTime2 = UsrTime1 ! CPU_TIME: Initialize in case of error before getting real data SimStrtTime = StrtTime ! CPU_TIME: Initialize in case of error before getting real data nt = 0 - + + CheckInputMode = .FALSE. + CkStage = 'Driver' ! default stage label; overridden before each named stage below + ! --- Driver initialization CALL NWTC_Init( ProgNameIN=version%Name ) - + InputFile = "" ! initialize to empty string to make sure it's input from the command line CALL CheckArgs( InputFile, Flag=FlagArg ) - IF ( LEN( TRIM(FlagArg) ) > 0 ) CALL NormStop() - + IF ( TRIM(FlagArg) == 'CHECKINPUT' ) THEN + CheckInputMode = .TRUE. + ELSE IF ( LEN( TRIM(FlagArg) ) > 0 ) THEN + CALL NormStop() ! -h/-v were already handled inside CheckArgs + END IF + ! Display the copyright notice and compile info: CALL DispCopyrightLicense( version%Name ) CALL DispCompileRuntimeInfo( version%Name ) ! Initialize modules + CkStage = 'Driver' call ReadDriverInputFile( InputFile, DriverData, ErrStat, ErrMsg ); call CheckError() + + IF ( CheckInputMode ) THEN + CALL CkIn_OpenReport( Checker, TRIM(DriverData%OutRootName)//'.driver', ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) + END IF + + CkStage = 'AirfoilInfo' call Init_AFI(DriverData%Airfoil_FileName, DriverData%AFInfo, ErrStat, ErrMsg); call CheckError() - call Init_AAmodule(DriverData, ErrStat, ErrMsg); call CheckError() + CkStage = 'AeroAcoustics' + call Init_AAmodule(DriverData, ErrStat, ErrMsg, CheckInputMode=CheckInputMode); call CheckError() + + IF ( CheckInputMode ) THEN + ! Reaching here means all three stages above completed without a fatal error (a fatal one + ! would have routed through CheckError's CkIn_DriverFail interception and never returned). + ! Record all three stages as passed, in order, then finish -- this call never returns. + CALL CkIn_Collect( Checker, 'Driver', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Driver', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'AirfoilInfo', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'AirfoilInfo', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'AeroAcoustics', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'AeroAcoustics', ErrStat2, ErrMsg2 ) + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns + END IF ! Init of time estimator t_global=0.0_DbKi @@ -105,6 +140,7 @@ subroutine CheckError() if (ErrStat /= ErrID_None) then call WrScr(TRIM(errMsg)) if (errStat >= AbortErrLev) then + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns call Dvr_End() end if ErrStat = ErrID_None diff --git a/modules/aerodyn/src/AeroAcoustics_Driver_Subs.f90 b/modules/aerodyn/src/AeroAcoustics_Driver_Subs.f90 index 15e2ef4205..67c1bb1fc5 100644 --- a/modules/aerodyn/src/AeroAcoustics_Driver_Subs.f90 +++ b/modules/aerodyn/src/AeroAcoustics_Driver_Subs.f90 @@ -419,12 +419,14 @@ subroutine Init_AFI(afName, AFInfo, ErrStat, ErrMsg) end subroutine Init_AFI !---------------------------------------------------------------------------------------------------------------------------------- !> This routine initializes the Airfoil Noise module from within AeroDyn. -SUBROUTINE Init_AAmodule( DriverData, ErrStat, ErrMsg ) +SUBROUTINE Init_AAmodule( DriverData, ErrStat, ErrMsg, CheckInputMode ) !.................................................................................................................................. type(Dvr_Data), intent(inout) :: DriverData !< AeroDyn-level initialization inputs integer(IntKi), intent( out) :: errStat !< Error status of the operation character(*), intent( out) :: errMsg !< Error message if ErrStat /= ErrID_None + logical, optional, intent(in ) :: CheckInputMode !< true under '-CheckInput': skip opening the driver output + !! file below so a validation-only run leaves no compute artifact behind ! Local variables real(DbKi) :: Interval ! DT @@ -472,9 +474,10 @@ SUBROUTINE Init_AAmodule( DriverData, ErrStat, ErrMsg ) ! --- AeroAcoustics initialization call call AA_Init(InitInp, DriverData%u, DriverData%p, DriverData%xd, DriverData%OtherState,DriverData%y, DriverData%m, Interval, DriverData%AFInfo, InitOut, ErrStat2, ErrMsg2 ) - call SetErrStat(ErrStat2,ErrMsg2, ErrStat, ErrMsg, RoutineName) + call SetErrStat(ErrStat2,ErrMsg2, ErrStat, ErrMsg, RoutineName) - if (ErrStat < AbortErrLev) then + ! -CheckInput: skip opening .out -- a validation-only run must leave no compute-output artifact behind + if (ErrStat < AbortErrLev .and. .not. (present(CheckInputMode) .and. CheckInputMode)) then call Dvr_InitializeOutputs(DriverData, InitOut, errStat2, errMsg2) call SetErrStat(ErrStat2,ErrMsg2, ErrStat, ErrMsg, RoutineName) end if diff --git a/modules/aerodyn/src/AeroDyn_Driver.f90 b/modules/aerodyn/src/AeroDyn_Driver.f90 index fb23ead32e..1c56605197 100644 --- a/modules/aerodyn/src/AeroDyn_Driver.f90 +++ b/modules/aerodyn/src/AeroDyn_Driver.f90 @@ -22,7 +22,8 @@ program AeroDyn_Driver use AeroDyn_Driver_Subs, only: idAnalysisRegular, idAnalysisTimeD, idAnalysisCombi use NWTC_IO use NWTC_Num, only: RunTimes, SimStatus, SimStatus_FirstTime - implicit none + use NWTC_CheckInput + implicit none ! Program variables REAL(ReKi) :: PrevClockTime ! Clock time at start of simulation in seconds [(s)] REAL(ReKi) :: UsrTime1 ! User CPU time for simulation initialization [(s)] @@ -35,20 +36,46 @@ program AeroDyn_Driver integer :: nt !< loop counter (for time step) integer(IntKi) :: iCase ! loop counter (for driver case) + LOGICAL :: CheckInputMode ! true if -CheckInput was given on the command line; set by Dvr_Init's + ! optional out-argument below (Dvr_SimData is Registry-generated, so the + ! flag is threaded back here instead of adding a field to it) (no initializer -- set below) + TYPE(CheckInputCollectorType) :: Checker ! -CheckInput result collector + CHARACTER(64) :: CkStage ! name of the -CheckInput stage/component currently executing (no initializer -- set below) + INTEGER(IntKi) :: ErrStat2 ! secondary error status, used only for -CheckInput report calls + CHARACTER(ErrMsgLen) :: ErrMsg2 ! secondary error message, used only for -CheckInput report calls + CALL DATE_AND_TIME ( Values=StrtTime ) ! Let's time the whole simulation CALL CPU_TIME ( UsrTime1 ) ! Initial time (this zeros the start time when used as a MATLAB function) UsrTime1 = MAX( 0.0_ReKi, UsrTime1 ) ! CPU_TIME: If a meaningful time cannot be returned, a processor-dependent negative value is returned ! ----- dat%initialized=.false. - call Dvr_Init(dat%dvr, dat%ADI, dat%FED, dat%SeaSt, dat%errStat, dat%errMsg); call CheckError() + CheckInputMode = .FALSE. + CkStage = 'Driver' ! default stage label, covers the Dvr_Init call below; overridden to 'Case'/'Case' per case + call Dvr_Init(dat%dvr, dat%ADI, dat%FED, dat%SeaSt, dat%errStat, dat%errMsg, CheckInputMode=CheckInputMode); call CheckError() + + IF ( CheckInputMode ) THEN + CALL CkIn_OpenReport( Checker, TRIM(dat%dvr%root)//'.driver', ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) + END IF do iCase= 1,dat%dvr%numCases + IF ( CheckInputMode ) CkStage = CaseLabel(iCase, dat%dvr%numCases) + ! Initial case - call Dvr_InitCase(iCase, dat%dvr, dat%ADI, dat%FED, dat%SeaSt, dat%errStat, dat%errMsg); call CheckError() + call Dvr_InitCase(iCase, dat%dvr, dat%ADI, dat%FED, dat%SeaSt, dat%errStat, dat%errMsg, CheckInputMode=CheckInputMode); call CheckError() dat%initialized=.true. - + + IF ( CheckInputMode ) THEN + ! validation only: every case is initialized above (validating it), but never time-stepped. + ! Mirror the normal path's inter-case teardown (below, after the time loop) before moving to + ! the next case -- without it, a combined-case deck's later cases would re-init on top of an + ! un-torn-down previous case. + call Dvr_EndCase(dat%dvr, dat%ADI, dat%initialized, dat%errStat, dat%errMsg); call CheckError() + CYCLE + END IF + ! Init of time estimator t_global=0.0_DbKi t_final=dat%dvr%numSteps*dat%dvr%dt @@ -75,18 +102,31 @@ program AeroDyn_Driver enddo ! Loop on cases + IF ( CheckInputMode ) THEN + ! Reaching here means every case's Dvr_InitCase above completed without a fatal error (a fatal + ! one would have routed through CheckError's CkIn_DriverFail interception and never returned). + ! Record each case as passed, in order, then finish -- this call never returns. + DO iCase = 1, dat%dvr%numCases + CkStage = CaseLabel(iCase, dat%dvr%numCases) + CALL CkIn_Collect( Checker, TRIM(CkStage), ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, TRIM(CkStage), ErrStat2, ErrMsg2 ) + END DO + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns + END IF + call Dvr_End() contains -!................................ +!................................ subroutine CheckError() if (dat%ErrStat /= ErrID_None) then call WrScr(TRIM(dat%errMsg)) if (dat%errStat >= AbortErrLev) then + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), dat%errStat, dat%errMsg ) ! never returns call Dvr_End() end if end if end subroutine CheckError -!................................ +!................................ subroutine Dvr_End() integer(IntKi) :: errStat2 ! local status of error message character(ErrMsgLen) :: errMsg2 ! local error message if ErrStat /= ErrID_None @@ -94,7 +134,7 @@ subroutine Dvr_End() call Dvr_CleanUp(dat%dvr, dat%ADI, dat%FED, dat%initialized, errStat2, errMsg2) CALL SetErrStat(errStat2, errMsg2, dat%errStat, dat%errMsg, 'Dvr_End') - if (dat%errStat >= AbortErrLev) then + if (dat%errStat >= AbortErrLev) then call WrScr('') CALL ProgAbort( 'AeroDyn Driver encountered simulation error level: '& //TRIM(GetErrStr(dat%errStat)), TrapErrors=.FALSE., TimeWait=3._ReKi ) ! wait 3 seconds (in case they double-clicked and got an error) @@ -102,6 +142,16 @@ subroutine Dvr_End() call NormStop() end if end subroutine Dvr_End -!................................ +!................................ + function CaseLabel(i, n) result(lbl) + integer(IntKi), intent(in) :: i, n + character(64) :: lbl + if (n > 1) then + lbl = 'Case'//trim(num2lstr(i)) + else + lbl = 'Case' + end if + end function CaseLabel +!................................ end program AeroDyn_Driver diff --git a/modules/aerodyn/src/AeroDyn_Driver_Subs.f90 b/modules/aerodyn/src/AeroDyn_Driver_Subs.f90 index a305d32879..25dbcfada7 100644 --- a/modules/aerodyn/src/AeroDyn_Driver_Subs.f90 +++ b/modules/aerodyn/src/AeroDyn_Driver_Subs.f90 @@ -91,13 +91,19 @@ module AeroDyn_Driver_Subs !---------------------------------------------------------------------------------------------------------------------------------- !> -subroutine Dvr_Init(dvr, ADI, FED, SeaSt, errStat, errMsg ) +subroutine Dvr_Init(dvr, ADI, FED, SeaSt, errStat, errMsg, CheckInputMode ) type(Dvr_SimData), intent( out) :: dvr !< driver data type(ADI_Data), intent( out) :: ADI !< AeroDyn/InflowWind data type(FED_Data), intent( out) :: FED !< Elastic wind turbine data (Fake ElastoDyn) type(SeaState_Data), intent( out) :: SeaSt !< SeaState data integer(IntKi) , intent( out) :: errStat !< Status of error message character(*) , intent( out) :: errMsg !< Error message if errStat /= ErrID_None + logical, optional , intent( out) :: CheckInputMode !< true if '-CheckInput' was given on the command line. + !! Dvr_SimData (dvr) is Registry-generated (AeroDyn_Driver_Types.f90), + !! so rather than regenerating the registry to add a field there, the + !! flag is read here (where CheckArgs/FlagArg already live) and threaded + !! back to the main program (AeroDyn_Driver.f90) via this optional + !! argument, which sets a program-level variable there. ! local variables integer(IntKi) :: errStat2 ! local status of error message character(ErrMsgLen) :: errMsg2 ! local error message if errStat /= ErrID_None @@ -106,13 +112,18 @@ subroutine Dvr_Init(dvr, ADI, FED, SeaSt, errStat, errMsg ) integer :: iWT ! Index on wind turbines/rotors errStat = ErrID_None errMsg = "" + if (present(CheckInputMode)) CheckInputMode = .FALSE. ! --- Driver initialization CALL NWTC_Init( ProgNameIN=version%Name ) - + InputFile = "" ! initialize to empty string to make sure it's input from the command line CALL CheckArgs( InputFile, Flag=FlagArg ) - IF ( LEN( TRIM(FlagArg) ) > 0 ) CALL NormStop() ! stop if user set a flag argument (like '-h' or '-v') + IF ( TRIM(FlagArg) == 'CHECKINPUT' ) THEN + IF ( PRESENT(CheckInputMode) ) CheckInputMode = .TRUE. + ELSE IF ( LEN( TRIM(FlagArg) ) > 0 ) THEN + CALL NormStop() ! -h/-v were already handled inside CheckArgs + END IF ! Display the copyright notice and compile info: CALL DispCopyrightLicense( version%Name ) @@ -140,7 +151,7 @@ end subroutine Dvr_Init !---------------------------------------------------------------------------------------------------------------------------------- !> -subroutine Dvr_InitCase(iCase, dvr, ADI, FED, SeaSt, errStat, errMsg ) +subroutine Dvr_InitCase(iCase, dvr, ADI, FED, SeaSt, errStat, errMsg, CheckInputMode ) integer(IntKi) , intent(in ) :: iCase type(Dvr_SimData) , intent(inout) :: dvr !< driver data type(ADI_Data) , intent(inout) :: ADI !< AeroDyn/InflowWind data @@ -148,15 +159,20 @@ subroutine Dvr_InitCase(iCase, dvr, ADI, FED, SeaSt, errStat, errMsg ) type(SeaState_Data) , intent(inout) :: SeaSt !< SeaState data integer(IntKi) , intent( out) :: errStat ! Status of error message character(*) , intent( out) :: errMsg ! Error message if errStat /= ErrID_None + logical, optional , intent(in ) :: CheckInputMode !< true under '-CheckInput': skip the output-file and VTK-reference + !! writes below so a validation-only run leaves no compute artifact behind ! local variables integer(IntKi) :: errStat2 ! local status of error message character(ErrMsgLen) :: errMsg2 ! local error message if errStat /= ErrID_None integer(IntKi) :: iWT, j !< logical :: needInitIW ! Need to initialize IfW if any changes to wind in combined cases + logical :: skipOutputInit ! true if CheckInputMode is present and .true. errStat = ErrID_None errMsg = "" needInitIW = .false. + skipOutputInit = .false. + if (present(CheckInputMode)) skipOutputInit = CheckInputMode dvr%out%root = dvr%root dvr%iCase = iCase ! for output only.. @@ -283,12 +299,16 @@ subroutine Dvr_InitCase(iCase, dvr, ADI, FED, SeaSt, errStat, errMsg ) call ADI_CalcOutput(ADI%inputTimes(1), ADI%u(1), ADI%p, ADI%x(1), ADI%xd(1), ADI%z(1), ADI%OtherState(1), ADI%y, ADI%m, errStat2, errMsg2); if(Failed()) return ! --- Initialize outputs - call Dvr_InitializeOutputs(dvr%numTurbines, dvr%out, dvr%numSteps, errStat2, errMsg2); if(Failed()) return + ! -CheckInput: skip opening [.Tn].out -- a validation-only run must leave no compute-output artifact behind + if ( .not. skipOutputInit ) then + call Dvr_InitializeOutputs(dvr%numTurbines, dvr%out, dvr%numSteps, errStat2, errMsg2); if(Failed()) return + end if call Dvr_CalcOutputDriver(dvr, ADI%y, FED, errStat2, errMsg2); if(Failed()) return ! --- Initialize VTK - if (dvr%out%WrVTK>0) then + ! -CheckInput: skip creating the vtk/ directory and writing the ground-surface reference file + if (dvr%out%WrVTK>0 .and. .not. skipOutputInit) then dvr%out%n_VTKTime = 1 dvr%out%VTKRefPoint = (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /) call SetVTKParameters(dvr%out, dvr, ADI, errStat2, errMsg2); if(Failed()) return @@ -396,7 +416,11 @@ subroutine Dvr_EndCase(dvr, ADI, initialized, errStat, errMsg) if (dvr%out%unOutFile(iWT) > 0) close(dvr%out%unOutFile(iWT)) enddo endif - if (dvr%out%fileFmt==idFmtBoth .or. dvr%out%fileFmt == idFmtBinary) then + ! dvr%out%storage is only allocated when output init actually ran (Dvr_InitOutput, skipped + ! entirely under -CheckInput's skipOutputInit); guard against writing an unallocated array -- + ! without this, calling Dvr_EndCase from the check-mode case loop segfaults for any case whose + ! driver input requests binary output. + if ( (dvr%out%fileFmt==idFmtBoth .or. dvr%out%fileFmt == idFmtBinary) .and. allocated(dvr%out%storage) ) then do iWT=1,dvr%numTurbines if (dvr%numTurbines >1) then sWT = '.T'//trim(num2lstr(iWT)) diff --git a/modules/aerodyn/src/UnsteadyAero_Driver.f90 b/modules/aerodyn/src/UnsteadyAero_Driver.f90 index f04e4391b7..127ba73d35 100644 --- a/modules/aerodyn/src/UnsteadyAero_Driver.f90 +++ b/modules/aerodyn/src/UnsteadyAero_Driver.f90 @@ -30,6 +30,7 @@ program UnsteadyAero_Driver use VersionInfo use LinDyn + use NWTC_CheckInput implicit none ! Variables @@ -48,33 +49,75 @@ program UnsteadyAero_Driver CHARACTER(200) :: git_commit TYPE(ProgDesc), PARAMETER :: version = ProgDesc( 'UnsteadyAero Driver', '', '' ) ! The version number of this program. + + LOGICAL :: CheckInputMode ! true if -CheckInput was given on the command line (no initializer -- set below) + TYPE(CheckInputCollectorType) :: Checker ! -CheckInput result collector + CHARACTER(64) :: CkStage ! name of the -CheckInput stage/component currently executing (no initializer -- set below) + INTEGER(IntKi) :: ErrStat2 ! secondary error status, used only for -CheckInput report calls + CHARACTER(ErrMsgLen) :: ErrMsg2 ! secondary error message, used only for -CheckInput report calls + INTEGER(IntKi) :: NumArgs ! number of command-line arguments + INTEGER(IntKi) :: ArgIdx ! loop counter for the command-line argument scan + CHARACTER(1024) :: ArgVal ! one command-line argument, raw + CHARACTER(1024) :: ArgUC ! ArgVal with the switch character stripped, upper-cased for flag matching + ! Initialize the NWTC library call NWTC_Init() - + ! Initialize error handling variables ErrMsg = '' ErrStat = ErrID_None - + CheckInputMode = .FALSE. + CkStage = 'Driver' ! default stage label; overridden before each named stage below + ! Display the copyright notice CALL DispCopyrightLicense( version%Name ) ! Obtain OpenFAST git commit hash git_commit = QueryGitVersion() ! Tell our users what they're running CALL WrScr(' Running '//TRIM( version%Name )//' a part of OpenFAST - '//TRIM(git_Commit)) - - - ! --- Parse the driver file if one - if ( command_argument_count() > 1 ) then - call print_help() - call NormStop() - endif - call get_command_argument(1, dvrFilename) + + + ! --- Parse the driver file if one + ! Scan the command line: this driver only ever counted arguments (>1 -> help+exit), so + ! "file -checkinput" used to be miscounted as too many arguments and print help + exit 0 (a + ! false pass). Replace with a real scan: the first non-flag argument is the driver input file; + ! a flag matching CHECKINPUT (either switch character, case-insensitive) enables -CheckInput + ! mode; any other flag, or a second non-flag argument, preserves this driver's existing + ! contract for unrecognized/too-many arguments (print help, exit). + dvrFilename = '' + NumArgs = command_argument_count() + do ArgIdx = 1, NumArgs + call get_command_argument( ArgIdx, ArgVal ) + if ( len_trim(ArgVal) > 0 .and. ( ArgVal(1:1) == SwChar .or. ArgVal(1:1) == '-' ) ) then + ArgUC = ArgVal(2:) + call Conv2UC( ArgUC ) + if ( trim(ArgUC) == 'CHECKINPUT' ) then + CheckInputMode = .true. + else + call print_help() + call NormStop() + end if + else if ( len_trim(dvrFilename) == 0 ) then + dvrFilename = ArgVal + else + call print_help() + call NormStop() + end if + end do + + CkStage = 'Driver' call ReadDriverInputFile( dvrFilename, dvr%p, errStat, errMsg ); call checkError() + IF ( CheckInputMode ) THEN + CALL CkIn_OpenReport( Checker, TRIM(dvr%p%OutRootName)//'.driver', ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) + END IF + ! --- Driver Parameters call Dvr_SetParameters(dvr%p, errStat, errMsg); call checkError() ! --- Initialize Elastic Section + CkStage = 'LinDyn' if ( dvr%p%SimMod == 3 ) then call LD_InitInputData(3, dvr%LD_InitInData, errStat, errMsg); call checkError() dvr%LD_InitInData%dt = dvr%p%dt @@ -101,6 +144,7 @@ program UnsteadyAero_Driver end if ! --- Init UA input data based on driver inputs + CkStage = 'UnsteadyAero' call driverInputsToUAInitData(dvr%p, dvr%UA_InitInData, dvr%AFI_Params, dvr%AFIndx, errStat, errMsg); call checkError() ! --- Initialize UnsteadyAero (need AFI) @@ -150,6 +194,27 @@ program UnsteadyAero_Driver ! --- Time marching loop call Dvr_InitializeOutputs(dvr%out, dvr%p%numSteps, errStat, errMsg) + IF ( CheckInputMode ) THEN + ! Reaching here means every stage above completed without a fatal error (a fatal one would have + ! routed through checkError's CkIn_DriverFail interception and never returned). Record all three + ! stages, marking LinDyn not_used when the driver input file doesn't enable SimMod 3, then + ! finish -- this call never returns. + CALL CkIn_Collect( Checker, 'Driver', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Driver', ErrStat2, ErrMsg2 ) + + IF ( dvr%p%SimMod == 3 ) THEN + CALL CkIn_Collect( Checker, 'LinDyn', ErrID_None, '' ) + ELSE + CALL CkIn_Collect( Checker, 'LinDyn', ErrID_None, '', Status='not_used' ) + END IF + CALL CkIn_ReportComponent( Checker, 'LinDyn', ErrStat2, ErrMsg2 ) + + CALL CkIn_Collect( Checker, 'UnsteadyAero', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'UnsteadyAero', ErrStat2, ErrMsg2 ) + + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns + END IF + if ( dvr%p%SimMod == 3 ) then ! --- Time marching loop @@ -286,10 +351,11 @@ end subroutine Cleanup subroutine checkError() if (ErrStat >= AbortErrLev) then - + + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns call Cleanup() call ProgAbort(ErrMsg) - + elseif ( ErrStat /= ErrID_None ) then call WrScr( trim(ErrMsg) ) diff --git a/modules/beamdyn/src/Driver_Beam.f90 b/modules/beamdyn/src/Driver_Beam.f90 index 0141190d7e..531e578fe9 100644 --- a/modules/beamdyn/src/Driver_Beam.f90 +++ b/modules/beamdyn/src/Driver_Beam.f90 @@ -22,6 +22,7 @@ PROGRAM BeamDyn_Driver_Program USE BeamDyn_driver_subs ! all other modules inherited through this one USE VersionInfo + USE NWTC_CheckInput IMPLICIT NONE @@ -69,7 +70,17 @@ PROGRAM BeamDyn_Driver_Program CHARACTER(200) :: git_commit ! String containing the current git commit hash TYPE(ProgDesc), PARAMETER :: version = ProgDesc( 'BeamDyn Driver', '', '' ) ! The version number of this program. - + + LOGICAL :: CheckInputMode ! true if -CheckInput was given on the command line (no initializer -- set below) + TYPE(CheckInputCollectorType) :: Checker ! -CheckInput result collector + CHARACTER(64) :: CkStage ! name of the -CheckInput stage/component currently executing (no initializer -- set below) + INTEGER(IntKi) :: ErrStat2 ! secondary error status, used only for -CheckInput report calls + CHARACTER(ErrMsgLen) :: ErrMsg2 ! secondary error message, used only for -CheckInput report calls + INTEGER(IntKi) :: NumArgs ! number of command-line arguments + INTEGER(IntKi) :: ArgIdx ! loop counter for the command-line argument scan + CHARACTER(1024) :: ArgVal ! one command-line argument, raw + CHARACTER(1024) :: ArgUC ! ArgVal with the switch character stripped, upper-cased for flag matching + ! ------------------------------------------------------------------------- ! Initialization of library (especially for screen output) @@ -92,11 +103,40 @@ PROGRAM BeamDyn_Driver_Program ! Initialization of glue-code time-step variables ! ------------------------------------------------------------------------- - CALL GET_COMMAND_ARGUMENT(1,DvrInputFile) + CheckInputMode = .FALSE. + CkStage = 'Driver' ! default stage label; overridden before each named stage below + DvrInputFile = '' + + ! Scan the command line: no flag dispatch existed here before, so this driver never used + ! CheckArgs -- the first non-flag argument is the driver input file; a flag matching + ! CHECKINPUT (either switch character, case-insensitive) enables -CheckInput mode; any other + ! flag is silently ignored (this driver family's existing convention for unrecognized flags). + NumArgs = COMMAND_ARGUMENT_COUNT() + DO ArgIdx = 1, NumArgs + CALL GET_COMMAND_ARGUMENT( ArgIdx, ArgVal ) + IF ( LEN_TRIM(ArgVal) > 0 .AND. ( ArgVal(1:1) == SwChar .OR. ArgVal(1:1) == '-' ) ) THEN + ArgUC = ArgVal(2:) + CALL Conv2UC( ArgUC ) + IF ( TRIM(ArgUC) == 'CHECKINPUT' ) THEN + CheckInputMode = .TRUE. + END IF + ! unrecognized flags are silently ignored -- matches this driver's pre-existing behavior + ELSE IF ( LEN_TRIM(DvrInputFile) == 0 ) THEN + DvrInputFile = ArgVal + END IF + END DO + CALL GetRoot(DvrInputFile,RootName) + + IF ( CheckInputMode ) THEN + CALL CkIn_OpenReport( Checker, TRIM(RootName)//'.driver', ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) + END IF + + CkStage = 'Driver' CALL BD_ReadDvrFile(DvrInputFile,dt_global,BD_InitInput,DvrData,ErrStat,ErrMsg) CALL CheckError() - + ! initialize the BD_InitInput values not in the driver input file BD_InitInput%RootName = TRIM(BD_Initinput%InputFile) BD_InitInput%RootName = TRIM(RootName)//'.BD' @@ -109,8 +149,9 @@ PROGRAM BeamDyn_Driver_Program !Module1: allocate Input and Output arrays; used for interpolation and extrapolation ALLOCATE(BD_Input(BD_interp_order + 1)) - ALLOCATE(BD_InputTimes(BD_interp_order + 1)) + ALLOCATE(BD_InputTimes(BD_interp_order + 1)) + CkStage = 'BeamDyn' CALL BD_Init(BD_InitInput & , BD_Input(1) & , BD_Parameter & @@ -140,9 +181,15 @@ PROGRAM BeamDyn_Driver_Program call CreateMultiPointMeshes(DvrData,BD_InitInput,BD_InitOutput,BD_Parameter,BD_OtherState, BD_Output, BD_Input(1), ErrStat, ErrMsg) call Transfer_MultipointLoads(DvrData, BD_Output, BD_Input(1), ErrStat, ErrMsg) - CALL Dvr_InitializeOutputFile(DvrOut,BD_InitOutput,RootName,ErrStat,ErrMsg) - CALL CheckError() - + ! -CheckInput: this call unconditionally opens .out -- skip it entirely in check mode so + ! a passing check run leaves no compute-output artifact behind; DvrOut is never used before the + ! P4 exit below (Dvr_WriteOutputLine is only reached inside the time-marching loop, which check + ! mode never enters). + IF ( .NOT. CheckInputMode ) THEN + CALL Dvr_InitializeOutputFile(DvrOut,BD_InitOutput,RootName,ErrStat,ErrMsg) + CALL CheckError() + END IF + ! initialize BD_Input and BD_InputTimes BD_InputTimes(1) = DvrData%t_initial @@ -159,22 +206,39 @@ PROGRAM BeamDyn_Driver_Program CALL CheckError() END DO + ! -CheckInput: both VTK reference blocks below are file-write side effects driven by the driver + ! input file's WrVTK setting -- skip them entirely in check mode so a passing check run leaves no + ! compute artifacts behind. Nothing after this point before the P4 exit depends on them. + IF ( .NOT. CheckInputMode ) THEN ! Write VTK reference if requested (ref is (0,0,0) - if (DvrData%WrVTK > 0) then - call SetVTKvars() - call MeshWrVTKreference( (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /), BD_Output%BldMotion, trim(DvrData%VTK_OutFileRoot)//'_BldMotion', ErrStat, ErrMsg ); call CheckError() - call MeshWrVTKreference( (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /), BD_Input(1)%PointLoad, trim(DvrData%VTK_OutFileRoot)//'_PointLoad', ErrStat, ErrMsg ); call CheckError() - call MeshWrVTKreference( (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /), BD_Input(1)%DistrLoad, trim(DvrData%VTK_OutFileRoot)//'_DistrLoad', ErrStat, ErrMsg ); call CheckError() - endif - ! Write VTK reference if requested (ref is (0,0,0) - if (DvrData%WrVTK == 2) then - n_t_vtk = 0 - call MeshWrVTK( (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /), BD_Output%BldMotion, trim(DvrData%VTK_OutFileRoot)//'_BldMotion', n_t_vtk, .true., ErrStat, ErrMsg, DvrData%VTK_tWidth ) - call MeshWrVTK( (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /), BD_Input(1)%PointLoad, trim(DvrData%VTK_OutFileRoot)//'_PointLoad', n_t_vtk, .true., ErrStat, ErrMsg, DvrData%VTK_tWidth ) - call MeshWrVTK( (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /), BD_Input(1)%DistrLoad, trim(DvrData%VTK_OutFileRoot)//'_DistrLoad', n_t_vtk, .true., ErrStat, ErrMsg, DvrData%VTK_tWidth ) - call CheckError() - endif - + if (DvrData%WrVTK > 0) then + call SetVTKvars() + call MeshWrVTKreference( (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /), BD_Output%BldMotion, trim(DvrData%VTK_OutFileRoot)//'_BldMotion', ErrStat, ErrMsg ); call CheckError() + call MeshWrVTKreference( (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /), BD_Input(1)%PointLoad, trim(DvrData%VTK_OutFileRoot)//'_PointLoad', ErrStat, ErrMsg ); call CheckError() + call MeshWrVTKreference( (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /), BD_Input(1)%DistrLoad, trim(DvrData%VTK_OutFileRoot)//'_DistrLoad', ErrStat, ErrMsg ); call CheckError() + endif + ! Write VTK reference if requested (ref is (0,0,0) + if (DvrData%WrVTK == 2) then + n_t_vtk = 0 + call MeshWrVTK( (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /), BD_Output%BldMotion, trim(DvrData%VTK_OutFileRoot)//'_BldMotion', n_t_vtk, .true., ErrStat, ErrMsg, DvrData%VTK_tWidth ) + call MeshWrVTK( (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /), BD_Input(1)%PointLoad, trim(DvrData%VTK_OutFileRoot)//'_PointLoad', n_t_vtk, .true., ErrStat, ErrMsg, DvrData%VTK_tWidth ) + call MeshWrVTK( (/0.0_SiKi, 0.0_SiKi, 0.0_SiKi /), BD_Input(1)%DistrLoad, trim(DvrData%VTK_OutFileRoot)//'_DistrLoad', n_t_vtk, .true., ErrStat, ErrMsg, DvrData%VTK_tWidth ) + call CheckError() + endif + END IF + + IF ( CheckInputMode ) THEN + ! Reaching here means every stage above completed without a fatal error (a fatal one would have + ! routed through CheckError's CkIn_DriverFail interception and never returned). A t=0 BD_CalcOutput + ! is compute (and its output-file write is a side effect), so the check ends here at init+meshes; + ! quasi-static init (RunQuasiStaticInit, applied inside BD_Init above) IS included -- it's part of + ! init. Record both stages, in order, then finish -- this call never returns. + CALL CkIn_Collect( Checker, 'Driver', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Driver', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'BeamDyn', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'BeamDyn', ErrStat2, ErrMsg2 ) + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns + END IF !......................... ! calculate outputs at t=0 @@ -285,8 +349,9 @@ subroutine CheckError() if (ErrStat /= ErrID_None) then call WrScr(TRIM(ErrMsg)) - + if (ErrStat >= AbortErrLev) then + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns call Dvr_End() end if end if diff --git a/modules/hydrodyn/src/HydroDyn_DriverCode.f90 b/modules/hydrodyn/src/HydroDyn_DriverCode.f90 index ffee717a36..582093d311 100644 --- a/modules/hydrodyn/src/HydroDyn_DriverCode.f90 +++ b/modules/hydrodyn/src/HydroDyn_DriverCode.f90 @@ -23,7 +23,8 @@ PROGRAM HydroDynDriver USE HydroDynDriverSubs - + USE NWTC_CheckInput + IMPLICIT NONE INTEGER(IntKi), PARAMETER :: NumInp = 1 ! Number of inputs sent to HydroDyn_UpdateStates @@ -89,13 +90,21 @@ PROGRAM HydroDynDriver CHARACTER(20) :: FlagArg ! Flag argument from command line + LOGICAL :: CheckInputMode ! true if -CheckInput was given on the command line (no initializer -- set below) + TYPE(CheckInputCollectorType) :: Checker ! -CheckInput result collector + CHARACTER(64) :: CkStage ! name of the -CheckInput stage/component currently executing (no initializer -- set below) + INTEGER(IntKi) :: ErrStat2 ! secondary error status, used only for -CheckInput report calls + CHARACTER(ErrMsgLen) :: ErrMsg2 ! secondary error message, used only for -CheckInput report calls + ! Variables Init Time = -99999 ! initialize to negative number for error messages ErrStat = ErrID_None ErrMsg = "" SeaState_Initialized = .false. HydroDyn_Initialized = .false. - + CheckInputMode = .FALSE. + CkStage = 'Driver' ! default stage label; overridden before each named stage below + !............................................................................................................................... ! Routines called in initialization !............................................................................................................................... @@ -122,8 +131,12 @@ PROGRAM HydroDynDriver drvrFilename = '' CALL CheckArgs( drvrFilename, Flag=FlagArg ) - IF ( LEN( TRIM(FlagArg) ) > 0 ) CALL NormStop() - + IF ( TRIM(FlagArg) == 'CHECKINPUT' ) THEN + CheckInputMode = .TRUE. + ELSE IF ( LEN( TRIM(FlagArg) ) > 0 ) THEN + CALL NormStop() ! -h/-v were already handled inside CheckArgs + END IF + ! Get the current time call date_and_time ( Values=StrtTime ) ! Let's time the whole simulation @@ -136,13 +149,19 @@ PROGRAM HydroDynDriver ! Parse the driver input file and run the simulation based on that file + CkStage = 'Driver' CALL ReadDriverInputFile( drvrFilename, drvrData, ErrStat, ErrMsg ) CALL CheckError() - + + IF ( CheckInputMode ) THEN + CALL CkIn_OpenReport( Checker, TRIM(drvrData%OutRootName)//'.driver', ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) + END IF + ! Read the PRPInputsFile: CALL ReadPRPInputsFile( drvrData, ErrStat, ErrMsg ) CALL CheckError() - + drvrData%OutData%NumOuts = 0 drvrData%OutData%n_Out = 0 drvrData%TMax = (drvrData%NSteps-1) * drvrData%TimeInterval ! Starting time is always t = 0.0 @@ -175,7 +194,8 @@ PROGRAM HydroDynDriver ! Initialize the HydroDyn module Interval = drvrData%TimeInterval - + + CkStage = 'SeaState' call SeaSt_Init( InitInData_SeaSt, u_SeaSt(1), p_SeaSt, x_SeaSt, xd_SeaSt, z_SeaSt, OtherState_SeaSt, y_SeaSt, m_SeaSt, Interval, InitOutData_SeaSt, ErrStat, ErrMsg ) SeaState_Initialized = .true. CALL CheckError() @@ -184,14 +204,15 @@ PROGRAM HydroDynDriver if ( Interval /= drvrData%TimeInterval) then ErrMsg = 'The SeaState Module attempted to change timestep interval, but this is not allowed. The SeaState Module must use the Driver Interval.' ErrStat = ErrID_Fatal - call HD_DvrEnd() + CALL CheckError() ! routes through the same chokepoint as every other fatal (was a direct HD_DvrEnd() call) end if - + ! Set HD Init Inputs based on SeaStates Init Outputs call SetHD_InitInputs() ! Initialize the module Interval = drvrData%TimeInterval + CkStage = 'HydroDyn' CALL HydroDyn_Init( InitInData_HD, u(1), p, x, xd, z, OtherState, y, m, Interval, InitOutData_HD, ErrStat, ErrMsg ) HydroDyn_Initialized = .true. CALL CheckError() @@ -199,12 +220,19 @@ PROGRAM HydroDynDriver IF ( Interval /= drvrData%TimeInterval) THEN ErrMsg = ' The HydroDyn Module attempted to change timestep interval, but this is not allowed. The HydroDyn Module must use the Driver Interval.' ErrStat = ErrID_Fatal - call HD_DvrEnd() + CALL CheckError() ! routes through the same chokepoint as every other fatal (was a direct HD_DvrEnd() call) END IF + CkStage = 'Driver' ! post-init mapping/interval validation below is attributed back to the Driver stage ! Initialization to concatenate all module data into a single output file - CALL InitOutputFile(InitOutData_HD, InitOutData_SeaSt, drvrData, ErrStat, ErrMsg ); CALL CheckError() + ! -CheckInput: drvrData%WrTxtOutFile is unconditionally .TRUE. (set in ReadDriverInputFile), so + ! InitOutputFile always opens .out -- skip the call entirely in check mode so a + ! passing -CheckInput run leaves no compute-output artifact behind; nothing after this point + ! before P4 depends on drvrData%OutData being populated. + IF ( .NOT. CheckInputMode ) THEN + CALL InitOutputFile(InitOutData_HD, InitOutData_SeaSt, drvrData, ErrStat, ErrMsg ); CALL CheckError() + END IF ! Destroy InitInput and InitOutput data (and nullify pointers to SeaState data) @@ -285,6 +313,19 @@ PROGRAM HydroDynDriver maxAngle = 0.0 mappingData%Ind = 1 ! initialize + IF ( CheckInputMode ) THEN + ! Reaching here means every stage above completed without a fatal error (a fatal one would have + ! routed through CheckError's CkIn_DriverFail interception and never returned). Record all three + ! stages as passed, in order, then finish -- this call never returns. + CALL CkIn_Collect( Checker, 'Driver', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Driver', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'SeaState', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'SeaState', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'HydroDyn', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'HydroDyn', ErrStat2, ErrMsg2 ) + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns + END IF + DO n = 1, drvrData%NSteps Time = (n-1) * drvrData%TimeInterval @@ -357,8 +398,9 @@ end subroutine SetHD_InitInputs subroutine CheckError() IF ( ErrStat /= ErrID_None) THEN - + IF ( ErrStat >= AbortErrLev ) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns CALL HD_DvrEnd() END IF diff --git a/modules/inflowwind/src/InflowWind_Driver.f90 b/modules/inflowwind/src/InflowWind_Driver.f90 index f1d7dcfb12..d25c52d9a5 100644 --- a/modules/inflowwind/src/InflowWind_Driver.f90 +++ b/modules/inflowwind/src/InflowWind_Driver.f90 @@ -32,6 +32,7 @@ PROGRAM InflowWind_Driver USE InflowWind_Driver_Subs ! Contains subroutines for the driver program USE IfW_FlowField USE InflowWind_Subs, only: CalculateOutput + USE NWTC_CheckInput IMPLICIT NONE @@ -93,6 +94,13 @@ PROGRAM InflowWind_Driver INTEGER(IntKi) :: ErrStatTmp CHARACTER(2048) :: ErrMsgTmp INTEGER(IntKi) :: LenErrMsgTmp ! Length of ErrMsgTmp + INTEGER(IntKi) :: ErrStat2 ! -CheckInput: temp error status for calls + CHARACTER(1024) :: ErrMsg2 ! -CheckInput: temp error message for calls + + ! -CheckInput support (no initializers on these -- set as early executable statements below) + LOGICAL :: CheckInputMode ! true if -CheckInput was given on the command line + TYPE(CheckInputCollectorType) :: Checker ! -CheckInput result collector + CHARACTER(64) :: CkStage ! name of the -CheckInput stage/component currently executing @@ -118,6 +126,10 @@ PROGRAM InflowWind_Driver ! Start the timer CALL CPU_TIME( Timer(1) ) + ! -CheckInput: no initializers on these -- set as early executable statements + CheckInputMode = .FALSE. + CkStage = 'Driver' ! default stage label; overridden before the InflowWind_Init call below + ! Set some CLSettings to null/default values CLSettings%ProgInfo = ProgInfo Settings%ProgInfo = ProgInfo @@ -148,6 +160,10 @@ PROGRAM InflowWind_Driver ErrStat = ErrID_None ENDIF + ! -CheckInput is command-line only (mirrors how Verbose/VVerbose are handled below -- not + ! merged into SettingsFlags by UpdateSettingsWithCL, so read it straight off the CL flags). + CheckInputMode = CLSettingsFlags%CheckInput + ! Check if we are doing verbose error reporting IF ( CLSettingsFlags%VVerbose ) THEN @@ -196,6 +212,7 @@ PROGRAM InflowWind_Driver ! Read the driver input file CALL ReadDvrIptFile( CLSettings%DvrIptFileName, SettingsFlags, Settings, ProgInfo, ErrStat, ErrMsg ) IF ( ErrStat >= AbortErrLev ) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns CALL ProgAbort( ErrMsg ) ELSEIF ( ErrStat /= ErrID_None ) THEN CALL WrScr( NewLine//ErrMsg ) @@ -220,6 +237,7 @@ PROGRAM InflowWind_Driver ! was read. CALL UpdateSettingsWithCL( SettingsFlags, Settings, CLSettingsFlags, CLSettings, .TRUE., ErrStat, ErrMsg ) IF ( ErrStat >= AbortErrLev ) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns CALL ProgAbort( ErrMsg ) ELSEIF ( ErrStat /= ErrID_None ) THEN CALL WrScr( NewLine//ErrMsg ) @@ -250,6 +268,7 @@ PROGRAM InflowWind_Driver ! input file was not read. CALL UpdateSettingsWithCL( SettingsFlags, Settings, CLSettingsFlags, CLSettings, .FALSE., ErrStat, ErrMsg ) IF ( ErrStat >= AbortErrLev ) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns CALL ProgAbort( ErrMsg ) ELSEIF ( ErrStat /= ErrID_None ) THEN CALL WrScr( NewLine//ErrMsg ) @@ -272,11 +291,16 @@ PROGRAM InflowWind_Driver ! Check if the points file exists, abort if not found INQUIRE( file=TRIM(Settings%PointsFileName), exist=TempFileExist ) - IF ( TempFileExist .eqv. .FALSE. ) CALL ProgAbort( "Cannot find the points file "//TRIM(Settings%PointsFileName)) + IF ( TempFileExist .eqv. .FALSE. ) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrID_Fatal, & + "Cannot find the points file "//TRIM(Settings%PointsFileName) ) ! never returns + CALL ProgAbort( "Cannot find the points file "//TRIM(Settings%PointsFileName)) + END IF ! Now read the file in and save the points CALL ReadPointsFile( Settings%PointsFileName, PointsXYZ, ErrStat,ErrMsg ) IF ( ErrStat >= AbortErrLev ) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns CALL ProgAbort( ErrMsg ) ELSEIF ( ErrStat /= 0 ) THEN CALL WrScr( NewLine//ErrMsg ) @@ -420,15 +444,23 @@ PROGRAM InflowWind_Driver CALL GetRoot( InflowWind_InitInp%InputFileName, InflowWind_InitInp%RootName ) !InflowWind_InitInp%RootName = "" END IF + ! -CheckInput: RootName is known now (pre-Init) -- open the report before InflowWind_Init runs so + ! a fatal from Init itself is caught. Use the un-suffixed root for the driver-level report name. + IF ( CheckInputMode ) THEN + CALL CkIn_OpenReport( Checker, TRIM(InflowWind_InitInp%RootName)//'.driver', ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) + END IF + InflowWind_InitInp%RootName = trim(InflowWind_InitInp%RootName)//'.IfW' InflowWind_InitInp%RadAvg = -1.0_ReKi ! let the IfW code guess what to use InflowWind_InitInp%BoxExceedAllow = SettingsFlags%BoxExceedAllowF ! Set flag for allowing points outside the wind box (alternate interpolation method for FF) - + IF ( IfWDriver_Verbose >= 5_IntKi ) CALL WrScr('Calling InflowWind_Init...') ! Set flag to calculate accelerations if requested InflowWind_InitInp%OutputAccel = SettingsFlags%OutputAccel + CkStage = 'InflowWind' CALL InflowWind_Init( InflowWind_InitInp, InflowWind_u1, InflowWind_p, & InflowWind_x, InflowWind_xd, InflowWind_z, InflowWind_OtherState, & InflowWind_y1, InflowWind_MiscVars, Settings%DT, InflowWind_InitOut, ErrStat, ErrMsg ) @@ -438,36 +470,40 @@ PROGRAM InflowWind_Driver end if call CheckCallErr('InflowWind_Init') + CkStage = 'Driver' ! post-Init file-conversion checks below are attributed back to the Driver stage ! Convert InflowWind file to HAWC format - IF (SettingsFlags%WrHAWC) THEN + ! -CheckInput: these are file-conversion side effects requested by their own CLI flags, not + ! part of input validation -- skip them entirely in check mode so a passing check run leaves + ! no conversion artifacts behind. + IF (SettingsFlags%WrHAWC .AND. .NOT. CheckInputMode) THEN CALL IfW_WriteHAWC( InflowWind_p%FlowField, InflowWind_InitInp%RootName, ErrStat, ErrMsg ) call CheckCallErr('IfW_WriteHAWC') END IF - + ! Convert InflowWind file to Native Bladed format - IF (SettingsFlags%WrBladed) THEN + IF (SettingsFlags%WrBladed .AND. .NOT. CheckInputMode) THEN CALL IfW_WriteBladed( InflowWind_p%FlowField, InflowWind_InitInp%RootName, ErrStat, ErrMsg ) call CheckCallErr('IfW_WriteBladed') END IF - IF (SettingsFlags%WrVTK) THEN + IF (SettingsFlags%WrVTK .AND. .NOT. CheckInputMode) THEN CALL IfW_WriteVTK( InflowWind_p%FlowField, InflowWind_InitInp%RootName, ErrStat, ErrMsg ) call CheckCallErr('IfW_WriteVTK') END IF - - - IF (SettingsFlags%WrUniform) THEN + + + IF (SettingsFlags%WrUniform .AND. .NOT. CheckInputMode) THEN CALL IfW_WriteUniform( InflowWind_p%FlowField, InflowWind_InitInp%RootName, ErrStat, ErrMsg ) call CheckCallErr('IfW_WriteUniform') END IF - - IF (Settings%NOutWindXY>0) THEN + + IF (Settings%NOutWindXY>0 .AND. .NOT. CheckInputMode) THEN do i=1,Settings%NOutWindXY CALL IfW_WriteXYslice( InflowWind_p%FlowField, InflowWind_InitInp%RootName, VTKsliceDir, Settings%OutWindZ(i), ErrStat, ErrMsg ) call CheckCallErr('IfW_WriteXYslice'//trim(Num2LStr(i))) @@ -497,6 +533,18 @@ PROGRAM InflowWind_Driver !-------------------------------------------------------------------------------------------------------------------------------- ! Setup any additional things + IF ( CheckInputMode ) THEN + ! Reaching here means every stage above completed without a fatal error (a fatal one would have + ! routed through CheckCallErr's CkIn_DriverFail interception, or one of the inline interceptions + ! above, and never returned). Record both stages as passed, in order, then finish -- this call + ! never returns, so the WindGrid/Points/FFT compute setup below is never reached in check mode. + CALL CkIn_Collect( Checker, 'Driver', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Driver', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'InflowWind', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'InflowWind', ErrStat2, ErrMsg2 ) + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns + END IF + if (SettingsFlags%WindGrid .or. SettingsFlags%PointsFile .or. SettingsFlags%FFTcalc) then ! we can skip all of this if we haven't asked for any output ! Timestep -- The timestep for the calling InflowWind_CalcOutput may need to be changed to what is in the file if the @@ -934,6 +982,7 @@ subroutine CheckCallErr(RoutineName) if (ErrStat > ErrID_None) then call WrScr( trim(ErrMsg) ) if ( ErrStat >= AbortErrLev ) then + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns call DriverCleanup() call ProgAbort( ErrMsg ) elseif ( IfWDriver_Verbose >= 7_IntKi ) then diff --git a/modules/inflowwind/src/InflowWind_Driver_Registry.txt b/modules/inflowwind/src/InflowWind_Driver_Registry.txt index 3863019d77..d40edccf10 100644 --- a/modules/inflowwind/src/InflowWind_Driver_Registry.txt +++ b/modules/inflowwind/src/InflowWind_Driver_Registry.txt @@ -54,6 +54,7 @@ typedef ^ ^ logical WrVTK - typedef ^ ^ logical WrUniform - .false. - "Requested file output as Uniform wind format?" - typedef ^ ^ logical XYslice - .false. - "Take XY slice at one elevation" - +typedef ^ ^ logical CheckInput - .false. - "specified -CheckInput mode on the command line" - diff --git a/modules/inflowwind/src/InflowWind_Driver_Subs.f90 b/modules/inflowwind/src/InflowWind_Driver_Subs.f90 index 98d7fdf293..a18c4bf4de 100644 --- a/modules/inflowwind/src/InflowWind_Driver_Subs.f90 +++ b/modules/inflowwind/src/InflowWind_Driver_Subs.f90 @@ -81,6 +81,7 @@ SUBROUTINE DispHelpText( ErrStat, ErrMsg ) CALL WrScr(" "//SwChar//"vtk -- convert contents of to vtk format ") CALL WrScr(" "//SwChar//"accel -- calculate wind acceleration in addition to velocity") CALL WrScr(" "//SwChar//"BoxExceedAllow -- set flag to allow FF points outside wind box") + CALL WrScr(" "//SwChar//"CheckInput -- validate the input deck, write a report, and exit") CALL WrScr(" "//SwChar//"help -- print this help menu and exit") CALL WrScr("") CALL WrScr(" Notes:") @@ -337,6 +338,9 @@ SUBROUTINE ParseArg( CLSettings, CLFlags, ThisArgUC, ThisArg, ifwFlagSet, ErrSta ELSEIF ( TRIM(ThisArgUC) == "ACCEL" ) THEN CLFlags%OutputAccel = .TRUE. RETURN + ELSEIF ( TRIM(ThisArgUC) == "CHECKINPUT" ) THEN + CLFlags%CheckInput = .TRUE. + RETURN ELSE CALL SetErrStat( ErrID_Warn," Unrecognized option '"//SwChar//TRIM(ThisArg)//"'. Ignoring. Use option "//SwChar//"help for list of options.", & ErrStat,ErrMsg,'ParseArg') diff --git a/modules/inflowwind/src/InflowWind_Driver_Types.f90 b/modules/inflowwind/src/InflowWind_Driver_Types.f90 index 70d5f39cb3..654bc00cc1 100644 --- a/modules/inflowwind/src/InflowWind_Driver_Types.f90 +++ b/modules/inflowwind/src/InflowWind_Driver_Types.f90 @@ -69,6 +69,7 @@ MODULE InflowWind_Driver_Types LOGICAL :: WrVTK = .false. !< Requested file output as VTK? [-] LOGICAL :: WrUniform = .false. !< Requested file output as Uniform wind format? [-] LOGICAL :: XYslice = .false. !< Take XY slice at one elevation [-] + LOGICAL :: CheckInput = .false. !< specified -CheckInput mode on the command line [-] END TYPE IfWDriver_Flags ! ======================= ! ========= IfWDriver_Settings ======= @@ -181,6 +182,7 @@ subroutine InflowWind_Driver_CopyIfWDriver_Flags(SrcIfWDriver_FlagsData, DstIfWD DstIfWDriver_FlagsData%WrVTK = SrcIfWDriver_FlagsData%WrVTK DstIfWDriver_FlagsData%WrUniform = SrcIfWDriver_FlagsData%WrUniform DstIfWDriver_FlagsData%XYslice = SrcIfWDriver_FlagsData%XYslice + DstIfWDriver_FlagsData%CheckInput = SrcIfWDriver_FlagsData%CheckInput end subroutine subroutine InflowWind_Driver_DestroyIfWDriver_Flags(IfWDriver_FlagsData, ErrStat, ErrMsg) @@ -224,6 +226,7 @@ subroutine InflowWind_Driver_PackIfWDriver_Flags(RF, Indata) call RegPack(RF, InData%WrVTK) call RegPack(RF, InData%WrUniform) call RegPack(RF, InData%XYslice) + call RegPack(RF, InData%CheckInput) if (RegCheckErr(RF, RoutineName)) return end subroutine @@ -259,6 +262,7 @@ subroutine InflowWind_Driver_UnPackIfWDriver_Flags(RF, OutData) call RegUnpack(RF, OutData%WrVTK); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%WrUniform); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%XYslice); if (RegCheckErr(RF, RoutineName)) return + call RegUnpack(RF, OutData%CheckInput); if (RegCheckErr(RF, RoutineName)) return end subroutine subroutine InflowWind_Driver_CopyIfWDriver_Settings(SrcIfWDriver_SettingsData, DstIfWDriver_SettingsData, CtrlCode, ErrStat, ErrMsg) diff --git a/modules/moordyn/src/MoorDyn_Driver.f90 b/modules/moordyn/src/MoorDyn_Driver.f90 index 772b46a7a0..d660588eea 100644 --- a/modules/moordyn/src/MoorDyn_Driver.f90 +++ b/modules/moordyn/src/MoorDyn_Driver.f90 @@ -24,8 +24,9 @@ PROGRAM MoorDyn_Driver USE MoorDyn USE SeaState_Types USE SeaState - USE NWTC_Library + USE NWTC_Library USE VersionInfo + USE NWTC_CheckInput IMPLICIT NONE @@ -131,21 +132,31 @@ PROGRAM MoorDyn_Driver CHARACTER(200) :: git_commit ! String containing the current git commit hash TYPE(ProgDesc), PARAMETER :: version = ProgDesc( 'MoorDyn Driver', '', '2024-01-18' ) + LOGICAL :: CheckInputMode ! true if -CheckInput was given on the command line (no initializer -- set below) + TYPE(CheckInputCollectorType) :: Checker ! -CheckInput result collector + CHARACTER(64) :: CkStage ! name of the -CheckInput stage/component currently executing (no initializer -- set below) + ErrMsg = "" ErrStat = ErrID_None UnEcho=-1 ! set to -1 as echo is no longer used by MD UnIn =-1 + CheckInputMode = .FALSE. + CkStage = 'Driver' ! default stage label; overridden before each named stage below + - ! TODO: Sort out error handling (two sets of flags currently used) - + CALL NWTC_Init( ProgNameIn=version%Name ) MD_InitInp%FileName = "MoorDyn.dat" ! initialize to empty string to make sure it's input from the command line CALL CheckArgs( MD_InitInp%FileName, Arg2=drvrInitInp%InputsFile, Flag=FlagArg ) - IF ( LEN( TRIM(FlagArg) ) > 0 ) CALL NormStop() + IF ( TRIM(FlagArg) == 'CHECKINPUT' ) THEN + CheckInputMode = .TRUE. + ELSE IF ( LEN( TRIM(FlagArg) ) > 0 ) THEN + CALL NormStop() ! -h/-v were already handled inside CheckArgs + END IF ! ! Display the copyright notice ! CALL DispCopyrightLicense( version%Name, ' Copyright (C) 2019 Matt Hall' ) @@ -164,9 +175,19 @@ PROGRAM MoorDyn_Driver ! Parse the driver input file and run the simulation based on that file CALL get_command_argument(1, drvrFilename) + ! -CheckInput re-fetch quirk: the raw arg 1 re-fetched above bypasses CheckArgs' parse, so if the + ! user typed "moordyn_driver -CheckInput file.dvr" it would be the flag itself, not the file name. + ! Under CheckInputMode use the CheckArgs-parsed file name instead; normal path is untouched. + IF ( CheckInputMode ) drvrFilename = MD_InitInp%FileName + CkStage = 'Driver' CALL ReadDriverInputFile( drvrFilename, drvrInitInp); - - ! do any initializing and allocating needed in prep for calling MD_Init + + IF ( CheckInputMode ) THEN + CALL CkIn_OpenReport( Checker, TRIM(drvrInitInp%OutRootName)//'.driver', ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) + END IF + + ! do any initializing and allocating needed in prep for calling MD_Init ! set the input file name and other environment terms MD_InitInp%Tmax = drvrInitInp%TMax @@ -222,6 +243,7 @@ PROGRAM MoorDyn_Driver ! -------------------------------- ----------------------------------- + CkStage = 'SeaState' IF (LEN_TRIM(drvrInitInp%SeaStateInputFile) > 0 ) THEN ! If SeaState input file path in driver input file is not empty. Error checks for Null pointer in MD_Init -> setupWaterKin ! Initialize the SeaState module InitInData_SeaSt%hasIce = .FALSE. @@ -248,8 +270,9 @@ PROGRAM MoorDyn_Driver MD_InitInp%WaveField => InitOutData_SeaSt%WaveField END IF - + ! call the initialization routine + CkStage = 'MoorDyn' CALL MD_Init( MD_InitInp, MD_u(1), MD_p, MD_x , MD_xd, MD_xc, MD_xo, MD_y, MD_m, dtC, MD_InitOut, ErrStat2, ErrMsg2 ) call AbortIfFailed() @@ -268,8 +291,8 @@ PROGRAM MoorDyn_Driver call WrScr('MoorDyn has '//trim(num2lstr(ncIn))//' coupled DOFs and/or active-tensioned inputs.') - - + + CkStage = 'Motions' if (drvrInitInp%InputsMod == 1 ) then if ( LEN( TRIM(drvrInitInp%InputsFile) ) < 1 ) then @@ -461,10 +484,11 @@ PROGRAM MoorDyn_Driver END DO - else + else nt = TMax/dtC - 1 ! number of coupling time steps - end if - + end if + CkStage = 'MoorDyn' ! post-motions setup below is attributed back to the MoorDyn stage + CALL WrScr(" ") call WrScr("Tmax - "//trim(Num2LStr(TMax))//" and nt="//trim(Num2LStr(nt))) CALL WrScr(" ") @@ -556,17 +580,50 @@ PROGRAM MoorDyn_Driver end do endif - end if ! InputsMod == 1 + end if ! InputsMod == 1 + + IF ( CheckInputMode ) THEN + ! Reaching here means every stage above completed without a fatal error (a fatal one would have + ! routed through AbortIfFailed's CkIn_DriverFail interception and never returned). Record all four + ! stages, marking SeaState/Motions not_used when the driver input file didn't enable them, then + ! finish -- this call never returns. This must run BEFORE MD_CalcOutput below: MD_CalcOutput + ! internally calls MDIO_WriteOutputs and would append a t=0 data row to .MD.out, but a + ! check-mode run only validates input and must not produce simulation output (the header row + ! written during MD_Init is an unavoidable module-scope ride-along and is fine to leave). + CALL CkIn_Collect( Checker, 'Driver', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Driver', ErrStat2, ErrMsg2 ) + + IF ( LEN_TRIM(drvrInitInp%SeaStateInputFile) > 0 ) THEN + CALL CkIn_Collect( Checker, 'SeaState', ErrID_None, '' ) + ELSE + CALL CkIn_Collect( Checker, 'SeaState', ErrID_None, '', Status='not_used' ) + END IF + CALL CkIn_ReportComponent( Checker, 'SeaState', ErrStat2, ErrMsg2 ) + + CALL CkIn_Collect( Checker, 'MoorDyn', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'MoorDyn', ErrStat2, ErrMsg2 ) + + IF ( drvrInitInp%InputsMod == 1 ) THEN + CALL CkIn_Collect( Checker, 'Motions', ErrID_None, '' ) + ELSE + CALL CkIn_Collect( Checker, 'Motions', ErrID_None, '', Status='not_used' ) + END IF + CALL CkIn_ReportComponent( Checker, 'Motions', ErrStat2, ErrMsg2 ) + + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns + END IF + CALL MD_CalcOutput( t, MD_u(1), MD_p, MD_x, MD_xd, MD_xc , MD_xo, MD_y, MD_m, ErrStat2, ErrMsg2 ); call AbortIfFailed() - - + + ! ------------------------------------------------------------------------- - ! BEGIN time marching + ! BEGIN time marching ! ------------------------------------------------------------------------- - + + call WrScr("Doing time marching now...") - + CALL SimStatus_FirstTime( PrevSimTime, PrevClockTime, SimStrtTime, SimStrtCPU, t, TMax ) DO i = 1,nt @@ -699,6 +756,10 @@ SUBROUTINE AbortIfFailed() if (ErrStat >= AbortErrLev .OR. ErrStat2 >= AbortErrLev) then call SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, 'MoorDyn_Driver') + ! SetErrStat above already folded ErrStat2/ErrMsg2 into ErrStat/ErrMsg (taking whichever was + ! more severe), so ErrStat/ErrMsg is the single authoritative fatal to report here. + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns + call EndAndCleanUp() Call ProgAbort(trim(ErrMsg)) elseif ( ErrStat2 /= ErrID_None ) THEN ! print messages as we get them (but don't call SetErrStat or they will be printed 2x) diff --git a/modules/nwtc-library/CMakeLists.txt b/modules/nwtc-library/CMakeLists.txt index 1e5395447c..d8857079ba 100644 --- a/modules/nwtc-library/CMakeLists.txt +++ b/modules/nwtc-library/CMakeLists.txt @@ -85,6 +85,7 @@ set(NWTCLIBS_SOURCES src/NWTC_Library_Types.f90 src/VTK.f90 src/YAML.f90 + src/NWTC_CheckInput.f90 src/JSON.f90 src/KdTree.f90 diff --git a/modules/nwtc-library/src/NWTC_CheckInput.f90 b/modules/nwtc-library/src/NWTC_CheckInput.f90 new file mode 100644 index 0000000000..79802b32ae --- /dev/null +++ b/modules/nwtc-library/src/NWTC_CheckInput.f90 @@ -0,0 +1,666 @@ +!********************************************************************************************************************************** +! LICENSING +! Copyright (C) 2026 National Renewable Energy Laboratory +! +! This file is part of the NWTC Subroutine Library. +! +! Licensed under the Apache License, Version 2.0 (the "License"); +! you may not use this file except in compliance with the License. +! You may obtain a copy of the License at +! +! http://www.apache.org/licenses/LICENSE-2.0 +! +! Unless required by applicable law or agreed to in writing, software +! distributed under the License is distributed on an "AS IS" BASIS, +! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +! See the License for the specific language governing permissions and +! limitations under the License. +!********************************************************************************************************************************** +!> Reusable pieces for every OpenFAST executable's `-CheckInput` mode: an in-memory collector for +!! per-component check results (built on the framework's SetErrStat/ErrStat/ErrMsg convention), a +!! console summary printer built on WrScr, and a crash-survivable incremental YAML report writer. +!! +!! Report liveness contract: CkIn_OpenReport writes a `check_status: crashed` placeholder that is never +!! rewritten (sequential formatted files cannot edit earlier lines); CkIn_CloseReport appends the +!! authoritative `overall_status:` block. A reader must treat a file with no trailing `overall_status:` +!! block as a crashed run. +MODULE NWTC_CheckInput + + USE NWTC_Library + USE YAML, ONLY: yaml_write_comm + + IMPLICIT NONE + + PRIVATE + + INTEGER(IntKi), PARAMETER, PUBLIC :: CkIn_St_Passed = 1 !< all checks for this component passed + INTEGER(IntKi), PARAMETER, PUBLIC :: CkIn_St_Failed = 2 !< one or more Severe/Fatal messages collected + INTEGER(IntKi), PARAMETER, PUBLIC :: CkIn_St_NotUsed = 3 !< component not enabled by the input file + INTEGER(IntKi), PARAMETER, PUBLIC :: CkIn_St_Crashed = 4 !< report-level liveness placeholder only + INTEGER(IntKi), PARAMETER, PUBLIC :: CkIn_St_Unavailable = 5 !< attempted with fallback data (upstream failed) + INTEGER(IntKi), PARAMETER, PUBLIC :: CkIn_St_Skipped = 6 !< stage not run (blocked by upstream failures) + + INTEGER(IntKi), PARAMETER :: CkIn_NameLen = 64 + INTEGER(IntKi), PARAMETER :: CkIn_MsgLen = 512 + + !> One collected message. Severity is the scalar ErrStat CkIn_Collect was called with: SetErrStat's + !! concatenation does not preserve per-line severity, so every line split from one call shares it. + TYPE, PUBLIC :: CkIn_MsgType + CHARACTER(CkIn_NameLen) :: Component = '' + INTEGER(IntKi) :: Severity = ErrID_None + CHARACTER(CkIn_NameLen) :: Source = '' !< inner RoutineName parsed from "RoutineName:text" + CHARACTER(CkIn_MsgLen) :: Text = '' !< message text with the "RoutineName:" prefix stripped + END TYPE CkIn_MsgType + + !> The collector. Arrays grow on demand; default-initialize and pass INTENT(INOUT) everywhere — + !! INTENT(OUT) would re-apply the default initializers and silently wipe collected data. + TYPE, PUBLIC :: CheckInputCollectorType + TYPE(CkIn_MsgType), ALLOCATABLE :: Msgs(:) + INTEGER(IntKi) :: NumMsgs = 0 + CHARACTER(CkIn_NameLen), ALLOCATABLE :: CompNames(:) !< distinct names, in order first seen + INTEGER(IntKi), ALLOCATABLE :: CompStat(:) !< aggregate CkIn_St_*, parallel to CompNames + INTEGER(IntKi) :: NumComps = 0 + INTEGER(IntKi) :: NumErrors = 0 + INTEGER(IntKi) :: NumWarnings = 0 + INTEGER(IntKi) :: UnYaml = -1 + CHARACTER(1024) :: YamlFileName = '' + END TYPE CheckInputCollectorType + + PUBLIC :: CkIn_Collect + PUBLIC :: CkIn_ComponentStatus + PUBLIC :: CkIn_StatusName + PUBLIC :: CkIn_WrSummary + PUBLIC :: CkIn_OpenReport + PUBLIC :: CkIn_ReportComponent + PUBLIC :: CkIn_CloseReport + PUBLIC :: CkIn_ExitCode + PUBLIC :: CkIn_DriverRecord + PUBLIC :: CkIn_DriverFinish + PUBLIC :: CkIn_DriverFail + +CONTAINS + + !======================================================================= + !> Records one component's check result. ErrStat/ErrMsg are exactly what a module Init returns via + !! the SetErrStat convention: ErrMsg may be multiple "RoutineName:text" lines joined by NewLine. + !! Status optionally forces a state that cannot be inferred from ErrStat alone + !! ('not_used' | 'unavailable' | 'skipped' | 'failed' | 'passed'). 'crashed' is deliberately not + !! accepted: a crashed component never returns to call this routine. + SUBROUTINE CkIn_Collect(collector, component, ErrStat, ErrMsg, Status) + + TYPE(CheckInputCollectorType), INTENT(INOUT) :: collector + CHARACTER(*), INTENT(IN) :: component + INTEGER(IntKi), INTENT(IN) :: ErrStat + CHARACTER(*), INTENT(IN) :: ErrMsg + CHARACTER(*), OPTIONAL, INTENT(IN) :: Status + + CHARACTER(:), ALLOCATABLE :: Remaining, Line, Src, Txt + INTEGER(IntKi) :: NLPos, ColonPos, CompStat + + IF ( PRESENT(Status) ) THEN + SELECT CASE ( TRIM(Status) ) + CASE ('not_used'); CompStat = CkIn_St_NotUsed + CASE ('unavailable'); CompStat = CkIn_St_Unavailable + CASE ('skipped'); CompStat = CkIn_St_Skipped + CASE ('failed'); CompStat = CkIn_St_Failed + CASE ('passed'); CompStat = CkIn_St_Passed + CASE DEFAULT + IF ( ErrStat >= ErrID_Severe ) THEN + CompStat = CkIn_St_Failed + ELSE + CompStat = CkIn_St_Passed + END IF + END SELECT + ELSE IF ( ErrStat >= ErrID_Severe ) THEN + CompStat = CkIn_St_Failed + ELSE + CompStat = CkIn_St_Passed + END IF + + CALL CkIn_SetComponentStatus( collector, component, CompStat ) + + IF ( ErrStat /= ErrID_None .AND. LEN_TRIM(ErrMsg) > 0 ) THEN + + Remaining = ErrMsg + + DO WHILE ( LEN_TRIM(Remaining) > 0 ) + + NLPos = INDEX( Remaining, NewLine ) + IF ( NLPos > 0 ) THEN + Line = Remaining(1:NLPos-1) + Remaining = Remaining( (NLPos + LEN(NewLine)): ) + ELSE + Line = Remaining + Remaining = '' + END IF + + IF ( LEN_TRIM(Line) > 0 ) THEN + + ! SetErrStat writes "RoutineName:text" -- split on the FIRST colon; a line with no + ! colon (not produced via SetErrStat) becomes text with a blank Source. + ColonPos = INDEX( Line, ':' ) + IF ( ColonPos > 1 ) THEN + Src = TRIM( ADJUSTL( Line(1:ColonPos-1) ) ) + Txt = TRIM( ADJUSTL( Line(ColonPos+1:) ) ) + ELSE + Src = '' + Txt = TRIM( ADJUSTL( Line ) ) + END IF + + CALL CkIn_AppendMsg( collector, component, ErrStat, Src, Txt ) + + IF ( ErrStat >= ErrID_Severe ) THEN + collector%NumErrors = collector%NumErrors + 1 + ELSE IF ( ErrStat == ErrID_Warn ) THEN + collector%NumWarnings = collector%NumWarnings + 1 + END IF + + END IF + + END DO + + END IF + + END SUBROUTINE CkIn_Collect + + !======================================================================= + FUNCTION CkIn_ComponentStatus(collector, component, Found) RESULT(Status) + TYPE(CheckInputCollectorType), INTENT(IN) :: collector + CHARACTER(*), INTENT(IN) :: component + LOGICAL, OPTIONAL, INTENT(OUT) :: Found + INTEGER(IntKi) :: Status + INTEGER(IntKi) :: idx + idx = CkIn_FindComponent( collector, component ) + IF ( idx > 0 ) THEN + Status = collector%CompStat(idx) + IF ( PRESENT(Found) ) Found = .TRUE. + ELSE + Status = CkIn_St_Unavailable + IF ( PRESENT(Found) ) Found = .FALSE. + END IF + END FUNCTION CkIn_ComponentStatus + + !======================================================================= + FUNCTION CkIn_StatusName(Status) RESULT(Name) + INTEGER(IntKi), INTENT(IN) :: Status + CHARACTER(11) :: Name + SELECT CASE (Status) + CASE (CkIn_St_Passed); Name = 'passed' + CASE (CkIn_St_Failed); Name = 'failed' + CASE (CkIn_St_NotUsed); Name = 'not_used' + CASE (CkIn_St_Crashed); Name = 'crashed' + CASE (CkIn_St_Unavailable); Name = 'unavailable' + CASE (CkIn_St_Skipped); Name = 'skipped' + CASE DEFAULT; Name = 'unavailable' + END SELECT + END FUNCTION CkIn_StatusName + + !======================================================================= + !> Prints the "INPUT CHECK SUMMARY" block: per-component status, every message with severity tag, + !! and the final PASSED/FAILED banner. Un present => that file unit; absent => console via WrScr. + SUBROUTINE CkIn_WrSummary(collector, Un) + + TYPE(CheckInputCollectorType), INTENT(IN) :: collector + INTEGER(IntKi), INTENT(IN), OPTIONAL :: Un + + INTEGER(IntKi) :: i, j + CHARACTER(72) :: Bar + CHARACTER(CkIn_NameLen + CkIn_MsgLen + 96) :: Line + + Bar = REPEAT( '=', 72 ) + + CALL CkIn_WrLine( Un, Bar ) + CALL CkIn_WrLine( Un, ' INPUT CHECK SUMMARY' ) + CALL CkIn_WrLine( Un, Bar ) + + DO i = 1, collector%NumComps + Line = ' '//TRIM(collector%CompNames(i))//': '//TRIM(CkIn_StatusBanner(collector%CompStat(i))) + CALL CkIn_WrLine( Un, Line ) + DO j = 1, collector%NumMsgs + IF ( TRIM(collector%Msgs(j)%Component) == TRIM(collector%CompNames(i)) ) THEN + IF ( collector%Msgs(j)%Severity >= ErrID_Severe ) THEN + Line = ' [error] ' + ELSE IF ( collector%Msgs(j)%Severity == ErrID_Warn ) THEN + Line = ' [warn] ' + ELSE + Line = ' [info] ' + END IF + IF ( LEN_TRIM(collector%Msgs(j)%Source) > 0 ) THEN + Line = TRIM(Line)//TRIM(collector%Msgs(j)%Source)//': '//TRIM(collector%Msgs(j)%Text) + ELSE + Line = TRIM(Line)//TRIM(collector%Msgs(j)%Text) + END IF + CALL CkIn_WrLine( Un, Line ) + END IF + END DO + END DO + + CALL CkIn_WrLine( Un, Bar ) + IF ( collector%NumErrors > 0 ) THEN + Line = ' INPUT CHECK FAILED: '//TRIM(Num2LStr(collector%NumErrors))//' errors, '// & + TRIM(Num2LStr(collector%NumWarnings))//' warnings' + ELSE + Line = ' INPUT CHECK PASSED' + IF ( collector%NumWarnings > 0 ) THEN + Line = TRIM(Line)//' ('//TRIM(Num2LStr(collector%NumWarnings))//' warnings)' + END IF + END IF + CALL CkIn_WrLine( Un, Line ) + CALL CkIn_WrLine( Un, Bar ) + + END SUBROUTINE CkIn_WrSummary + + !======================================================================= + !> Opens .verify.yaml and writes the header + CRASHED liveness placeholder (never + !! rewritten -- see module header contract). + SUBROUTINE CkIn_OpenReport(collector, RootName, ErrStat, ErrMsg) + + TYPE(CheckInputCollectorType), INTENT(INOUT) :: collector + CHARACTER(*), INTENT(IN) :: RootName + INTEGER(IntKi), INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + + CHARACTER(*), PARAMETER :: RoutineName = 'CkIn_OpenReport' + INTEGER(IntKi) :: ErrStat2 + CHARACTER(ErrMsgLen) :: ErrMsg2 + + ErrStat = ErrID_None + ErrMsg = '' + + collector%YamlFileName = TRIM(RootName)//'.verify.yaml' + + CALL GetNewUnit( collector%UnYaml, ErrStat2, ErrMsg2 ) + CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) + IF ( ErrStat >= AbortErrLev ) THEN + ! No unit was obtained; undo the YamlFileName set above so a failed open is indistinguishable + ! from never-opened -- CkIn_CloseReport's last-resort branch keys off LEN_TRIM(YamlFileName)==0. + collector%UnYaml = -1 + collector%YamlFileName = '' + RETURN + END IF + + CALL OpenFOutFile( collector%UnYaml, TRIM(collector%YamlFileName), ErrStat2, ErrMsg2 ) + CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) + IF ( ErrStat >= AbortErrLev ) THEN + ! The file did not open; do not leave a stale/invalid unit number OR filename behind -- see + ! the comment above and CkIn_CloseReport's last-resort branch. + collector%UnYaml = -1 + collector%YamlFileName = '' + RETURN + END IF + + CALL yaml_write_comm( collector%UnYaml, & + 'OpenFAST -CheckInput report, auto-generated '//CurDate()//' '//CurTime()//' -- do not hand-edit', & + ErrStat2, ErrMsg2 ) + + WRITE (collector%UnYaml, '(A)') 'check_status: crashed # liveness placeholder; see "overall_status" at EOF' + WRITE (collector%UnYaml, '(A)') 'components:' + + FLUSH(collector%UnYaml) + + END SUBROUTINE CkIn_OpenReport + + !======================================================================= + !> Appends one component's status + messages to the open report, then FLUSHes so a segfault in the + !! NEXT component's init still leaves this block intact on disk. Call once per component, + !! immediately after its CkIn_Collect call. + SUBROUTINE CkIn_ReportComponent(collector, component, ErrStat, ErrMsg) + + TYPE(CheckInputCollectorType), INTENT(IN) :: collector + CHARACTER(*), INTENT(IN) :: component + INTEGER(IntKi), INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + + CHARACTER(*), PARAMETER :: RoutineName = 'CkIn_ReportComponent' + INTEGER(IntKi) :: Un, Stat, i, nErr, nWarn, nMsg, IOS + + ErrStat = ErrID_None + ErrMsg = '' + Un = collector%UnYaml + + IF ( Un <= 0 ) THEN + CALL SetErrStat( ErrID_Severe, 'Report file is not open; call CkIn_OpenReport first.', ErrStat, ErrMsg, RoutineName ) + RETURN + END IF + + Stat = CkIn_ComponentStatus( collector, component ) + nErr = 0 + nWarn = 0 + nMsg = 0 + DO i = 1, collector%NumMsgs + IF ( TRIM(collector%Msgs(i)%Component) == TRIM(component) ) THEN + nMsg = nMsg + 1 + IF ( collector%Msgs(i)%Severity >= ErrID_Severe ) nErr = nErr + 1 + IF ( collector%Msgs(i)%Severity == ErrID_Warn ) nWarn = nWarn + 1 + END IF + END DO + + WRITE (Un, '(2X,"- name: ",A)', IOSTAT=IOS) TRIM(component) + WRITE (Un, '(4X,"status: ",A)') TRIM(CkIn_StatusName(Stat)) + WRITE (Un, '(4X,"errors: ",I0)') nErr + WRITE (Un, '(4X,"warnings: ",I0)') nWarn + + IF ( nMsg == 0 ) THEN + WRITE (Un, '(4X,"messages: []")') + ELSE + WRITE (Un, '(4X,"messages:")') + DO i = 1, collector%NumMsgs + IF ( TRIM(collector%Msgs(i)%Component) == TRIM(component) ) THEN + WRITE (Un, '(6X,"- severity: ",A)') TRIM(CkIn_SeverityName(collector%Msgs(i)%Severity)) + IF ( LEN_TRIM(collector%Msgs(i)%Source) > 0 ) THEN + WRITE (Un, '(8X,"source: ",A)') TRIM(collector%Msgs(i)%Source) + END IF + WRITE (Un, '(8X,"text: """,A,"""")') CkIn_YamlEscape( TRIM(collector%Msgs(i)%Text) ) + END IF + END DO + END IF + + IF ( IOS /= 0 ) THEN + CALL SetErrStat( ErrID_Severe, 'Error writing component "'//TRIM(component)//'" to '// & + TRIM(collector%YamlFileName)//'.', ErrStat, ErrMsg, RoutineName ) + END IF + + FLUSH(Un) + + END SUBROUTINE CkIn_ReportComponent + + !======================================================================= + !> Writes the authoritative overall_status/counts block and closes the report. + SUBROUTINE CkIn_CloseReport(collector, ErrStat, ErrMsg) + + TYPE(CheckInputCollectorType), INTENT(INOUT) :: collector + INTEGER(IntKi), INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + + CHARACTER(*), PARAMETER :: RoutineName = 'CkIn_CloseReport' + INTEGER(IntKi) :: Un, Overall, IOS, i + INTEGER(IntKi) :: ErrStat2 + CHARACTER(ErrMsgLen) :: ErrMsg2 + + ErrStat = ErrID_None + ErrMsg = '' + Un = collector%UnYaml + + IF ( Un <= 0 ) THEN + IF ( LEN_TRIM(collector%YamlFileName) == 0 ) THEN + ! The report was NEVER opened -- typically a driver failure before RootName was known + ! (early parse/settings failure). Open a last-resort report so CkIn_DriverFinish's + ! guarantee (a verify.yaml always exists after finish) still holds, then backfill every + ! component collected so far: they were skipped while the file was closed, but the + ! collector still holds everything. + CALL CkIn_OpenReport( collector, 'checkinput', ErrStat2, ErrMsg2 ) + CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) + IF ( ErrStat >= AbortErrLev ) RETURN + + WRITE (collector%UnYaml, '(A)') '# input root name unknown at failure time' + + DO i = 1, collector%NumComps + CALL CkIn_ReportComponent( collector, collector%CompNames(i), ErrStat2, ErrMsg2 ) + CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) + END DO + + Un = collector%UnYaml + ELSE + ! Opened at some point, then already closed (UnYaml <= 0 but YamlFileName is set) -- + ! that IS a caller bug: CkIn_CloseReport should only ever be called once. + CALL SetErrStat( ErrID_Severe, 'Report file is not open; call CkIn_OpenReport first.', ErrStat, ErrMsg, RoutineName ) + RETURN + END IF + END IF + + ! Unify with CkIn_ExitCode: a per-component CkIn_St_Failed can occur even when NumErrors is 0 + ! (e.g. a fatal collected with an empty ErrMsg never increments NumErrors but still marks the + ! component failed) -- deriving Overall straight from NumErrors would then disagree with the + ! process exit code. Ask CkIn_ExitCode for the authoritative answer instead. + IF ( CkIn_ExitCode(collector) == 0 ) THEN + Overall = CkIn_St_Passed + ELSE + Overall = CkIn_St_Failed + END IF + + WRITE (Un, '(A)', IOSTAT=IOS) 'overall_status: '//TRIM(CkIn_StatusName(Overall)) + WRITE (Un, '(A,I0)') 'total_errors: ', collector%NumErrors + WRITE (Un, '(A,I0)') 'total_warnings: ', collector%NumWarnings + WRITE (Un, '(A,I0)') 'components_checked: ', collector%NumComps + + FLUSH(Un) + CLOSE(Un) + + IF ( IOS /= 0 ) THEN + CALL SetErrStat( ErrID_Severe, 'Error finalizing '//TRIM(collector%YamlFileName)//'.', ErrStat, ErrMsg, RoutineName ) + END IF + + collector%UnYaml = -1 + + END SUBROUTINE CkIn_CloseReport + + !======================================================================= + !> 0 if no Severe/Fatal message and no failed component; 1 otherwise. Warnings never affect it. + FUNCTION CkIn_ExitCode(collector) RESULT(Code) + TYPE(CheckInputCollectorType), INTENT(IN) :: collector + INTEGER(IntKi) :: Code + INTEGER(IntKi) :: i + Code = 0 + IF ( collector%NumErrors > 0 ) THEN + Code = 1 + RETURN + END IF + DO i = 1, collector%NumComps + IF ( collector%CompStat(i) == CkIn_St_Failed ) THEN + Code = 1 + RETURN + END IF + END DO + END FUNCTION CkIn_ExitCode + + !======================================================================= + !> Driver-side convenience: CkIn_Collect then CkIn_ReportComponent for that component. If the + !! report file is not open, CkIn_ReportComponent's severe is swallowed into a local ErrStat2/ErrMsg2 + !! and does not propagate -- the collector state is the point, and a caller mid-run with no report + !! open yet must not crash or abort over it. + SUBROUTINE CkIn_DriverRecord(collector, component, ErrStat, ErrMsg) + + TYPE(CheckInputCollectorType), INTENT(INOUT) :: collector + CHARACTER(*), INTENT(IN) :: component + INTEGER(IntKi), INTENT(IN) :: ErrStat + CHARACTER(*), INTENT(IN) :: ErrMsg + + INTEGER(IntKi) :: ErrStat2 + CHARACTER(ErrMsgLen) :: ErrMsg2 + + CALL CkIn_Collect( collector, component, ErrStat, ErrMsg ) + CALL CkIn_ReportComponent( collector, component, ErrStat2, ErrMsg2 ) + + END SUBROUTINE CkIn_DriverRecord + + !======================================================================= + !> Driver-side convenience: prints the console summary, closes the report (warning on error rather + !! than aborting -- the report is best-effort at this point), then exits the process with the + !! collector's exit code. Never returns. + !! + !! Guarantee: after this returns (i.e. right before the process exits), a verify.yaml always + !! exists on disk -- even for failures that occurred before the driver knew its RootName. In that + !! case CkIn_CloseReport opens a last-resort 'checkinput.verify.yaml' in the CWD and backfills any + !! components collected so far; a normal run's .verify.yaml is unaffected. + SUBROUTINE CkIn_DriverFinish(collector) + + TYPE(CheckInputCollectorType), INTENT(INOUT) :: collector + + INTEGER(IntKi) :: ErrStat2 + CHARACTER(ErrMsgLen) :: ErrMsg2 + + CALL CkIn_WrSummary( collector ) + + CALL CkIn_CloseReport( collector, ErrStat2, ErrMsg2 ) + IF ( ErrStat2 /= ErrID_None ) THEN + CALL WrScr( 'WARNING: '//TRIM(ErrMsg2) ) + END IF + + CALL ProgExit( CkIn_ExitCode(collector) ) + + END SUBROUTINE CkIn_DriverFinish + + !======================================================================= + !> Driver-side convenience for a fatal, unrecoverable failure: record it, then finish and exit. + !! Never returns. + SUBROUTINE CkIn_DriverFail(collector, component, ErrStat, ErrMsg) + + TYPE(CheckInputCollectorType), INTENT(INOUT) :: collector + CHARACTER(*), INTENT(IN) :: component + INTEGER(IntKi), INTENT(IN) :: ErrStat + CHARACTER(*), INTENT(IN) :: ErrMsg + + CALL CkIn_DriverRecord( collector, component, ErrStat, ErrMsg ) + CALL CkIn_DriverFinish( collector ) + + END SUBROUTINE CkIn_DriverFail + + !======================================================================= + ! ---- private helpers ---- + + SUBROUTINE CkIn_WrLine(Un, Str) + INTEGER(IntKi), INTENT(IN), OPTIONAL :: Un + CHARACTER(*), INTENT(IN) :: Str + IF ( PRESENT(Un) ) THEN + WRITE (Un, '(A)') TRIM(Str) + ELSE + CALL WrScr( TRIM(Str) ) + END IF + END SUBROUTINE CkIn_WrLine + + FUNCTION CkIn_StatusBanner(Status) RESULT(Txt) + INTEGER(IntKi), INTENT(IN) :: Status + CHARACTER(11) :: Txt + SELECT CASE (Status) + CASE (CkIn_St_Passed); Txt = 'PASSED' + CASE (CkIn_St_Failed); Txt = 'FAILED' + CASE (CkIn_St_NotUsed); Txt = 'NOT USED' + CASE (CkIn_St_Crashed); Txt = 'CRASHED' + CASE (CkIn_St_Unavailable); Txt = 'UNAVAILABLE' + CASE (CkIn_St_Skipped); Txt = 'SKIPPED' + CASE DEFAULT; Txt = 'UNKNOWN' + END SELECT + END FUNCTION CkIn_StatusBanner + + FUNCTION CkIn_SeverityName(Severity) RESULT(Name) + INTEGER(IntKi), INTENT(IN) :: Severity + CHARACTER(5) :: Name + SELECT CASE (Severity) + CASE (ErrID_Info); Name = 'info' + CASE (ErrID_Warn); Name = 'warn' + CASE (ErrID_Severe); Name = 'error' + CASE (ErrID_Fatal); Name = 'fatal' + CASE DEFAULT; Name = 'none' + END SELECT + END FUNCTION CkIn_SeverityName + + !> Minimal YAML double-quoted-scalar escaping (backslash, then double-quote). Error text routinely + !! contains ':' and sometimes '"'; YAML.f90's writers have no escaping and no list-of-mappings + !! support, so messages are hand-written quoted scalars. + FUNCTION CkIn_YamlEscape(Str) RESULT(Esc) + CHARACTER(*), INTENT(IN) :: Str + CHARACTER(:), ALLOCATABLE :: Esc + INTEGER(IntKi) :: i + Esc = '' + DO i = 1, LEN_TRIM(Str) + SELECT CASE ( Str(i:i) ) + CASE ('\') + Esc = Esc//'\\' + CASE ('"') + Esc = Esc//'\"' + CASE DEFAULT + Esc = Esc//Str(i:i) + END SELECT + END DO + END FUNCTION CkIn_YamlEscape + + FUNCTION CkIn_FindComponent(collector, component) RESULT(Idx) + TYPE(CheckInputCollectorType), INTENT(IN) :: collector + CHARACTER(*), INTENT(IN) :: component + INTEGER(IntKi) :: Idx + INTEGER(IntKi) :: i + Idx = 0 + DO i = 1, collector%NumComps + IF ( TRIM(collector%CompNames(i)) == TRIM(component) ) THEN + Idx = i + RETURN + END IF + END DO + END FUNCTION CkIn_FindComponent + + SUBROUTINE CkIn_SetComponentStatus(collector, component, Status) + TYPE(CheckInputCollectorType), INTENT(INOUT) :: collector + CHARACTER(*), INTENT(IN) :: component + INTEGER(IntKi), INTENT(IN) :: Status + INTEGER(IntKi) :: idx + idx = CkIn_FindComponent( collector, component ) + IF ( idx > 0 ) THEN + ! Failed status is sticky: a later benign collect for the same component (e.g. an info-level + ! note after a fatal) must not downgrade the aggregate back to passed. + IF ( collector%CompStat(idx) == CkIn_St_Failed .AND. Status == CkIn_St_Passed ) RETURN + ! Unavailable is likewise sticky against Passed: a component marked unavailable was attempted + ! only against fabricated/upstream-tainted data, so a later benign collect for it "succeeding" + ! must not be allowed to silently launder that into Passed. A real Failed still beats Unavailable + ! (an actual failure is strictly more informative than a taint marker), and explicit not_used / + ! skipped / failed overrides still apply normally. + IF ( collector%CompStat(idx) == CkIn_St_Unavailable .AND. Status == CkIn_St_Passed ) RETURN + collector%CompStat(idx) = Status + ELSE + CALL CkIn_GrowComps( collector ) + collector%NumComps = collector%NumComps + 1 + collector%CompNames(collector%NumComps) = component + collector%CompStat(collector%NumComps) = Status + END IF + END SUBROUTINE CkIn_SetComponentStatus + + SUBROUTINE CkIn_AppendMsg(collector, component, Severity, Source, Text) + TYPE(CheckInputCollectorType), INTENT(INOUT) :: collector + CHARACTER(*), INTENT(IN) :: component + INTEGER(IntKi), INTENT(IN) :: Severity + CHARACTER(*), INTENT(IN) :: Source + CHARACTER(*), INTENT(IN) :: Text + CALL CkIn_GrowMsgs( collector ) + collector%NumMsgs = collector%NumMsgs + 1 + collector%Msgs(collector%NumMsgs)%Component = component + collector%Msgs(collector%NumMsgs)%Severity = Severity + collector%Msgs(collector%NumMsgs)%Source = Source + collector%Msgs(collector%NumMsgs)%Text = Text + END SUBROUTINE CkIn_AppendMsg + + !> ALLOCATE+MOVE_ALLOC doubling growth (portable across the older compilers OpenFAST supports; + !! whole-array realloc-on-assignment for derived-type arrays is less universally reliable). + SUBROUTINE CkIn_GrowComps(collector) + TYPE(CheckInputCollectorType), INTENT(INOUT) :: collector + CHARACTER(CkIn_NameLen), ALLOCATABLE :: TmpNames(:) + INTEGER(IntKi), ALLOCATABLE :: TmpStat(:) + INTEGER(IntKi) :: OldCap, NewCap + IF ( .NOT. ALLOCATED(collector%CompNames) ) THEN + ALLOCATE( collector%CompNames(8) ) + ALLOCATE( collector%CompStat(8) ) + RETURN + END IF + OldCap = SIZE(collector%CompNames) + IF ( collector%NumComps < OldCap ) RETURN + NewCap = OldCap * 2 + ALLOCATE( TmpNames(NewCap) ); TmpNames(1:OldCap) = collector%CompNames + ALLOCATE( TmpStat(NewCap) ); TmpStat(1:OldCap) = collector%CompStat + CALL MOVE_ALLOC( TmpNames, collector%CompNames ) + CALL MOVE_ALLOC( TmpStat, collector%CompStat ) + END SUBROUTINE CkIn_GrowComps + + SUBROUTINE CkIn_GrowMsgs(collector) + TYPE(CheckInputCollectorType), INTENT(INOUT) :: collector + TYPE(CkIn_MsgType), ALLOCATABLE :: TmpMsgs(:) + INTEGER(IntKi) :: OldCap, NewCap + IF ( .NOT. ALLOCATED(collector%Msgs) ) THEN + ALLOCATE( collector%Msgs(16) ) + RETURN + END IF + OldCap = SIZE(collector%Msgs) + IF ( collector%NumMsgs < OldCap ) RETURN + NewCap = OldCap * 2 + ALLOCATE( TmpMsgs(NewCap) ); TmpMsgs(1:OldCap) = collector%Msgs + CALL MOVE_ALLOC( TmpMsgs, collector%Msgs ) + END SUBROUTINE CkIn_GrowMsgs + +END MODULE NWTC_CheckInput diff --git a/modules/nwtc-library/tests/nwtc_library_utest.F90 b/modules/nwtc-library/tests/nwtc_library_utest.F90 index 4d18a5f213..f79f9a5848 100644 --- a/modules/nwtc-library/tests/nwtc_library_utest.F90 +++ b/modules/nwtc-library/tests/nwtc_library_utest.F90 @@ -5,6 +5,7 @@ program nwtc_library_utest use test_NWTC_IO_FileInfo, only: test_NWTC_IO_FileInfo_suite use test_NWTC_RandomNumber, only: test_NWTC_RandomNumber_suite use test_NWTC_C_Binding, only: test_NWTC_C_Binding_suite +use test_NWTC_CheckInput, only: test_NWTC_CheckInput_suite use NWTC_Num implicit none @@ -19,7 +20,8 @@ program nwtc_library_utest testsuites = [ & new_testsuite("test_NWTC_IO_FileInfo", test_NWTC_IO_FileInfo_suite), & new_testsuite("test_NWTC_RandomNumber_suite", test_NWTC_RandomNumber_suite), & - new_testsuite("test_NWTC_C_Binding", test_NWTC_C_Binding_suite) & + new_testsuite("test_NWTC_C_Binding", test_NWTC_C_Binding_suite), & + new_testsuite("test_NWTC_CheckInput", test_NWTC_CheckInput_suite) & ] do is = 1, size(testsuites) diff --git a/modules/nwtc-library/tests/test_NWTC_CheckInput.F90 b/modules/nwtc-library/tests/test_NWTC_CheckInput.F90 new file mode 100644 index 0000000000..4a59b65458 --- /dev/null +++ b/modules/nwtc-library/tests/test_NWTC_CheckInput.F90 @@ -0,0 +1,233 @@ +module test_NWTC_CheckInput + +use testdrive, only: new_unittest, unittest_type, error_type, check +use NWTC_Library, only: IntKi, ErrMsgLen, NewLine, ErrID_None, ErrID_Info, ErrID_Warn, ErrID_Severe, ErrID_Fatal, AbortErrLev +use NWTC_CheckInput ! NWTC_CheckInput has a PRIVATE default and does NOT re-export NWTC_Library names + +implicit none + +private +public :: test_NWTC_CheckInput_suite + +contains + +subroutine test_NWTC_CheckInput_suite(testsuite) + type(unittest_type), allocatable, intent(out) :: testsuite(:) + testsuite = [ & + new_unittest("test_collect_splits_multiline_message", test_collect_splits_multiline_message), & + new_unittest("test_collect_no_error_is_passed", test_collect_no_error_is_passed), & + new_unittest("test_collect_warning_does_not_fail", test_collect_warning_does_not_fail), & + new_unittest("test_collect_status_override", test_collect_status_override), & + new_unittest("test_skipped_status", test_skipped_status), & + new_unittest("test_failed_status_sticky", test_failed_status_sticky), & + new_unittest("test_unavailable_status_sticky", test_unavailable_status_sticky), & + new_unittest("test_component_status_unknown", test_component_status_unknown), & + new_unittest("test_exit_code", test_exit_code), & + new_unittest("test_empty_message_fatal", test_empty_message_fatal), & + new_unittest("test_driverrecord_fatal", test_driverrecord_fatal), & + new_unittest("test_closereport_lastresort", test_closereport_lastresort), & + new_unittest("test_closereport_after_failed_open", test_closereport_after_failed_open) & + ] +end subroutine + +subroutine test_collect_splits_multiline_message(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + character(ErrMsgLen) :: msg + + ! Exactly the shape NWTC_Base::SetErrStat produces for two severe errors in sequence: + ! ErrMess = TRIM(ErrMess)//new_line('a')//TRIM(RoutineName)//':'//TRIM(ErrMessLcl) + msg = 'SD_Init:Number of joints must be at least 2.'//NewLine// & + 'SD_Init:Member 3 references an undefined joint.' + + call CkIn_Collect(collector, 'SubDyn', ErrID_Severe, msg) + + call check(error, collector%NumMsgs, 2); if (allocated(error)) return + call check(error, collector%NumErrors, 2); if (allocated(error)) return + call check(error, collector%NumWarnings, 0); if (allocated(error)) return + call check(error, trim(collector%Msgs(1)%Source), 'SD_Init'); if (allocated(error)) return + call check(error, trim(collector%Msgs(1)%Text), 'Number of joints must be at least 2.'); if (allocated(error)) return + call check(error, trim(collector%Msgs(2)%Text), 'Member 3 references an undefined joint.'); if (allocated(error)) return + call check(error, CkIn_ComponentStatus(collector, 'SubDyn'), CkIn_St_Failed) +end subroutine + +subroutine test_collect_no_error_is_passed(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + call CkIn_Collect(collector, 'AeroDyn', ErrID_None, '') + call check(error, collector%NumMsgs, 0); if (allocated(error)) return + call check(error, CkIn_ComponentStatus(collector, 'AeroDyn'), CkIn_St_Passed) +end subroutine + +subroutine test_collect_warning_does_not_fail(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + call CkIn_Collect(collector, 'ServoDyn', ErrID_Warn, 'ServoDyn_Init:Using default gains.') + call check(error, collector%NumWarnings, 1); if (allocated(error)) return + call check(error, collector%NumErrors, 0); if (allocated(error)) return + call check(error, CkIn_ComponentStatus(collector, 'ServoDyn'), CkIn_St_Passed) +end subroutine + +subroutine test_collect_status_override(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + call CkIn_Collect(collector, 'HydroDyn', ErrID_None, '', Status='not_used') + call check(error, CkIn_ComponentStatus(collector, 'HydroDyn'), CkIn_St_NotUsed) +end subroutine + +subroutine test_skipped_status(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + call CkIn_Collect(collector, 'FAST_InitMappings', ErrID_Info, & + 'blocked by upstream module failure(s)', Status='skipped') + call check(error, CkIn_ComponentStatus(collector, 'FAST_InitMappings'), CkIn_St_Skipped); if (allocated(error)) return + call check(error, collector%NumErrors, 0) ! info-level note is not an error +end subroutine + +subroutine test_failed_status_sticky(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + ! A fatal collect followed by a clean collect for the SAME component must stay failed: + call CkIn_Collect(collector, 'ElastoDyn', ErrID_Fatal, 'ED_Init:Blade file not found.') + call CkIn_Collect(collector, 'ElastoDyn', ErrID_None, '') + call check(error, CkIn_ComponentStatus(collector, 'ElastoDyn'), CkIn_St_Failed) +end subroutine + +subroutine test_unavailable_status_sticky(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + ! An 'unavailable' mark (attempted only against fabricated/tainted upstream data) must not be + ! silently laundered into Passed by a later benign collect for the same component: + call CkIn_Collect(collector, 'AeroDyn', ErrID_Info, & + 'ElastoDyn initialization failed; attempted with stubbed ElastoDyn interface data', & + Status='unavailable') + call CkIn_Collect(collector, 'AeroDyn', ErrID_None, '') + call check(error, CkIn_ComponentStatus(collector, 'AeroDyn'), CkIn_St_Unavailable); if (allocated(error)) return + ! But a real failure still beats the taint marker: + call CkIn_Collect(collector, 'AeroDyn', ErrID_Fatal, 'AD_Init:msg') + call check(error, CkIn_ComponentStatus(collector, 'AeroDyn'), CkIn_St_Failed) +end subroutine + +subroutine test_component_status_unknown(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + logical :: found + call check(error, CkIn_ComponentStatus(collector, 'DoesNotExist', found), CkIn_St_Unavailable); if (allocated(error)) return + call check(error, found, .false.) +end subroutine + +subroutine test_exit_code(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + call check(error, CkIn_ExitCode(collector), 0); if (allocated(error)) return + call CkIn_Collect(collector, 'BeamDyn', ErrID_Fatal, 'BD_Init:Blade input file not found.') + call check(error, CkIn_ExitCode(collector), 1) +end subroutine + +subroutine test_empty_message_fatal(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + ! A fatal collect with an empty message: CkIn_Collect still marks the component failed (that + ! branch does not depend on ErrMsg), but it does NOT increment NumErrors (only the message-split + ! loop does that, and it is gated on LEN_TRIM(ErrMsg) > 0). This is exactly the divergence + ! CkIn_CloseReport's overall_status must not reproduce -- it now derives Overall from + ! CkIn_ExitCode (component status), not from NumErrors, so this empty-message fatal is not + ! silently reported as passed. + call CkIn_Collect(collector, 'X', ErrID_Fatal, '') + call check(error, collector%NumErrors, 0); if (allocated(error)) return + call check(error, CkIn_ComponentStatus(collector, 'X'), CkIn_St_Failed); if (allocated(error)) return + call check(error, CkIn_ExitCode(collector), 1) +end subroutine + +subroutine test_driverrecord_fatal(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + ! No CkIn_OpenReport call: CkIn_ReportComponent's "not open" severe must be swallowed by + ! CkIn_DriverRecord (collector state is still updated) rather than crashing or propagating. + call CkIn_DriverRecord(collector, 'HydroDyn', ErrID_Fatal, 'HD_Init:WAMIT file not found.') + call check(error, CkIn_ComponentStatus(collector, 'HydroDyn'), CkIn_St_Failed); if (allocated(error)) return + call check(error, CkIn_ExitCode(collector), 1) +end subroutine + +subroutine test_closereport_lastresort(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + integer(IntKi) :: ErrStat, Un, IOS + character(ErrMsgLen) :: ErrMsg + character(*), parameter :: FileName = 'checkinput.verify.yaml' + character(256) :: Line + logical :: FileExists, FoundOverallFailed, FoundDriverComp + + ! Never opened: driver failed before it knew its RootName. Clean up any leftover file from a + ! previous aborted run of this test first. + inquire(file=FileName, exist=FileExists) + if (FileExists) then + open(newunit=Un, file=FileName, status='old') + close(Un, status='delete') + end if + + call CkIn_Collect(collector, 'Driver', ErrID_Fatal, 'X:bad arg') + call CkIn_CloseReport(collector, ErrStat, ErrMsg) + + call check(error, ErrStat < AbortErrLev); if (allocated(error)) return + + inquire(file=FileName, exist=FileExists) + call check(error, FileExists); if (allocated(error)) return + + FoundOverallFailed = .false. + FoundDriverComp = .false. + open(newunit=Un, file=FileName, status='old', action='read') + do + read(Un, '(A)', iostat=IOS) Line + if (IOS /= 0) exit + if (index(Line, 'overall_status: failed') > 0) FoundOverallFailed = .true. + if (index(Line, '- name: Driver') > 0) FoundDriverComp = .true. + end do + close(Un, status='delete') + + call check(error, FoundOverallFailed); if (allocated(error)) return + call check(error, FoundDriverComp) +end subroutine + +subroutine test_closereport_after_failed_open(error) + type(error_type), allocatable, intent(out) :: error + type(CheckInputCollectorType) :: collector + integer(IntKi) :: ErrStat, Un, IOS + character(ErrMsgLen) :: ErrMsg + character(*), parameter :: FileName = 'checkinput.verify.yaml' + character(256) :: Line + logical :: FileExists, FoundOverallFailed + + ! Primary open must fail (the directory does not exist): CkIn_OpenReport must reset BOTH + ! UnYaml and YamlFileName on this failure path so the failed-open case is indistinguishable + ! from never-opened -- otherwise CkIn_CloseReport takes the "caller bug" severe branch and + ! writes nothing, defeating the last-resort report entirely. + inquire(file=FileName, exist=FileExists) + if (FileExists) then + open(newunit=Un, file=FileName, status='old') + close(Un, status='delete') + end if + + call CkIn_OpenReport(collector, '/nonexistent_dir_xyz/deck', ErrStat, ErrMsg) + call check(error, ErrStat >= AbortErrLev); if (allocated(error)) return + + call CkIn_Collect(collector, 'Driver', ErrID_Fatal, 'X:bad arg') + call CkIn_CloseReport(collector, ErrStat, ErrMsg) + call check(error, ErrStat < AbortErrLev); if (allocated(error)) return + + inquire(file=FileName, exist=FileExists) + call check(error, FileExists); if (allocated(error)) return + + FoundOverallFailed = .false. + open(newunit=Un, file=FileName, status='old', action='read') + do + read(Un, '(A)', iostat=IOS) Line + if (IOS /= 0) exit + if (index(Line, 'overall_status: failed') > 0) FoundOverallFailed = .true. + end do + close(Un, status='delete') + + call check(error, FoundOverallFailed) +end subroutine + +end module diff --git a/modules/openfast-library/src/FAST_Registry.txt b/modules/openfast-library/src/FAST_Registry.txt index fd3a13b961..61e0c8152d 100644 --- a/modules/openfast-library/src/FAST_Registry.txt +++ b/modules/openfast-library/src/FAST_Registry.txt @@ -151,6 +151,7 @@ typedef ^ FAST_ParameterType IntKi CompSoil - - - "Compute soil-structural dynam typedef ^ FAST_ParameterType IntKi MHK - - - "MHK turbine type (switch) {0=Not an MHK turbine; 1=Fixed MHK turbine; 2=Floating MHK turbine}" - typedef ^ FAST_ParameterType LOGICAL UseDWM - - - "Use the DWM module in AeroDyn" - typedef ^ FAST_ParameterType LOGICAL Linearize - - - "Linearization analysis (flag)" - +typedef ^ FAST_ParameterType LOGICAL CheckInputMode - .false. - "-CheckInput run: initialize all modules with attempt-everything error collection, no time marching (flag)" - typedef ^ FAST_ParameterType IntKi WaveFieldMod - - - "Wave field handling (-) (switch) 0: use individual HydroDyn inputs without adjustment, 1: adjust wave phases based on turbine offsets from farm origin" - typedef ^ FAST_ParameterType logical FarmIntegration - .false. - "whether this is called from FAST.Farm (or another program that doesn't want FAST to call all of the init stuff first)" - typedef ^ FAST_ParameterType SiKi TurbinePos {3} - - "Initial position of turbine base (origin used for graphics)" m diff --git a/modules/openfast-library/src/FAST_Subs.f90 b/modules/openfast-library/src/FAST_Subs.f90 index e78026054c..9cb1388d80 100644 --- a/modules/openfast-library/src/FAST_Subs.f90 +++ b/modules/openfast-library/src/FAST_Subs.f90 @@ -25,6 +25,7 @@ MODULE FAST_Subs use FAST_ModTypes use FAST_ModGlue use VersionInfo + use NWTC_CheckInput use FAST_Funcs use FAST_Solver use FAST_Mapping, only: FAST_InitMappings @@ -103,9 +104,81 @@ SUBROUTINE FAST_InitializeAll_T( t_initial, TurbID, Turbine, ErrStat, ErrMsg, In END SUBROUTINE FAST_InitializeAll_T !---------------------------------------------------------------------------------------------------------------------------------- +!> -CheckInput driver: attempt-everything initialization of every enabled module (no time stepping), +!! console summary + .verify.yaml report, process exit code 0 (valid) / 1 (any fatal input error). +!! Terminates the process itself via ProgExit; never returns to the caller. +SUBROUTINE FAST_CheckInput_T( Turbine ) + + TYPE(FAST_TurbineType), INTENT(INOUT) :: Turbine + + TYPE(CheckInputCollectorType) :: CkInCollector + INTEGER(IntKi) :: ErrStat, ExitCode + CHARACTER(ErrMsgLen) :: ErrMsg + LOGICAL :: FIAHasMsgs + + Turbine%TurbID = 1 + Turbine%p_FAST%CheckInputMode = .true. ! read by Failed() inside FAST_InitializeAll + + CALL FAST_InitializeAll( 0.0_DbKi, Turbine%m_Glue, Turbine%p_FAST, Turbine%y_FAST, Turbine%m_FAST, & + Turbine%ED, Turbine%SED, Turbine%BD, Turbine%SrvD, Turbine%AD, Turbine%ADsk, Turbine%ExtLd, Turbine%IfW, Turbine%ExtInfw, & + Turbine%SeaSt, Turbine%HD, Turbine%SD, Turbine%ExtPtfm, Turbine%MAP, Turbine%FEAM, Turbine%MD, Turbine%Orca, & + Turbine%IceF, Turbine%IceD, Turbine%SlD, .false., ErrStat, ErrMsg, CkInCollector=CkInCollector ) + IF (ErrStat >= AbortErrLev) THEN + ! Only failures that bypass the collect-and-continue path entirely land here -- e.g. FailedAlloc's + ! array-allocation checks, which call Cleanup()/return unconditionally regardless of CheckInputMode. + ! Every module Init failure itself takes the collect path above instead: CheckInputMode is already + ! set (see above) before FAST_InitializeAll is called, so Failed() routes those through CkIn_Collect + ! and continues rather than returning here. + CALL CkIn_Collect( CkInCollector, 'FAST_InitializeAll', ErrStat, ErrMsg ) + END IF + ! Flush whatever Failed() collected under this label during the call (every module's collect-and-continue + ! error lands here until Task 7 gives each module its own CurrentComponent) so it appears in the YAML report. + ! Only report when something was actually collected -- otherwise this would add a spurious + ! "unavailable, 0 messages" entry to a clean run's report. CkIn_ComponentStatus's own return value isn't + ! needed here (only the Found= side-output is); discard it inline rather than storing it in an unused + ! variable -- CkIn_St_* is always >= CkIn_St_Passed, so FIAHasMsgs is what actually gates this. + IF ( CkIn_ComponentStatus( CkInCollector, 'FAST_InitializeAll', Found=FIAHasMsgs ) >= CkIn_St_Passed .AND. FIAHasMsgs ) THEN + CALL CkIn_ReportComponent( CkInCollector, 'FAST_InitializeAll', ErrStat, ErrMsg ) + END IF + + ! Mappings + solver init require every module's meshes committed; only run when nothing failed. + IF ( CkIn_ExitCode(CkInCollector) == 0 ) THEN + CALL FAST_InitMappings(Turbine%m_Glue%Mappings, Turbine%m_Glue%ModData, Turbine, ErrStat, ErrMsg) + CALL CkIn_Collect( CkInCollector, 'FAST_InitMappings', ErrStat, ErrMsg ) + CALL CkIn_ReportComponent( CkInCollector, 'FAST_InitMappings', ErrStat, ErrMsg ) + + IF ( CkIn_ExitCode(CkInCollector) == 0 ) THEN + CALL FAST_SolverInit(Turbine%p_FAST, Turbine%p_Glue%TC, Turbine%m_Glue%TC, & + Turbine%m_Glue%ModData, Turbine%m_Glue%Mappings, Turbine, ErrStat, ErrMsg) + CALL CkIn_Collect( CkInCollector, 'FAST_SolverInit', ErrStat, ErrMsg ) + CALL CkIn_ReportComponent( CkInCollector, 'FAST_SolverInit', ErrStat, ErrMsg ) + ELSE + CALL CkIn_Collect( CkInCollector, 'FAST_SolverInit', ErrID_Info, & + 'blocked by upstream failure(s)', Status='skipped' ) + CALL CkIn_ReportComponent( CkInCollector, 'FAST_SolverInit', ErrStat, ErrMsg ) + END IF + ELSE + CALL CkIn_Collect( CkInCollector, 'FAST_InitMappings', ErrID_Info, & + 'blocked by upstream module failure(s)', Status='skipped' ) + CALL CkIn_ReportComponent( CkInCollector, 'FAST_InitMappings', ErrStat, ErrMsg ) + CALL CkIn_Collect( CkInCollector, 'FAST_SolverInit', ErrID_Info, & + 'blocked by upstream module failure(s)', Status='skipped' ) + CALL CkIn_ReportComponent( CkInCollector, 'FAST_SolverInit', ErrStat, ErrMsg ) + END IF + + CALL CkIn_WrSummary( CkInCollector ) ! full self-contained stdout summary + CALL CkIn_CloseReport( CkInCollector, ErrStat, ErrMsg ) ! authoritative overall_status trailer + ExitCode = CkIn_ExitCode( CkInCollector ) + + ! Normal shutdown (module End + close outputs + destroy turbine) WITHOUT its stop, then our exit code: + CALL ExitThisProgram_T( Turbine, ErrID_None, StopTheProgram=.FALSE., SkipRunTimeMsg=.TRUE. ) + CALL ProgExit( ExitCode ) + +END SUBROUTINE FAST_CheckInput_T +!---------------------------------------------------------------------------------------------------------------------------------- !> Routine to call Init routine for each module. This routine sets all of the init input data for each module. SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SED, BD, SrvD, AD, ADsk, ExtLd, IfW, ExtInfw, SeaSt, HD, SD, ExtPtfm, & - MAPp, FEAM, MD, Orca, IceF, IceD, SlD, CompAeroMaps, ErrStat, ErrMsg, InFile, ExternInitData ) + MAPp, FEAM, MD, Orca, IceF, IceD, SlD, CompAeroMaps, ErrStat, ErrMsg, InFile, ExternInitData, CkInCollector ) use ElastoDyn_Parameters, only: Method_RK4 @@ -145,9 +218,13 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE TYPE(FAST_ExternInitType), OPTIONAL, INTENT(IN) :: ExternInitData !< Initialization input data from an external source (Simulink) + TYPE(CheckInputCollectorType), OPTIONAL, INTENT(INOUT) :: CkInCollector !< -CheckInput accumulator; when present + !! and p_FAST%CheckInputMode, Failed() collects and continues + ! local variables CHARACTER(1024) :: InputFile !< A CHARACTER string containing the name of the primary FAST input file TYPE(FAST_InitData) :: Init !< Initialization data for all modules + CHARACTER(64) :: CurrentComponent ! component label used by Failed() when collecting REAL(ReKi) :: AirDens ! air density for initialization/normalization of ExternalInflow data @@ -164,6 +241,9 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE INTEGER(IntKi) :: StateAryLB ! States array lower bound INTEGER(IntKi) :: StateAryUB ! States array upper bound logical :: CallStart + logical :: BDRootMotionOK ! -CheckInput guard: ED BladeRootMotion usable this instance + logical :: ExtInfw_OK ! -CheckInput guard: ExtInfw preconditions satisfied + logical :: SlD_OK ! -CheckInput guard: SoilDyn precondition (CompSub==Module_SD) satisfied REAL(R8Ki) :: theta(3) ! angles for hub orientation matrix for aeromaps @@ -174,6 +254,7 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE !.......... ErrStat = ErrID_None ErrMsg = "" + CurrentComponent = 'FAST_InitializeAll' p_FAST%CompAeroMaps = CompAeroMaps @@ -246,6 +327,98 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE if (Failed()) return end if + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_OpenReport(CkInCollector, trim(p_FAST%OutFileRoot), ErrStat2, ErrMsg2) + if (ErrStat2 >= AbortErrLev) call WrScr('Warning: could not open -CheckInput report file: '//trim(ErrMsg2)) + end if + + ! -CheckInput: mark every module the input file does not enable as 'not_used' up front, before any + ! module init is attempted. Modules that ARE enabled get their own passed/failed/unavailable report + ! after their init block completes (see the per-module CurrentComponent labels below); this sweep + ! covers only the disabled ones, so each component is reported exactly once. + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + if (p_FAST%CompElast /= Module_SED) then + call CkIn_Collect(CkInCollector, 'Simplified-ElastoDyn', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'Simplified-ElastoDyn', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompElast == Module_SED) then + call CkIn_Collect(CkInCollector, 'ElastoDyn', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'ElastoDyn', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompElast /= Module_BD) then + call CkIn_Collect(CkInCollector, 'BeamDyn', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'BeamDyn', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompInflow /= Module_IfW) then + call CkIn_Collect(CkInCollector, 'InflowWind', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'InflowWind', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompSeaSt /= Module_SeaSt) then + call CkIn_Collect(CkInCollector, 'SeaState', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'SeaState', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompAero /= Module_AD .and. p_FAST%CompAero /= Module_ExtLd) then + call CkIn_Collect(CkInCollector, 'AeroDyn', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'AeroDyn', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompAero /= Module_ADsk) then + call CkIn_Collect(CkInCollector, 'AeroDisk', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'AeroDisk', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompSoil /= Module_SlD) then + call CkIn_Collect(CkInCollector, 'SoilDyn', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'SoilDyn', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompSub /= Module_SD) then + call CkIn_Collect(CkInCollector, 'SubDyn', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'SubDyn', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompSub /= Module_ExtPtfm) then + call CkIn_Collect(CkInCollector, 'ExtPtfm', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'ExtPtfm', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompHydro /= Module_HD) then + call CkIn_Collect(CkInCollector, 'HydroDyn', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'HydroDyn', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompMooring /= Module_MAP) then + call CkIn_Collect(CkInCollector, 'MAP', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'MAP', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompMooring /= Module_MD) then + call CkIn_Collect(CkInCollector, 'MoorDyn', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'MoorDyn', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompMooring /= Module_FEAM) then + call CkIn_Collect(CkInCollector, 'FEAMooring', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'FEAMooring', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompMooring /= Module_Orca) then + call CkIn_Collect(CkInCollector, 'OrcaFlex', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'OrcaFlex', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompIce /= Module_IceF) then + call CkIn_Collect(CkInCollector, 'IceFloe', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'IceFloe', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompIce /= Module_IceD) then + call CkIn_Collect(CkInCollector, 'IceDyn', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'IceDyn', ErrStat2, ErrMsg2) + end if + if (p_FAST%CompServo /= Module_SrvD) then + call CkIn_Collect(CkInCollector, 'ServoDyn', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'ServoDyn', ErrStat2, ErrMsg2) + end if + + ! ExternalInflow/ExternalLoads require ExternInitData, which the -CheckInput CLI entry point + ! never supplies (see FAST_CheckInput_T's call to FAST_InitializeAll); mark both not_used + ! unconditionally since they can never actually be attempted here. + call CkIn_Collect(CkInCollector, 'ExternalInflow', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'ExternalInflow', ErrStat2, ErrMsg2) + call CkIn_Collect(CkInCollector, 'ExternalLoads', ErrID_None, '', Status='not_used') + call CkIn_ReportComponent(CkInCollector, 'ExternalLoads', ErrStat2, ErrMsg2) + end if + ! Allocate array to hold number of blades per rotor call AllocAry(p_FAST%RotNumBld, p_FAST%NRotors, "p_FAST%RotNumBld", ErrStat2, ErrMsg2); if (Failed()) return @@ -301,6 +474,8 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE case (Module_SED) ! Simplified-ElastoDyn + CurrentComponent = 'Simplified-ElastoDyn' + allocate(SED%Input (InputAryLB:InputAryUB), stat=ErrStat2); if (FailedAlloc("SED%Input")) return allocate(SED%InputTimes (InputAryUB ), stat=ErrStat2); if (FailedAlloc("SED%InputTimes")) return allocate(SED%x (StateAryUB ), stat=ErrStat2); if (FailedAlloc("SED%x")) return @@ -316,6 +491,41 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE dt_module = p_FAST%DT CALL SED_Init( Init%InData_SED, SED%Input(1), SED%p, SED%x(STATE_CURR), SED%xd(STATE_CURR), SED%z(STATE_CURR), SED%OtherSt(STATE_CURR), & SED%y, SED%m, dt_module, Init%OutData_SED, ErrStat2, ErrMsg2 ) + ! -CheckInput guard: SED_Init failing leaves its output meshes uncommitted (see StubOutputPointMesh + ! above for why that matters). Capture the failure here -- before Failed() collects it below -- and + ! synthesize neutral stand-ins so every remaining module that reads SED%y can still be attempted. + ! Mirrors the ElastoDyn treatment below; SED excludes BeamDyn (mutually exclusive CompElast value) + ! and SubDyn (rejected earlier in ValidateInputData), so neither gets a disclosure mark here. + if (ErrStat2 >= AbortErrLev .and. p_FAST%CheckInputMode .and. present(CkInCollector)) then + call StubOutputPointMesh(SED%y%HubPtMotion) + call StubOutputPointMesh(SED%y%NacelleMotion) + call StubOutputPointMesh(SED%y%PlatformPtMesh) + ! -CheckInput disclosure: every enabled direct consumer of SED's output data is about to be + ! attempted against the stubbed meshes above. Mark each one 'unavailable' now so the report + ! discloses that its checks (if any) ran on fabricated Simplified-ElastoDyn data rather than + ! silently reporting them as passed. Gated on each module's own Comp switch so disabled modules + ! are not marked. + if (p_FAST%CompAero == Module_AD .or. p_FAST%CompAero == Module_ExtLd) then + call CkIn_Collect(CkInCollector, 'AeroDyn', ErrID_Info, & + 'Simplified-ElastoDyn initialization failed; attempted with stubbed interface data', & + Status='unavailable') + end if + if (p_FAST%CompAero == Module_ADsk) then + call CkIn_Collect(CkInCollector, 'AeroDisk', ErrID_Info, & + 'Simplified-ElastoDyn initialization failed; attempted with stubbed interface data', & + Status='unavailable') + end if + if (p_FAST%CompInflow == Module_IfW) then + call CkIn_Collect(CkInCollector, 'InflowWind', ErrID_Info, & + 'Simplified-ElastoDyn initialization failed; attempted with stubbed interface data', & + Status='unavailable') + end if + if (p_FAST%CompServo == Module_SrvD) then + call CkIn_Collect(CkInCollector, 'ServoDyn', ErrID_Info, & + 'Simplified-ElastoDyn initialization failed; attempted with stubbed interface data', & + Status='unavailable') + end if + end if if (Failed()) return ! Add module to array of modules, return if errors occurred @@ -325,9 +535,16 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE ! Save number of blades p_FAST%RotNumBld(1) = Init%OutData_SED%NumBl - + + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'Simplified-ElastoDyn', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'Simplified-ElastoDyn', ErrStat2, ErrMsg2) + end if + case default ! ElastoDyn - + + CurrentComponent = 'ElastoDyn' + ! Allocate module data arrays allocate(ED%Input (InputAryLB:InputAryUB, p_FAST%NRotors), stat=ErrStat2); if (FailedAlloc("ED%Input")) return allocate(ED%InputTimes (InputAryUB, p_FAST%NRotors ), stat=ErrStat2); if (FailedAlloc("ED%InputTimes")) return @@ -360,8 +577,53 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE CALL ED_Init(Init%InData_ED, ED%Input(INPUT_CURR, iRot), ED%p(iRot), ED%x(iRot, STATE_CURR), & ED%xd(iRot, STATE_CURR), ED%z(iRot, STATE_CURR), ED%OtherSt(iRot, STATE_CURR), & ED%y(iRot), ED%m(iRot), dt_module, Init%OutData_ED(iRot), ErrStat2, ErrMsg2) + ! -CheckInput guard: ED_Init failing leaves its output meshes uncommitted (see StubOutputPointMesh + ! above for why that matters). Capture the failure here -- before Failed() collects it below -- and + ! synthesize neutral stand-ins so every remaining module that reads ED%y(iRot) can still be attempted. + if (ErrStat2 >= AbortErrLev .and. p_FAST%CheckInputMode .and. present(CkInCollector)) then + call StubOutputPointMesh(ED%y(iRot)%HubPtMotion) + call StubOutputPointMesh(ED%y(iRot)%NacelleMotion) + call StubOutputPointMesh(ED%y(iRot)%PlatformPtMesh) + ! -CheckInput disclosure: every enabled direct consumer of ED's output data is about to be + ! attempted against the stubbed meshes above (or, for BeamDyn, cannot be attempted at all -- + ! with ED failed, p_FAST%NumBD is never set, so BD's per-blade init loop zero-trips and BD + ! never calls into the collector on its own). Mark each one 'unavailable' now so the report + ! discloses that its checks (if any) ran on fabricated ElastoDyn data rather than silently + ! reporting them as passed. Gated on each module's own Comp switch so disabled modules are + ! not marked. + if (p_FAST%CompElast == Module_BD) then + call CkIn_Collect(CkInCollector, 'BeamDyn', ErrID_Info, & + 'ElastoDyn initialization failed; BeamDyn instances cannot be attempted (blade count unavailable)', & + Status='unavailable') + end if + if (p_FAST%CompAero == Module_AD .or. p_FAST%CompAero == Module_ExtLd) then + call CkIn_Collect(CkInCollector, 'AeroDyn', ErrID_Info, & + 'ElastoDyn initialization failed; attempted with stubbed ElastoDyn interface data', & + Status='unavailable') + end if + if (p_FAST%CompAero == Module_ADsk) then + call CkIn_Collect(CkInCollector, 'AeroDisk', ErrID_Info, & + 'ElastoDyn initialization failed; attempted with stubbed ElastoDyn interface data', & + Status='unavailable') + end if + if (p_FAST%CompInflow == Module_IfW) then + call CkIn_Collect(CkInCollector, 'InflowWind', ErrID_Info, & + 'ElastoDyn initialization failed; attempted with stubbed ElastoDyn interface data', & + Status='unavailable') + end if + if (p_FAST%CompSub == Module_SD) then + call CkIn_Collect(CkInCollector, 'SubDyn', ErrID_Info, & + 'ElastoDyn initialization failed; attempted with stubbed ElastoDyn interface data', & + Status='unavailable') + end if + if (p_FAST%CompServo == Module_SrvD) then + call CkIn_Collect(CkInCollector, 'ServoDyn', ErrID_Info, & + 'ElastoDyn initialization failed; attempted with stubbed ElastoDyn interface data', & + Status='unavailable') + end if + end if if (Failed()) return - + ! Add module to array of modules, return if errors occurred CALL MV_AddModule(m_Glue%ModData, Module_ED, 'ED', iRot, dt_module, p_FAST%DT, & Init%OutData_ED(iRot)%Vars, p_FAST%Linearize, ErrStat2, ErrMsg2, iRotor=iRot) @@ -388,6 +650,11 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE end if end do + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'ElastoDyn', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'ElastoDyn', ErrStat2, ErrMsg2) + end if + end select ! SED/ED @@ -422,6 +689,8 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE if (p_FAST%CompElast == Module_BD) then + CurrentComponent = 'BeamDyn' + ! Set initialization input Init%InData_BD%DynamicSolve = .TRUE. ! FAST can only couple to BeamDyn when dynamic solve is used. Init%InData_BD%Linearize = p_FAST%Linearize @@ -443,14 +712,38 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%InData_BD%RootName = TRIM(p_FAST%OutFileRoot)//'.'//TRIM(y_FAST%Module_Abrev(Module_BD))& &//'.R'//TRIM(Num2LStr(iRot))//'.B'//TRIM(Num2LStr(k)) Init%InData_BD%InputFile = p_FAST%BDBldFile(k, iRot) - Init%InData_BD%GlbPos = ED%y(iRot)%BladeRootMotion(k)%Position(:,1) ! {:} - - "Initial Position Vector of the local blade coordinate system" - Init%InData_BD%GlbRot = ED%y(iRot)%BladeRootMotion(k)%RefOrientation(:,:,1) ! {:}{:} - - "Initial direction cosine matrix of the local blade coordinate system" - ! These outputs are set in ElastoDyn only when BeamDyn is used: - Init%InData_BD%RootDisp = ED%y(iRot)%BladeRootMotion(k)%TranslationDisp(:,1) ! {:} - - "Initial root displacement" - Init%InData_BD%RootOri = ED%y(iRot)%BladeRootMotion(k)%Orientation(:,:,1) ! {:}{:} - - "Initial root orientation" - Init%InData_BD%RootVel(1:3) = ED%y(iRot)%BladeRootMotion(k)%TranslationVel(:,1) ! {:} - - "Initial root velocities and angular velocities" - Init%InData_BD%RootVel(4:6) = ED%y(iRot)%BladeRootMotion(k)%RotationVel(:,1) ! {:} - - "Initial root velocities and angular velocities" + ! -CheckInput guard: if ED_Init failed, BladeRootMotion may be unallocated, undersized for + ! this blade, or uncommitted -- dereferencing its POINTER components (Position/RefOrientation/ + ! ...) segfaults. Feed neutral geometry and mark BeamDyn tainted. + BDRootMotionOK = .false. + if (allocated(ED%y(iRot)%BladeRootMotion)) then + if (k <= size(ED%y(iRot)%BladeRootMotion)) then + if (ED%y(iRot)%BladeRootMotion(k)%committed) then + Init%InData_BD%GlbPos = ED%y(iRot)%BladeRootMotion(k)%Position(:,1) ! {:} - - "Initial Position Vector of the local blade coordinate system" + Init%InData_BD%GlbRot = ED%y(iRot)%BladeRootMotion(k)%RefOrientation(:,:,1) ! {:}{:} - - "Initial direction cosine matrix of the local blade coordinate system" + + ! These outputs are set in ElastoDyn only when BeamDyn is used: + Init%InData_BD%RootDisp = ED%y(iRot)%BladeRootMotion(k)%TranslationDisp(:,1) ! {:} - - "Initial root displacement" + Init%InData_BD%RootOri = ED%y(iRot)%BladeRootMotion(k)%Orientation(:,:,1) ! {:}{:} - - "Initial root orientation" + Init%InData_BD%RootVel(1:3) = ED%y(iRot)%BladeRootMotion(k)%TranslationVel(:,1) ! {:} - - "Initial root velocities and angular velocities" + Init%InData_BD%RootVel(4:6) = ED%y(iRot)%BladeRootMotion(k)%RotationVel(:,1) ! {:} - - "Initial root velocities and angular velocities" + BDRootMotionOK = .true. + end if + end if + end if + if (.not. BDRootMotionOK) then + Init%InData_BD%GlbPos = 0.0_ReKi + Init%InData_BD%GlbRot = reshape([1._R8Ki,0._R8Ki,0._R8Ki, 0._R8Ki,1._R8Ki,0._R8Ki, 0._R8Ki,0._R8Ki,1._R8Ki],[3,3]) + Init%InData_BD%RootDisp = 0.0_ReKi + Init%InData_BD%RootOri = Init%InData_BD%GlbRot + Init%InData_BD%RootVel = 0.0_ReKi + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'BeamDyn', ErrID_Info, & + 'blade '//trim(Num2LStr(k))//' of rotor '//trim(Num2LStr(iRot))//': ElastoDyn root motion mesh unavailable (upstream failure); attempted with neutral geometry', & + Status='unavailable') + end if + end if ! Call module initialization routine dt_module = p_FAST%DT @@ -469,6 +762,12 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE END DO end do + + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'BeamDyn', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'BeamDyn', ErrStat2, ErrMsg2) + end if + END IF !---------------------------------------------------------------------------- @@ -486,6 +785,8 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE select case(p_FAST%CompInflow) case (Module_IfW) + CurrentComponent = 'InflowWind' + Init%InData_IfW%Linearize = p_FAST%Linearize Init%InData_IfW%InputFileName = p_FAST%InflowFile Init%InData_IfW%RootName = TRIM(p_FAST%OutFileRoot)//'.'//TRIM(y_FAST%Module_Abrev(Module_IfW)) @@ -508,10 +809,33 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%InData_IfW%HubPosition = SED%y%HubPtMotion%Position(:,1) Init%InData_IfW%RadAvg = Init%OutData_SED%BladeLength case (Module_ED) - Init%InData_IfW%HubPosition = ED%y(1)%HubPtMotion%Position(:,1) + ! -CheckInput guard: if ED_Init failed, HubPtMotion is uncommitted and its POINTER + ! Position component is NULL -- dereferencing segfaults. Feed a neutral hub position. + ! The else-branch below is unreachable when ED itself fails (its case-default block above + ! already stubs HubPtMotion to committed) -- kept as defense-in-depth for other uncommitted-mesh + ! causes. + if (ED%y(1)%HubPtMotion%committed) then + Init%InData_IfW%HubPosition = ED%y(1)%HubPtMotion%Position(:,1) + else + Init%InData_IfW%HubPosition = 0.0_ReKi + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'InflowWind', ErrID_Info, & + 'ElastoDyn hub motion mesh unavailable (upstream failure); attempted with neutral hub position', & + Status='unavailable') + end if + end if Init%InData_IfW%RadAvg = Init%OutData_ED(1)%BladeLength case (Module_BD) - Init%InData_IfW%HubPosition = ED%y(1)%HubPtMotion%Position(:,1) + if (ED%y(1)%HubPtMotion%committed) then + Init%InData_IfW%HubPosition = ED%y(1)%HubPtMotion%Position(:,1) + else + Init%InData_IfW%HubPosition = 0.0_ReKi + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'InflowWind', ErrID_Info, & + 'ElastoDyn hub motion mesh unavailable (upstream failure); attempted with neutral hub position', & + Status='unavailable') + end if + end if Init%InData_IfW%RadAvg = 0.0_ReKi do k = 1, p_FAST%NumBD Init%InData_IfW%RadAvg = Init%InData_IfW%RadAvg + TwoNorm(BD%y(k)%BldMotion%Position(:,1) - BD%y(k)%BldMotion%Position(:,BD%y(k)%BldMotion%Nnodes)) @@ -553,6 +877,11 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%OutData_IfW%Vars, p_FAST%Linearize, ErrStat2, ErrMsg2) if (Failed()) return + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'InflowWind', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'InflowWind', ErrStat2, ErrMsg2) + end if + case (Module_ExtInfw) ! ExtInfw requires initialization of AD first, so nothing executed here case default @@ -574,6 +903,8 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE if ( p_FAST%CompSeaSt == Module_SeaSt ) then + CurrentComponent = 'SeaState' + Init%InData_SeaSt%TMax = p_FAST%TMax Init%InData_SeaSt%Gravity = p_FAST%Gravity Init%InData_SeaSt%defWtrDens = p_FAST%WtrDens @@ -625,6 +956,23 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE end if + ! -CheckInput guard: Init%OutData_SeaSt%WaveField is a POINTER (NULL if SeaSt_Init failed or SeaState + ! is off); HydroDyn/MAP/MoorDyn dereference it below without checks. Give them a zero-valued field. + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + if (.not. associated(Init%OutData_SeaSt%WaveField)) then + allocate(Init%OutData_SeaSt%WaveField) ! all scalar components carry safe default initializers + if (p_FAST%CompSeaSt == Module_SeaSt) then + call CkIn_Collect(CkInCollector, 'SeaState', ErrID_Info, & + 'downstream modules given a zero-valued WaveField fallback (upstream SeaState failure)') + end if + end if + ! Report SeaState now that its stub-fallback disclosure (if any) has been collected above. + if (p_FAST%CompSeaSt == Module_SeaSt) then + call CkIn_Collect(CkInCollector, 'SeaState', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'SeaState', ErrStat2, ErrMsg2) + end if + end if + !---------------------------------------------------------------------------- ! Initialize AeroDyn / ADsk !---------------------------------------------------------------------------- @@ -633,6 +981,8 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE case (Module_AD, Module_ExtLd) + CurrentComponent = 'AeroDyn' + ! Allocate module data arrays allocate(AD%Input (InputAryLB:InputAryUB), stat=ErrStat2); if (FailedAlloc("AD%Input")) return allocate(AD%InputTimes (InputAryUB ), stat=ErrStat2); if (FailedAlloc("AD%InputTimes")) return @@ -689,20 +1039,55 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%InData_AD%rotors(iRot)%NacellePosition = SED%y%NacelleMotion%Position(:,1) Init%InData_AD%rotors(iRot)%NacelleOrientation = SED%y%NacelleMotion%RefOrientation(:,:,1) do k = 1, p_FAST%RotNumBld(iRot) - Init%InData_AD%rotors(iRot)%BladeRootPosition(:,k) = SED%y%BladeRootMotion(k)%Position(:,1) - Init%InData_AD%rotors(iRot)%BladeRootOrientation(:,:,k) = SED%y%BladeRootMotion(k)%RefOrientation(:,:,1) + ! -CheckInput guard: mirrors the ED/BeamDyn root-motion guard above -- if SED_Init failed + ! before committing BladeRootMotion (or the array is unallocated/undersized), feed neutral + ! geometry instead of dereferencing a NULL/out-of-bounds mesh. In practice p_FAST%RotNumBld + ! is 0 for early SED failures, so this loop usually zero-trips; guarded here anyway as + ! defense-in-depth for late-stage failures. + if (allocated(SED%y%BladeRootMotion)) then + if (k <= size(SED%y%BladeRootMotion)) then + if (SED%y%BladeRootMotion(k)%committed) then + Init%InData_AD%rotors(iRot)%BladeRootPosition(:,k) = SED%y%BladeRootMotion(k)%Position(:,1) + Init%InData_AD%rotors(iRot)%BladeRootOrientation(:,:,k) = SED%y%BladeRootMotion(k)%RefOrientation(:,:,1) + cycle + end if + end if + end if + Init%InData_AD%rotors(iRot)%BladeRootPosition(:,k) = 0.0_ReKi + Init%InData_AD%rotors(iRot)%BladeRootOrientation(:,:,k) = reshape([1._R8Ki,0._R8Ki,0._R8Ki, 0._R8Ki,1._R8Ki,0._R8Ki, 0._R8Ki,0._R8Ki,1._R8Ki],[3,3]) end do elseif (p_FAST%CompElast == Module_ED .or. p_FAST%CompElast == Module_BD) then - Init%InData_AD%rotors(iRot)%HubPosition = ED%y(iRot)%HubPtMotion%Position(:,1) - Init%InData_AD%rotors(iRot)%HubOrientation = ED%y(iRot)%HubPtMotion%RefOrientation(:,:,1) - Init%InData_AD%rotors(iRot)%NacellePosition = ED%y(iRot)%NacelleMotion%Position(:,1) - Init%InData_AD%rotors(iRot)%NacelleOrientation = ED%y(iRot)%NacelleMotion%RefOrientation(:,:,1) - do k = 1, p_FAST%RotNumBld(iRot) - Init%InData_AD%rotors(iRot)%BladeRootPosition(:,k) = ED%y(iRot)%BladeRootMotion(k)%Position(:,1) - Init%InData_AD%rotors(iRot)%BladeRootOrientation(:,:,k) = ED%y(iRot)%BladeRootMotion(k)%RefOrientation(:,:,1) - end do + ! -CheckInput guard: if ED_Init failed, its output meshes are uncommitted and their POINTER + ! components are NULL -- dereferencing segfaults. Feed neutral geometry and mark AD tainted. + ! The else-branch below is unreachable when ED itself fails (its case-default block above + ! already stubs these meshes to committed) -- kept as defense-in-depth for other + ! uncommitted-mesh causes. + if (ED%y(iRot)%HubPtMotion%committed) then + Init%InData_AD%rotors(iRot)%HubPosition = ED%y(iRot)%HubPtMotion%Position(:,1) + Init%InData_AD%rotors(iRot)%HubOrientation = ED%y(iRot)%HubPtMotion%RefOrientation(:,:,1) + Init%InData_AD%rotors(iRot)%NacellePosition = ED%y(iRot)%NacelleMotion%Position(:,1) + Init%InData_AD%rotors(iRot)%NacelleOrientation = ED%y(iRot)%NacelleMotion%RefOrientation(:,:,1) + do k = 1, p_FAST%RotNumBld(iRot) + Init%InData_AD%rotors(iRot)%BladeRootPosition(:,k) = ED%y(iRot)%BladeRootMotion(k)%Position(:,1) + Init%InData_AD%rotors(iRot)%BladeRootOrientation(:,:,k) = ED%y(iRot)%BladeRootMotion(k)%RefOrientation(:,:,1) + end do + else + Init%InData_AD%rotors(iRot)%HubPosition = 0.0_ReKi + Init%InData_AD%rotors(iRot)%HubOrientation = reshape([1._R8Ki,0._R8Ki,0._R8Ki, 0._R8Ki,1._R8Ki,0._R8Ki, 0._R8Ki,0._R8Ki,1._R8Ki],[3,3]) + Init%InData_AD%rotors(iRot)%NacellePosition = 0.0_ReKi + Init%InData_AD%rotors(iRot)%NacelleOrientation = Init%InData_AD%rotors(iRot)%HubOrientation + do k = 1, p_FAST%RotNumBld(iRot) + Init%InData_AD%rotors(iRot)%BladeRootPosition(:,k) = 0.0_ReKi + Init%InData_AD%rotors(iRot)%BladeRootOrientation(:,:,k) = Init%InData_AD%rotors(iRot)%HubOrientation + end do + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'AeroDyn', ErrID_Info, & + 'rotor '//trim(Num2LStr(iRot))//': ElastoDyn output mesh unavailable (upstream failure); attempted with neutral geometry', & + Status='unavailable') + end if + end if endif @@ -741,8 +1126,15 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE AirDens = Init%OutData_AD%rotors(1)%AirDens + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'AeroDyn', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'AeroDyn', ErrStat2, ErrMsg2) + end if + case (Module_ADsk) + CurrentComponent = 'AeroDisk' + ! Allocate module data arrays allocate(ADsk%Input (InputAryLB:InputAryUB), stat=ErrStat2); if (FailedAlloc("ADsk%Input")) return allocate(ADsk%InputTimes (InputAryUB ), stat=ErrStat2); if (FailedAlloc("ADsk%InputTimes")) return @@ -785,6 +1177,11 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE ! AeroDisk may override the AirDens value. Store this to inform other modules AirDens = Init%OutData_ADsk%AirDens + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'AeroDisk', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'AeroDisk', ErrStat2, ErrMsg2) + end if + end select ! CompAero !---------------------------------------------------------------------------- @@ -824,14 +1221,30 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE !---------------------------------------------------------------------------- IF ( p_FAST%CompInflow == Module_ExtInfw ) THEN + ! -CheckInput: this block's own label -- without it, a failure here would still carry whatever + ! CurrentComponent the previous module's block left set (AeroDyn/AeroDisk), misattributing the error. + CurrentComponent = 'ExternalInflow' + + ExtInfw_OK = .false. IF ( PRESENT(ExternInitData) ) THEN Init%InData_ExtInfw%NumActForcePtsBlade = ExternInitData%NumActForcePtsBlade Init%InData_ExtInfw%NumActForcePtsTower = ExternInitData%NumActForcePtsTower + ExtInfw_OK = .true. ELSE CALL SetErrStat( ErrID_Fatal, 'ExternalInflow integration can be used only with external input data (not the stand-alone executable).', ErrStat, ErrMsg, RoutineName ) - CALL Cleanup() - RETURN + ! -CheckInput guard: without ExternInitData, everything below dereferences an absent OPTIONAL + ! argument -- skip only this module's remaining init work instead of aborting the whole run. + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, trim(CurrentComponent), ErrStat, ErrMsg) + ErrStat = ErrID_None + ErrMsg = '' + else + CALL Cleanup() + RETURN + end if END IF + + IF (ExtInfw_OK) THEN ! get blade and tower info from AD. Assumption made that all blades have same spanwise characteristics Init%InData_ExtInfw%BladeLength = Init%OutData_AD%rotors(1)%BladeProps(1)%BlSpn(Init%OutData_AD%rotors(1)%BladeProps(1)%NumBlNds) if (allocated(Init%OutData_AD%rotors(1)%TwrElev)) then @@ -853,7 +1266,7 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE ! Set node clustering type Init%InData_ExtInfw%NodeClusterType = ExternInitData%NodeClusterType - + ! Set up the data structures for integration with ExternalInflow CALL Init_ExtInfw(Init%InData_ExtInfw, p_FAST, AirDens, AD%Input(1), Init%OutData_AD, AD%y, ExtInfw, Init%OutData_ExtInfw, ErrStat2, ErrMsg2) if (Failed()) return @@ -868,6 +1281,7 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE ! Set pointer to flowfield -- I would prefer that we did this through the AD_Init, but AD_InitOut results are required for ExtInfw_Init IF (p_FAST%CompAero == Module_AD) AD%p%FlowField => Init%OutData_ExtInfw%FlowField + END IF endif !---------------------------------------------------------------------------- @@ -887,12 +1301,24 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE ! SoilDyn case (Module_SlD) + CurrentComponent = 'SoilDyn' + ! SoilDyn requires SubDyn + SlD_OK = .true. if (p_FAST%CompSub /= Module_SD) then call SetErrStat(ErrID_Fatal, "SoilDyn requires SubDyn (CompSub = 1)", ErrStat, ErrMsg, RoutineName) - return + ! -CheckInput guard: skip only SoilDyn's remaining init work instead of aborting the whole run. + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, trim(CurrentComponent), ErrStat, ErrMsg) + ErrStat = ErrID_None + ErrMsg = '' + SlD_OK = .false. + else + return + end if end if + if (SlD_OK) then ! Initialization input Init%InData_SlD%InputFile = p_FAST%SoilFile Init%InData_SlD%RootName = p_FAST%OutFileRoot @@ -913,6 +1339,12 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE CALL MV_AddModule(m_Glue%ModData, Module_SlD, 'SlD', 1, dt_module, p_FAST%DT, & Init%OutData_SlD%Vars, p_FAST%Linearize, ErrStat2, ErrMsg2) if (Failed()) return + end if + + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'SoilDyn', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'SoilDyn', ErrStat2, ErrMsg2) + end if end select @@ -940,6 +1372,8 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE case (Module_SD) + CurrentComponent = 'SubDyn' + Init%InData_SD%Linearize = p_FAST%Linearize Init%InData_SD%g = p_FAST%Gravity Init%InData_SD%SDInputFile = p_FAST%SubFile @@ -978,7 +1412,20 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%InData_SD%nTP = p_FAST%NRotors call AllocAry(Init%InData_SD%TP_RefPoint, 3, p_FAST%NRotors, "TP_RefPoint", ErrStat2, ErrMsg2); if (Failed()) return do iRot = 1, p_FAST%NRotors - Init%InData_SD%TP_RefPoint(:,iRot) = ED%y(iRot)%PlatformPtMesh%Position(:,1) + ! -CheckInput guard: ED_Init commits PlatformPtMesh before any successful return; the else-branch + ! below is reachable only after a collected ED failure (uncommitted mesh, NULL Position pointer). + ! In practice it is unreachable even then -- ED's case-default block already stubs this mesh to + ! committed -- kept as defense-in-depth for other uncommitted-mesh causes. + if (ED%y(iRot)%PlatformPtMesh%committed) then + Init%InData_SD%TP_RefPoint(:,iRot) = ED%y(iRot)%PlatformPtMesh%Position(:,1) + else + Init%InData_SD%TP_RefPoint(:,iRot) = 0.0_ReKi + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'SubDyn', ErrID_Info, & + 'rotor '//trim(Num2LStr(iRot))//': ElastoDyn platform mesh unavailable (upstream failure); attempted with zeroed position', & + Status='unavailable') + end if + end if end do ! Call module initialization routine @@ -1003,8 +1450,15 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE ! Use PlatformPosInit from ED above end if + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'SubDyn', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'SubDyn', ErrStat2, ErrMsg2) + end if + case (Module_ExtPtfm) + CurrentComponent = 'ExtPtfm' + Init%InData_ExtPtfm%InputFile = p_FAST%SubFile Init%InData_ExtPtfm%RootName = trim(p_FAST%OutFileRoot)//'.'//y_FAST%Module_Abrev(Module_ExtPtfm) Init%InData_ExtPtfm%Linearize = p_FAST%Linearize @@ -1025,6 +1479,11 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%OutData_ExtPtfm%Vars, p_FAST%Linearize, ErrStat2, ErrMsg2) if (Failed()) return + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'ExtPtfm', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'ExtPtfm', ErrStat2, ErrMsg2) + end if + end select !---------------------------------------------------------------------------- @@ -1041,6 +1500,8 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE IF (p_FAST%CompHydro == Module_HD) THEN + CurrentComponent = 'HydroDyn' + Init%InData_HD%Gravity = p_FAST%Gravity Init%InData_HD%UseInputFile = .TRUE. Init%InData_HD%InputFile = p_FAST%HydroFile @@ -1083,6 +1544,11 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%OutData_HD%Vars, p_FAST%Linearize, ErrStat2, ErrMsg2) if (Failed()) return + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'HydroDyn', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'HydroDyn', ErrStat2, ErrMsg2) + end if + END IF ! CompHydro !---------------------------------------------------------------------------- @@ -1123,7 +1589,9 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE select case (p_FAST%CompMooring) - case (Module_MAP) + case (Module_MAP) + + CurrentComponent = 'MAP' !bjj: until we modify this, MAP requires HydroDyn to be used. (perhaps we could send air density from AeroDyn or something...) @@ -1153,7 +1621,14 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%OutData_MAP%Vars, p_FAST%Linearize, ErrStat2, ErrMsg2) if (Failed()) return - case (Module_MD) + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'MAP', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'MAP', ErrStat2, ErrMsg2) + end if + + case (Module_MD) + + CurrentComponent = 'MoorDyn' ! some new allocations needed with version that's compatible with farm-level use allocate(Init%InData_MD%PtfmInit (6,1), stat=ErrStat2); if (FailedAlloc("Init%InData_MD%PtfmInit")) return @@ -1189,7 +1664,14 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%OutData_MD%Vars, p_FAST%Linearize, ErrStat2, ErrMsg2) if (Failed()) return - case (Module_FEAM) + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'MoorDyn', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'MoorDyn', ErrStat2, ErrMsg2) + end if + + case (Module_FEAM) + + CurrentComponent = 'FEAMooring' Init%InData_FEAM%InputFile = p_FAST%MooringFile ! This needs to be set according to what is in the FAST input file. Init%InData_FEAM%RootName = TRIM(p_FAST%OutFileRoot)//'.'//TRIM(y_FAST%Module_Abrev(Module_FEAM)) @@ -1213,7 +1695,14 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%OutData_FEAM%Vars, .false., ErrStat2, ErrMsg2) if (Failed()) return - case (Module_Orca) + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'FEAMooring', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'FEAMooring', ErrStat2, ErrMsg2) + end if + + case (Module_Orca) + + CurrentComponent = 'OrcaFlex' Init%InData_Orca%InputFile = p_FAST%MooringFile Init%InData_Orca%RootName = p_FAST%OutFileRoot @@ -1232,6 +1721,11 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%OutData_Orca%Vars, .false., ErrStat2, ErrMsg2) if (Failed()) return + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'OrcaFlex', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'OrcaFlex', ErrStat2, ErrMsg2) + end if + END select !---------------------------------------------------------------------------- @@ -1252,6 +1746,8 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE IF (p_FAST%CompIce == Module_IceF) THEN + CurrentComponent = 'IceFloe' + Init%InData_IceF%InputFile = p_FAST%IceFile Init%InData_IceF%RootName = TRIM(p_FAST%OutFileRoot)//'.'//TRIM(y_FAST%Module_Abrev(Module_IceF)) Init%InData_IceF%simLength = p_FAST%TMax !bjj: IceFloe stores this as single-precision (ReKi) TMax is DbKi @@ -1269,6 +1765,11 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%OutData_IceF%Vars, .false., ErrStat2, ErrMsg2) if (Failed()) return + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'IceFloe', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'IceFloe', ErrStat2, ErrMsg2) + end if + end if !------------------------------------- @@ -1293,6 +1794,8 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE IF (p_FAST%CompIce == Module_IceD) THEN + CurrentComponent = 'IceDyn' + Init%InData_IceD%InputFile = p_FAST%IceFile Init%InData_IceD%RootName = TRIM(p_FAST%OutFileRoot)//'.'//TRIM(y_FAST%Module_Abrev(Module_IceD))//'1' Init%InData_IceD%MSL2SWL = Init%OutData_SeaSt%WaveField%MSL2SWL @@ -1336,7 +1839,16 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE !bjj: we're going to force this to have the same timestep because I don't want to have to deal with n IceD modules with n timesteps. IF (.NOT. EqualRealNos(dt_module, dt_IceD)) THEN CALL SetErrStat(ErrID_Fatal,"All instances of IceDyn (one per support-structure leg) must be the same",ErrStat,ErrMsg,RoutineName) - return + ! -CheckInput guard: skip only this leg's remaining init work (the MV_AddModule call below) + ! and continue attempting the other legs, instead of aborting the whole run. + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, trim(CurrentComponent), ErrStat, ErrMsg) + ErrStat = ErrID_None + ErrMsg = '' + cycle + else + return + end if END IF ! Add module to list of modules @@ -1345,6 +1857,11 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE if (Failed()) return END DO + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'IceDyn', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'IceDyn', ErrStat2, ErrMsg2) + end if + END IF !---------------------------------------------------------------------------- @@ -1365,6 +1882,8 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE IF ( p_FAST%CompServo == Module_SrvD ) THEN + CurrentComponent = 'ServoDyn' + ! Loop through the number of rotors do iRot = 1, p_FAST%NRotors @@ -1407,7 +1926,14 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%InData_SrvD%PtfmRefOrient(1:3,1:3)= SED%y%PlatformPtMesh%RefOrientation(1:3,1:3,1) ! R8Ki Init%InData_SrvD%PtfmOrient(1:3,1:3) = SED%y%PlatformPtMesh%Orientation(1:3,1:3,1) ! R8Ki Init%InData_SrvD%RotSpeedRef = Init%OutData_SED%RotSpeed - Init%InData_SrvD%BlPitchInit = Init%OutData_SED%BlPitch + ! -CheckInput guard: mirrors the ED guard below -- if SED_Init failed, InitOutput%BlPitch + ! (ALLOCATABLE) was never allocated; assigning an unallocated allocatable to another + ! allocatable is undefined behavior and segfaults. + if (allocated(Init%OutData_SED%BlPitch)) then + Init%InData_SrvD%BlPitchInit = Init%OutData_SED%BlPitch + else + Init%InData_SrvD%BlPitchInit = 0.0_ReKi + end if else Init%InData_SrvD%NacRefPos(1:3) = ED%y(iRot)%NacelleMotion%Position(1:3,1) Init%InData_SrvD%NacTransDisp(1:3) = ED%y(iRot)%NacelleMotion%TranslationDisp(1:3,1) ! R8Ki @@ -1422,7 +1948,13 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE Init%InData_SrvD%PtfmRefOrient(1:3,1:3)= ED%y(iRot)%PlatformPtMesh%RefOrientation(1:3,1:3,1) ! R8Ki Init%InData_SrvD%PtfmOrient(1:3,1:3) = ED%y(iRot)%PlatformPtMesh%Orientation(1:3,1:3,1) ! R8Ki Init%InData_SrvD%RotSpeedRef = Init%OutData_ED(iRot)%RotSpeed - Init%InData_SrvD%BlPitchInit = Init%OutData_ED(iRot)%BlPitch + ! -CheckInput guard: if ED_Init failed, InitOutput%BlPitch (ALLOCATABLE) was never allocated; + ! assigning an unallocated allocatable to another allocatable is undefined behavior and segfaults. + if (allocated(Init%OutData_ED(iRot)%BlPitch)) then + Init%InData_SrvD%BlPitchInit = Init%OutData_ED(iRot)%BlPitch + else + Init%InData_SrvD%BlPitchInit = 0.0_ReKi + end if endif ! Set blade root info -- used for Blade StC. Set from SED even though SED is not compatible -- we won't know @@ -1430,10 +1962,23 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE select case (p_FAST%CompElast) case (Module_SED) do k = 1, p_FAST%RotNumBld(iRot) - Init%InData_SrvD%BladeRootRefPos(:,k) = SED%y%BladeRootMotion(k)%Position(:,1) - Init%InData_SrvD%BladeRootTransDisp(:,k) = SED%y%BladeRootMotion(k)%TranslationDisp(:,1) - Init%InData_SrvD%BladeRootRefOrient(:,:,k)= SED%y%BladeRootMotion(k)%RefOrientation(:,:,1) - Init%InData_SrvD%BladeRootOrient(:,:,k) = SED%y%BladeRootMotion(k)%Orientation(:,:,1) + ! -CheckInput guard: mirrors the AeroDyn SED guard above -- feed neutral geometry if + ! BladeRootMotion is unallocated/undersized/uncommitted rather than dereferencing it. + if (allocated(SED%y%BladeRootMotion)) then + if (k <= size(SED%y%BladeRootMotion)) then + if (SED%y%BladeRootMotion(k)%committed) then + Init%InData_SrvD%BladeRootRefPos(:,k) = SED%y%BladeRootMotion(k)%Position(:,1) + Init%InData_SrvD%BladeRootTransDisp(:,k) = SED%y%BladeRootMotion(k)%TranslationDisp(:,1) + Init%InData_SrvD%BladeRootRefOrient(:,:,k)= SED%y%BladeRootMotion(k)%RefOrientation(:,:,1) + Init%InData_SrvD%BladeRootOrient(:,:,k) = SED%y%BladeRootMotion(k)%Orientation(:,:,1) + cycle + end if + end if + end if + Init%InData_SrvD%BladeRootRefPos(:,k) = 0.0_ReKi + Init%InData_SrvD%BladeRootTransDisp(:,k) = 0.0_R8Ki + Init%InData_SrvD%BladeRootRefOrient(:,:,k)= reshape([1._R8Ki,0._R8Ki,0._R8Ki, 0._R8Ki,1._R8Ki,0._R8Ki, 0._R8Ki,0._R8Ki,1._R8Ki],[3,3]) + Init%InData_SrvD%BladeRootOrient(:,:,k) = Init%InData_SrvD%BladeRootRefOrient(:,:,k) end do case (Module_ED, Module_BD) do k = 1, p_FAST%RotNumBld(iRot) @@ -1501,11 +2046,21 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE if (ErrStat >= AbortErrLev) return end do - + + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + call CkIn_Collect(CkInCollector, 'ServoDyn', ErrID_None, '') + call CkIn_ReportComponent(CkInCollector, 'ServoDyn', ErrStat2, ErrMsg2) + end if + END IF + ! -CheckInput: every module's own label has now been reported; restore the generic label so any + ! remaining failure below (FAST_InitOutput, VTK setup, ...) is attributed to FAST_InitializeAll + ! rather than misattributed to ServoDyn (the last per-module label set above). + CurrentComponent = 'FAST_InitializeAll' + !---------------------------------------------------------------------------- - ! Set up output for glue code + ! Set up output for glue code ! (must be done after all modules are initialized so we have their WriteOutput information) !---------------------------------------------------------------------------- @@ -1529,7 +2084,18 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE m_FAST%t_global = t_initial ! Initialize external inputs for first step + ! + ! -CheckInput guard: when SrvD_Init fails, Failed() (see above) collects the error and keeps + ! going instead of returning -- but SrvD%Input(INPUT_CURR,1)'s allocatable members (including + ! ExternalBlPitchCom, indexed below) are never allocated if SrvD_Init fails before reaching its + ! own Init_u allocation (e.g. its input file is missing, as in the AeroMap corpus-sweep case). + ! In the normal (non-CheckInput) path this whole section is unreachable after a real SrvD_Init + ! failure because Failed() already returned above, so gate the dereferences on the array being + ! allocated rather than adding a redundant CheckInputMode branch; m_FAST%ExternInput's fields + ! already default-initialize to zero (FAST_Types.f90), so skipping this block leaves them at a + ! sane value and ServoDyn's own failure has already been collected/reported above. if ( p_FAST%CompServo == MODULE_SrvD ) then + if (allocated(SrvD%Input(INPUT_CURR,1)%ExternalBlPitchCom)) then m_FAST%ExternInput%GenTrq = SrvD%Input(INPUT_CURR,1)%ExternalGenTrq !0.0_ReKi m_FAST%ExternInput%ElecPwr = SrvD%Input(INPUT_CURR,1)%ExternalElecPwr @@ -1561,6 +2127,7 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE m_FAST%ExternInput%CableDeltaLdot = 0.0_Reki endif end if + end if !---------------------------------------------------------------------------- ! Cleanup @@ -1573,14 +2140,53 @@ SUBROUTINE FAST_InitializeAll( t_initial, m_Glue, p_FAST, y_FAST, m_FAST, ED, SE SUBROUTINE Cleanup() ! Destroy initialization data - CALL FAST_DestroyInitData( Init, ErrStat2, ErrMsg2 ) + CALL FAST_DestroyInitData( Init, ErrStat2, ErrMsg2 ) CALL SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName) END SUBROUTINE Cleanup + SUBROUTINE StubOutputPointMesh(mesh) + ! -CheckInput helper: when an upstream module's Init fails, its output meshes never get committed, + ! so their POINTER components (Position/Orientation/...) are NULL. Every downstream module that reads + ! such a mesh (there are many call sites -- ED's HubPtMotion/NacelleMotion/PlatformPtMesh are read by + ! InflowWind/AeroDyn/BeamDyn/SubDyn/ServoDyn/HydroDyn/MAP/MoorDyn/...) would otherwise segfault. + ! Rather than guard every read site, build a committed, all-zero/identity stand-in mesh once, right + ! after the failed Init call, so the rest of FAST_InitializeAll can proceed and every remaining + ! module still gets attempted. Only called under CheckInputMode with a collector present. + TYPE(MeshType), INTENT(INOUT) :: mesh + INTEGER(IntKi) :: ErrStat3 + CHARACTER(ErrMsgLen) :: ErrMsg3 + call MeshCreate(mesh, COMPONENT_OUTPUT, 1, ErrStat3, ErrMsg3, & + Orientation=.true., TranslationDisp=.true., TranslationVel=.true., RotationVel=.true., & + TranslationAcc=.true., RotationAcc=.true.) + if (ErrStat3 >= AbortErrLev) return + call MeshPositionNode(mesh, 1, [0.0_ReKi, 0.0_ReKi, 0.0_ReKi], ErrStat3, ErrMsg3) + if (ErrStat3 >= AbortErrLev) return + call MeshConstructElement(mesh, ELEMENT_POINT, ErrStat3, ErrMsg3, p1=1) + if (ErrStat3 >= AbortErrLev) return + call MeshCommit(mesh, ErrStat3, ErrMsg3) + if (ErrStat3 >= AbortErrLev) return + mesh%Orientation = mesh%RefOrientation + mesh%TranslationDisp = 0.0_R8Ki + mesh%TranslationVel = 0.0_ReKi + mesh%RotationVel = 0.0_ReKi + mesh%TranslationAcc = 0.0_ReKi + mesh%RotationAcc = 0.0_ReKi + END SUBROUTINE StubOutputPointMesh + logical function Failed() call SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName) Failed = ErrStat >= AbortErrLev - if (Failed) call Cleanup() + if (Failed) then + if (p_FAST%CheckInputMode .and. present(CkInCollector)) then + ! -CheckInput: record and keep going so every remaining module is still attempted. + call CkIn_Collect(CkInCollector, trim(CurrentComponent), ErrStat, ErrMsg) + ErrStat = ErrID_None + ErrMsg = '' + Failed = .false. + else + call Cleanup() + end if + end if end function Failed logical function FailedAlloc(txt) @@ -2345,7 +2951,20 @@ SUBROUTINE FAST_InitOutput( p_FAST, y_FAST, Init, ErrStat, ErrMsg ) if (allocated(Init%OutData_Orca%WriteOutputHdr)) y_FAST%numOuts(Module_Orca) = size(Init%OutData_Orca%WriteOutputHdr) if (allocated(Init%OutData_IceF%WriteOutputHdr)) y_FAST%numOuts(Module_IceF) = size(Init%OutData_IceF%WriteOutputHdr) if (allocated(Init%OutData_IceD%WriteOutputHdr)) y_FAST%numOuts(Module_IceD) = size(Init%OutData_IceD%WriteOutputHdr) * p_FAST%numIceLegs - IF (allocated(Init%OutData_SlD%WriteOutputHdr)) y_FAST%numOuts(Module_SlD) = size(Init%OutData_SlD%WriteOutputHdr) + ! -CheckInput guard: SlD_Init (SoilDyn.f90) can leave WriteOutputHdr allocated but + ! WriteOutputUnt not -- when SlD_REDWINsetup() fails it sets ErrStat=Fatal without an + ! immediate return, so the next line's own (unrelated, successful) AllocAry(WriteOutputHdr,...) + ! is still followed by "if (Failed()) return" that now trips on the *stale* Fatal ErrStat, + ! returning before AllocAry(WriteOutputUnt,...) runs. In the normal (non-CheckInput) path this + ! is harmless -- SlD_Init's own Fatal return makes FAST_InitializeAll's Failed() abort the run + ! immediately, long before FAST_InitOutput reads either array. Under CheckInputMode, Failed() + ! collects and continues instead, so FAST_InitOutput below is reached with only WriteOutputHdr + ! allocated; require both arrays (not just WriteOutputHdr) before trusting SoilDyn has any + ! outputs, so the read loop over WriteOutputHdr/WriteOutputUnt is skipped rather than + ! dereferencing the unallocated WriteOutputUnt. SoilDyn's own failure is already collected/ + ! reported via Failed() where SlD_Init is called, above. + IF (allocated(Init%OutData_SlD%WriteOutputHdr) .and. allocated(Init%OutData_SlD%WriteOutputUnt)) & + y_FAST%numOuts(Module_SlD) = size(Init%OutData_SlD%WriteOutputHdr) !...................................................... ! Initialize the output channel names and units diff --git a/modules/openfast-library/src/FAST_Types.f90 b/modules/openfast-library/src/FAST_Types.f90 index 543823a9bd..ae252d8609 100644 --- a/modules/openfast-library/src/FAST_Types.f90 +++ b/modules/openfast-library/src/FAST_Types.f90 @@ -165,6 +165,7 @@ MODULE FAST_Types INTEGER(IntKi) :: MHK = 0_IntKi !< MHK turbine type (switch) {0=Not an MHK turbine; 1=Fixed MHK turbine; 2=Floating MHK turbine} [-] LOGICAL :: UseDWM = .false. !< Use the DWM module in AeroDyn [-] LOGICAL :: Linearize = .false. !< Linearization analysis (flag) [-] + LOGICAL :: CheckInputMode = .false. !< -CheckInput run: initialize all modules with attempt-everything error collection, no time marching (flag) [-] INTEGER(IntKi) :: WaveFieldMod = 0_IntKi !< Wave field handling (-) (switch) 0: use individual HydroDyn inputs without adjustment, 1: adjust wave phases based on turbine offsets from farm origin [-] LOGICAL :: FarmIntegration = .false. !< whether this is called from FAST.Farm (or another program that doesn't want FAST to call all of the init stuff first) [-] REAL(SiKi) , DIMENSION(1:3) :: TurbinePos = 0.0_R4Ki !< Initial position of turbine base (origin used for graphics) [m] @@ -1184,6 +1185,7 @@ subroutine FAST_CopyParam(SrcParamData, DstParamData, CtrlCode, ErrStat, ErrMsg) DstParamData%MHK = SrcParamData%MHK DstParamData%UseDWM = SrcParamData%UseDWM DstParamData%Linearize = SrcParamData%Linearize + DstParamData%CheckInputMode = SrcParamData%CheckInputMode DstParamData%WaveFieldMod = SrcParamData%WaveFieldMod DstParamData%FarmIntegration = SrcParamData%FarmIntegration DstParamData%TurbinePos = SrcParamData%TurbinePos @@ -1415,6 +1417,7 @@ subroutine FAST_PackParam(RF, Indata) call RegPack(RF, InData%MHK) call RegPack(RF, InData%UseDWM) call RegPack(RF, InData%Linearize) + call RegPack(RF, InData%CheckInputMode) call RegPack(RF, InData%WaveFieldMod) call RegPack(RF, InData%FarmIntegration) call RegPack(RF, InData%TurbinePos) @@ -1539,6 +1542,7 @@ subroutine FAST_UnPackParam(RF, OutData) call RegUnpack(RF, OutData%MHK); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%UseDWM); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%Linearize); if (RegCheckErr(RF, RoutineName)) return + call RegUnpack(RF, OutData%CheckInputMode); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%WaveFieldMod); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%FarmIntegration); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%TurbinePos); if (RegCheckErr(RF, RoutineName)) return diff --git a/modules/orcaflex-interface/src/OrcaDriver.f90 b/modules/orcaflex-interface/src/OrcaDriver.f90 index 5addc01640..e81ca473fe 100644 --- a/modules/orcaflex-interface/src/OrcaDriver.f90 +++ b/modules/orcaflex-interface/src/OrcaDriver.f90 @@ -33,6 +33,7 @@ PROGRAM OrcaDriver USE OrcaDriver_Types USE OrcaDriver_Subs USE OrcaFlexInterface + USE NWTC_CheckInput IMPLICIT NONE @@ -85,6 +86,13 @@ PROGRAM OrcaDriver INTEGER(IntKi) :: ErrStatTmp CHARACTER(2048) :: ErrMsgTmp INTEGER(IntKi) :: LenErrMsgTmp ! Length of ErrMsgTmp + INTEGER(IntKi) :: ErrStat2 ! -CheckInput: temp error status for calls + CHARACTER(1024) :: ErrMsg2 ! -CheckInput: temp error message for calls + + ! -CheckInput support (no initializers on these -- set as early executable statements below) + LOGICAL :: CheckInputMode ! true if -CheckInput was given on the command line + TYPE(CheckInputCollectorType) :: Checker ! -CheckInput result collector + CHARACTER(64) :: CkStage ! name of the -CheckInput stage/component currently executing !-------------------------------------------------------------------------- @@ -105,6 +113,9 @@ PROGRAM OrcaDriver ! Start the timer CALL CPU_TIME( Timer(1) ) + ! -CheckInput: no initializers on these -- set as early executable statements + CheckInputMode = .FALSE. + CkStage = 'Driver' ! default stage label; overridden before the Orca_Init call below ! Set some CLSettings to null/default values CLSettings%DvrIptFileName = "" ! No input name name until set @@ -135,6 +146,7 @@ PROGRAM OrcaDriver CLSettingsFlags%PointsOutputInit = .FALSE. ! Points output file not started CLSettingsFlags%Verbose = .FALSE. ! Turn on verbose error reporting? CLSettingsFlags%VVerbose = .FALSE. ! Turn on very verbose error reporting? + CLSettingsFlags%CheckInput = .FALSE. ! -CheckInput mode requested on the command line ! Initialize the driver settings to their default values (same as the CL -- command line -- values) @@ -154,6 +166,10 @@ PROGRAM OrcaDriver ErrMsg = '' ENDIF + ! -CheckInput is command-line only (mirrors how Verbose/VVerbose are handled below -- not + ! merged into SettingsFlags by UpdateSettingsWithCL, so read it straight off the CL flags). + CheckInputMode = CLSettingsFlags%CheckInput + ! Check if we are doing verbose error reporting IF ( CLSettingsFlags%VVerbose ) THEN @@ -202,6 +218,7 @@ PROGRAM OrcaDriver ! Read the driver input file CALL ReadDvrIptFile( CLSettings%DvrIptFileName, SettingsFlags, Settings, ProgInfo, ErrStat, ErrMsg ) IF ( ErrStat >= AbortErrLev ) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns CALL ProgAbort( ErrMsg ) ELSEIF ( ErrStat /= 0 ) THEN CALL WrScr( NewLine//ErrMsg ) @@ -227,6 +244,7 @@ PROGRAM OrcaDriver ! was read. CALL UpdateSettingsWithCL( SettingsFlags, Settings, CLSettingsFlags, CLSettings, .TRUE., ErrStat, ErrMsg ) IF ( ErrStat >= AbortErrLev ) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns CALL ProgAbort( ErrMsg ) ELSEIF ( ErrStat /= ErrID_None ) THEN CALL WrScr( NewLine//ErrMsg ) @@ -254,6 +272,7 @@ PROGRAM OrcaDriver ! input file was not read. CALL UpdateSettingsWithCL( SettingsFlags, Settings, CLSettingsFlags, CLSettings, .FALSE., ErrStat, ErrMsg ) IF ( ErrStat >= AbortErrLev ) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns CALL ProgAbort( ErrMsg ) ELSEIF ( ErrStat /= ErrID_None ) THEN CALL WrScr( NewLine//ErrMsg ) @@ -276,11 +295,16 @@ PROGRAM OrcaDriver IF ( SettingsFlags%PointsFile ) THEN INQUIRE( file=TRIM(Settings%PointsFileName), exist=TempFileExist ) - IF ( TempFileExist .eqv. .FALSE. ) CALL ProgAbort( "Cannot find the points file "//TRIM(Settings%PointsFileName)) + IF ( TempFileExist .eqv. .FALSE. ) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrID_Fatal, & + "Cannot find the points file "//TRIM(Settings%PointsFileName) ) ! never returns + CALL ProgAbort( "Cannot find the points file "//TRIM(Settings%PointsFileName)) + END IF ! Now read the file in and save the points CALL ReadPointsFile( Settings%PointsFileName, SettingsFlags%PointsDegrees, TimeList, PointsList, VelocList, AccelList, ErrStat,ErrMsg ) IF ( ErrStat >= AbortErrLev ) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns CALL ProgAbort( ErrMsg ) ELSEIF ( ErrStat /= 0 ) THEN CALL WrScr( NewLine//ErrMsg ) @@ -343,20 +367,30 @@ PROGRAM OrcaDriver ! Some initialization settings Orca_InitInp%InputFile = Settings%OrcaIptFileName - CALL GetRoot( Orca_InitInp%InputFile, Orca_InitInp%RootName ) + CALL GetRoot( Orca_InitInp%InputFile, Orca_InitInp%RootName ) Orca_InitInp%TMax = Settings%TMax - + + ! -CheckInput: RootName is known now (pre-Init) -- open the report before Orca_Init runs so a + ! fatal from Init itself (e.g. the OrcaFlex DLL failing to load) is caught. + IF ( CheckInputMode ) THEN + CALL CkIn_OpenReport( Checker, TRIM(Orca_InitInp%RootName)//'.driver', ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) + END IF IF ( OrcaDriver_Verbose >= 5_IntKi ) CALL WrScr('Calling Orca_Init...') + CkStage = 'OrcaFlexInterface' CALL Orca_Init( Orca_InitInp, Orca_u, Orca_p, & Orca_x, Orca_xd, Orca_z, Orca_OtherState, & Orca_y, Orca_m, Settings%DT, Orca_InitOut, ErrStat, ErrMsg ) ! Make sure no errors occurred that give us reason to terminate now. + ! -CheckInput: the OrcaFlex DLL dlopen happens inside Orca_Init -- a missing/broken DLL must + ! surface as a failed 'OrcaFlexInterface' component with a completed report, not a crash. IF ( ErrStat >= AbortErrLev ) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns CALL DriverCleanup() CALL ProgAbort( ErrMsg ) ELSEIF ( ( ErrStat /= ErrID_None ) .AND. ( OrcaDriver_Verbose >= 7_IntKi ) ) THEN @@ -369,6 +403,7 @@ PROGRAM OrcaDriver ErrStat = ErrID_None ErrMsg = '' ENDIF + CkStage = 'Driver' ! post-Init timestep checks below are attributed back to the Driver stage @@ -376,6 +411,17 @@ PROGRAM OrcaDriver IF ( OrcaDriver_Verbose >= 5_IntKi ) CALL WrScr(NewLine//'Orca_Init CALL returned without errors.'//NewLine) + IF ( CheckInputMode ) THEN + ! Reaching here means every stage above completed without a fatal error (a fatal one would have + ! routed through one of the CkIn_DriverFail interceptions above and never returned). Record both + ! stages as passed, in order, then finish -- this call never returns, so the CalcOutput loop and + ! all output writes below are never reached in check mode. + CALL CkIn_Collect( Checker, 'Driver', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Driver', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'OrcaFlexInterface', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'OrcaFlexInterface', ErrStat2, ErrMsg2 ) + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns + END IF !-------------------------------------------------------------------------------------------------------------------------------- diff --git a/modules/orcaflex-interface/src/OrcaDriver_Subs.f90 b/modules/orcaflex-interface/src/OrcaDriver_Subs.f90 index e07b6105c1..39e4f852f3 100644 --- a/modules/orcaflex-interface/src/OrcaDriver_Subs.f90 +++ b/modules/orcaflex-interface/src/OrcaDriver_Subs.f90 @@ -70,6 +70,7 @@ SUBROUTINE DispHelpText( ErrStat, ErrMsg ) CALL WrScr(" comma delimited FILE.") CALL WrScr(" "//SwChar//"v -- increase verbose level to 7 ") CALL WrScr(" "//SwChar//"vv -- increase verbose level to 10 ") + CALL WrScr(" "//SwChar//"CheckInput -- validate the input deck, write a report, and exit") CALL WrScr(" "//SwChar//"help -- print this help menu and exit") CALL WrScr("") CALL WrScr(" Notes:") @@ -308,6 +309,9 @@ SUBROUTINE ParseArg( CLSettings, CLFlags, ThisArgUC, ThisArg, ifwFlagSet, ErrSta ELSEIF ( ThisArgUC(1:1) == "V" ) THEN CLFlags%Verbose = .TRUE. RETURN + ELSEIF ( TRIM(ThisArgUC)== "CHECKINPUT" ) THEN + CLFlags%CheckInput = .TRUE. + RETURN ELSE CALL SetErrStat( ErrID_Warn," Unrecognized option '"//SwChar//TRIM(ThisArg)//"'. Ignoring. Use option "//SwChar//"help for list of options.", & ErrStat,ErrMsg,RoutineName) diff --git a/modules/orcaflex-interface/src/OrcaDriver_Types.f90 b/modules/orcaflex-interface/src/OrcaDriver_Types.f90 index 78bbd7bf9b..fe8ecc45e6 100644 --- a/modules/orcaflex-interface/src/OrcaDriver_Types.f90 +++ b/modules/orcaflex-interface/src/OrcaDriver_Types.f90 @@ -65,6 +65,7 @@ MODULE OrcaDriver_Types LOGICAL :: PointsOutputInit = .FALSE. !< Is the Points output file initialized LOGICAL :: Verbose = .FALSE. !< Verbose error reporting LOGICAL :: VVerbose = .FALSE. !< Very Verbose error reporting + LOGICAL :: CheckInput = .FALSE. !< specified -CheckInput mode on the command line END TYPE OrcaDriver_Flags diff --git a/modules/seastate/src/SeaState_DriverCode.f90 b/modules/seastate/src/SeaState_DriverCode.f90 index f1ae3fb7a0..894196b02e 100644 --- a/modules/seastate/src/SeaState_DriverCode.f90 +++ b/modules/seastate/src/SeaState_DriverCode.f90 @@ -28,7 +28,8 @@ program SeaStateDriver use SeaState_Output use ModMesh_Types use VersionInfo - + use NWTC_CheckInput + implicit none type SeaSt_Drvr_InitInput @@ -110,9 +111,15 @@ program SeaStateDriver type(ProgDesc), parameter :: version = ProgDesc( 'SeaState Driver', '', '' ) ! The version number of this program. + logical :: CheckInputMode ! true if -CheckInput was given on the command line (no initializer -- set below) + type(CheckInputCollectorType) :: Checker ! -CheckInput result collector + character(64) :: CkStage ! name of the -CheckInput stage/component currently executing (no initializer -- set below) + ! Variables Init Time = -99999 - + CheckInputMode = .FALSE. + CkStage = 'Driver' ! default stage label; overridden before each named stage below + !............................................................................................................................... ! Routines called in initialization !............................................................................................................................... @@ -139,7 +146,11 @@ program SeaStateDriver drvrFilename = '' call CheckArgs( drvrFilename, Flag=FlagArg ) - if ( LEN( TRIM(FlagArg) ) > 0 ) call NormStop() + IF ( TRIM(FlagArg) == 'CHECKINPUT' ) THEN + CheckInputMode = .TRUE. + ELSE IF ( LEN( TRIM(FlagArg) ) > 0 ) THEN + call NormStop() ! -h/-v were already handled inside CheckArgs + END IF ! Display the copyright notice call DispCopyrightLicense( version%Name ) @@ -147,11 +158,18 @@ program SeaStateDriver ! Parse the driver input file and run the simulation based on that file + CkStage = 'Driver' call ReadDriverInputFile( drvrFilename, drvrInitInp, ErrStat, ErrMsg ) if (errStat >= AbortErrLev) then ! Clean up and exit call SeaSt_DvrCleanup() end if + + IF ( CheckInputMode ) THEN + CALL CkIn_OpenReport( Checker, TRIM(drvrInitInp%OutRootName)//'.driver', ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) + END IF + InitInData%Gravity = drvrInitInp%Gravity InitInData%defWtrDens = drvrInitInp%WtrDens InitInData%defWtrDpth = drvrInitInp%WtrDpth @@ -188,6 +206,7 @@ program SeaStateDriver ! Initialize the module Interval = drvrInitInp%TimeInterval + CkStage = 'SeaState' call SeaSt_Init( InitInData, u(1), p, x, xd, z, OtherState, y, m, Interval, InitOutData, ErrStat, ErrMsg ) if (errStat >= AbortErrLev) then ! Clean up and exit @@ -197,13 +216,15 @@ program SeaStateDriver if ( Interval /= drvrInitInp%TimeInterval) then call SetErrStat( ErrID_Fatal, 'The SeaState Module attempted to change timestep interval, but this is not allowed. The SeaState Module must use the Driver Interval.', ErrStat, ErrMsg, 'Driver') - call SeaSt_DvrCleanup() + call SeaSt_DvrCleanup() end if + CkStage = 'Driver' ! post-init wave-elevation-output/destroy validation below is attributed back to the Driver stage ! Write the gridded wave elevation data to a file - - if ( drvrInitInp%WaveElevVis ) call WaveElevGrid_Output (drvrInitInp, InitInData, InitOutData, p, ErrStat, ErrMsg) + ! -CheckInput: this genuinely writes a compute artifact (the wave elevation grid file), not just + ! validation, so it is skipped entirely in check mode regardless of the WaveElevVis flag. + if ( drvrInitInp%WaveElevVis .AND. .NOT. CheckInputMode ) call WaveElevGrid_Output (drvrInitInp, InitInData, InitOutData, p, ErrStat, ErrMsg) if (errStat >= AbortErrLev) then ! Clean up and exit call SeaSt_DvrCleanup() @@ -231,7 +252,17 @@ program SeaStateDriver ! loop through time steps - + IF ( CheckInputMode ) THEN + ! Reaching here means every stage above completed without a fatal error (a fatal one would have + ! routed through SeaSt_DvrCleanup's CkIn_DriverFail interception and never returned). Record both + ! stages as passed, in order, then finish -- this call never returns. + CALL CkIn_Collect( Checker, 'Driver', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Driver', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'SeaState', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'SeaState', ErrStat2, ErrMsg2 ) + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns + END IF + do n = 1, drvrInitInp%NSteps Time = (n-1) * drvrInitInp%TimeInterval @@ -276,7 +307,14 @@ subroutine SeaSt_DvrCleanup() errStat2 = ErrID_None errMsg2 = "" - + + ! -CheckInput: SeaSt_DvrCleanup is the single chokepoint every failure reaches, but it is also + ! called unconditionally on the success teardown path (after the time loop) -- so a fatal must be + ! distinguished from a clean exit before intercepting. ErrStat/ErrMsg here are the program-level + ! ones (host-associated; this is a CONTAINS'd subroutine), exactly what every call site above set + ! before calling in. + IF ( CheckInputMode .AND. ErrStat >= AbortErrLev ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns + call SeaSt_DestroyInitInput( InitInData, errStat2, errMsg2 ) call SetErrStat( errStat2, errMsg2, errStat, errMsg, 'SeaSt_DvrCleanup' ) diff --git a/modules/simple-elastodyn/src/driver/SED_Driver.f90 b/modules/simple-elastodyn/src/driver/SED_Driver.f90 index 900c7ea938..8c8da45a3f 100644 --- a/modules/simple-elastodyn/src/driver/SED_Driver.f90 +++ b/modules/simple-elastodyn/src/driver/SED_Driver.f90 @@ -27,6 +27,7 @@ PROGRAM SED_Driver USE SED_Types USE SED_Driver_Subs USE SED_Driver_Types + USE NWTC_CheckInput IMPLICIT NONE @@ -80,6 +81,13 @@ PROGRAM SED_Driver integer(IntKi) :: TmpIdx !< Index of last point accessed by dimension INTEGER(IntKi) :: ErrStat !< Status of error message CHARACTER(ErrMsgLen) :: ErrMsg !< Error message if ErrStat /= ErrID_None + INTEGER(IntKi) :: ErrStat2 !< -CheckInput: temp error status for calls + CHARACTER(ErrMsgLen) :: ErrMsg2 !< -CheckInput: temp error message for calls + + ! -CheckInput support (no initializers on these -- set as early executable statements below) + LOGICAL :: CheckInputMode !< true if -CheckInput was given on the command line + TYPE(CheckInputCollectorType) :: Checker !< -CheckInput result collector + CHARACTER(64) :: CkStage !< name of the -CheckInput stage/component currently executing CHARACTER(200) :: git_commit ! String containing the current git commit hash TYPE(ProgDesc), PARAMETER :: version = ProgDesc( 'SED Driver', '', '' ) ! The version number of this program. @@ -102,6 +110,10 @@ PROGRAM SED_Driver ! Start the timer call CPU_TIME( Timer(1) ) + ! -CheckInput: no initializers on these -- set as early executable statements + CheckInputMode = .FALSE. + CkStage = 'Driver' ! default stage label; overridden before the SED_Init call below + ! Initialize the driver settings to their default values (same as the CL -- command line -- values) call InitSettingsFlags( ProgInfo, CLSettings, CLSettingsFlags ) Settings = CLSettings @@ -111,6 +123,10 @@ PROGRAM SED_Driver call RetrieveArgs( CLSettings, CLSettingsFlags, ErrStat, ErrMsg ) call CheckErr('') + ! -CheckInput is command-line only (mirrors how Verbose/VVerbose are handled below -- not + ! merged into SettingsFlags by UpdateSettingsWithCL, so read it straight off the CL flags). + CheckInputMode = CLSettingsFlags%CheckInput + ! Check if we are doing verbose error reporting IF ( CLSettingsFlags%VVerbose ) SEDDriver_Verbose = 10_IntKi IF ( CLSettingsFlags%Verbose ) SEDDriver_Verbose = 7_IntKi @@ -169,6 +185,17 @@ PROGRAM SED_Driver ELSE + ! -CheckInput: the direct "-sed" input-file mode never populates CaseTime/CaseData (those + ! are only read by ParseDvrIptFile above, in the driver-input-file branch) -- the time-step + ! setup below dereferences them unconditionally and would crash if they were never + ! allocated (mirrors the same pre-existing gap in the AeroDisk driver). Fail cleanly with a + ! clear message under -CheckInput instead of routing into it. + IF ( CheckInputMode ) THEN + CALL CkIn_DriverFail( Checker, 'Driver', ErrID_Fatal, & + 'Simplified-ElastoDyn -CheckInput requires a driver input file (the direct SED-file mode, "'// & + SwChar//'sed", is not supported under -CheckInput).' ) ! never returns + END IF + ! VVerbose error reporting IF ( SEDDriver_Verbose >= 10_IntKi ) CALL WrScr('No driver input file used. Updating driver settings with command line arguments') @@ -180,6 +207,14 @@ PROGRAM SED_Driver CALL UpdateSettingsWithCL( SettingsFlags, Settings, CLSettingsFlags, CLSettings, SettingsFlags%DvrIptFile, ErrStat, ErrMsg ) call CheckErr('') + ! -CheckInput: RootName is known now (pre-Init, driver-input-file mode only -- the direct + ! "-sed" mode already exited above under CheckInputMode). Open the report before SED_Init runs + ! so a fatal from Init itself is caught. + IF ( CheckInputMode ) THEN + CALL CkIn_OpenReport( Checker, TRIM(Settings%OutRootName)//'.driver', ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) + END IF + ! Verbose error reporting IF ( SEDDriver_Verbose >= 10_IntKi ) THEN CALL WrScr(NewLine//'--- Driver settings after copying over CL settings: ---') @@ -240,8 +275,10 @@ PROGRAM SED_Driver InitInData%RootName = Settings%OutRootName ! Initialize the module + CkStage = 'Simplified-ElastoDyn' CALL SED_Init( InitInData, u(1), p, x, xd, z, OtherState, y, misc, TimeInterval, InitOutData, ErrStat, ErrMsg ) call CheckErr('After Init: '); + CkStage = 'Driver' ! remaining post-Init checks below are attributed back to the Driver stage ! Make sure don't use a High-speed brake with RK4 method if (.not. EqualRealNos( maxval(abs(CaseData(2,:))), 0.0_ReKi)) then @@ -258,14 +295,31 @@ PROGRAM SED_Driver enddo ! Set the output file - call GetRoot(Settings%OutRootName,OutputFileRootName) - call Dvr_InitializeOutputFile(DvrOut, InitOutData, OutputFileRootName, ErrStat, ErrMsg) - call CheckErr('Setting output file'); + ! -CheckInput: opening the output file is a real compute artifact -- skip it entirely in check + ! mode so a passing check run leaves no output file behind. + IF ( .NOT. CheckInputMode ) THEN + call GetRoot(Settings%OutRootName,OutputFileRootName) + call Dvr_InitializeOutputFile(DvrOut, InitOutData, OutputFileRootName, ErrStat, ErrMsg) + call CheckErr('Setting output file'); + END IF ! Destroy initialization data CALL SED_DestroyInitInput( InitInData, ErrStat, ErrMsg ) CALL SED_DestroyInitOutput( InitOutData, ErrStat, ErrMsg ) + IF ( CheckInputMode ) THEN + ! Reaching here means every stage above completed without a fatal error (a fatal one would have + ! routed through CheckErr's CkIn_DriverFail interception, or the -sed-mode interception above, + ! and never returned). Record both stages as passed, in order, then finish -- this call never + ! returns, so the VTK-reference-mesh write and time-marching loop below are never reached in + ! check mode. + CALL CkIn_Collect( Checker, 'Driver', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Driver', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'Simplified-ElastoDyn', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Simplified-ElastoDyn', ErrStat2, ErrMsg2 ) + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns + END IF + n = 0 if (Settings%WrVTK > 0_IntKi) then call WrVTK_refMeshes(Settings, p, y, ErrStat,ErrMsg) @@ -361,7 +415,12 @@ subroutine CheckErr(Text) character(*), intent(in) :: Text IF ( ErrStat /= ErrID_None ) THEN ! Check if there was an error and do something about it if necessary CALL WrScr( Text//trim(ErrMsg) ) - if ( ErrStat >= AbortErrLev ) call ProgEnd() + if ( ErrStat >= AbortErrLev ) then + ! -CheckInput: ProgEnd calls SED_End (which can overwrite ErrStat/ErrMsg) before + ! ProgAbort, so the collector call must fire first. + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns + call ProgEnd() + endif END IF end subroutine CheckErr subroutine ProgEnd() diff --git a/modules/simple-elastodyn/src/driver/SED_Driver_Subs.f90 b/modules/simple-elastodyn/src/driver/SED_Driver_Subs.f90 index 554526d1ff..6677002d3b 100644 --- a/modules/simple-elastodyn/src/driver/SED_Driver_Subs.f90 +++ b/modules/simple-elastodyn/src/driver/SED_Driver_Subs.f90 @@ -50,6 +50,7 @@ SUBROUTINE DispHelpText() CALL WrScr(" "//SwChar//"v -- verbose output ") CALL WrScr(" "//SwChar//"vv -- very verbose output ") CALL WrScr(" "//SwChar//"NonLinear -- only return non-linear portion of reaction force") + CALL WrScr(" "//SwChar//"CheckInput -- validate the input deck, write a report, and exit") CALL WrScr(" "//SwChar//"help -- print this help menu and exit") CALL WrScr("") CALL WrScr(" Notes:") @@ -84,6 +85,7 @@ subroutine InitSettingsFlags( ProgInfo, CLSettings, CLFlags ) CLFlags%DTDefault = .FALSE. ! specified 'DEFAULT' for resolution in time CLFlags%Verbose = .FALSE. ! Turn on verbose error reporting? CLFlags%VVerbose = .FALSE. ! Turn on very verbose error reporting? + CLFlags%CheckInput = .FALSE. ! -CheckInput mode requested on the command line end subroutine InitSettingsFlags @@ -286,6 +288,9 @@ SUBROUTINE ParseArg( CLSettings, CLFlags, ThisArgUC, ThisArg, sedFlagSet, ErrSta ELSEIF ( ThisArgUC(1:1) == "V" ) THEN CLFlags%Verbose = .TRUE. RETURN + ELSEIF ( TRIM(ThisArgUC) == "CHECKINPUT" ) THEN + CLFlags%CheckInput = .TRUE. + RETURN ELSE CALL SetErrStat( ErrID_Warn," Unrecognized option '"//SwChar//TRIM(ThisArg)//"'. Ignoring. Use option "//SwChar//"help for list of options.", & ErrStat,ErrMsg,'ParseArg') diff --git a/modules/simple-elastodyn/src/driver/SED_Driver_Types.f90 b/modules/simple-elastodyn/src/driver/SED_Driver_Types.f90 index 65b6ed4167..52ccb5152e 100644 --- a/modules/simple-elastodyn/src/driver/SED_Driver_Types.f90 +++ b/modules/simple-elastodyn/src/driver/SED_Driver_Types.f90 @@ -48,6 +48,7 @@ module SED_Driver_Types logical :: DTDefault = .FALSE. !< specified a 'DEFAULT' for the time resolution logical :: Verbose = .FALSE. !< Verbose error reporting logical :: VVerbose = .FALSE. !< Very Verbose error reporting + logical :: CheckInput = .FALSE. !< specified -CheckInput mode on the command line end type SEDDriver_Flags diff --git a/modules/soildyn/src/driver/SoilDyn_Driver.f90 b/modules/soildyn/src/driver/SoilDyn_Driver.f90 index 586aa1a832..8e01cea794 100644 --- a/modules/soildyn/src/driver/SoilDyn_Driver.f90 +++ b/modules/soildyn/src/driver/SoilDyn_Driver.f90 @@ -27,6 +27,7 @@ PROGRAM SoilDyn_Driver USE SoilDyn_Driver_Subs USE SoilDyn_Driver_Types USE REDWINinterface, only: REDWINinterface_GetStiffMatrix + USE NWTC_CheckInput IMPLICIT NONE @@ -80,6 +81,14 @@ PROGRAM SoilDyn_Driver integer(IntKi) :: TmpIdx(6) !< Index of last point accessed by dimension INTEGER(IntKi) :: ErrStat !< Status of error message CHARACTER(ErrMsgLen) :: ErrMsg !< Error message if ErrStat /= ErrID_None + INTEGER(IntKi) :: ErrStat2 !< -CheckInput: temp error status for calls + CHARACTER(ErrMsgLen) :: ErrMsg2 !< -CheckInput: temp error message for calls + + ! -CheckInput support (no initializers on these -- set as early executable statements below) + LOGICAL :: CheckInputMode !< true if -CheckInput was given on the command line + TYPE(CheckInputCollectorType) :: Checker !< -CheckInput result collector + CHARACTER(64) :: CkStage !< name of the -CheckInput stage/component currently executing + CHARACTER(1024) :: DvrRootName !< -CheckInput: root name derived early (lexically) for the report file CHARACTER(200) :: git_commit ! String containing the current git commit hash TYPE(ProgDesc), PARAMETER :: version = ProgDesc( 'SoilDyn Driver', '', '' ) ! The version number of this program. @@ -102,6 +111,10 @@ PROGRAM SoilDyn_Driver ! Start the timer call CPU_TIME( Timer(1) ) + ! -CheckInput: no initializers on these -- set as early executable statements + CheckInputMode = .FALSE. + CkStage = 'Driver' ! default stage label; overridden before the SlD_Init call below + ! Initialize the driver settings to their default values (same as the CL -- command line -- values) call InitSettingsFlags( ProgInfo, CLSettings, CLSettingsFlags ) Settings = CLSettings @@ -116,6 +129,10 @@ PROGRAM SoilDyn_Driver ErrStat = ErrID_None ENDIF + ! -CheckInput is command-line only (mirrors how Verbose/VVerbose are handled below -- not + ! merged into SettingsFlags by UpdateSettingsWithCL, so read it straight off the CL flags). + CheckInputMode = CLSettingsFlags%CheckInput + ! Check if we are doing verbose error reporting IF ( CLSettingsFlags%VVerbose ) SlDDriver_Verbose = 10_IntKi IF ( CLSettingsFlags%Verbose ) SlDDriver_Verbose = 7_IntKi @@ -177,6 +194,15 @@ PROGRAM SoilDyn_Driver CALL UpdateSettingsWithCL( SettingsFlags, Settings, CLSettingsFlags, CLSettings, SettingsFlags%DvrIptFile, ErrStat, ErrMsg ) call CheckErr('') + ! -CheckInput: RootName is normally only derived post-Init (see the GetRoot call further below, + ! from Settings%SlDIptFileName) -- that name is already known now, so derive it early (a pure + ! lexical operation, safe to do before Init) purely to name the driver-level report. + IF ( CheckInputMode ) THEN + CALL GetRoot( Settings%SlDIptFileName, DvrRootName ) + CALL CkIn_OpenReport( Checker, TRIM(DvrRootName)//'.driver', ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) + END IF + ! Verbose error reporting IF ( SlDDriver_Verbose >= 10_IntKi ) THEN CALL WrScr(NewLine//'--- Driver settings after copying over CL settings: ---') @@ -262,21 +288,44 @@ PROGRAM SoilDyn_Driver InitInData%InputFile = Settings%SldIptFileName ! Initialize the module + ! -CheckInput: the REDWIN DLL (if configured) loads inside SlD_Init -- a missing/broken DLL + ! must surface as a failed 'SoilDyn' component with a completed report, not a crash. This + ! block is an inline duplicate of CheckErr (does not call it), so it needs its own interception. + CkStage = 'SoilDyn' CALL SlD_Init( InitInData, u(1), p, x, xd, z, OtherState, y, misc, TimeInterval, InitOutData, ErrStat, ErrMsg ) IF ( ErrStat /= ErrID_None ) THEN ! Check if there was an error and do something about it if necessary CALL WrScr( 'After Init: '//ErrMsg ) - if ( ErrStat >= AbortErrLev ) call ProgEnd() + if ( ErrStat >= AbortErrLev ) then + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns + call ProgEnd() + endif END IF + CkStage = 'Driver' ! output-file setup below is attributed back to the Driver stage ! Set the output file - call GetRoot(Settings%SlDIptFileName,OutputFileRootName) - call Dvr_InitializeOutputFile(DvrOut, InitOutData, OutputFileRootName, ErrStat, ErrMsg) - call CheckErr('Setting output file'); + ! -CheckInput: opening the output file is a real compute artifact -- skip it entirely in check + ! mode so a passing check run leaves no output file behind. + IF ( .NOT. CheckInputMode ) THEN + call GetRoot(Settings%SlDIptFileName,OutputFileRootName) + call Dvr_InitializeOutputFile(DvrOut, InitOutData, OutputFileRootName, ErrStat, ErrMsg) + call CheckErr('Setting output file'); + END IF ! Destroy initialization data CALL SlD_DestroyInitInput( InitInData, ErrStat, ErrMsg ) CALL SlD_DestroyInitOutput( InitOutData, ErrStat, ErrMsg ) + IF ( CheckInputMode ) THEN + ! Reaching here means every stage above completed without a fatal error (a fatal one would have + ! routed through the SlD_Init interception above or CheckErr's, and never returned). Record both + ! stages as passed, in order, then finish -- this call never returns, so the stiffness-matrix + ! printout and time-marching loop below are never reached in check mode. + CALL CkIn_Collect( Checker, 'Driver', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Driver', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'SoilDyn', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'SoilDyn', ErrStat2, ErrMsg2 ) + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns + END IF ! If requested, get the stiffness matrix if ( SettingsFlags%StiffMatOut .and. p%CalcOption==Calc_REDWIN ) then @@ -359,7 +408,10 @@ subroutine CheckErr(Text) character(*), intent(in) :: Text IF ( ErrStat /= ErrID_None ) THEN ! Check if there was an error and do something about it if necessary CALL WrScr( Text//ErrMsg ) - if ( ErrStat >= AbortErrLev ) call ProgEnd() + if ( ErrStat >= AbortErrLev ) then + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrStat, ErrMsg ) ! never returns + call ProgEnd() + endif END IF end subroutine CheckErr subroutine ProgEnd() diff --git a/modules/soildyn/src/driver/SoilDyn_Driver_Subs.f90 b/modules/soildyn/src/driver/SoilDyn_Driver_Subs.f90 index 25c15d9d1e..08bd6f7068 100644 --- a/modules/soildyn/src/driver/SoilDyn_Driver_Subs.f90 +++ b/modules/soildyn/src/driver/SoilDyn_Driver_Subs.f90 @@ -49,6 +49,7 @@ SUBROUTINE DispHelpText() CALL WrScr(" "//SwChar//"v -- verbose output ") CALL WrScr(" "//SwChar//"vv -- very verbose output ") CALL WrScr(" "//SwChar//"NonLinear -- only return non-linear portion of reaction force") + CALL WrScr(" "//SwChar//"CheckInput -- validate the input deck, write a report, and exit") CALL WrScr(" "//SwChar//"help -- print this help menu and exit") CALL WrScr("") CALL WrScr(" Notes:") @@ -86,6 +87,7 @@ subroutine InitSettingsFlags( ProgInfo, CLSettings, CLFlags ) CLFlags%DTDefault = .FALSE. ! specified 'DEFAULT' for resolution in time CLFlags%Verbose = .FALSE. ! Turn on verbose error reporting? CLFlags%VVerbose = .FALSE. ! Turn on very verbose error reporting? + CLFlags%CheckInput = .FALSE. ! -CheckInput mode requested on the command line end subroutine InitSettingsFlags @@ -286,6 +288,9 @@ SUBROUTINE ParseArg( CLSettings, CLFlags, ThisArgUC, ThisArg, sldFlagSet, ErrSta ELSEIF ( ThisArgUC(1:1) == "V" ) THEN CLFlags%Verbose = .TRUE. RETURN + ELSEIF ( TRIM(ThisArgUC) == "CHECKINPUT" ) THEN + CLFlags%CheckInput = .TRUE. + RETURN ELSE CALL SetErrStat( ErrID_Warn," Unrecognized option '"//SwChar//TRIM(ThisArg)//"'. Ignoring. Use option "//SwChar//"help for list of options.", & ErrStat,ErrMsg,'ParseArg') diff --git a/modules/soildyn/src/driver/SoilDyn_Driver_Types.f90 b/modules/soildyn/src/driver/SoilDyn_Driver_Types.f90 index c49808c140..2e04c8a912 100644 --- a/modules/soildyn/src/driver/SoilDyn_Driver_Types.f90 +++ b/modules/soildyn/src/driver/SoilDyn_Driver_Types.f90 @@ -45,6 +45,7 @@ MODULE SoilDyn_Driver_Types LOGICAL :: DTDefault = .FALSE. !< specified a 'DEFAULT' for the time resolution LOGICAL :: Verbose = .FALSE. !< Verbose error reporting LOGICAL :: VVerbose = .FALSE. !< Very Verbose error reporting + LOGICAL :: CheckInput = .FALSE. !< specified -CheckInput mode on the command line END TYPE SlDDriver_Flags diff --git a/modules/turbsim/src/TurbSim.f90 b/modules/turbsim/src/TurbSim.f90 index 768a43b859..a10cefed35 100644 --- a/modules/turbsim/src/TurbSim.f90 +++ b/modules/turbsim/src/TurbSim.f90 @@ -58,6 +58,7 @@ PROGRAM TurbSim USE TS_Profiles use TS_CohStructures use VersionInfo +USE NWTC_CheckInput IMPLICIT NONE @@ -85,6 +86,12 @@ PROGRAM TurbSim CHARACTER(1024) :: InFile ! Name of the TurbSim input file. CHARACTER(20) :: FlagArg ! flag argument from command line +LOGICAL :: CheckInputMode ! true if -CheckInput was given on the command line (no initializer -- set below) +TYPE(CheckInputCollectorType) :: Checker ! -CheckInput result collector +CHARACTER(64) :: CkStage ! name of the -CheckInput stage/component currently executing (no initializer -- set below) +INTEGER(IntKi) :: ErrStat2 ! secondary error status, used only for -CheckInput report calls +CHARACTER(ErrMsgLen) :: ErrMsg2 ! secondary error message, used only for -CheckInput report calls + !BONNIE:***************************** ! Time = TIMEF() ! Initialize the Wall Clock Time counter @@ -92,13 +99,20 @@ PROGRAM TurbSim p%US = -1 +CheckInputMode = .FALSE. +CkStage = 'Input' ! default stage label; overridden before each named stage below + ! ... Initialize NWTC Library (open console, set pi constants) ... -CALL NWTC_Init( ProgNameIN=TurbSim_Ver%Name, EchoLibVer=.FALSE. ) +CALL NWTC_Init( ProgNameIN=TurbSim_Ver%Name, EchoLibVer=.FALSE. ) ! Check for command line arguments. InFile = 'TurbSim.inp' ! default name for input file CALL CheckArgs( InFile, Flag=FlagArg ) -IF ( LEN( TRIM(FlagArg) ) > 0 ) CALL NormStop() +IF ( TRIM(FlagArg) == 'CHECKINPUT' ) THEN + CheckInputMode = .TRUE. +ELSE IF ( LEN( TRIM(FlagArg) ) > 0 ) THEN + CALL NormStop() ! -h/-v were already handled inside CheckArgs +END IF ! Print out program name, version, and date. @@ -111,6 +125,11 @@ PROGRAM TurbSim CALL GetRoot( InFile, p%RootName ) +IF ( CheckInputMode ) THEN + CALL CkIn_OpenReport( Checker, TRIM(p%RootName), ErrStat2, ErrMsg2 ) + IF (ErrStat2 >= AbortErrLev) CALL WrScr('Warning: could not open -CheckInput report: '//TRIM(ErrMsg2)) +END IF + ! Open input file and summary file. CALL OpenSummaryFile( p%RootName, p%US, p%DescStr, ErrStat, ErrMsg ) @@ -118,13 +137,23 @@ PROGRAM TurbSim ! Get input parameters. +CkStage = 'Input' CALL ReadInputFile(InFile, p, OtherSt_RandNum, ErrStat, ErrMsg) CALL CheckError(ErrStat, ErrMsg) -CALL WrSum_EchoInputs(p) +CALL WrSum_EchoInputs(p) call WrSum_UserInput(p%met,p%usr, p%US) +CkStage = 'Validation' CALL TS_ValidateInput(p, ErrStat, ErrMsg) +IF ( CheckInputMode .AND. ErrStat /= ErrID_None .AND. ErrStat < AbortErrLev ) THEN + ! TS_ValidateInput is an accumulating validator: non-fatal (warning-level) messages returned here + ! are otherwise only printed by CheckError's non-fatal branch and discarded. Collect them now, into + ! this same host-scope ErrStat/ErrMsg, before the fatal case (if any) is handled by CheckError's own + ! CkIn_DriverFail interception below -- that path collects independently, so this branch must skip + ! the fatal case to avoid double-recording the same messages. + CALL CkIn_Collect( Checker, 'Validation', ErrStat, ErrMsg ) +END IF CALL CheckError(ErrStat, ErrMsg) @@ -133,13 +162,15 @@ PROGRAM TurbSim !.................................................................................................................................. ! Define the other parameters for the time series. +CkStage = 'Grid' CALL CreateGrid( p%grid, p%usr, p%UHub, p%WrFile(FileExt_TWR), ErrStat, ErrMsg ) CALL CheckError(ErrStat, ErrMsg) - + !.................................................................................................................................. ! Calculate mean velocity and direction profiles: !.................................................................................................................................. +CkStage = 'Profiles' ! Wind speed: CALL AllocAry(U, SIZE(p%grid%Z), 'u (steady, u-component winds)', ErrStat, ErrMsg ) CALL CheckError(ErrStat, ErrMsg) @@ -179,10 +210,27 @@ PROGRAM TurbSim END IF - + +CkStage = 'Summary' CALL WrSum_SpecModel( p, U, HWindDir, VWindDir, ErrStat, ErrMsg ) CALL CheckError(ErrStat, ErrMsg) +IF ( CheckInputMode ) THEN + ! Reaching here means every stage above completed without a fatal error (a fatal one would have + ! routed through CheckError's CkIn_DriverFail interception and never returned). Record all five + ! stages as passed, in order, then finish -- this call never returns. + CALL CkIn_Collect( Checker, 'Input', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Input', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'Validation', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Validation', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'Grid', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Grid', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'Profiles', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Profiles', ErrStat2, ErrMsg2 ) + CALL CkIn_Collect( Checker, 'Summary', ErrID_None, '' ) + CALL CkIn_ReportComponent( Checker, 'Summary', ErrStat2, ErrMsg2 ) + CALL CkIn_DriverFinish( Checker ) ! summary + close + ProgExit(CkIn_ExitCode) -- never returns +END IF !.................................................................................................................................. ! Get the single-point power spectral densities @@ -364,6 +412,8 @@ SUBROUTINE CheckError(ErrID,Msg) IF (ErrID >= AbortErrLev) THEN + IF ( CheckInputMode ) CALL CkIn_DriverFail( Checker, TRIM(CkStage), ErrID, Msg ) ! never returns + IF (ALLOCATED(PhaseAngles)) DEALLOCATE(PhaseAngles) IF (ALLOCATED(S )) DEALLOCATE(S ) IF (ALLOCATED(V )) DEALLOCATE(V ) diff --git a/modules/version/src/VersionInfo.f90 b/modules/version/src/VersionInfo.f90 index 8f5ba66e10..c0c037f35c 100644 --- a/modules/version/src/VersionInfo.f90 +++ b/modules/version/src/VersionInfo.f90 @@ -277,6 +277,17 @@ SUBROUTINE CheckArgs ( Arg1, ErrStat, Arg2, Flag, InputArgArray ) RETURN END IF + CASE ('CHECKINPUT') + + IF ( SecondArgumentSet .AND. .NOT. FirstArgumentSet ) THEN + Arg1 = Arg2 + END IF + IF ( .NOT. FirstArgumentSet .AND. .NOT. SecondArgumentSet ) THEN + CALL INVALID_SYNTAX( 'the input-check capability requires at least one argument: -CheckInput' ) + CALL CLEANUP() + RETURN + END IF + CASE DEFAULT CALL INVALID_SYNTAX( 'unknown command-line argument given: '//TRIM(FlagIter) ) CALL CLEANUP() diff --git a/modules/version/tests/test_VersionInfo_CheckArgs.F90 b/modules/version/tests/test_VersionInfo_CheckArgs.F90 index 6d709fcc5a..fe2b37d1f1 100644 --- a/modules/version/tests/test_VersionInfo_CheckArgs.F90 +++ b/modules/version/tests/test_VersionInfo_CheckArgs.F90 @@ -25,6 +25,7 @@ subroutine test_VersionInfo_CheckArgs_suite(testsuite) new_unittest("test_help2", test_help2), & new_unittest("test_version1", test_version1), & new_unittest("test_version2", test_version2), & + new_unittest("test_checkinput_flag", test_checkinput_flag), & new_unittest("test_no_args_no_default", test_no_args_no_default), & new_unittest("test_unsupported_flag", test_unsupported_flag), & new_unittest("test_restart_bad_syntax", test_restart_bad_syntax) & @@ -295,6 +296,25 @@ subroutine test_version2(error) deallocate (argument_array) end subroutine +subroutine test_checkinput_flag(error) + type(error_type), allocatable, intent(out) :: error + + character(1024) :: filename, second_argument, flag + integer(IntKi) :: error_status + character(16), dimension(:), allocatable :: argument_array + + filename = "" + allocate (argument_array(2)) + argument_array = ["input.fst ", "-checkinput "] + call hide_terminal_output() + call CheckArgs(filename, error_status, second_argument, flag, argument_array) + call show_terminal_output() + call check(error, "input.fst", filename); if (allocated(error)) return + call check(error, 0, error_status); if (allocated(error)) return + call check(error, "CHECKINPUT", flag); if (allocated(error)) return + deallocate (argument_array) +end subroutine + ! FAILING CASES ! ************************************************************************ diff --git a/reg_tests/CMakeLists.txt b/reg_tests/CMakeLists.txt index 4c99654a21..6695474c08 100644 --- a/reg_tests/CMakeLists.txt +++ b/reg_tests/CMakeLists.txt @@ -80,6 +80,10 @@ set(CTEST_AERODISK_EXECUTABLE "${CMAKE_BINARY_DIR}/modules/aerodisk/aerodisk_dri # Set the SED executable configuration option and default set(CTEST_SED_EXECUTABLE "${CMAKE_BINARY_DIR}/modules/simple-elastodyn/sed_driver" CACHE FILEPATH "Specify the SED driver executable to use in testing.") +# Set the TurbSim executable configuration option and default (used by the -CheckInput tests only; +# TurbSim has no baseline regression tests of its own in this file) +set(CTEST_TURBSIM_EXECUTABLE "${CMAKE_BINARY_DIR}/modules/turbsim/turbsim${CMAKE_EXECUTABLE_SUFFIX}" CACHE FILEPATH "Specify the TurbSim executable to use in testing.") + # Set the testing tolerance set(CTEST_RTEST_RTOL "2" CACHE STRING "Sets the relative orders of magnitude to allow to deviate from the baseline.") diff --git a/reg_tests/CTestList.cmake b/reg_tests/CTestList.cmake index c66a067977..dfa70635d6 100644 --- a/reg_tests/CTestList.cmake +++ b/reg_tests/CTestList.cmake @@ -93,6 +93,71 @@ function(of_regression TESTNAME LABEL) regression(${TEST_SCRIPT} ${OPENFAST_EXECUTABLE} ${SOURCE_DIRECTORY} ${BUILD_DIRECTORY} " " ${TESTNAME} "${LABEL}" " ") endfunction(of_regression) +# openfast -CheckInput: runs against a fixture generated at test time (copy of an r-test +# case, optionally corrupted) -- see executeCheckInputTest.py. No baseline comparison. +function(of_checkinput TESTNAME CASE LABEL) + set(TEST_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/executeCheckInputTest.py") + set(EXECUTABLE "${CTEST_OPENFAST_EXECUTABLE}") + set(SOURCE_CASE "${CMAKE_CURRENT_LIST_DIR}/r-test/glue-codes/openfast/${CASE}") + set(BUILD_DIRECTORY "${CTEST_BINARY_DIR}/glue-codes/openfast/${TESTNAME}") + + # Same path hygiene as regression(): normalize to the native separator, then double any + # backslash it introduced (Windows) so it survives CTestTestfile.cmake being re-parsed. + file(TO_NATIVE_PATH "${EXECUTABLE}" EXECUTABLE) + file(TO_NATIVE_PATH "${TEST_SCRIPT}" TEST_SCRIPT) + file(TO_NATIVE_PATH "${SOURCE_CASE}" SOURCE_CASE) + file(TO_NATIVE_PATH "${BUILD_DIRECTORY}" BUILD_DIRECTORY) + + string(REPLACE "\\" "\\\\" EXECUTABLE ${EXECUTABLE}) + string(REPLACE "\\" "\\\\" TEST_SCRIPT ${TEST_SCRIPT}) + string(REPLACE "\\" "\\\\" SOURCE_CASE ${SOURCE_CASE}) + string(REPLACE "\\" "\\\\" BUILD_DIRECTORY ${BUILD_DIRECTORY}) + + add_test(${TESTNAME} ${Python_EXECUTABLE} ${TEST_SCRIPT} + ${EXECUTABLE} + ${SOURCE_CASE} + ${BUILD_DIRECTORY} + ${ARGN}) + set_tests_properties(${TESTNAME} PROPERTIES TIMEOUT 900 LABELS "${LABEL}") +endfunction(of_checkinput) + +# -CheckInput for a module driver / FAST.Farm / TurbSim (as opposed to the openfast glue-code +# cases of_checkinput handles): takes the executable and the case's *full* source directory +# explicitly, since these live under reg_tests/r-test/modules// or +# reg_tests/r-test/glue-codes/fast-farm/, not .../glue-codes/openfast/. Also takes +# the case-relative --input deck explicitly (executeCheckInputTest.py's *.fst glob doesn't fit +# module driver decks -- they're named .fst/.fstf/.dvr/.inp/.ipt at the case author's discretion). +# Kept as a separate function from of_checkinput rather than overloading it: the argument shapes +# genuinely differ (explicit SOURCE_DIR + INPUT vs. a CASE name resolved under a fixed openfast +# r-test root), and overloading would make both call sites harder to read for no real reuse win -- +# the two functions share everything else (path-escaping dance, TIMEOUT/LABELS) via the same +# underlying executeCheckInputTest.py script and add_test() shape. +function(driver_checkinput TESTNAME EXECUTABLE SOURCE_DIR INPUT LABEL) + set(TEST_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/executeCheckInputTest.py") + set(BUILD_DIRECTORY "${CTEST_BINARY_DIR}/checkinput/${TESTNAME}") + + # Same path hygiene as of_checkinput()/regression(): normalize to the native separator, then + # double any backslash it introduced (Windows) so it survives CTestTestfile.cmake being + # re-parsed. + file(TO_NATIVE_PATH "${EXECUTABLE}" EXECUTABLE) + file(TO_NATIVE_PATH "${TEST_SCRIPT}" TEST_SCRIPT) + file(TO_NATIVE_PATH "${SOURCE_DIR}" SOURCE_DIR) + file(TO_NATIVE_PATH "${BUILD_DIRECTORY}" BUILD_DIRECTORY) + + string(REPLACE "\\" "\\\\" EXECUTABLE ${EXECUTABLE}) + string(REPLACE "\\" "\\\\" TEST_SCRIPT ${TEST_SCRIPT}) + string(REPLACE "\\" "\\\\" SOURCE_DIR ${SOURCE_DIR}) + string(REPLACE "\\" "\\\\" BUILD_DIRECTORY ${BUILD_DIRECTORY}) + + add_test(${TESTNAME} ${Python_EXECUTABLE} ${TEST_SCRIPT} + ${EXECUTABLE} + ${SOURCE_DIR} + ${BUILD_DIRECTORY} + --input ${INPUT} + ${ARGN}) + set_tests_properties(${TESTNAME} PROPERTIES TIMEOUT 900 LABELS "${LABEL}") +endfunction(driver_checkinput) + function(of_aeromap_regression TESTNAME LABEL) set(TEST_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/executeOpenfastRegressionCase.py") set(OPENFAST_EXECUTABLE "${CTEST_OPENFAST_EXECUTABLE}") @@ -587,3 +652,177 @@ sed_regression("sed_test_freewheel" "simple-elastodyn" # Wavetank library interface (MD + SS + AD) py_wavetank_regression("py_wavetank_test1" "wavetank;aerodyn;moordyn;seastate;python;scaled") + +# openfast -CheckInput: representative decks initialize cleanly (positive), and two +# independent module errors are both reported in a single run (negative). Fixtures are +# generated at test time by executeCheckInputTest.py -- nothing here touches r-test. +# +# NOTE on case selection: the original candidates for the two 5MW positive cases +# (5MW_OC3Mnpl_DLL_WTurb_WavesIrr, 5MW_Land_BD_DLL_WTurb) both drive ServoDyn through the +# Bladed-style DISCON DLL (PCMode=5, VSContrl=5). That DLL is only produced by the +# `regression_test_controllers` custom target (reg_tests/CMakeLists.txt), which is not +# built by the default `openfast` target and was not compiled in this build -- wiring it +# in as a test dependency here would be a much larger change than this task's scope. +# Substituted instead with two controller-free cases (verified via grep for +# PCMode/VSContrl/DLL_FileName in their ServoDyn decks, and by a manual run of +# executeCheckInputTest.py against each): +# - 5MW_Land_BD_Init: CompServo=0 (no ServoDyn at all) -- BeamDyn-flavored, as preferred. +# - AWT_YFix_WSt: PCMode=0, VSContrl=0, DLL_FileName "unused" -- ElastoDyn+AeroDyn+ +# InflowWind+ServoDyn coverage without a DLL. +of_checkinput(checkinput_AOC_WSt AOC_WSt "checkinput;openfast" + --expect-exit 0 --expect-status passed) +of_checkinput(checkinput_5MW_BD 5MW_Land_BD_Init "checkinput;openfast;beamdyn" + --expect-exit 0 --expect-status passed) +of_checkinput(checkinput_AWT AWT_YFix_WSt "checkinput;openfast" + --expect-exit 0 --expect-status passed) +# negative: two independent module errors must BOTH be reported in one run +# +# NOTE on backslash count: add_test()'s generator re-escapes embedded '"' correctly when +# it writes build/reg_tests/CTestTestfile.cmake, but copies embedded '\' through verbatim +# instead of doubling it. That file is parsed again (by CMake escape rules) when ctest +# runs, which then collapses '\\' -> '\' a second time and errors ("Invalid character +# escape") on any leftover lone backslash-letter sequence. So every literal backslash +# that must survive to the Python regex (i.e. anything but the already-single-escaped +# quotes) needs 4 backslashes here, not 2, to still be '\S'/'\s'/'\d'/'\1'/'\2' once +# CTestTestfile.cmake is itself parsed. Verified by inspecting the generated +# CTestTestfile.cmake and confirming `ctest -N -R checkinput` parses cleanly. +of_checkinput(checkinput_multi_error AOC_WSt "checkinput;openfast" + --corrupt "*ElastoDyn*.dat::\"(\\\\S+)\"(\\\\s*BldFile.?1)::\"__missing__.dat\"\\\\2" + --corrupt "*InflowWind*.dat::^\\\\s*\\\\d+(\\\\s*WindType):: 99\\\\1" + --expect-exit 1 --expect-status failed --expect-min-fatals 2 + --expect-component-failed ElastoDyn --expect-component-failed InflowWind) +# negative: Simplified-ElastoDyn (SED) failure path -- guards against the segfault-on-failure gap +# fixed alongside this test (SED's HubPtMotion/NacelleMotion/PlatformPtMesh/BladeRootMotion were read +# downstream, unguarded, by InflowWind/AeroDyn/AeroDisk/ServoDyn). Corrupt NumBl to 0 so SED_Init fails +# in SEDInput_ValidateInput, before any of its output meshes are committed -- exactly the case that used +# to crash. This deck's ServoDyn uses a Bladed-style DLL controller (Windows .dll) that may also fail to +# load on macOS/Linux; that is expected and does not affect the assertions below -- what matters is that +# SED's own failure is attributed and the process does not crash (an overall_status line proves liveness). +of_checkinput(checkinput_SED_error 5MW_Land_DLL_WTurb_SED "checkinput;openfast;sed" + --corrupt "*Simplified-ElastoDyn*.dat::^(\\\\s*)\\\\d+(\\\\s*NumBl)::\\\\g<1>0\\\\g<2>" + --expect-exit 1 --expect-status failed --expect-min-fatals 1 + --expect-component-failed Simplified-ElastoDyn) + +# negative: 5MW_Land_AeroMap, uncorrupted (fails as-shipped -- this deck dir has no ServoDyn input +# file of its own). Regression case for the FAST_InitializeAll segfault found by a 173-case corpus +# sweep: ServoDyn's Init fails here (missing NRELOffshrBsline5MW_Onshore_ServoDyn.dat), and +# FAST_InitializeAll's -CheckInput collect-and-continue then fell through to the "Initialize +# external inputs for first step" block after FAST_InitOutput, unconditionally indexing +# SrvD%Input(INPUT_CURR,1)%ExternalBlPitchCom/ExternalBlAirfoilCom -- allocatable arrays that a +# failed SrvD_Init never allocates -- and segfaulted. Fixed by gating that block on the array +# actually being allocated. +of_checkinput(checkinput_aeromap_srvd_missing 5MW_Land_AeroMap "checkinput;openfast" + --expect-exit 1 --expect-status failed + --expect-component-failed ServoDyn) + +# negative: 5MW_OC3Mnpl_Sld_REDWIN, uncorrupted (fails as-shipped): ElastoDyn fails on a bad +# numeric input (PtfmXZIner), SeaState's input file is missing, SoilDyn's REDWIN DLL cannot be +# loaded on this platform, and HydroDyn then fails for lack of SeaState data -- a multi-module +# failure cascade. Regression case for the second FAST_InitializeAll segfault found by the same +# corpus sweep: when SlD_Init's REDWIN setup fails, it leaves Init%OutData_SlD%WriteOutputHdr +# allocated but WriteOutputUnt not (SoilDyn.f90's own bug -- a stale Fatal ErrStat trips the next, +# otherwise-successful AllocAry's "if (Failed()) return" before WriteOutputUnt is allocated). +# FAST_InitOutput derived y_FAST%numOuts(Module_SlD) from WriteOutputHdr alone and then indexed +# WriteOutputUnt(i) too, segfaulting on the unallocated array. Fixed by requiring both arrays +# allocated before trusting SoilDyn has any outputs. +of_checkinput(checkinput_redwin_cascade 5MW_OC3Mnpl_Sld_REDWIN "checkinput;openfast;soildyn" + --expect-exit 1 --expect-status failed + --expect-component-failed ElastoDyn) + +# openfast -CheckInput, round 2: FAST.Farm, TurbSim, and module drivers (Plan 2, Task 9). Same +# no-baseline-comparison contract as above, via driver_checkinput() (see its definition for why +# it's a separate function from of_checkinput). Each case/corruption below was run by hand with +# executeCheckInputTest.py against the built executable before being wired in here (per this +# task's brief) to confirm the regex actually matches the copied file and the expected component +# fails -- not just eyeballed against the source case. +# +# Executables NOT covered by CTest here (smoke-tested only, in their own Plan-2 task reports -- +# .superpowers/sdd/plan2-task-{4,5,7}-report.md): aeroacoustics_driver, seastate_driver, +# hydrodyn_driver, aerodisk_driver, sed_driver, soildyn_driver, orca_driver, unsteadyaero_driver. +# No CTest r-test case exists for aeroacoustics_driver at all; the others either have no r-test +# case (soildyn, orca -- hand-built decks were used for their smokes) or were left for a future +# pass to keep this task's diff bounded to the brief's explicit minimum set. Logged here per the +# brief ("no silent caps") -- see this commit's body for the same list. + +# TurbSim: case has no ../ references, so copy_case_with_siblings() does a plain copy. +driver_checkinput(checkinput_turbsim "${CTEST_TURBSIM_EXECUTABLE}" + "${CMAKE_CURRENT_LIST_DIR}/r-test/glue-codes/openfast/SWRT/Wind" "35m_16mps.inp" + "checkinput;turbsim" + --expect-exit 0 --expect-status passed) +driver_checkinput(checkinput_turbsim_bad "${CTEST_TURBSIM_EXECUTABLE}" + "${CMAKE_CURRENT_LIST_DIR}/r-test/glue-codes/openfast/SWRT/Wind" "35m_16mps.inp" + "checkinput;turbsim" + --corrupt "*.inp::^(\\\\s*)6(\\\\s*NumGrid_Z)::\\\\g<1>-5\\\\g<2>" + --expect-exit 1 --expect-status failed --expect-min-fatals 1 + --expect-component-failed Input) + +# FAST.Farm: guarded by BUILD_FASTFARM exactly like the ff_regression() registrations above. +# MD_Shared chosen over the brief's suggested TSinflow/AMReX (see plan2-task-3-report.md): those +# use a Bladed-style DISCON DLL not buildable/available on this machine for the positive case; +# MD_Shared is DLL-free (CompServo=0) and also exercises SharedMooring + WakeAddedTurbulence=NOT +# USED in one case. Negative corrupts turbine 2's ElastoDyn NumBl (3->0) to prove per-turbine +# attribution ("Turbines: FAILED", not a blanket farm-level failure). +if(BUILD_FASTFARM) + driver_checkinput(checkinput_fastfarm "${CTEST_FASTFARM_EXECUTABLE}" + "${CMAKE_CURRENT_LIST_DIR}/r-test/glue-codes/fast-farm/MD_Shared" "FAST.Farm.fstf" + "checkinput;fastfarm" + --expect-exit 0 --expect-status passed) + driver_checkinput(checkinput_fastfarm_turbine_error "${CTEST_FASTFARM_EXECUTABLE}" + "${CMAKE_CURRENT_LIST_DIR}/r-test/glue-codes/fast-farm/MD_Shared" "FAST.Farm.fstf" + "checkinput;fastfarm" + --corrupt "*ElastoDynT2*.dat::^(\\\\s*)3(\\\\s*NumBl)::\\\\g<1>0\\\\g<2>" + --expect-exit 1 --expect-status failed --expect-min-fatals 1 + --expect-component-failed Turbines) +endif() + +# aerodyn_driver: single-case deck (ad_MHK_RM1_Fixed); negative points the first AFNames entry at +# a nonexistent airfoil file. +driver_checkinput(checkinput_addriver "${CTEST_AERODYN_EXECUTABLE}" + "${CMAKE_CURRENT_LIST_DIR}/r-test/modules/aerodyn/ad_MHK_RM1_Fixed" "ad_driver.dvr" + "checkinput;aerodyn" + --expect-exit 0 --expect-status passed) +driver_checkinput(checkinput_addriver_bad "${CTEST_AERODYN_EXECUTABLE}" + "${CMAKE_CURRENT_LIST_DIR}/r-test/modules/aerodyn/ad_MHK_RM1_Fixed" "ad_driver.dvr" + "checkinput;aerodyn" + --corrupt "MHK_RM1_Fixed_AeroDyn.dat::Airfoils/NACA6_1000::Airfoils/MISSING_AIRFOIL" + --expect-exit 1 --expect-status failed --expect-min-fatals 1 + --expect-component-failed Case) + +# moordyn_driver: md_waterkin2 exercises SeaState-coupled water kinematics; negative corrupts +# line 1's LineType so MD_Init can't match it to a defined line type. +driver_checkinput(checkinput_moordyn "${CTEST_MOORDYN_EXECUTABLE}" + "${CMAKE_CURRENT_LIST_DIR}/r-test/modules/moordyn/md_waterkin2" "md_driver.inp" + "checkinput;moordyn" + --expect-exit 0 --expect-status passed) +driver_checkinput(checkinput_moordyn_bad "${CTEST_MOORDYN_EXECUTABLE}" + "${CMAKE_CURRENT_LIST_DIR}/r-test/modules/moordyn/md_waterkin2" "md_driver.inp" + "checkinput;moordyn" + --corrupt "moordyn.dat::^main(\\\\s)::GARBAGE_LINE_TYPE\\\\g<1>" + --expect-exit 1 --expect-status failed --expect-min-fatals 1 + --expect-component-failed MoorDyn) + +# beamdyn_driver: bd_static_cantilever_beam; negative points BldFile at a nonexistent blade +# properties file. +driver_checkinput(checkinput_beamdyn_driver "${CTEST_BEAMDYN_EXECUTABLE}" + "${CMAKE_CURRENT_LIST_DIR}/r-test/modules/beamdyn/bd_static_cantilever_beam" "bd_driver.inp" + "checkinput;beamdyn" + --expect-exit 0 --expect-status passed) +driver_checkinput(checkinput_beamdyn_driver_bad "${CTEST_BEAMDYN_EXECUTABLE}" + "${CMAKE_CURRENT_LIST_DIR}/r-test/modules/beamdyn/bd_static_cantilever_beam" "bd_driver.inp" + "checkinput;beamdyn" + --corrupt "bd_primary.inp::beam_props\\\\.inp::missing_beam_props.inp" + --expect-exit 1 --expect-status failed --expect-min-fatals 1 + --expect-component-failed BeamDyn) + +# inflowwind_driver: ifw_uniform (WindType=2); negative points the uniform wind file at a +# nonexistent path. +driver_checkinput(checkinput_inflowwind "${CTEST_INFLOWWIND_EXECUTABLE}" + "${CMAKE_CURRENT_LIST_DIR}/r-test/modules/inflowwind/ifw_uniform" "ifw_driver.inp" + "checkinput;inflowwind" + --expect-exit 0 --expect-status passed) +driver_checkinput(checkinput_inflowwind_bad "${CTEST_INFLOWWIND_EXECUTABLE}" + "${CMAKE_CURRENT_LIST_DIR}/r-test/modules/inflowwind/ifw_uniform" "ifw_driver.inp" + "checkinput;inflowwind" + --corrupt "ifw_primary.inp::uniform\\\\.hh::missing_uniform.hh" + --expect-exit 1 --expect-status failed --expect-min-fatals 1 + --expect-component-failed InflowWind) diff --git a/reg_tests/executeCheckInputTest.py b/reg_tests/executeCheckInputTest.py new file mode 100644 index 0000000000..ebdc3a0c34 --- /dev/null +++ b/reg_tests/executeCheckInputTest.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Run ` -CheckInput ` on a copy of an r-test case (optionally corrupted first) +and assert exit code + .verify.yaml contents. No baseline comparison needed. + +Fixture handling: r-test glue-codes/openfast cases are not self-contained -- they +reference sibling case directories by relative path (e.g. AOC_WSt's ElastoDyn deck +points at "../AOC/AOC_Blade.dat"; the 5MW cases point at "../5MW_Baseline/..."). +Copying only the case directory therefore breaks every module that follows such a +reference. After copying the case, this script scans the copied input files for +"..//" references and copies each referenced sibling directory straight from +the pristine r-test tree, placing it as a sibling of the copied case directory -- +the same relative layout r-test itself has -- so the relative paths resolve exactly +as they do "at home". Nothing is ever written back into the r-test submodule. +""" +import argparse +import re +import shutil +import subprocess +import sys +from pathlib import Path + +# Matches a relative reference to a sibling case directory, e.g. "../AOC/AOC_Blade.dat" +# or "../5MW_Baseline/ServoData/DISCON.dll" -> captures "AOC" / "5MW_Baseline". +SIBLING_RE = re.compile(r'\.\./([A-Za-z0-9_.\-]+)/') +# Extensions worth scanning for sibling-directory references. +SIBLING_SCAN_GLOBS = ("*.fst", "*.dat", "*.inp", "*.txt") + + +def copy_case_with_siblings(src: Path, container: Path) -> Path: + """Copy the r-test case dir `src` into container/case, then discover and copy + every sibling case directory it references (via '..//' relative paths) + from src's parent into container/ -- i.e. as a sibling of container/case, + matching the layout the case expects in r-test itself. Returns the case dir.""" + work = container / "case" + shutil.copytree(src, work) + + siblings = set() + for pattern in SIBLING_SCAN_GLOBS: + for f in work.rglob(pattern): + try: + text = f.read_text(errors="replace") + except OSError: + continue + siblings.update(SIBLING_RE.findall(text)) + + for name in sorted(siblings): + sdir = src.parent / name + dst = container / name + if sdir.is_dir() and not dst.exists(): + shutil.copytree(sdir, dst) + + return work + + +def corrupt(case_dir: Path, spec: str): + """spec: '::::' applied once, first matching file.""" + fileglob, pattern, repl = spec.split("::", 2) + for f in sorted(case_dir.rglob(fileglob)): + text = f.read_text(errors="replace") + new, n = re.subn(pattern, repl, text, count=1, flags=re.M) + if n: + f.write_text(new) + return f + sys.exit(f"corruption spec matched no file: {spec}") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("executable"); ap.add_argument("source_case"); ap.add_argument("build_dir") + ap.add_argument("--corrupt", action="append", default=[]) + ap.add_argument("--input", default=None, + help="relpath (inside the copied case) of the input deck to pass to " + "-CheckInput. Default: the case's own *.fst glob (openfast/glue-code " + "cases). Module driver decks aren't named *.fst (.dvr/.inp/.ipt/...), " + "so those registrations must pass this explicitly.") + ap.add_argument("--report-glob", default="*.verify.yaml", + help="glob (relative to the copied case dir) used to find the written " + "report. Default matches both '.verify.yaml' (glue-code/module " + "roots) and '.driver.verify.yaml' (module drivers), since both " + "end in '.verify.yaml'.") + ap.add_argument("--exe-args", action="append", default=[], + help="extra argument(s) to pass to the executable before -CheckInput/input, " + "e.g. a driver flag some module needs unconditionally.") + ap.add_argument("--expect-exit", type=int, required=True) + ap.add_argument("--expect-status", choices=["passed", "failed"], required=True) + ap.add_argument("--expect-min-fatals", type=int, default=0) + ap.add_argument("--expect-component-failed", action="append", default=[], + help="component name that must have status: failed") + a = ap.parse_args() + + src = Path(a.source_case); container = Path(a.build_dir) + if container.exists(): shutil.rmtree(container) + container.mkdir(parents=True) + work = copy_case_with_siblings(src, container) + for spec in a.corrupt: corrupt(work, spec) + + if a.input: + deck = work / a.input + if not deck.exists(): + sys.exit(f"--input {a.input!r} does not exist under copied case {work}") + else: + deck = next(work.glob("*.fst")) + r = subprocess.run([a.executable, *a.exe_args, "-CheckInput", deck.name], cwd=work, + capture_output=True, text=True, timeout=600) + print(r.stdout[-4000:]); print(r.stderr[-2000:], file=sys.stderr) + + ok = True + if r.returncode != a.expect_exit: + print(f"FAIL: exit {r.returncode} != {a.expect_exit}"); ok = False + if "INPUT CHECK" not in r.stdout: + print("FAIL: stdout has no INPUT CHECK summary"); ok = False + + reports = list(work.glob(a.report_glob)) + if not reports: + print("FAIL: no report matching --report-glob written"); sys.exit(1) + y = reports[0].read_text() + + m = re.search(r"^overall_status:\s*(\w+)", y, re.M) + if not m: + print("FAIL: no trailing overall_status block (crash-liveness contract)"); ok = False + elif m.group(1) != a.expect_status: + print(f"FAIL: overall_status {m.group(1)} != {a.expect_status}"); ok = False + + nfatal = len(re.findall(r"severity: (?:fatal|error)", y)) + if nfatal < a.expect_min_fatals: + print(f"FAIL: {nfatal} fatal/error messages < {a.expect_min_fatals}"); ok = False + + for comp in a.expect_component_failed: + block = re.search(rf"- name: {re.escape(comp)}\n(?: .*\n)*? status: (\w+)", y) + if not block or block.group(1) != "failed": + got = block.group(1) if block else "absent" + print(f"FAIL: component {comp} status {got} != failed"); ok = False + + # console/file parity: every yaml message must appear on stdout. + # The console writer hard-wraps long messages (sometimes mid-word), so compare + # against a whitespace-normalized stdout, but keep summary-entry boundaries: + # entries start with a "[error]/[warn]/[info]" tag, so join wrapped lines onto + # their preceding tagged line before comparing. + entries = [] + for line in r.stdout.splitlines(): + s = line.strip() + if re.match(r"\[(error|warn|info)\]", s) or not entries: + entries.append(s) + else: + entries[-1] += " " + s + def squash(t): return re.sub(r"\s+", "", t) + squashed_entries = [squash(e) for e in entries] + for raw_text in re.findall(r'text: "(.*)"', y): + # Messages containing a literal '"' come back from the YAML text field with it + # backslash-escaped (\"); the console writer prints the raw, unescaped character. + # Unescape the *full* message before truncating to [:60] -- unescaping only the + # truncated slice is unsafe, since the slice boundary can fall between the + # backslash and the quote it's escaping, leaving an unmatched lone backslash. + text = raw_text.replace('\\"', '"') + if text and not any(squash(text[:60]) in e for e in squashed_entries): + print(f"FAIL: yaml message missing from stdout summary: {text[:60]}"); ok = False + + sys.exit(0 if ok else 1) + +if __name__ == "__main__": + main() diff --git a/unit_tests/CMakeLists.txt b/unit_tests/CMakeLists.txt index c441a90a20..fa5df79b76 100644 --- a/unit_tests/CMakeLists.txt +++ b/unit_tests/CMakeLists.txt @@ -68,6 +68,7 @@ add_executable(nwtc_library_utest ${PROJECT_SOURCE_DIR}/modules/nwtc-library/tests/test_NWTC_IO_FileInfo.F90 ${PROJECT_SOURCE_DIR}/modules/nwtc-library/tests/test_NWTC_RandomNumber.F90 ${PROJECT_SOURCE_DIR}/modules/nwtc-library/tests/test_NWTC_C_Binding.F90 + ${PROJECT_SOURCE_DIR}/modules/nwtc-library/tests/test_NWTC_CheckInput.F90 ) target_link_libraries(nwtc_library_utest nwtclibs testdrivelib)