diff --git a/app/cabledyn.f90 b/app/cabledyn.f90 index 841b9cc..750a3fc 100644 --- a/app/cabledyn.f90 +++ b/app/cabledyn.f90 @@ -22,7 +22,7 @@ PROGRAM cabledyn USE CableDyn_Conventions, ONLY: CD_CHANNEL_FMT USE CableDyn_DeckDriver, ONLY: CD_Run_Deck_Driver, CD_Deck_Query_dtM, & CD_Classify_Dynamic_Completion, CD_DECKDRV_OK, & - CD_DECKDRV_BADINPUT, CD_DECKDRV_SOLVEFAIL, CD_DECK_PATHLEN + CD_DECKDRV_BADINPUT, CD_DECKDRV_SOLVEFAIL, CD_DECK_PATHLEN, CD_INIT_NOTE_LEN USE CableDyn_PathIO, ONLY: CD_Get_Argument, CD_Native_Path, CD_Lock_Path, CD_PATH_OK, & CD_LOCK_ACQUIRED, CD_LOCK_HELD, CD_Open_Read, CD_Open_Write USE CableDyn_OpenFAST_Aggregate, ONLY: CD_AGG_ModuleType, CD_AGG_Init_From_Deck, & @@ -777,7 +777,7 @@ SUBROUTINE print_mixed_initialization(aggregate, stat, msg) INTEGER :: n_lines, n_points, n_sections, n_ei0, n_finite, i, line_id, es REAL(wp) :: tension, force(3), inclination, declination, azimuth CHARACTER(4096) :: em - CHARACTER(2048) :: note + CHARACTER(CD_INIT_NOTE_LEN) :: note CALL CD_AGG_GetInitMetadata(aggregate, n_lines, n_points, n_sections, n_ei0, n_finite, es, em) IF (es /= CD_AGG_OK) THEN diff --git a/doc/openfast.rst b/doc/openfast.rst index e22d65a..7292de4 100644 --- a/doc/openfast.rst +++ b/doc/openfast.rst @@ -278,7 +278,7 @@ channels ``AnchFz``, ``AnchFH`` and ``TDPGrd``) and units are in :doc:` * - ``.CD.Line.range.out`` - CableDyn-owned, for a line with the ``LINES`` ``Outputs`` flag ``r``: the range graph, the minimum, maximum and mean at every node over the committed steps from ``RangeStart``, - written when the run reaches ``TMax`` (:doc:`outputs`) + written at the end of a run that completes (:doc:`outputs`) * - ``.CD.rst.dat`` - temporary CableDyn-owned copy of the deck, written in the ``MooringFile`` folder only when a run restarts from a checkpoint (see *Lifecycle features*), and removed after use diff --git a/doc/outputs.rst b/doc/outputs.rst index 758d435..ee1b2fb 100644 --- a/doc/outputs.rst +++ b/doc/outputs.rst @@ -389,10 +389,12 @@ samples are the rows of ``.CD.out``: the state at ``t = 0`` and every step, once each whatever ``DT``, ``DT_Out`` and the correction iterations are; a step OpenFAST corrects counts once, with its corrected state. The accumulators are double precision on every OpenFAST build and travel in the checkpoint, so a restart continues them. The file is written at -the end of a run that reaches ``TMax``; the run removes one left by an earlier run when it starts, -and one that stops before ``TMax`` says so and leaves none. An OpenFAST run stopped early on -purpose (``CalcSteady`` once steady) counts as stopping early. A FAST.Farm shared mooring and the -C API write no range file and reject the flag by name. +the end of a run that completes, whenever the host ends it: at ``TMax``, at a steady state found +by ``CalcSteady``, or at FAST.Farm's last whole ``DT_Low``, and holds the samples up to there. +The run removes a file left by an earlier run when it starts; a run that stops on an error, in any +module, says so and leaves none. A turbine's own CableDyn instance in FAST.Farm (``CompMooring = 5`` +in its ``.fst``) writes ``.T.CD.Line.range.out``. A FAST.Farm shared mooring and +the C API write no range file and reject the flag by name. Layout: title line, header row, **units row**, then one row per node, End A first. The title reads ``CableDyn range graph (line ; samples from t = s to t = s; public node order @@ -1022,9 +1024,9 @@ output root: The first data row is the static initialisation at ``t = 0``; every further row is written after each committed CableDyn step, at every ``dtM`` boundary. It is independent of OpenFAST's ``DT_Out`` and never repeats held values between -CableDyn steps. If OpenFAST corrects a step (predictor–corrector iterations), the provisional row -is replaced rather than duplicated; the file is flushed after every row, so it is readable during -a run and intact after a crash. +CableDyn steps. If OpenFAST retries a step, at any ``dtM``, the provisional row is replaced rather +than duplicated, and a restore of an operating point (``CalcSteady``) keeps the rows of the run; +the file is flushed after every row, so it is readable during a run and intact after a crash. Select CableDyn channels in the ``OUTPUTS`` section of the CableDyn ``MooringFile``; they are not members of the top-level ``.fst`` ``OutList``. The ``TDP`` channels are evaluated from the diff --git a/integration/openfast/patches/0014-fast-subs.patch b/integration/openfast/patches/0014-fast-subs.patch index 21027d3..972f52a 100644 --- a/integration/openfast/patches/0014-fast-subs.patch +++ b/integration/openfast/patches/0014-fast-subs.patch @@ -1,5 +1,5 @@ diff --git a/modules/openfast-library/src/FAST_Subs.f90 b/modules/openfast-library/src/FAST_Subs.f90 -index 48d3be5ed..3284a8c5d 100644 +index 48d3be5ed..2ad5a40fc 100644 --- a/modules/openfast-library/src/FAST_Subs.f90 +++ b/modules/openfast-library/src/FAST_Subs.f90 @@ -40,6 +40,7 @@ MODULE FAST_Subs @@ -540,7 +540,18 @@ index 48d3be5ed..3284a8c5d 100644 end if ! If we are doing AeroMaps, there is leftover data in AD15 parameters -@@ -7621,7 +7752,7 @@ contains +@@ -6900,6 +7031,10 @@ SUBROUTINE ExitThisProgram_T( Turbine, ErrLevel_in, StopTheProgram, ErrLocMsg, S + if (associated(Turbine%AD%p%FlowField)) deallocate(Turbine%AD%p%FlowField) + endif + ++ ! CableDyn writes its whole-run outputs only after a run that ended without an error. FAST.Farm ++ ! ends its turbines with no error level either way and sets this itself after its last step. ++ if (ErrorLevel < AbortErrLev .and. .not. Turbine%p_FAST%FarmIntegration) Turbine%CD%m%HostComplete = .true. ++ + ! End all modules + if (allocated(Turbine%m_Glue%ModData)) then + CALL FAST_ModEnd(Turbine%m_Glue%ModData, Turbine, ErrStat, ErrMsg) +@@ -7621,7 +7756,7 @@ contains ! Write VTK output call WriteVTK(TimeVTK, T%p_FAST, T%y_FAST, & T%ED, T%SED, T%BD, T%AD, T%IfW, T%ExtInfw, T%SeaSt, T%HD, T%SD, & @@ -549,4 +560,3 @@ index 48d3be5ed..3284a8c5d 100644 end subroutine - diff --git a/integration/openfast/patches/0023-fast-farm-end.patch b/integration/openfast/patches/0023-fast-farm-end.patch new file mode 100644 index 0000000..5348ac1 --- /dev/null +++ b/integration/openfast/patches/0023-fast-farm-end.patch @@ -0,0 +1,17 @@ +diff --git a/glue-codes/fast-farm/src/FAST_Farm.f90 b/glue-codes/fast-farm/src/FAST_Farm.f90 +index c3d63ed75..c7ecdcedc 100644 +--- a/glue-codes/fast-farm/src/FAST_Farm.f90 ++++ b/glue-codes/fast-farm/src/FAST_Farm.f90 +@@ -160,6 +160,12 @@ type(All_FastFarm_Data) :: farm + ! End: + !............................................................................................................................... + ++ ! the run is complete: a turbine's CableDyn instance writes its whole-run outputs (an abort, in ++ ! CheckError, ends the turbines without this) ++ do i_turb = 1, farm%p%NumTurbines ++ farm%FWrap(i_turb)%m%Turbine%CD%m%HostComplete = .true. ++ end do ++ + call FARM_End(farm, ErrStat, ErrMsg) + + ! Finalize AMReX library diff --git a/src/CableDyn_DeckDriver.f90 b/src/CableDyn_DeckDriver.f90 index 1f6f783..bf1bd50 100644 --- a/src/CableDyn_DeckDriver.f90 +++ b/src/CableDyn_DeckDriver.f90 @@ -204,7 +204,7 @@ MODULE CableDyn_DeckDriver PUBLIC :: CD_Deck_Query_dtM PUBLIC :: CD_End_Deck_Aggregate PUBLIC :: CD_DeckAggregateType - PUBLIC :: CD_LineInitSummary + PUBLIC :: CD_LineInitSummary, CD_INIT_NOTE_LEN ! Coupled Rigid6 runtime: the aggregate facade owns a CD_Rigid6RuntimeType and marches ! it over the EI=0 system each mooring step (opaque; RigidBodyState/DeckOptions stay private). PUBLIC :: CD_Rigid6RuntimeType @@ -1133,6 +1133,12 @@ MODULE CableDyn_DeckDriver TYPE(HydroUpdateWorkspace) :: work END TYPE + ! Length of the initialisation notes of one line, and of a caller's buffer for them + ! (CD_LineInitSummary%note). A line reports each of its notes at most once per end, fewer than + ! 32 in all, and none is longer than 1024 characters, so every note fits and the omission marker + ! of append_note_line is never reached. + INTEGER, PARAMETER :: CD_INIT_NOTE_LEN = 32*1024 + TYPE :: CD_LineInitSummary !! Engineering quantities at the converged static initial condition. The !! fairlead force is the force exerted by the line on public End A and the @@ -1148,8 +1154,7 @@ MODULE CableDyn_DeckDriver ! finite-EI line (compression, mesh refinement, accepted residual floor, a run past its ! anchor), the fold note of an EI = 0 line (ei0_fold_note), and the static strain note and ! the dtM advisory (axial_period_note) of any line; empty when there is nothing to report. - ! It holds a whole cable note (1024) and the two others. - CHARACTER(2048) :: note = '' + CHARACTER(CD_INIT_NOTE_LEN) :: note = '' END TYPE CD_LineInitSummary TYPE :: CD_DeckAggregateType @@ -1187,7 +1192,7 @@ MODULE CableDyn_DeckDriver TYPE(CD_SystemType) :: sys TYPE(CD_HFMF_ModuleType), ALLOCATABLE :: cables(:) ! Static-initialisation notes of each finite-EI cable (build_one_hermite_cable). - CHARACTER(1024), ALLOCATABLE :: cable_note(:) + CHARACTER(CD_INIT_NOTE_LEN), ALLOCATABLE :: cable_note(:) ! Deck point ids of each cable's End A and End B (Point

F channels). INTEGER, ALLOCATABLE :: cable_points(:, :) REAL(wp) :: gravity = CD_ZERO, rho_water = CD_ZERO diff --git a/src/CableDyn_HermiteCableDynamic.f90 b/src/CableDyn_HermiteCableDynamic.f90 index 6b16ccb..d6551c5 100644 --- a/src/CableDyn_HermiteCableDynamic.f90 +++ b/src/CableDyn_HermiteCableDynamic.f90 @@ -335,6 +335,10 @@ MODULE CableDyn_HermiteCableDynamic REAL(wp) :: snap_fc_t = 0.0_wp, snap_fc_d0(3, 2) = 0.0_wp LOGICAL :: snap_fc_valid = .FALSE. INTEGER :: snap_fc_clears = 0 + ! fc_clears when the first step after the snapshot began: what happens to the cache after + ! that (the setters of an output probe) is no part of the path a restored step must repeat + LOGICAL :: snap_stepped = .FALSE. + INTEGER :: snap_step_clears = 0 ! Generalised-alpha blend: .TRUE. blends the forces (the default), .FALSE. evaluates the ! forces at the blended configuration q_alpha = (1-af) q_{n+1} + af q_n. LOGICAL :: force_blend = CD_HC_FORCE_BLEND_DEFAULT @@ -3874,6 +3878,10 @@ SUBROUTINE dyn_step_core(model, dt, max_iter, tol, ErrStat, ErrMsg, iters_out, r ! lengths the committed state was reached with (active line control: l0_c): reused from ! the previous step's converged evaluation when its key matches the committed state ! exactly, evaluated here otherwise (one residual assembly). + IF (.NOT. model%snap_stepped) THEN + model%snap_stepped = .TRUE. + model%snap_step_clears = model%fc_clears + END IF fc_hit = .FALSE. IF (model%force_blend) fc_hit = force_cache_matches() ! Cross-step factor reuse needs the step to continue bitwise from the state the carried @@ -7946,6 +7954,7 @@ SUBROUTINE CD_HermiteCable_Dyn_Snapshot(model, ErrStat, ErrMsg) ! the cached committed force goes with the state it describes (see fc_clears) model%snap_fc_valid = model%fc_valid .AND. ALLOCATED(model%fc_R) model%snap_fc_clears = model%fc_clears + model%snap_stepped = .FALSE. IF (model%snap_fc_valid) THEN model%snap_fc_R = model%fc_R model%snap_fc_q = model%fc_q @@ -8036,11 +8045,12 @@ SUBROUTINE CD_HermiteCable_Dyn_Restore(model, ErrStat, ErrMsg) model%max_step_rotation = model%snap_max_step_rotation model%recovery_event_count = model%snap_recovery_event_count model%recovery_max_substeps_used = model%snap_recovery_max_substeps_used - ! The snapshot's cached committed force comes back with its state unless a setter has - ! invalidated the cache since the snapshot, so the next step takes the path it took after - ! the snapshot (the cache differs from a fresh evaluation in the last bits where a - ! friction anchor slid). Otherwise the next step evaluates it afresh. - IF (model%snap_fc_valid .AND. model%snap_fc_clears == model%fc_clears) THEN + ! The snapshot's cached committed force comes back with its state unless a setter + ! invalidated the cache between the snapshot and the step that followed it, so the next + ! step takes the path that step took (the cache differs from a fresh evaluation in the last + ! bits where a friction anchor slid). Otherwise the next step evaluates it afresh. + IF (.NOT. model%snap_stepped) model%snap_step_clears = model%fc_clears + IF (model%snap_fc_valid .AND. model%snap_fc_clears == model%snap_step_clears) THEN IF (model%tw_active) model%tw_fcR = model%snap_tw_fcR model%fc_R = model%snap_fc_R model%fc_q = model%snap_fc_q diff --git a/src/openfast/CableDyn_OF.f90 b/src/openfast/CableDyn_OF.f90 index 66d9137..36a03d2 100644 --- a/src/openfast/CableDyn_OF.f90 +++ b/src/openfast/CableDyn_OF.f90 @@ -49,11 +49,11 @@ MODULE CableDyn !! Fixed/Coupled(Vessel) points, EI=0 mooring lines (BA damping + the Morison set), !! finite-EI power cables, penalty seabed, nonzero initial platform displacement !! (PtfmInit), the module's own mooring step (supercycling + zero-order-hold loads), - !! glue correction iterations (NumCrctn > 0: snapshot-rewind re-advance), SeaState + !! glue step retries (snapshot-rewind re-advance at any dtM/DT), SeaState !! wave/current kinematics sampled at the line nodes (single-turbine only; see the !! farm scope at the use_extf guard), range graphs of the LINES flag r (single-turbine - !! only: .Line.range.out, sampled at every committed mooring step, written when - !! the run reaches TMax), checkpoint restart (rebuild from the packed + !! only: .Line.range.out, sampled at every committed mooring step, written at the + !! end of a run the host reports complete), checkpoint restart (rebuild from the packed !! deck + state mirror, accepted on committed mooring-interval boundaries), !! linearization with the line dynamics (as MoorDyn-F: the continuous states are the !! EI=0 lines' interior-node positions and the finite-EI cables' free DOFs, positions @@ -83,7 +83,7 @@ MODULE CableDyn USE CableDyn_Conventions, ONLY: CD_NATIVE_CHANNEL_FMT USE CableDyn_Hydro, ONLY: CD_Current_Profile_Velocity, CD_SeaState_Steady_Current, CD_HYDRO_OK USE CableDyn_DeckDriver, ONLY: CD_Deck_Query_dtM, CD_DECKDRV_OK, CD_Is_Line_Node_Channel, & - CD_LINE_NODE_NUMBERING_NOTE + CD_LINE_NODE_NUMBERING_NOTE, CD_INIT_NOTE_LEN USE CableDyn_OpenFAST_Aggregate, ONLY: CD_AGG_ModuleType, CD_AGG_Init_From_Deck, CD_AGG_UpdateStates_Moving, & CD_AGG_Step_Moving, CD_AGG_CalcOutput, CD_AGG_GetMovingPointMesh, & CD_AGG_Snapshot, CD_AGG_Restore, & @@ -250,6 +250,7 @@ END FUNCTION cd_cpu_guard_armed ! (sample_range); ws_range is the double-precision transfer buffer to the OtherState ! mirror, which a checkpoint packs and a correction iteration's state copy rewinds. LOGICAL :: range_on = .FALSE. + LOGICAL :: range_sampled = .FALSE. ! this instance has sampled (not a checkpoint alone) REAL(wp), ALLOCATABLE :: ws_range(:) ! SeaState ambient-fluid sampling (host-driven wave/current kinematics): when the ! glue's WaveField declares waves or a current, the aggregate is built wave-capable @@ -272,6 +273,13 @@ END FUNCTION cd_cpu_guard_armed ! rebuild_from_checkpoint. INTEGER(IntKi) :: serial = 0 REAL(DbKi) :: t_commit = -1.0_DbKi + ! t_commit_prev: the commit the last advance started from, which a glue retry of that step + ! restores (step_retry). t_run_end: the last time the run has committed, later than + ! t_commit after a restore of an operating point; rewound marks that state, in which the + ! rows and the range samples past t_commit stand until the run takes another step. + REAL(DbKi) :: t_commit_prev = -1.0_DbKi + REAL(DbKi) :: t_run_end = -1.0_DbKi + LOGICAL :: rewound = .FALSE. ! Opt-in end-to-end profiler. CABLEDYN_PROFILE=1 arms these counters and ! the existing detailed Hermite profiler; ordinary production runs pay no ! SYSTEM_CLOCK overhead. Times are accumulated only for successful calls. @@ -436,21 +444,21 @@ SUBROUTINE open_native_outputs( id, root, t_start, ErrStat, ErrMsg, write_profil END SUBROUTINE open_native_outputs !---------------------------------------------------------------------------------------------------------------------------------- - SUBROUTINE restore_native_outputs( id, old_id, root, t_start, restart_serial, dt_moor, nsuper, & - ErrStat, ErrMsg ) - !! Transfer the run-level native history from a superseded in-process solver - !! instance to its checkpoint reconstruction. If the restored state rewinds - !! time, remove the abandoned committed rows before handing over the still-open - !! unit. A fresh-process restart has no live owner and creates a new history. - INTEGER(IntKi), INTENT(IN) :: id, old_id, restart_serial, nsuper + SUBROUTINE restore_native_outputs( id, old_id, root, t_start, restart_serial, ErrStat, ErrMsg ) + !! Transfer the run-level native history from a superseded in-process solver instance to + !! its checkpoint reconstruction: the restore of an operating point (CalcSteady) keeps the + !! rows of the run, and only a later step abandons those past the restored commit + !! (resume_after_rewind). A fresh-process restart has no live owner and creates a new + !! history. Neither route writes the static profile: the state here is the checkpoint's, + !! not the static equilibrium that file records. + INTEGER(IntKi), INTENT(IN) :: id, old_id, restart_serial CHARACTER(*), INTENT(IN) :: root - REAL(DbKi), INTENT(IN) :: t_start, dt_moor + REAL(DbKi), INTENT(IN) :: t_start INTEGER(IntKi), INTENT(OUT) :: ErrStat CHARACTER(*), INTENT(OUT) :: ErrMsg character(*), parameter :: RoutineName = 'CableDyn:restore_native_outputs' - INTEGER(IntKi) :: ntrim, k, ios - REAL(DbKi) :: delta, tol + INTEGER(IntKi) :: ios LOGICAL :: live_owner, same_history ErrStat = ErrID_None @@ -465,14 +473,14 @@ SUBROUTINE restore_native_outputs( id, old_id, root, t_start, restart_serial, dt END IF END IF IF (.NOT. live_owner) THEN - CALL open_native_outputs( id, root, t_start, ErrStat, ErrMsg ) + CALL open_native_outputs( id, root, t_start, ErrStat, ErrMsg, write_profile=.FALSE. ) RETURN END IF ! A fresh-process restart first passes through CD_Init, which creates a ! provisional t=0 stream. The restored generation stamp differs, so release ! that stream and recreate it at the checkpoint. It must not be treated as a - ! same-process rewind because it contains no pre-checkpoint history. + ! same-process restore because it contains no pre-checkpoint history. IF (.NOT. same_history) THEN CLOSE (Inst(old_id)%native_un, IOSTAT=ios) IF (ios /= 0) THEN @@ -485,60 +493,74 @@ SUBROUTINE restore_native_outputs( id, old_id, root, t_start, restart_serial, dt Inst(old_id)%native_wrote_n = -123456789_IntKi Inst(old_id)%native_wrote_t = 0.0_DbKi Inst(old_id)%native_pending = .FALSE. - CALL open_native_outputs( id, root, t_start, ErrStat, ErrMsg ) + CALL open_native_outputs( id, root, t_start, ErrStat, ErrMsg, write_profile=.FALSE. ) RETURN END IF - tol = 1.0E-7_DbKi*MAX(1.0_DbKi, ABS(t_start), ABS(Inst(old_id)%native_wrote_t)) - delta = Inst(old_id)%native_wrote_t - t_start - IF (delta < -tol) THEN - CALL SetErrStat( ErrID_Fatal, 'The live native history ends before the restored committed state.', & - ErrStat, ErrMsg, RoutineName ) - RETURN + Inst(id)%native_un = Inst(old_id)%native_un + Inst(id)%native_root = Inst(old_id)%native_root + Inst(id)%native_wrote_t = Inst(old_id)%native_wrote_t + Inst(id)%native_wrote_n = Inst(old_id)%native_wrote_n + Inst(id)%native_pending = .FALSE. + + Inst(old_id)%native_un = -1_IntKi + Inst(old_id)%native_root = '' + Inst(old_id)%native_wrote_n = -123456789_IntKi + Inst(old_id)%native_wrote_t = 0.0_DbKi + Inst(old_id)%native_pending = .FALSE. + END SUBROUTINE restore_native_outputs + + SUBROUTINE resume_after_rewind( inst_e, other, ErrStat, ErrMsg ) + !! The run takes a step from a restored state that lies before its last commit: the rows + !! and the range samples past the restored commit now belong to an abandoned branch. + TYPE(CD_OF_Instance), INTENT(INOUT) :: inst_e + TYPE(CD_OtherStateType), INTENT(IN ) :: other + INTEGER(IntKi), INTENT( OUT) :: ErrStat + CHARACTER(*), INTENT( OUT) :: ErrMsg + + character(*), parameter :: RoutineName = 'CableDyn:resume_after_rewind' + INTEGER(IntKi) :: ntrim, k, ios + REAL(DbKi) :: delta, tol + + ErrStat = ErrID_None + ErrMsg = '' + inst_e%rewound = .FALSE. + inst_e%t_run_end = inst_e%t_commit + IF (inst_e%range_on) THEN + CALL restore_range_state( inst_e, other, ErrStat, ErrMsg ) + IF (ErrStat >= AbortErrLev) RETURN END IF - ntrim = MAX(0_IntKi, NINT(MAX(delta, 0.0_DbKi)/dt_moor, IntKi)) - IF (ABS(delta - REAL(ntrim, DbKi)*dt_moor) > tol) THEN + IF (inst_e%native_un <= 0_IntKi) RETURN + tol = 1.0E-7_DbKi*MAX(1.0_DbKi, ABS(inst_e%t_commit), ABS(inst_e%native_wrote_t)) + delta = MAX(inst_e%native_wrote_t - inst_e%t_commit, 0.0_DbKi) + ntrim = NINT(delta/REAL(inst_e%dt_moor, DbKi), IntKi) + IF (ABS(delta - REAL(ntrim, DbKi)*REAL(inst_e%dt_moor, DbKi)) > tol) THEN CALL SetErrStat( ErrID_Fatal, 'The native-history rewind is not an integer number of CableDyn steps.', & ErrStat, ErrMsg, RoutineName ) RETURN END IF DO k = 1, ntrim - BACKSPACE (Inst(old_id)%native_un, IOSTAT=ios) + BACKSPACE (inst_e%native_un, IOSTAT=ios) IF (ios /= 0) THEN CALL SetErrStat( ErrID_Fatal, 'Could not remove abandoned native-output record '// & TRIM(Num2LStr(k))//' of '//TRIM(Num2LStr(ntrim))//' while rewinding from t = '// & - TRIM(Num2LStr(Inst(old_id)%native_wrote_t))//' s to t = '// & - TRIM(Num2LStr(t_start))//' s.', & - ErrStat, ErrMsg, RoutineName ) + TRIM(Num2LStr(inst_e%native_wrote_t))//' s to t = '// & + TRIM(Num2LStr(inst_e%t_commit))//' s.', ErrStat, ErrMsg, RoutineName ) RETURN END IF END DO IF (ntrim > 0_IntKi) THEN - ENDFILE (Inst(old_id)%native_un, IOSTAT=ios) - IF (ios == 0) BACKSPACE (Inst(old_id)%native_un, IOSTAT=ios) + ENDFILE (inst_e%native_un, IOSTAT=ios) + IF (ios == 0) BACKSPACE (inst_e%native_un, IOSTAT=ios) IF (ios /= 0) THEN CALL SetErrStat( ErrID_Fatal, 'Could not truncate the abandoned native-output branch.', & ErrStat, ErrMsg, RoutineName ) RETURN END IF END IF - - Inst(id)%native_un = Inst(old_id)%native_un - Inst(id)%native_root = Inst(old_id)%native_root - Inst(id)%native_wrote_t = t_start - IF (Inst(old_id)%native_wrote_n >= 0_IntKi) THEN - Inst(id)%native_wrote_n = MAX(-1_IntKi, Inst(old_id)%native_wrote_n - ntrim*MAX(1_IntKi, nsuper)) - ELSE - Inst(id)%native_wrote_n = Inst(old_id)%native_wrote_n - END IF - Inst(id)%native_pending = .FALSE. - - Inst(old_id)%native_un = -1_IntKi - Inst(old_id)%native_root = '' - Inst(old_id)%native_wrote_n = -123456789_IntKi - Inst(old_id)%native_wrote_t = 0.0_DbKi - Inst(old_id)%native_pending = .FALSE. - END SUBROUTINE restore_native_outputs + inst_e%native_wrote_n = -123456789_IntKi + inst_e%native_wrote_t = inst_e%t_commit + END SUBROUTINE resume_after_rewind !---------------------------------------------------------------------------------------------------------------------------------- SUBROUTINE CD_Init(InitInp, u, p, x, xd, z, other, y, m, DTcoupling, InitOut, ErrStat, ErrMsg) @@ -578,7 +600,8 @@ SUBROUTINE CD_Init(InitInp, u, p, x, xd, z, other, y, m, DTcoupling, InitOut, Er CHARACTER(16) :: host_kin REAL(wp), ALLOCATABLE :: static_cur(:,:) ! the host's steady current for the static start CHARACTER(ChanLen) :: chan_hdr, chan_unt - CHARACTER(1024) :: native_root, init_note + CHARACTER(1024) :: native_root + CHARACTER(CD_INIT_NOTE_LEN) :: init_note INTEGER(IntKi) :: note_at, note_end ErrStat = ErrID_None @@ -1046,6 +1069,7 @@ SUBROUTINE CD_Init(InitInp, u, p, x, xd, z, other, y, m, DTcoupling, InitOut, Er p%dtCoupling = REAL(DTcoupling, DbKi) p%PtfmInit = REAL(ptfm6, ReKi) ! the rebuild reapplies the displaced-start geometry p%Tmax = InitInp%Tmax + p%Linearize = InitInp%Linearize ! DTcoupling itself is deliberately left untouched (SubSteps = 1 in MV_AddModule). ! The module provides no visualization meshes: say so when the glue asks for them ! (WrVTK > 0) instead of leaving the mooring silently absent from the VTK output. @@ -1286,6 +1310,8 @@ SUBROUTINE CD_Init(InitInp, u, p, x, xd, z, other, y, m, DTcoupling, InitOut, Er g_serial = g_serial + 1_IntKi Inst(id)%serial = g_serial Inst(id)%t_commit = 0.0_DbKi ! committed at the static IC (t = 0) + Inst(id)%t_commit_prev = 0.0_DbKi + Inst(id)%t_run_end = 0.0_DbKi other%restart_serial = g_serial other%t_commit = Inst(id)%t_commit other%pred_t = -1.0_DbKi ! no previous commit yet (load predictor) @@ -1345,7 +1371,7 @@ SUBROUTINE CD_Init(InitInp, u, p, x, xd, z, other, y, m, DTcoupling, InitOut, Er IF (ErrStat >= AbortErrLev) RETURN ! Range graphs (LINES flag r): the first sample is the state of the t = 0 output row. The - ! files of an earlier run go now: this run writes its own only when it reaches TMax. + ! files of an earlier run go now: this run writes its own only when it completes. Inst(id)%range_on = CD_AGG_Range_Graphs( Inst(id)%agg ) IF (Inst(id)%range_on) THEN CALL CD_AGG_Range_Discard( Inst(id)%agg, TRIM(native_root), es, em ) @@ -1468,6 +1494,24 @@ LOGICAL FUNCTION instance_matches( m, other ) RESULT(ok) ok = .TRUE. END FUNCTION instance_matches + LOGICAL FUNCTION step_retry( m, other, n ) RESULT(retry) + !! TRUE when the glue runs the mooring step n again: the solver's correction loop copies + !! the states of the step's start back over the predicted ones before each pass, so the + !! OtherState then stands at the commit that step started from, while the instance has + !! advanced from it. The aggregate's step-start snapshot rewinds that exactly, at any + !! dtM/DT; the time of the retried call lies inside the mooring interval, not on a commit. + TYPE(CD_MiscVarType), INTENT(IN) :: m + TYPE(CD_OtherStateType), INTENT(IN) :: other + INTEGER(IntKi), INTENT(IN) :: n + retry = .FALSE. + IF (.NOT. ALLOCATED(Inst)) RETURN + IF (m%CDInst < 1 .OR. m%CDInst > SIZE(Inst)) RETURN + IF (.NOT. Inst(m%CDInst)%active) RETURN + IF (Inst(m%CDInst)%serial /= other%restart_serial) RETURN + IF (n /= Inst(m%CDInst)%last_n .OR. n /= Inst(m%CDInst)%last_solve_n) RETURN + retry = ABS(Inst(m%CDInst)%t_commit_prev - other%t_commit) <= 1.0E-9_DbKi*MAX(1.0_DbKi, ABS(other%t_commit)) + END FUNCTION step_retry + SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) !! CHECKPOINT-RESTART: rebuild the module's internal solver instance from the !! registry-packed data alone -- the deck copy in p, the state mirror in x, the @@ -1489,7 +1533,7 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) character(*), parameter :: RoutineName = 'CableDyn:rebuild_from_checkpoint' CHARACTER(2100) :: rst_deck INTEGER(IntKi) :: id, old_id, stale_id, i, es, ncp, nlines, il, ndofl, nin, k, ic, csz, koff - LOGICAL :: use_extf, profile_requested, preserve_profile + LOGICAL :: use_extf, profile_requested, preserve_profile, same_run REAL(wp), ALLOCATABLE :: static_cur(:,:) ! the host's steady current for the static start LOGICAL :: restored ! held fluid fields restored from the checkpoint CHARACTER(ErrMsgLen) :: em @@ -1697,12 +1741,40 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) RETURN END IF END IF - ! The range graphs continue from the checkpoint: the rebuild sized them, the restored - ! OtherState holds them through other%t_commit (a same-process rewind drops the samples - ! of the abandoned steps with it). + ! The same run restoring one of its own states (CalcSteady's operating points) keeps what + ! it has committed, the range samples too, until it steps on from there + ! (resume_after_rewind). A checkpoint in a new process continues from the restored + ! OtherState, which holds the range graphs through other%t_commit. + same_run = .FALSE. + IF (old_id >= 1_IntKi .AND. old_id <= SIZE(Inst) .AND. old_id /= id) & + same_run = Inst(old_id)%active .AND. Inst(old_id)%serial == other%restart_serial + Inst(id)%t_run_end = other%t_commit + IF (same_run) THEN + Inst(id)%t_run_end = MAX(Inst(old_id)%t_run_end, other%t_commit) + Inst(id)%rewound = Inst(id)%t_run_end > other%t_commit + 1.0E-9_DbKi*MAX(1.0_DbKi, ABS(other%t_commit)) + END IF Inst(id)%range_on = CD_AGG_Range_Graphs( Inst(id)%agg ) IF (Inst(id)%range_on) THEN - CALL restore_range_state( Inst(id), other, ErrStat2, ErrMsg2 ) + IF (same_run) THEN + Inst(id)%range_sampled = Inst(old_id)%range_sampled + ALLOCATE (Inst(id)%ws_range(CD_AGG_Range_MirrorSize( Inst(id)%agg ))) + CALL CD_AGG_Range_Get_State( Inst(old_id)%agg, Inst(id)%ws_range, es, em ) + IF (es == CD_AGG_OK) CALL CD_AGG_Range_Set_State( Inst(id)%agg, Inst(id)%ws_range, es, em ) + IF (es /= CD_AGG_OK) CALL SetErrStat( ErrID_Fatal, 'CableDyn mooring module: range graphs of the '// & + 'restored run: '//TRIM(em), ErrStat, ErrMsg, RoutineName ) + ELSE + CALL restore_range_state( Inst(id), other, ErrStat2, ErrMsg2 ) + CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) + END IF + IF (ErrStat >= AbortErrLev) THEN + CALL release_instance( id ) + RETURN + END IF + END IF + ! A linearizing run keeps its state map: CalcSteady rewinds to the operating point through + ! this rebuild, and a restart may linearize later. + IF (p%Linearize) THEN + CALL lin_state_map( Inst(id), m, ErrStat2, ErrMsg2 ) CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) IF (ErrStat >= AbortErrLev) THEN CALL release_instance( id ) @@ -1726,12 +1798,11 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) Inst(stale_id)%native_un = -1_IntKi Inst(stale_id)%native_root = '' END IF - ! A same-process replacement adopts the run-level stream from its predecessor. - ! Any rows beyond the restored commit are removed before the open unit changes - ! owner. A fresh-process restart has no live unit and starts a new result file at - ! the checkpoint state, matching the normal OpenFAST restart-output policy. + ! A same-process replacement adopts the run-level stream from its predecessor, rows and + ! all. A fresh-process restart has no live unit and starts a new result file at the + ! checkpoint state, matching the normal OpenFAST restart-output policy. CALL restore_native_outputs( id, old_id, TRIM(p%RootName), other%t_commit, other%restart_serial, & - p%dtM0, Inst(id)%nsuper, ErrStat2, ErrMsg2 ) + ErrStat2, ErrMsg2 ) CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) IF (ErrStat >= AbortErrLev) THEN CALL release_instance( id ) @@ -1744,6 +1815,7 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) ! adopt the restored identity (the OtherState is authoritative across the restart) Inst(id)%serial = other%restart_serial Inst(id)%t_commit = other%t_commit + Inst(id)%t_commit_prev = other%t_commit Inst(id)%last_n = -1_IntKi Inst(id)%last_solve_n = -1_IntKi Inst(id)%loads_cached = .FALSE. @@ -1797,7 +1869,7 @@ SUBROUTINE CD_UpdateStates( t, n, u, t_array, p, x, xd, z, other, m, ErrStat, Er ! PREDICTED input for the upcoming interval (t_array(1) = t + DT), one glue step ! AHEAD of the committed boundary; overlaying it would start the restored run from ! a boundary it never had. Pick the u(:) entry whose time is t. - IF (.NOT. instance_matches(m, other)) THEN + IF (.NOT. (step_retry(m, other, n) .OR. instance_matches(m, other))) THEN iu_t = 1_IntKi DO i_u = 1, SIZE(t_array) IF (ABS(t_array(i_u) - t) <= 1.0E-6_DbKi*MAX(1.0_DbKi, ABS(t))) THEN @@ -1810,18 +1882,25 @@ SUBROUTINE CD_UpdateStates( t, n, u, t_array, p, x, xd, z, other, m, ErrStat, Er IF (ErrStat >= AbortErrLev) RETURN END IF id = m%CDInst + IF (Inst(id)%rewound) THEN + CALL resume_after_rewind( Inst(id), other, ErrStat2, ErrMsg2 ) + CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) + IF (ErrStat >= AbortErrLev) RETURN + END IF IF (Inst(id)%prof_enabled) THEN CALL SYSTEM_CLOCK(prof_c0, prof_rate) Inst(id)%prof_n_update = Inst(id)%prof_n_update + 1_IntKi END IF - ! Correction iterations (NumCrctn > 0): the glue re-runs UpdateStates over the SAME - ! interval with corrected inputs. A re-entry over a glue step where nothing was - ! advanced (an intermediate supercycle call) is a no-op; a re-entry over a COMPLETED + ! Correction iterations: the glue re-runs UpdateStates over the SAME interval with + ! corrected inputs when its solver retries a step. A re-entry over a glue step where nothing + ! was advanced (an intermediate supercycle call) is a no-op; a re-entry over a COMPLETED ! mooring step rewinds the aggregate to the snapshot taken at that step's start and ! falls through to re-advance with the corrected inputs -- exactly the ! copy-states-back semantics a registry-state module gets from the glue, expressed - ! on the module's internal state. + ! on the module's internal state. The glue's copy has taken the OtherState back with the + ! states, so what the first pass mirrored there and does not repeat is mirrored again: + ! the load predictor, shifted once per mooring step. IF (n == Inst(id)%last_n) THEN IF (Inst(id)%last_solve_n /= n) THEN IF (Inst(id)%prof_enabled) THEN @@ -1837,6 +1916,9 @@ SUBROUTINE CD_UpdateStates( t, n, u, t_array, p, x, xd, z, other, m, ErrStat, Er TRIM(em), ErrStat, ErrMsg, RoutineName ) RETURN END IF + Inst(id)%t_commit = Inst(id)%t_commit_prev + other%t_commit = Inst(id)%t_commit + CALL mirror_predictor_loads( Inst(id), other ) END IF ! Supercycling (CableDyn's own dtM): the mooring interval [.., (n+1)*DT] completes only @@ -1954,7 +2036,9 @@ SUBROUTINE CD_UpdateStates( t, n, u, t_array, p, x, xd, z, other, m, ErrStat, Er ! the interval end is t + dtCoupling (t + dt_moor from here overshoots by ! dt_moor - DT under supercycling -- the same arithmetic as the WaveField ! sampling time). + Inst(id)%t_commit_prev = Inst(id)%t_commit Inst(id)%t_commit = t + p%dtCoupling + Inst(id)%t_run_end = Inst(id)%t_commit other%t_commit = Inst(id)%t_commit CALL pack_state_mirror( Inst(id), m, x, ErrStat2, ErrMsg2 ) CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) @@ -2802,6 +2886,7 @@ SUBROUTINE sample_range( inst_e, other, t_sample, ErrStat, ErrMsg ) END IF IF (.NOT. ALLOCATED(other%range_state)) ALLOCATE (other%range_state(nr)) other%range_state = REAL(inst_e%ws_range, DbKi) + inst_e%range_sampled = .TRUE. END SUBROUTINE sample_range SUBROUTINE restore_range_state( inst_e, other, ErrStat, ErrMsg ) @@ -2836,24 +2921,32 @@ SUBROUTINE shift_predictor_loads( inst_e, other ) !! continues the extrapolation from the same two commits. TYPE(CD_OF_Instance), INTENT(INOUT) :: inst_e TYPE(CD_OtherStateType), INTENT(INOUT) :: other - INTEGER(IntKi) :: nl IF (inst_e%loads_cached .AND. inst_e%loads_at_n == inst_e%last_solve_n) THEN inst_e%pred_fld = inst_e%ws_fld inst_e%pred_mom = inst_e%ws_moment inst_e%pred_t = inst_e%t_commit - nl = 3_IntKi*inst_e%ncp - IF (ALLOCATED(other%pred_loads)) THEN - IF (SIZE(other%pred_loads) /= 2*nl) DEALLOCATE (other%pred_loads) - END IF - IF (.NOT. ALLOCATED(other%pred_loads)) ALLOCATE (other%pred_loads(2*nl)) - other%pred_loads(1:nl) = RESHAPE(REAL(inst_e%ws_fld, DbKi), [nl]) - other%pred_loads(nl + 1:2*nl) = RESHAPE(REAL(inst_e%ws_moment, DbKi), [nl]) ELSE inst_e%pred_t = -1.0_DbKi END IF - other%pred_t = inst_e%pred_t + CALL mirror_predictor_loads( inst_e, other ) END SUBROUTINE shift_predictor_loads + SUBROUTINE mirror_predictor_loads( inst_e, other ) + !! The load predictor's previous commit, as the instance holds it, into the OtherState. + TYPE(CD_OF_Instance), INTENT(IN ) :: inst_e + TYPE(CD_OtherStateType), INTENT(INOUT) :: other + INTEGER(IntKi) :: nl + other%pred_t = inst_e%pred_t + IF (inst_e%pred_t < 0.0_DbKi) RETURN + nl = 3_IntKi*inst_e%ncp + IF (ALLOCATED(other%pred_loads)) THEN + IF (SIZE(other%pred_loads) /= 2*nl) DEALLOCATE (other%pred_loads) + END IF + IF (.NOT. ALLOCATED(other%pred_loads)) ALLOCATE (other%pred_loads(2*nl)) + other%pred_loads(1:nl) = RESHAPE(REAL(inst_e%pred_fld, DbKi), [nl]) + other%pred_loads(nl + 1:2*nl) = RESHAPE(REAL(inst_e%pred_mom, DbKi), [nl]) + END SUBROUTINE mirror_predictor_loads + SUBROUTINE restore_calcoutput_probe( inst_e, m, ErrStat, ErrMsg ) !! Restore every state surface after the output-probe !! overlay. This is used on both success and every post-overlay error exit. @@ -3322,18 +3415,19 @@ SUBROUTINE CD_End(u, p, x, xd, z, other, y, m, ErrStat , ErrMsg) IF (ALLOCATED(Inst)) THEN IF (m%CDInst >= 1 .AND. m%CDInst <= SIZE(Inst)) THEN IF (Inst(m%CDInst)%active) THEN - ! The range graphs cover the whole run, so only a run that reached TMax writes them - ! (as the standalone driver, which writes them on completion); the glue also comes - ! here after an abort. TMax passes through ReKi, so the test allows its rounding. + ! The range graphs cover the whole run, so a run that ended on an error writes none + ! (as the standalone driver, which writes them on completion). The glue comes here + ! after an abort too, so the host says which it was (m%HostComplete): a run can also + ! complete before TMax, on CalcSteady or at FAST.Farm's last whole DT_Low. A process + ! that only restored a checkpoint and stepped nothing (-VTKLin) leaves the file as it is. IF (Inst(m%CDInst)%range_on) THEN - IF (Inst(m%CDInst)%t_commit >= p%Tmax - 1.0E-6_DbKi*p%dtM0 - & - 2.0_DbKi*REAL(EPSILON(1.0_ReKi), DbKi)*ABS(p%Tmax)) THEN + IF (.NOT. m%HostComplete) THEN + CALL WrScr( ' CableDyn: the run stopped on an error at t = '// & + TRIM(Num2LStr(Inst(m%CDInst)%t_commit))//' s: no range graph is written.' ) + ELSE IF (Inst(m%CDInst)%range_sampled) THEN CALL CD_AGG_Range_Write( Inst(m%CDInst)%agg, TRIM(p%RootName), range_es, range_em ) IF (range_es /= CD_AGG_OK) CALL SetErrStat( ErrID_Fatal, 'CableDyn mooring module: '// & TRIM(range_em), ErrStat, ErrMsg, RoutineName ) - ELSE - CALL WrScr( ' CableDyn: the run ended at t = '//TRIM(Num2LStr(Inst(m%CDInst)%t_commit))// & - ' s, before TMax = '//TRIM(Num2LStr(p%Tmax))//' s: no range graph is written.' ) END IF END IF ! the rotation- and snap-resolved sub-steps of the EI=0 lines (ei0_substeps), as the diff --git a/src/openfast/CableDyn_OF_Driver.f90 b/src/openfast/CableDyn_OF_Driver.f90 index 97ee9a9..ec4c051 100644 --- a/src/openfast/CableDyn_OF_Driver.f90 +++ b/src/openfast/CableDyn_OF_Driver.f90 @@ -172,6 +172,7 @@ PROGRAM CableDyn_OF_Driver END DO CLOSE (un_out) + m%HostComplete = .TRUE. CALL CD_End(u(1), p, x, xd, z, other, y, m, es, em) IF (es >= AbortErrLev) CALL fail_out('CD_End: '//TRIM(em)) CALL WrScr('CableDyn_OF_Driver: completed '//TRIM(Num2LStr(nglue))//' steps.') diff --git a/src/openfast/CableDyn_Registry.txt b/src/openfast/CableDyn_Registry.txt index 11194c8..a0281fe 100644 --- a/src/openfast/CableDyn_Registry.txt +++ b/src/openfast/CableDyn_Registry.txt @@ -385,6 +385,7 @@ typedef ^ ^ IntKi nCpldPoints {:} typedef ^ ^ IntKi NConns - 0 - "number of Connect type Points - not to be confused with NPoints" "" typedef ^ ^ IntKi NAnchs - 0 - "number of Anchor type Points" "" typedef ^ ^ DbKi Tmax - - - "simulation duration" "[s]" +typedef ^ ^ LOGICAL Linearize - .FALSE. - "the run linearizes: a rebuilt instance needs the linearization state map" - typedef ^ ^ IntKi tScheme - 0 - "Time integration scheme (0 = RK2, 1 = RK4). Default is RK2" - typedef ^ ^ DbKi g - 9.81 - "gravitational constant (positive)" "[m/s^2]" typedef ^ ^ DbKi rhoW - 1025 - "density of seawater" "[kg/m^3]" @@ -514,3 +515,4 @@ typedef ^ ^ IntKi BathGrid_npoints {:} typedef ^ ^ GridInterp_MiscVarType WaveField_m - - - "misc var information from the SeaState Interpolation module" - typedef ^ ^ LOGICAL IC_gen - .FALSE. - "boolean to indicate dynamic relaxation occuring" "-" typedef ^ ^ IntKi CDInst - 0 - "CableDyn mooring-backend instance handle (0 = none / needs rebuild after restart)" "" +typedef ^ ^ LOGICAL HostComplete - .FALSE. - "set by the host before CD_End when the run ended without an error" - diff --git a/src/openfast/CableDyn_Types.f90 b/src/openfast/CableDyn_Types.f90 index 0cb3b4c..ff9b78b 100644 --- a/src/openfast/CableDyn_Types.f90 +++ b/src/openfast/CableDyn_Types.f90 @@ -421,6 +421,7 @@ MODULE CableDyn_Types INTEGER(IntKi) :: NConns = 0 !< number of Connect type Points - not to be confused with NPoints [] INTEGER(IntKi) :: NAnchs = 0 !< number of Anchor type Points [] REAL(DbKi) :: Tmax = 0.0_R8Ki !< simulation duration [[s]] + LOGICAL :: Linearize = .FALSE. !< the run linearizes: a rebuilt instance needs the linearization state map [-] INTEGER(IntKi) :: tScheme = 0 !< Time integration scheme (0 = RK2, 1 = RK4). Default is RK2 [-] REAL(DbKi) :: g = 9.81 !< gravitational constant (positive) [[m/s^2]] REAL(DbKi) :: rhoW = 1025 !< density of seawater [[kg/m^3]] @@ -541,6 +542,7 @@ MODULE CableDyn_Types TYPE(GridInterp_MiscVarType) :: WaveField_m !< misc var information from the SeaState Interpolation module [-] LOGICAL :: IC_gen = .FALSE. !< boolean to indicate dynamic relaxation occuring [-] INTEGER(IntKi) :: CDInst = 0 !< CableDyn mooring-backend instance handle (0 = none / needs rebuild after restart) [] + LOGICAL :: HostComplete = .FALSE. !< set by the host before CD_End when the run ended without an error [-] END TYPE CD_MiscVarType ! ======================= integer(IntKi), public, parameter :: CD_x_states = 1 ! CD%states @@ -3133,6 +3135,7 @@ subroutine CD_CopyParam(SrcParamData, DstParamData, CtrlCode, ErrStat, ErrMsg) DstParamData%NConns = SrcParamData%NConns DstParamData%NAnchs = SrcParamData%NAnchs DstParamData%Tmax = SrcParamData%Tmax + DstParamData%Linearize = SrcParamData%Linearize DstParamData%tScheme = SrcParamData%tScheme DstParamData%g = SrcParamData%g DstParamData%rhoW = SrcParamData%rhoW @@ -3502,6 +3505,7 @@ subroutine CD_PackParam(RF, Indata) call RegPack(RF, InData%NConns) call RegPack(RF, InData%NAnchs) call RegPack(RF, InData%Tmax) + call RegPack(RF, InData%Linearize) call RegPack(RF, InData%tScheme) call RegPack(RF, InData%g) call RegPack(RF, InData%rhoW) @@ -3612,6 +3616,7 @@ subroutine CD_UnPackParam(RF, OutData) call RegUnpack(RF, OutData%NConns); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%NAnchs); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%Tmax); if (RegCheckErr(RF, RoutineName)) return + call RegUnpack(RF, OutData%Linearize); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%tScheme); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%g); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%rhoW); if (RegCheckErr(RF, RoutineName)) return @@ -4547,6 +4552,7 @@ subroutine CD_CopyMisc(SrcMiscData, DstMiscData, CtrlCode, ErrStat, ErrMsg) if (ErrStat >= AbortErrLev) return DstMiscData%IC_gen = SrcMiscData%IC_gen DstMiscData%CDInst = SrcMiscData%CDInst + DstMiscData%HostComplete = SrcMiscData%HostComplete end subroutine subroutine CD_DestroyMisc(MiscData, ErrStat, ErrMsg) @@ -4827,6 +4833,7 @@ subroutine CD_PackMisc(RF, Indata) call GridInterp_PackMisc(RF, InData%WaveField_m) call RegPack(RF, InData%IC_gen) call RegPack(RF, InData%CDInst) + call RegPack(RF, InData%HostComplete) if (RegCheckErr(RF, RoutineName)) return end subroutine @@ -4980,6 +4987,7 @@ subroutine CD_UnPackMisc(RF, OutData) call GridInterp_UnpackMisc(RF, OutData%WaveField_m) ! WaveField_m call RegUnpack(RF, OutData%IC_gen); if (RegCheckErr(RF, RoutineName)) return call RegUnpack(RF, OutData%CDInst); if (RegCheckErr(RF, RoutineName)) return + call RegUnpack(RF, OutData%HostComplete); if (RegCheckErr(RF, RoutineName)) return end subroutine subroutine CD_Input_ExtrapInterp(u, t, u_out, t_out, ErrStat, ErrMsg) diff --git a/tests/test_documentation.py b/tests/test_documentation.py index 51f6dab..41ff57c 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -185,6 +185,13 @@ def test_windows_release_is_publicly_reproducible(self) -> None: farm_patch, f"FAST.Farm host environment propagation lost: {token}", ) + # both hosts tell CableDyn that a run completed, which gates its whole-run outputs + for hook in ("0014-fast-subs.patch", "0023-fast-farm-end.patch"): + self.assertIn( + "%CD%m%HostComplete = .true.", + (ROOT / "integration" / "openfast" / "patches" / hook).read_text(encoding="utf-8"), + f"run-completion signal lost: {hook}", + ) self.assertNotIn("CD_InitInp%g = 9.81", farm_patch) self.assertNotIn("CD_InitInp%rhoW = 1025.0", farm_patch) farm_registry = ( diff --git a/tests/test_openfast_aggregate.f90 b/tests/test_openfast_aggregate.f90 index 0454605..8b8482b 100644 --- a/tests/test_openfast_aggregate.f90 +++ b/tests/test_openfast_aggregate.f90 @@ -23,7 +23,7 @@ PROGRAM test_openfast_aggregate !! 8) line control -- CONTROL on EI=0 lines and finite-EI cables (alone and mixed), its !! fail-closed bounds, rewind and bit-identical checkpoint restart (--line-control-only). USE CableDyn_Precision, ONLY: wp - USE CableDyn_DeckDriver, ONLY: CD_Deck_Query_dtM, CD_DECKDRV_OK + USE CableDyn_DeckDriver, ONLY: CD_Deck_Query_dtM, CD_DECKDRV_OK, CD_INIT_NOTE_LEN USE CableDyn_OpenFAST_Aggregate, ONLY: CD_AGG_ModuleType, CD_AGG_Init_From_Deck, CD_AGG_NMovingPoints, & CD_AGG_GetMovingPointMesh, CD_AGG_UpdateStates_Moving, & CD_AGG_Step_Moving, CD_AGG_CalcOutput, & @@ -136,6 +136,7 @@ PROGRAM test_openfast_aggregate CALL case_anchor_uplift_channels() CALL case_output_channels() CALL case_range_graph_host() + CALL case_long_init_notes() CALL case_pure_cable_deck() CALL case_cable_load_feedback() CALL case_end_connection_orientation_and_moment() @@ -161,6 +162,7 @@ PROGRAM test_openfast_aggregate CALL case_step_atomicity() CALL case_ptfm_init_equivalence() CALL case_correction_rewind() + CALL case_retry_after_probe() CALL case_external_fluid() CALL case_still_water_deck_ambient() CALL case_external_fluid_report() @@ -1130,6 +1132,39 @@ SUBROUTINE range_march(a, r0, dt, step, amplitude, label) CALL require(es == CD_AGG_OK .AND. cv, label//': '//TRIM(em)) END SUBROUTINE range_march + SUBROUTINE case_long_init_notes() + !! A cable that collects a long list of initialisation notes: the two stiffener notes of each + !! end, the friction resolution note and, appended after them on the line summary, the dtM + !! advisory. They run past the 1024 characters of a cable's note and of the OpenFAST shell's + !! old buffer; every note arrives whole, in a buffer of CD_INIT_NOTE_LEN. + TYPE(CD_AGG_ModuleType) :: agg + REAL(wp) :: ten, force(3), incl, decl, azi + INTEGER :: es, line_id, nlines, k + CHARACTER(512) :: em + CHARACTER(CD_INIT_NOTE_LEN) :: note + + CALL write_touchdown_cable_deck('agg_long_notes.dat', 'FairTen2', stiffeners=.TRUE.) + CALL CD_AGG_Init_From_Deck(agg, 'agg_long_notes.dat', 0.1_wp, es, em) + CALL require(es == CD_AGG_OK, 'long-notes:init: '//TRIM(em)) + IF (es /= CD_AGG_OK) RETURN + CALL CD_AGG_GetInitLine(agg, 2, line_id, ten, force, incl, decl, azi, es, em, note=note) + CALL require(es == CD_AGG_OK .AND. line_id == 2, 'long-notes:line') + WRITE (*, '(A,I0)') ' initialisation notes of the cable, characters: ', LEN_TRIM(note) + CALL require(LEN_TRIM(note) > 1024, 'long-notes:longer-than-the-old-buffer') + CALL require(INDEX(note, 'stiffener at End A spans') > 0 .AND. INDEX(note, 'stiffener at End A sleeves') > 0 .AND. & + INDEX(note, 'stiffener at End B spans') > 0 .AND. INDEX(note, 'stiffener at End B sleeves') > 0, & + 'long-notes:the-four-stiffener-notes') + CALL require(INDEX(note, 'seabed friction acts at the nodes') > 0, 'long-notes:the-friction-note') + CALL require(INDEX(note, 'axial period') > 0, 'long-notes:the-dtM-advisory-appended-last') + CALL require(INDEX(note, 'notes omitted') == 0, 'long-notes:nothing-omitted') + nlines = 1 + DO k = 1, LEN_TRIM(note) + IF (note(k:k) == NEW_LINE('a')) nlines = nlines + 1 + END DO + CALL require(nlines >= 6, 'long-notes:one-text-line-per-note') + CALL CD_AGG_End(agg, es, em) + END SUBROUTINE case_long_init_notes + SUBROUTINE check_static_profile(path, expected_rows) CHARACTER(*), INTENT(IN) :: path INTEGER, INTENT(IN) :: expected_rows @@ -2777,6 +2812,77 @@ PURE FUNCTION test_zyx_dcm(theta) RESULT(m) m(3, 3) = cx*cy END FUNCTION test_zyx_dcm + SUBROUTINE case_retry_after_probe() + !! A host that retries a step after an output probe of it (the OpenFAST shell at dtM = DT): the + !! probe's setters clear the cached committed force, but the retried step must take the path + !! the first pass took, which reused that cache. A fresh evaluation differs from it in the last + !! bits where a friction anchor slid. The run with retries matches one without bit for bit. + TYPE(CD_AGG_ModuleType) :: agg, once + REAL(wp), PARAMETER :: DT = 0.05_wp, OMEGA = 2.0_wp*PI/8.0_wp, AMP = 12.0_wp + INTEGER, PARAMETER :: NSTEP = 160 + REAL(wp) :: r0(3, 2), tgt(3, 2), v(3, 2), a(3, 2), lds(3, 2), tc + INTEGER :: es, ni, s + LOGICAL :: cv, st + CHARACTER(512) :: em + + CALL write_touchdown_cable_deck('agg_retry_probe.dat', 'FairTen2') + CALL CD_AGG_Init_From_Deck(agg, 'agg_retry_probe.dat', DT, es, em) + CALL require(es == CD_AGG_OK, 'retry-probe:init: '//TRIM(em)) + IF (es /= CD_AGG_OK) RETURN + CALL CD_AGG_Init_From_Deck(once, 'agg_retry_probe.dat', DT, es, em) + CALL require(es == CD_AGG_OK, 'retry-probe:init-once: '//TRIM(em)) + IF (es /= CD_AGG_OK) RETURN + CALL CD_AGG_GetMovingPointMesh(agg, r0, v, a, lds, es, em) + DO s = 1, NSTEP + tc = REAL(s, wp)*DT + tgt = r0 + tgt(1, 2) = r0(1, 2) + AMP*SIN(OMEGA*tc) + v = 0.0_wp + v(1, 2) = AMP*OMEGA*COS(OMEGA*tc) + a = 0.0_wp + a(1, 2) = -AMP*OMEGA**2*SIN(OMEGA*tc) + CALL CD_AGG_Snapshot(agg, es, em) + CALL CD_AGG_Step_Moving(agg, DT, tgt, v, a, cv, st, ni, es, em, t_committed=tc) + CALL require(es == CD_AGG_OK .AND. cv, 'retry-probe:first-pass: '//TRIM(em)) + CALL retry_probe_cycle(agg, tgt, v, a, tc) + CALL CD_AGG_Restore(agg, es, em) + CALL require(es == CD_AGG_OK, 'retry-probe:restore: '//TRIM(em)) + CALL CD_AGG_Step_Moving(agg, DT, tgt, v, a, cv, st, ni, es, em, t_committed=tc) + CALL require(es == CD_AGG_OK .AND. cv, 'retry-probe:second-pass: '//TRIM(em)) + CALL retry_probe_cycle(agg, tgt, v, a, tc) + CALL CD_AGG_Snapshot(once, es, em) + CALL CD_AGG_Step_Moving(once, DT, tgt, v, a, cv, st, ni, es, em, t_committed=tc) + CALL require(es == CD_AGG_OK .AND. cv, 'retry-probe:once: '//TRIM(em)) + IF (es /= CD_AGG_OK) EXIT + CALL retry_probe_cycle(once, tgt, v, a, tc) + END DO + WRITE (*, '(A,ES10.3)') ' retried against single steps, largest cable state difference: ', & + nan_max_abs(agg%cables(1)%line%q - once%cables(1)%line%q) + CALL require(nan_max_abs(agg%cables(1)%line%q - once%cables(1)%line%q) <= 0.0_wp, 'retry-probe:positions') + CALL require(nan_max_abs(agg%cables(1)%line%v - once%cables(1)%line%v) <= 0.0_wp, 'retry-probe:velocities') + CALL require(nan_max_abs(agg%cables(1)%line%a - once%cables(1)%line%a) <= 0.0_wp, 'retry-probe:accelerations') + CALL CD_AGG_End(agg, es, em) + CALL CD_AGG_End(once, es, em) + END SUBROUTINE case_retry_after_probe + + SUBROUTINE retry_probe_cycle(a, tgt, v, acc, t) + !! An output probe at the committed kinematics and the cable's state mirror back after it, as + !! the OpenFAST shell's CalcOutput does at dtM = DT. + TYPE(CD_AGG_ModuleType), INTENT(INOUT) :: a + REAL(wp), INTENT(IN) :: tgt(:, :), v(:, :), acc(:, :), t + REAL(wp), ALLOCATABLE :: buf(:) + INTEGER :: es + CHARACTER(512) :: em + + ALLOCATE (buf(CD_HFMF_MirrorSize(a%cables(1)))) + CALL CD_HFMF_PackMirror(a%cables(1), buf, es, em) + CALL require(es == CD_HFMF_OK, 'retry-probe:pack: '//TRIM(em)) + CALL CD_AGG_UpdateStates_Moving(a, tgt, v, acc, es, em, output_probe=.TRUE.) + CALL require(es == CD_AGG_OK, 'retry-probe:probe: '//TRIM(em)) + CALL CD_HFMF_UnpackMirror(a%cables(1), buf, t, es, em) + CALL require(es == CD_HFMF_OK, 'retry-probe:unpack: '//TRIM(em)) + END SUBROUTINE retry_probe_cycle + SUBROUTINE case_correction_rewind() !! Correction-iteration semantics at the aggregate level (the OpenFAST shell's !! NumCrctn > 0 building block): snapshot a committed state, advance with motion A, @@ -5480,10 +5586,12 @@ SUBROUTINE case_anchor_uplift_channels() CALL CD_AGG_End(agg, es, em) END SUBROUTINE case_anchor_uplift_channels - SUBROUTINE write_touchdown_cable_deck(path, outputs_row) + SUBROUTINE write_touchdown_cable_deck(path, outputs_row, stiffeners) !! IEA-15MW mooring chain (line 1, Fixed anchor point 2) + touchdown power cable (line 2, - !! Fixed anchor point 4) on a 200 m seabed, with the given OUTPUTS row(s). + !! Fixed anchor point 4) on a 200 m seabed, with the given OUTPUTS row(s); stiffeners adds a + !! bend stiffener over each end of the cable, one deck element long. CHARACTER(*), INTENT(IN) :: path, outputs_row + LOGICAL, INTENT(IN), OPTIONAL :: stiffeners INTEGER :: u, ios OPEN (NEWUNIT=u, FILE=path, STATUS='REPLACE', ACTION='WRITE', IOSTAT=ios) @@ -5515,6 +5623,15 @@ SUBROUTINE write_touchdown_cable_deck(path, outputs_row) WRITE (u, '(A)') '2 bare 80.0 20' WRITE (u, '(A)') '2 buoy 100.0 25' WRITE (u, '(A)') '2 bare 420.0 42' + IF (PRESENT(stiffeners)) THEN + IF (stiffeners) THEN + WRITE (u, '(A)') '--- STIFFENERS ---' + WRITE (u, '(A)') 'LineID End Length RootOD TipOD InnerD E Density' + WRITE (u, '(A)') '(-) (-) (m) (m) (m) (m) (Pa) (kg/m^3)' + WRITE (u, '(A)') '2 A 4.0 0.45 0.18 0.17 5.0e7 1150' + WRITE (u, '(A)') '2 B 4.0 0.45 0.18 0.17 5.0e7 1150' + END IF + END IF WRITE (u, '(A)') '--- OPTIONS ---' WRITE (u, '(A)') '9.80665 g' WRITE (u, '(A)') '1025.0 rhoW'