From e8017f9365da0dc7fe6961023d3132c82cd57b3e Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Sat, 7 Nov 2026 22:31:42 +0900 Subject: [PATCH 1/5] fix(openfast): keep every initialisation note of a coupled line The OpenFAST shell read a line's notes into 1024 characters while the summary held 2048, so a long list was cut in mid-word, and a cable's own 1024-character note buffer dropped the notes past it, the omission marker with them. A line with stiffener notes at both ends, the friction note and the dtM advisory lost the friction note and the tail of the advisory. All the holders now share CD_INIT_NOTE_LEN (32 KiB): a line reports fewer than 32 notes of at most 1024 characters, so every note fits. Results are unchanged. Signed-off-by: Jae Hoon Seo --- app/cabledyn.f90 | 4 +-- src/CableDyn_DeckDriver.f90 | 13 +++++--- src/openfast/CableDyn_OF.f90 | 5 +-- tests/test_openfast_aggregate.f90 | 51 +++++++++++++++++++++++++++++-- 4 files changed, 62 insertions(+), 11 deletions(-) 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/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/openfast/CableDyn_OF.f90 b/src/openfast/CableDyn_OF.f90 index 66d9137..17db298 100644 --- a/src/openfast/CableDyn_OF.f90 +++ b/src/openfast/CableDyn_OF.f90 @@ -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, & @@ -578,7 +578,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 diff --git a/tests/test_openfast_aggregate.f90 b/tests/test_openfast_aggregate.f90 index 0454605..0d37c46 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() @@ -1130,6 +1131,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 @@ -5480,10 +5514,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 +5551,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' From ac9ed7b5905565ef6174a625daa09848b5b017a7 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Sat, 7 Nov 2026 22:58:19 +0900 Subject: [PATCH 2/5] fix(openfast): keep the linearization state map across a rebuild A rebuilt instance lost the map of its linearization states, so a run that rebuilt before it linearized stopped with "dXdu exists only for a linearization run": CalcSteady rewinds to its operating point through the rebuild, and a restart of a linearizing run takes the same path. The parameters now record that the run linearizes, and the rebuild maps the states again. This adds a parameter to the OpenFAST checkpoint. Signed-off-by: Jae Hoon Seo --- src/openfast/CableDyn_OF.f90 | 11 +++++++++++ src/openfast/CableDyn_Registry.txt | 1 + src/openfast/CableDyn_Types.f90 | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/src/openfast/CableDyn_OF.f90 b/src/openfast/CableDyn_OF.f90 index 17db298..e37dafc 100644 --- a/src/openfast/CableDyn_OF.f90 +++ b/src/openfast/CableDyn_OF.f90 @@ -1047,6 +1047,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. @@ -1710,6 +1711,16 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) 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 ) + RETURN + END IF + END IF ! A fresh-process restart first passes through CD_Init, whose provisional instance ! owns the output stream of the same root; the checkpointed m%CDInst need not be ! that slot. Find it so its stream is released and the slot freed with old_id. diff --git a/src/openfast/CableDyn_Registry.txt b/src/openfast/CableDyn_Registry.txt index 11194c8..87d335a 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]" diff --git a/src/openfast/CableDyn_Types.f90 b/src/openfast/CableDyn_Types.f90 index 0cb3b4c..badddc8 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]] @@ -3133,6 +3134,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 +3504,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 +3615,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 From b3afe8d37ec112bb52414abeb83a94b6acde2f9f Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Sat, 7 Nov 2026 22:58:19 +0900 Subject: [PATCH 3/5] fix(openfast): accept a restore forward in time in the .CD.out history CalcSteady linearizes at its saved operating points in time order, so after the rewind to the first one each further restore goes forward. The rebuild took that for a damaged history and stopped the run at the second operating point. A forward restore has no rows to trim and now goes on. Signed-off-by: Jae Hoon Seo --- src/openfast/CableDyn_OF.f90 | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/openfast/CableDyn_OF.f90 b/src/openfast/CableDyn_OF.f90 index e37dafc..bb9c1b8 100644 --- a/src/openfast/CableDyn_OF.f90 +++ b/src/openfast/CableDyn_OF.f90 @@ -489,14 +489,11 @@ SUBROUTINE restore_native_outputs( id, old_id, root, t_start, restart_serial, dt RETURN END IF + ! A restore can also go forward: CalcSteady linearizes at several saved operating points, in + ! time order, after rewinding to the first. There is nothing to trim then. 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 - END IF - ntrim = MAX(0_IntKi, NINT(MAX(delta, 0.0_DbKi)/dt_moor, IntKi)) + delta = MAX(Inst(old_id)%native_wrote_t - t_start, 0.0_DbKi) + ntrim = NINT(delta/dt_moor, IntKi) IF (ABS(delta - REAL(ntrim, DbKi)*dt_moor) > tol) THEN CALL SetErrStat( ErrID_Fatal, 'The native-history rewind is not an integer number of CableDyn steps.', & ErrStat, ErrMsg, RoutineName ) From 166133f0ec0da73453ecb109ff7cdf4b9352eb7c Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Sat, 7 Nov 2026 22:56:33 +0900 Subject: [PATCH 4/5] fix(openfast): write the range graphs when the host says the run completed CD_End took a run that had reached TMax for a completed one. FAST.Farm ends its turbines at the last whole DT_Low, so a per-turbine instance with a TMax off that grid completed normally and wrote no range file; a run that CalcSteady stopped early did the same; and an abort in another module after CableDyn's last commit was written as if complete. The hosts now say it: ExitThisProgram_T sets CD%m%HostComplete after a run without an error, and FAST.Farm, which ends its turbines the same way on an abort, sets it after its last step (patches 0014 and 0023). CD_End writes on that signal alone. A process that only restored a checkpoint and stepped nothing (-VTKLin) leaves the file as it is. Signed-off-by: Jae Hoon Seo --- doc/openfast.rst | 2 +- doc/outputs.rst | 10 ++++--- .../openfast/patches/0014-fast-subs.patch | 16 ++++++++-- .../openfast/patches/0023-fast-farm-end.patch | 17 +++++++++++ src/openfast/CableDyn_OF.f90 | 29 ++++++++++++------- src/openfast/CableDyn_OF_Driver.f90 | 1 + src/openfast/CableDyn_Registry.txt | 1 + src/openfast/CableDyn_Types.f90 | 4 +++ tests/test_documentation.py | 7 +++++ 9 files changed, 68 insertions(+), 19 deletions(-) create mode 100644 integration/openfast/patches/0023-fast-farm-end.patch 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..2457e14 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``. The title line gives the last sample. +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 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/openfast/CableDyn_OF.f90 b/src/openfast/CableDyn_OF.f90 index bb9c1b8..3e7b68a 100644 --- a/src/openfast/CableDyn_OF.f90 +++ b/src/openfast/CableDyn_OF.f90 @@ -52,8 +52,8 @@ MODULE CableDyn !! glue correction iterations (NumCrctn > 0: snapshot-rewind re-advance), 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 @@ -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 @@ -1344,7 +1345,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 ) @@ -1707,6 +1708,10 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) CALL release_instance( id ) RETURN END IF + ! a same-process rewind goes on with a run that has sampled + IF (old_id >= 1_IntKi .AND. old_id <= SIZE(Inst) .AND. old_id /= id) & + Inst(id)%range_sampled = Inst(old_id)%active .AND. Inst(old_id)%range_sampled .AND. & + Inst(old_id)%serial == other%restart_serial END IF ! A linearizing run keeps its state map: CalcSteady rewinds to the operating point through ! this rebuild, and a restart may linearize later. @@ -2811,6 +2816,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 ) @@ -3331,18 +3337,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 87d335a..a0281fe 100644 --- a/src/openfast/CableDyn_Registry.txt +++ b/src/openfast/CableDyn_Registry.txt @@ -515,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 badddc8..ff9b78b 100644 --- a/src/openfast/CableDyn_Types.f90 +++ b/src/openfast/CableDyn_Types.f90 @@ -542,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 @@ -4551,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) @@ -4831,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 @@ -4984,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 = ( From df72393b2a66bc182c8de7ea4c1dcd17c34f7118 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Sat, 7 Nov 2026 23:35:38 +0900 Subject: [PATCH 5/5] fix(openfast): retry a mooring step at any dtM/DT, keep a run through its restores When the glue's solver retries a step it copies the states of the step's start back over the predicted ones, the OtherState included. CableDyn took that for a rewound checkpoint: at dtM = DT it rebuilt the whole model at every retry, and at dtM > DT it stopped, because the retried call lies inside the mooring interval and the rebuild accepts only a commit. A retry is now recognised for what it is (step_retry) and takes the aggregate's step-start snapshot, at any ratio. The load predictor, which shifts once per mooring step, is mirrored again into the copied-back OtherState. A retried step matches one that was never retried bit for bit. That held at dtM = DT only after a change in the cable: an output probe after the first pass cleared the cached committed force, and the restore then dropped it, though the step had started from it. A restore now judges the cache as it stood when the step began. With retries gone from the rebuild, what remains in a run is the restore of an operating point, so a restore no longer drops rows or range samples: .CD.out and the range graphs run to the end of a CalcSteady run. They are abandoned only if the run steps on from a restored state (resume_after_rewind). A rebuild no longer writes .CD.static.out: its state is the checkpoint's, and a restart overwrote the static equilibrium with it. Signed-off-by: Jae Hoon Seo --- doc/outputs.rst | 8 +- src/CableDyn_HermiteCableDynamic.f90 | 20 ++- src/openfast/CableDyn_OF.f90 | 222 ++++++++++++++++++--------- tests/test_openfast_aggregate.f90 | 72 +++++++++ 4 files changed, 241 insertions(+), 81 deletions(-) diff --git a/doc/outputs.rst b/doc/outputs.rst index 2457e14..ee1b2fb 100644 --- a/doc/outputs.rst +++ b/doc/outputs.rst @@ -390,7 +390,7 @@ step, once each whatever ``DT``, ``DT_Out`` and the correction iterations are; a 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 completes, whenever the host ends it: at ``TMax``, at a steady state found -by ``CalcSteady``, or at FAST.Farm's last whole ``DT_Low``. The title line gives the last sample. +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 @@ -1024,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/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 3e7b68a..36a03d2 100644 --- a/src/openfast/CableDyn_OF.f90 +++ b/src/openfast/CableDyn_OF.f90 @@ -49,7 +49,7 @@ 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 at the @@ -273,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. @@ -437,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 @@ -466,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 @@ -486,57 +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 - ! A restore can also go forward: CalcSteady linearizes at several saved operating points, in - ! time order, after rewinding to the first. There is nothing to trim then. - tol = 1.0E-7_DbKi*MAX(1.0_DbKi, ABS(t_start), ABS(Inst(old_id)%native_wrote_t)) - delta = MAX(Inst(old_id)%native_wrote_t - t_start, 0.0_DbKi) - ntrim = NINT(delta/dt_moor, IntKi) - IF (ABS(delta - REAL(ntrim, DbKi)*dt_moor) > tol) THEN + 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 + 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) @@ -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) @@ -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,21 +1741,35 @@ 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 ) - CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) + 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 - ! a same-process rewind goes on with a run that has sampled - IF (old_id >= 1_IntKi .AND. old_id <= SIZE(Inst) .AND. old_id /= id) & - Inst(id)%range_sampled = Inst(old_id)%active .AND. Inst(old_id)%range_sampled .AND. & - Inst(old_id)%serial == other%restart_serial END IF ! A linearizing run keeps its state map: CalcSteady rewinds to the operating point through ! this rebuild, and a restart may linearize later. @@ -1740,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 ) @@ -1758,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. @@ -1811,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 @@ -1824,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 @@ -1851,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 @@ -1968,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 ) @@ -2851,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. diff --git a/tests/test_openfast_aggregate.f90 b/tests/test_openfast_aggregate.f90 index 0d37c46..8b8482b 100644 --- a/tests/test_openfast_aggregate.f90 +++ b/tests/test_openfast_aggregate.f90 @@ -162,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() @@ -2811,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,