From 34524ed8576aec6951b1d9245349f1ad5dc6f197 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Sun, 8 Nov 2026 00:57:10 +0900 Subject: [PATCH 1/6] fix(openfast): restart from a checkpoint anywhere in a mooring interval A checkpoint written between two commits stopped the restart: the rebuild overlaid the glue's input at the restart time, which the state of the last commit was never reached with, so ChkptTime had to be a multiple of dtM. Between commits nothing of the solver moves, and the coming solve takes its input from the glue, so the state of the last commit is all a restart needs. The OtherState now keeps what that commit was reached with (commit_input): the coupled kinematics and the line-control command, from Init on. The rebuild stands on these at the commit's clock, wherever t lies in the mooring step after it, and refuses only a time outside that step. A restart on the last glue step of an interval reaches the solve with no CalcOutput since the commit, so the committed loads the load predictor shifts are evaluated there first. A restarted run matches the uninterrupted one bit for bit from every glue step of the interval, in .CD.out and the range graphs. A checkpoint written before this change cannot be restarted: OtherState has a new field. Signed-off-by: Jae Hoon Seo --- doc/openfast.rst | 5 +- src/openfast/CableDyn_OF.f90 | 191 ++++++++++++++++++++++------- src/openfast/CableDyn_Registry.txt | 1 + src/openfast/CableDyn_Types.f90 | 18 +++ 4 files changed, 169 insertions(+), 46 deletions(-) diff --git a/doc/openfast.rst b/doc/openfast.rst index 7292de4..67b2375 100644 --- a/doc/openfast.rst +++ b/doc/openfast.rst @@ -392,9 +392,8 @@ without an auxiliary process: with ``dtM`` larger than ``DT``, the loads of the solve before it (the load predictor), so a restart continues the run bit for bit at any ``dtM``. The accumulated range graphs (``LINES`` flag ``r``) are part of it, so a restarted run writes the range files of an uninterrupted one. A - checkpoint is accepted only on a committed CableDyn - step, so choose ``ChkptTime`` as a whole multiple of ``dtM``; a checkpoint written between two - commits stops the restart with a message that says so. On restart the module writes a + checkpoint can fall on any glue step: ``ChkptTime`` need not be a multiple of ``dtM``, and a run + restarted between two commits goes on as the uninterrupted run. On restart the module writes a temporary deck copy, ``.CD.rst.dat``, beside the ``MooringFile``, rebuilds the model from it and removes it; files the deck names (``bathymetryFile``, a WaterKin file, Syrope tables) resolve against the ``MooringFile`` folder as in the original run. Keep restart files with the exact executable and complete input set. diff --git a/src/openfast/CableDyn_OF.f90 b/src/openfast/CableDyn_OF.f90 index 36a03d2..a6b6361 100644 --- a/src/openfast/CableDyn_OF.f90 +++ b/src/openfast/CableDyn_OF.f90 @@ -280,6 +280,9 @@ END FUNCTION cd_cpu_guard_armed REAL(DbKi) :: t_commit_prev = -1.0_DbKi REAL(DbKi) :: t_run_end = -1.0_DbKi LOGICAL :: rewound = .FALSE. + ! restored_unserved: a rebuilt, supercycled instance whose committed loads have not been + ! evaluated yet (serve_committed_loads) + LOGICAL :: restored_unserved = .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. @@ -1270,13 +1273,13 @@ SUBROUTINE CD_Init(InitInp, u, p, x, xd, z, other, y, m, DTcoupling, InitOut, Er ! normal step, so a state violating the invariant cannot even advance, let alone ! be packed. The vector's real payload is the COMMITTED boundary kinematics: the ! vessel-end kinematics the last advance committed differ in the last bits from the - ! restored input mesh (the glue's input at the restart time, not the extrapolated one - ! the advance used), and on a point deck that round-off is amplified through the + ! glue's input at that time (the advance used an extrapolated one), and on a point + ! deck that round-off is amplified through the ! explicit-point window. A finite-EI cable carries its committed boundary in its own ! mirror block (CD_HFMF_BoundaryMirrorSize). IF (Inst(id)%agg%has_sys) THEN ! the committed boundary kinematics [q; v; a] of the EI=0 lines' coupled endpoints: - ! the rebuild moves them to the restored input mesh, then reloads them from here + ! the rebuild moves them to the committed input, then reloads them from here nstates = nstates + 3*CD_System_NSystemCoupledDOF( Inst(id)%agg%sys%fast%system ) END IF IF (CD_AGG_NDynamicPoints( Inst(id)%agg ) > 0 .AND. Inst(id)%agg%has_sys) THEN @@ -1315,6 +1318,16 @@ SUBROUTINE CD_Init(InitInp, u, p, x, xd, z, other, y, m, DTcoupling, InitOut, Er other%restart_serial = g_serial other%t_commit = Inst(id)%t_commit other%pred_t = -1.0_DbKi ! no previous commit yet (load predictor) + ! what the state at t = 0 stands on, for a checkpoint written before the first commit + CALL CD_AGG_GetMovingPointMesh( Inst(id)%agg, Inst(id)%sv_pos, Inst(id)%sv_vel, Inst(id)%sv_acc, & + Inst(id)%ws_fld, es, em, orientation=Inst(id)%sv_orient, & + angular_velocity=Inst(id)%sv_omega, angular_acceleration=Inst(id)%sv_alpha, & + cable_loads=.FALSE. ) + IF (es /= CD_AGG_OK) THEN + CALL SetErrStat( ErrID_Fatal, 'CableDyn mooring module: '//TRIM(em), ErrStat, ErrMsg, RoutineName ); RETURN + END IF + CALL save_commit_input( Inst(id), other, Inst(id)%sv_pos, Inst(id)%sv_vel, Inst(id)%sv_acc, & + Inst(id)%sv_orient, Inst(id)%sv_omega, Inst(id)%sv_alpha, u ) ! ------------------------------------------------------------------------------ ! Output channels: the deck OUTPUTS list in the OrcaFlex vocabulary, emitted through @@ -1518,9 +1531,12 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) !! coupled mesh in u -- exactly the data an `openfast -restart` run unpacks. The !! deck is re-parsed and the static build re-run (structure, meshes, hydro !! configuration), then the committed dynamic state is overlaid: coupled endpoint - !! kinematics from the input mesh, line interiors from the mirror with the - !! acceleration re-derived (the EI=0 mirror is MoorDyn-layout [rd; r]), and each - !! cable's [v; q; a] free-DOF mirror reloaded exactly with the restart clock. + !! kinematics and line-control command as the last commit was reached with them + !! (other%commit_input), line interiors from the mirror (the EI=0 mirror is + !! MoorDyn-layout [rd; r; rdd]), and each cable's [v; q; a] free-DOF mirror, all at the + !! clock of that commit. The restart time t can lie anywhere in the mooring step that + !! follows the commit: between commits nothing of the solver moves, and the input of the + !! coming solve is the glue's, so a run restarted there goes on as the uninterrupted one. REAL(DbKi), INTENT(IN ) :: t TYPE(CD_InputType), INTENT(IN ) :: u1 TYPE(CD_ParameterType), INTENT(IN ) :: p @@ -1554,16 +1570,13 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) RETURN END IF - ! A checkpoint written mid-mooring-interval carries interiors from the LAST - ! committed solve (other%t_commit) while the glue would overlay the boundary at - ! the newer t -- a state no continuous run ever had. Fail closed: restarts are - ! valid only on committed interval boundaries (choose ChkptTime as a multiple - ! of the mooring step dtM). - IF (ABS(REAL(t, DbKi) - other%t_commit) > 1.0E-6_DbKi*MAX(1.0_DbKi, ABS(other%t_commit))) THEN - CALL SetErrStat( ErrID_Fatal, 'CableDyn mooring module: the checkpoint was written at t = '// & - TRIM(Num2LStr(t))//' s, between mooring commits (last commit at '// & - TRIM(Num2LStr(other%t_commit))//' s). Choose ChkptTime as a multiple of the '// & - 'mooring step dtM so the restored state is a committed interval boundary.', & + ! The state of a run at t is its last commit, at most one mooring step back: a restored + ! time and commit further apart are not a state of this run. Fail closed. + IF (t < other%t_commit - 1.0E-6_DbKi*MAX(1.0_DbKi, ABS(other%t_commit)) .OR. & + t > other%t_commit + p%dtM0 - p%dtCoupling + 1.0E-6_DbKi*MAX(1.0_DbKi, ABS(other%t_commit))) THEN + CALL SetErrStat( ErrID_Fatal, 'CableDyn mooring module: the restored state at t = '// & + TRIM(Num2LStr(t))//' s has its last commit at '//TRIM(Num2LStr(other%t_commit))// & + ' s, outside the mooring step dtM = '//TRIM(Num2LStr(p%dtM0))//' s before it.', & ErrStat, ErrMsg, RoutineName ) RETURN END IF @@ -1636,8 +1649,8 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) END IF ! Dynamic (Free/Connect) point states ride two trailing mirror blocks: the compact ! coupled-motion vector (the committed endpoint/boundary kinematics -- the vessel - ! ends' committed accelerations differ at round-off from the restored input mesh, - ! which a point deck amplifies) and the per-point store. A deck whose layout + ! ends' committed accelerations differ at round-off from the glue's input at that + ! time, which a point deck amplifies) and the per-point store. A deck whose layout ! changed since the checkpoint fails closed on the mirror-extent checks. Inst(id)%ncp = ncp @@ -1677,16 +1690,14 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) END IF END IF - ! overlay the committed dynamic state: coupled endpoints from the restored mesh... - CALL mesh_to_arrays( u1%CoupledKinematics(1), ncp, Inst(id)%ws_pos, Inst(id)%ws_vel, & - Inst(id)%ws_acc, Inst(id)%ws_orient, Inst(id)%ws_omega, Inst(id)%ws_alpha, & - ErrStat2, ErrMsg2 ) + ! overlay the committed dynamic state: coupled endpoints as the last commit took them... + CALL restore_commit_kinematics( Inst(id), other, ErrStat2, ErrMsg2 ) CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) IF (ErrStat >= AbortErrLev) RETURN ! a cable restrained in torsion first takes its committed state: moving its fairlead ! frame from the static pose to the restart pose is bounded (pi/2) against the twist ! it had at the checkpoint, not at t = 0 - CALL reload_cable_mirrors( Inst(id), m, x%states, REAL(t, wp), .TRUE., koff, ErrStat2, ErrMsg2 ) + CALL reload_cable_mirrors( Inst(id), m, x%states, REAL(other%t_commit, wp), .TRUE., koff, ErrStat2, ErrMsg2 ) CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) IF (ErrStat >= AbortErrLev) RETURN CALL CD_AGG_UpdateStates_Moving( Inst(id)%agg, Inst(id)%ws_pos, Inst(id)%ws_vel, Inst(id)%ws_acc, es, em, & @@ -1707,7 +1718,16 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) ! line interiors and cable mirrors reload below, which bring back the committed state ! of the uninterrupted run, a cable's committed force included. IF (ALLOCATED(u1%DeltaL)) THEN - CALL CD_AGG_Apply_LineControl( Inst(id)%agg, REAL(u1%DeltaL, wp), REAL(u1%DeltaLdot, wp), es, em, & + k = 24_IntKi*ncp + ic = SIZE(u1%DeltaL) + IF (SIZE(other%commit_input) /= k + 2_IntKi*ic) THEN + CALL SetErrStat( ErrID_Fatal, 'CableDyn mooring module: checkpoint rebuild: the committed '// & + 'line-control command does not match the control channels.', & + ErrStat, ErrMsg, RoutineName ) + RETURN + END IF + CALL CD_AGG_Apply_LineControl( Inst(id)%agg, REAL(other%commit_input(k + 1:k + ic), wp), & + REAL(other%commit_input(k + ic + 1:k + 2_IntKi*ic), wp), es, em, & restored=.TRUE. ) IF (es /= CD_AGG_OK) THEN CALL SetErrStat( ErrID_Fatal, 'CableDyn mooring module: line-control apply after the '// & @@ -1717,12 +1737,12 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) END IF ! ...line interiors + cable free-DOF mirrors, exactly (committed accelerations ! reload from the mirror -- shared helper with the state-const CalcOutput) - CALL reload_interiors_ary( Inst(id), m, x%states, REAL(t, wp), ErrStat2, ErrMsg2 ) + CALL reload_interiors_ary( Inst(id), m, x%states, REAL(other%t_commit, wp), ErrStat2, ErrMsg2 ) CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) IF (ErrStat >= AbortErrLev) RETURN ! restore the ambient-fluid fields the reference's committed state carried. The last - ! pre-checkpoint solve sampled the WaveField at ITS interval end (= the restart time) at + ! pre-checkpoint solve sampled the WaveField at ITS interval end (= the last commit) at ! the node positions of its START, and held them: the checkpoint carries them, and with ! supercycling they act on the loads served until the next solve. Init saves the first ! fields, so a checkpoint of an instance that takes host fluid always carries them; @@ -1734,7 +1754,8 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) Inst(id)%active = .TRUE. ! sample_wavefield reads through the live instance IF (use_extf) THEN CALL restore_held_fluid( Inst(id), other, restored, ErrStat2, ErrMsg2 ) - IF (ErrStat2 < AbortErrLev .AND. .NOT. restored) CALL sample_wavefield( Inst(id), p, m, t, ErrStat2, ErrMsg2 ) + IF (ErrStat2 < AbortErrLev .AND. .NOT. restored) & + CALL sample_wavefield( Inst(id), p, m, other%t_commit, ErrStat2, ErrMsg2 ) CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) IF (ErrStat >= AbortErrLev) THEN CALL release_instance( id ) @@ -1819,6 +1840,7 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) Inst(id)%last_n = -1_IntKi Inst(id)%last_solve_n = -1_IntKi Inst(id)%loads_cached = .FALSE. + Inst(id)%restored_unserved = Inst(id)%nsuper > 1_IntKi ! the load predictor's previous commit (shift_predictor_loads) Inst(id)%pred_t = other%pred_t IF (Inst(id)%pred_t >= 0.0_DbKi) THEN @@ -1855,7 +1877,7 @@ SUBROUTINE CD_UpdateStates( t, n, u, t_array, p, x, xd, z, other, m, ErrStat, Er CHARACTER(*) , INTENT( OUT) :: ErrMsg character(*), parameter :: RoutineName = 'CD_UpdateStates' - INTEGER(IntKi) :: ErrStat2, es, id, niter, iu_t, i_u + INTEGER(IntKi) :: ErrStat2, es, id, niter INTEGER(IntKi) :: prof_c0, prof_c1, prof_rate, prof_phase0 CHARACTER(ErrMsgLen) :: ErrMsg2 CHARACTER(ErrMsgLen) :: em @@ -1864,20 +1886,9 @@ SUBROUTINE CD_UpdateStates( t, n, u, t_array, p, x, xd, z, other, m, ErrStat, Er ErrStat = ErrID_None ErrMsg = '' ! CHECKPOINT-RESTART: a restored OtherState that does not match the live instance - ! (fresh process, or a same-process rewind) rebuilds the solver from p + x + u. - ! The overlay must use the input AT the restart time t -- u(1) here is the glue's - ! 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. + ! (fresh process, or a same-process rewind) rebuilds the solver from p + x + other. 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 - iu_t = i_u - EXIT - END IF - END DO - CALL rebuild_from_checkpoint( t, u(iu_t), p, x, other, m, ErrStat2, ErrMsg2 ) + CALL rebuild_from_checkpoint( t, u(1), p, x, other, m, ErrStat2, ErrMsg2 ) CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) IF (ErrStat >= AbortErrLev) RETURN END IF @@ -1959,6 +1970,11 @@ SUBROUTINE CD_UpdateStates( t, n, u, t_array, p, x, xd, z, other, m, ErrStat, Er END IF CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) IF (ErrStat < AbortErrLev) THEN + IF (Inst(id)%restored_unserved) THEN + CALL serve_committed_loads( Inst(id), ErrStat2, ErrMsg2 ) + CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) + IF (ErrStat >= AbortErrLev) RETURN + END IF ! Load predictor: a new mooring step (not a correction re-entry over the same n) ! makes the loads served since the last commit the previous ones. IF (Inst(id)%nsuper > 1_IntKi .AND. Inst(id)%agg%predict_loads .AND. & @@ -2040,6 +2056,8 @@ SUBROUTINE CD_UpdateStates( t, n, u, t_array, p, x, xd, z, other, m, ErrStat, Er Inst(id)%t_commit = t + p%dtCoupling Inst(id)%t_run_end = Inst(id)%t_commit other%t_commit = Inst(id)%t_commit + CALL save_commit_input( Inst(id), other, Inst(id)%ws_pos, Inst(id)%ws_vel, Inst(id)%ws_acc, & + Inst(id)%ws_orient, Inst(id)%ws_omega, Inst(id)%ws_alpha, Inst(id)%u_interp ) CALL pack_state_mirror( Inst(id), m, x, ErrStat2, ErrMsg2 ) CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) ! one range-graph sample per commit, here where the OtherState can carry it: a @@ -2701,6 +2719,7 @@ SUBROUTINE CD_CalcOutput( t, u, p, x, xd, z, other, y, m, ErrStat, ErrMsg ) END IF Inst(id)%loads_cached = .TRUE. Inst(id)%loads_at_n = Inst(id)%last_solve_n + Inst(id)%restored_unserved = .FALSE. END IF ! restore the committed coupled kinematics saved above (state-const CalcOutput), @@ -2947,6 +2966,93 @@ SUBROUTINE mirror_predictor_loads( inst_e, other ) other%pred_loads(nl + 1:2*nl) = RESHAPE(REAL(inst_e%pred_mom, DbKi), [nl]) END SUBROUTINE mirror_predictor_loads + SUBROUTINE save_commit_input( inst_e, other, pos, vel, acc, orient, omega, alpha, u_ctrl ) + !! What the state now committed was reached with, into the OtherState: the coupled + !! kinematics [position; velocity; acceleration; orientation; angular velocity; angular + !! acceleration] and the line-control command [DeltaL; DeltaLdot] of u_ctrl. The glue's + !! input moves on between commits, so a restart inside the mooring step that follows + !! rebuilds from this, not from the input at its own time. + TYPE(CD_OF_Instance), INTENT(IN ) :: inst_e + TYPE(CD_OtherStateType), INTENT(INOUT) :: other + REAL(wp), INTENT(IN ) :: pos(:,:), vel(:,:), acc(:,:), orient(:,:,:), omega(:,:), alpha(:,:) + TYPE(CD_InputType), INTENT(IN ) :: u_ctrl + INTEGER(IntKi) :: n3, nc, k + n3 = 3_IntKi*inst_e%ncp + nc = 0_IntKi + IF (ALLOCATED(u_ctrl%DeltaL)) nc = SIZE(u_ctrl%DeltaL) + IF (ALLOCATED(other%commit_input)) THEN + IF (SIZE(other%commit_input) /= 8_IntKi*n3 + 2_IntKi*nc) DEALLOCATE (other%commit_input) + END IF + IF (.NOT. ALLOCATED(other%commit_input)) ALLOCATE (other%commit_input(8_IntKi*n3 + 2_IntKi*nc)) + other%commit_input(1:n3) = RESHAPE(REAL(pos, DbKi), [n3]) + other%commit_input(n3 + 1:2*n3) = RESHAPE(REAL(vel, DbKi), [n3]) + other%commit_input(2*n3 + 1:3*n3) = RESHAPE(REAL(acc, DbKi), [n3]) + other%commit_input(3*n3 + 1:6*n3) = RESHAPE(REAL(orient, DbKi), [3*n3]) + other%commit_input(6*n3 + 1:7*n3) = RESHAPE(REAL(omega, DbKi), [n3]) + other%commit_input(7*n3 + 1:8*n3) = RESHAPE(REAL(alpha, DbKi), [n3]) + IF (nc > 0_IntKi) THEN + k = 8_IntKi*n3 + other%commit_input(k + 1:k + nc) = REAL(u_ctrl%DeltaL, DbKi) + other%commit_input(k + nc + 1:k + 2_IntKi*nc) = REAL(u_ctrl%DeltaLdot, DbKi) + END IF + END SUBROUTINE save_commit_input + + SUBROUTINE restore_commit_kinematics( inst_e, other, ErrStat, ErrMsg ) + !! The coupled kinematics of the last commit (save_commit_input) into the workspace. + TYPE(CD_OF_Instance), INTENT(INOUT) :: inst_e + TYPE(CD_OtherStateType), INTENT(IN ) :: other + INTEGER(IntKi), INTENT( OUT) :: ErrStat + CHARACTER(*), INTENT( OUT) :: ErrMsg + INTEGER(IntKi) :: ncp, n3 + ErrStat = ErrID_None + ErrMsg = '' + ncp = inst_e%ncp + n3 = 3_IntKi*ncp + IF (.NOT. ALLOCATED(other%commit_input)) THEN + ErrStat = ErrID_Fatal + ErrMsg = 'CableDyn mooring module: checkpoint rebuild: the checkpoint carries no committed input.' + RETURN + END IF + IF (SIZE(other%commit_input) < 8_IntKi*n3) THEN + ErrStat = ErrID_Fatal + ErrMsg = 'CableDyn mooring module: checkpoint rebuild: the committed input does not match the coupled points.' + RETURN + END IF + inst_e%ws_pos = RESHAPE(REAL(other%commit_input(1:n3), wp), [3, ncp]) + inst_e%ws_vel = RESHAPE(REAL(other%commit_input(n3 + 1:2*n3), wp), [3, ncp]) + inst_e%ws_acc = RESHAPE(REAL(other%commit_input(2*n3 + 1:3*n3), wp), [3, ncp]) + inst_e%ws_orient = RESHAPE(REAL(other%commit_input(3*n3 + 1:6*n3), wp), [3, 3, ncp]) + inst_e%ws_omega = RESHAPE(REAL(other%commit_input(6*n3 + 1:7*n3), wp), [3, ncp]) + inst_e%ws_alpha = RESHAPE(REAL(other%commit_input(7*n3 + 1:8*n3), wp), [3, ncp]) + END SUBROUTINE restore_commit_kinematics + + SUBROUTINE serve_committed_loads( inst_e, ErrStat, ErrMsg ) + !! A rebuild on the last glue step of a mooring interval reaches the solve with no + !! CalcOutput since the commit, though the uninterrupted run evaluated the committed loads + !! there and the load predictor shifts them. They are evaluated here, as CalcOutput does; + !! the channels are not needed, as the advance that follows invalidates the cache. + TYPE(CD_OF_Instance), INTENT(INOUT) :: inst_e + INTEGER(IntKi), INTENT( OUT) :: ErrStat + CHARACTER(*), INTENT( OUT) :: ErrMsg + INTEGER(IntKi) :: es + CHARACTER(ErrMsgLen) :: em + ErrStat = ErrID_None + ErrMsg = '' + CALL CD_AGG_CalcOutput( inst_e%agg, es, em ) + IF (es == CD_AGG_OK) & + CALL CD_AGG_GetMovingPointMesh( inst_e%agg, inst_e%sv_pos, inst_e%sv_vel, inst_e%sv_acc, inst_e%ws_fld, & + es, em, orientation=inst_e%sv_orient, moment=inst_e%ws_moment, & + angular_velocity=inst_e%sv_omega, angular_acceleration=inst_e%sv_alpha ) + IF (es /= CD_AGG_OK) THEN + ErrStat = ErrID_Fatal + ErrMsg = 'CableDyn mooring module: committed loads of the restored state: '//TRIM(em) + RETURN + END IF + inst_e%loads_cached = .TRUE. + inst_e%loads_at_n = inst_e%last_solve_n + inst_e%restored_unserved = .FALSE. + END SUBROUTINE serve_committed_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. @@ -4751,8 +4857,7 @@ SUBROUTINE reload_interiors_ary(inst_, m, states, t_clock, ErrStat, ErrMsg) ! guarantees the vector's values regardless). npd = 9*CD_AGG_NDynamicPoints( inst_%agg ) IF (npd == 0 .AND. inst_%agg%has_sys) THEN - ! no dynamic points: restore the committed boundary kinematics (the restored input - ! mesh carries the glue's input at the restart time, not the committed kinematics) + ! no dynamic points: restore the committed boundary kinematics ncd = CD_System_NSystemCoupledDOF( inst_%agg%sys%fast%system ) IF (koff + 3*ncd > SIZE(states)) THEN CALL SetErrStat( ErrID_Fatal, 'CableDyn mooring module: the coupled-motion mirror extends '// & diff --git a/src/openfast/CableDyn_Registry.txt b/src/openfast/CableDyn_Registry.txt index a0281fe..41e8a19 100644 --- a/src/openfast/CableDyn_Registry.txt +++ b/src/openfast/CableDyn_Registry.txt @@ -363,6 +363,7 @@ typedef ^ ^ DbKi pred_t - typedef ^ ^ DbKi pred_loads {:} - - "load predictor: the coupled forces (3 x ncp) and then moments (3 x ncp) of that solve" - typedef ^ ^ DbKi fluid_held {:} - - "the held ambient-fluid fields of the last committed mooring step (velocity, acceleration, free-surface elevation, dynamic pressure at the line nodes), restored at a restart instead of re-sampled" - typedef ^ ^ DbKi range_state {:} - - "the accumulated range graphs (LINES flag r) through the last committed mooring step, in double precision, restored at a restart" - +typedef ^ ^ DbKi commit_input {:} - - "what the last commit was reached with: the coupled kinematics (position, velocity, acceleration, orientation, angular velocity, angular acceleration; 24 x ncp) then the line-control command (DeltaL, DeltaLdot); a rebuild stands on it, at any time of the mooring step after the commit" - ## ============================== Parameters ============================================================================================================================================ typedef ^ ParameterType IntKi nLineTypes - 0 - "number of line types" "" diff --git a/src/openfast/CableDyn_Types.f90 b/src/openfast/CableDyn_Types.f90 index ff9b78b..a7f24bd 100644 --- a/src/openfast/CableDyn_Types.f90 +++ b/src/openfast/CableDyn_Types.f90 @@ -397,6 +397,7 @@ MODULE CableDyn_Types REAL(DbKi) , DIMENSION(:), ALLOCATABLE :: pred_loads !< load predictor: the coupled forces (3 x ncp) and then moments (3 x ncp) of that solve [-] REAL(DbKi) , DIMENSION(:), ALLOCATABLE :: fluid_held !< the held ambient-fluid fields of the last committed mooring step (velocity, acceleration, free-surface elevation, dynamic pressure at the line nodes), restored at a restart instead of re-sampled [-] REAL(DbKi) , DIMENSION(:), ALLOCATABLE :: range_state !< the accumulated range graphs (LINES flag r) through the last committed mooring step, in double precision, restored at a restart [-] + REAL(DbKi) , DIMENSION(:), ALLOCATABLE :: commit_input !< what the last commit was reached with: the coupled kinematics (position, velocity, acceleration, orientation, angular velocity, angular acceleration; 24 x ncp) then the line-control command (DeltaL, DeltaLdot); a rebuild stands on it, at any time of the mooring step after the commit [-] END TYPE CD_OtherStateType ! ======================= ! ========= CD_ParameterType ======= @@ -3019,6 +3020,18 @@ subroutine CD_CopyOtherState(SrcOtherStateData, DstOtherStateData, CtrlCode, Err end if DstOtherStateData%range_state = SrcOtherStateData%range_state end if + if (allocated(SrcOtherStateData%commit_input)) then + LB(1:1) = lbound(SrcOtherStateData%commit_input) + UB(1:1) = ubound(SrcOtherStateData%commit_input) + if (.not. allocated(DstOtherStateData%commit_input)) then + allocate(DstOtherStateData%commit_input(LB(1):UB(1)), stat=ErrStat2) + if (ErrStat2 /= 0) then + call SetErrStat(ErrID_Fatal, 'Error allocating DstOtherStateData%commit_input.', ErrStat, ErrMsg, RoutineName) + return + end if + end if + DstOtherStateData%commit_input = SrcOtherStateData%commit_input + end if end subroutine subroutine CD_DestroyOtherState(OtherStateData, ErrStat, ErrMsg) @@ -3037,6 +3050,9 @@ subroutine CD_DestroyOtherState(OtherStateData, ErrStat, ErrMsg) if (allocated(OtherStateData%range_state)) then deallocate(OtherStateData%range_state) end if + if (allocated(OtherStateData%commit_input)) then + deallocate(OtherStateData%commit_input) + end if end subroutine subroutine CD_PackOtherState(RF, Indata) @@ -3050,6 +3066,7 @@ subroutine CD_PackOtherState(RF, Indata) call RegPackAlloc(RF, InData%pred_loads) call RegPackAlloc(RF, InData%fluid_held) call RegPackAlloc(RF, InData%range_state) + call RegPackAlloc(RF, InData%commit_input) if (RegCheckErr(RF, RoutineName)) return end subroutine @@ -3067,6 +3084,7 @@ subroutine CD_UnPackOtherState(RF, OutData) call RegUnpackAlloc(RF, OutData%pred_loads); if (RegCheckErr(RF, RoutineName)) return call RegUnpackAlloc(RF, OutData%fluid_held); if (RegCheckErr(RF, RoutineName)) return call RegUnpackAlloc(RF, OutData%range_state); if (RegCheckErr(RF, RoutineName)) return + call RegUnpackAlloc(RF, OutData%commit_input); if (RegCheckErr(RF, RoutineName)) return end subroutine subroutine CD_CopyParam(SrcParamData, DstParamData, CtrlCode, ErrStat, ErrMsg) From 4bce9e7c134f320b6fe758d2f60c2056ddbff413 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Sun, 8 Nov 2026 00:57:12 +0900 Subject: [PATCH 2/6] fix(openfast): remove an earlier run's range graphs once the deck is parsed The range graphs of an earlier run went late in CD_Init, so an initialisation that stopped before that, on the statics, on a refusal or on a warning at AbortLevel WARNING, left them to be taken for this run's. They now go as soon as the parsed deck names them, before anything else of the initialisation can fail. A rebuild of a run under way passes no root and leaves them. A deck that does not parse names no line, and a module that stops before CableDyn starts never reaches it: both leave every output of the earlier run as it was. Signed-off-by: Jae Hoon Seo --- doc/outputs.rst | 5 +-- src/CableDyn_DeckDriver.f90 | 22 +++++++++++-- src/CableDyn_OpenFAST_Aggregate.f90 | 7 +++-- src/CableDyn_RangeOutput.f90 | 47 ++++++++++++++++++---------- src/openfast/CableDyn_OF.f90 | 27 ++++++++-------- tests/test_openfast_aggregate.f90 | 48 +++++++++++++++++++++++++++++ 6 files changed, 118 insertions(+), 38 deletions(-) diff --git a/doc/outputs.rst b/doc/outputs.rst index c66e9e6..f0d847a 100644 --- a/doc/outputs.rst +++ b/doc/outputs.rst @@ -393,8 +393,9 @@ corrects counts once, with its corrected state. The accumulators are double prec 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``, 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`` +The run removes a file left by an earlier run as soon as its deck is parsed, and a run that stops on +an error after that, in any module, leaves none. A deck that does not parse, or a module that stops +before CableDyn starts, leaves every output of the earlier run as it was. 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. diff --git a/src/CableDyn_DeckDriver.f90 b/src/CableDyn_DeckDriver.f90 index 303dbec..2fa6c39 100644 --- a/src/CableDyn_DeckDriver.f90 +++ b/src/CableDyn_DeckDriver.f90 @@ -151,7 +151,8 @@ MODULE CableDyn_DeckDriver CD_Sea_Parse_Train, CD_Sea_Add_Train, CD_Sea_Synthesise, CD_Sea_Kinematics USE CableDyn_RangeOutput, ONLY: CD_RangeSet, CD_RangeLine, CD_Range_Setup, CD_Range_End, CD_Range_Needs_Sample, & CD_Range_Commit_Sample, CD_Range_Clearance, CD_Range_TDP_Evaluate, & - CD_Range_TDP_Reference, CD_Range_Write_Files, CD_Is_TDP_Channel, & + CD_Range_TDP_Reference, CD_Range_Write_Files, CD_Range_Discard_File, & + CD_Is_TDP_Channel, & CD_Parse_TDP_Channel, CD_RANGE_OK, CD_RANGE_BADINPUT, CD_RQ_TENSION, & CD_RQ_CURVATURE, CD_RQ_BEND, CD_RQ_DECLINATION, CD_RQ_CLEARANCE, CD_TDP_N, & CD_RQ_TORQUE, CD_RQ_TWIST @@ -7414,7 +7415,8 @@ SUBROUTINE CD_Init_Deck_Aggregate(deck_path, dt, agg, ErrStat, ErrMsg, env_gravi ptfm_init, external_fluid, coupled_positions, & n_turbines, turbine_ref_pos, farm_ptfm_init, forbid_deck_ambient, run_tmax, & range_files, host_orientation, static_current, prescribed_motion, & - static_run, deck_current, deck_turbines, static_deck_current) + static_run, deck_current, deck_turbines, static_deck_current, & + range_discard_root) !! Parse a MIXED deck ONCE and partition its lines into {EI=0 mooring} and !! {finite-EI cable}, returning the raw parts the OpenFAST aggregate facade wraps: !! the EI=0 mooring SYSTEM built from the mooring subset (over ALL deck points, the @@ -7485,6 +7487,11 @@ SUBROUTINE CD_Init_Deck_Aggregate(deck_path, dt, agg, ErrStat, ErrMsg, env_gravi ! standalone mixed route, the single-turbine OpenFAST module); a host that does not (a FAST.Farm ! shared mooring, the C API) leaves it out, and the flag fails closed there. LOGICAL, INTENT(IN), OPTIONAL :: range_files + ! range_discard_root: the output root of a run that starts here: the range files of an earlier + ! run go as soon as the parsed deck names them, so nothing that fails after this point (the + ! statics, the host's own checks) leaves one to be taken for this run's. A rebuild of a run + ! already under way leaves it out. + CHARACTER(*), INTENT(IN), OPTIONAL :: range_discard_root ! host_orientation: the caller passes each moving point's orientation with every update ! (the OpenFAST and FAST.Farm shells, and the standalone mixed route, which holds the frame ! or turns it with its vesselMotion table), which turns the fairlead frame of a finite-EI @@ -7575,6 +7582,17 @@ SUBROUTINE CD_Init_Deck_Aggregate(deck_path, dt, agg, ErrStat, ErrMsg, env_gravi env_rho_water=env_rho_water, env_wtrdpth=env_wtrdpth, allow_turbine=.NOT. own_turbines, & failures=fails, controls=ctrls, run_tmax=run_tmax, host_coupled_objects=.TRUE., & static_run=static_run) + IF (ErrStat == CD_DECKDRV_OK .AND. PRESENT(range_discard_root)) THEN + DO i = 1, SIZE(lines) + IF (.NOT. line_output_has_flag(lines(i)%outputs, 'r')) CYCLE + CALL CD_Range_Discard_File(range_discard_root, lines(i)%id, ErrStat, ErrMsg) + IF (ErrStat /= CD_RANGE_OK) THEN + ErrStat = CD_DECKDRV_BADINPUT + RETURN + END IF + END DO + ErrStat = CD_DECKDRV_OK + END IF IF (ErrStat == CD_DECKDRV_OK) THEN host_turns = .FALSE. IF (PRESENT(host_orientation)) host_turns = host_orientation diff --git a/src/CableDyn_OpenFAST_Aggregate.f90 b/src/CableDyn_OpenFAST_Aggregate.f90 index 6ca8f96..e10cac4 100644 --- a/src/CableDyn_OpenFAST_Aggregate.f90 +++ b/src/CableDyn_OpenFAST_Aggregate.f90 @@ -296,7 +296,7 @@ SUBROUTINE CD_AGG_Init_From_Deck(self, deck_path, dt, ErrStat, ErrMsg, env_gravi ptfm_init, external_fluid, coupled_positions, & n_turbines, turbine_ref_pos, farm_ptfm_init, forbid_deck_ambient, run_tmax, & range_files, host_orientation, static_current, prescribed_motion, static_run, & - deck_current, deck_turbines, static_deck_current) + deck_current, deck_turbines, static_deck_current, range_discard_root) !! Build the aggregate from ONE mixed deck: partition its lines into {EI=0 mooring} !! and {finite-EI cable}, wrap the mooring subset into a CD_FMF, and own one CD_HFMF !! per cable. Same signature philosophy as CD_FMF_Init_From_Deck (host environment @@ -332,6 +332,8 @@ SUBROUTINE CD_AGG_Init_From_Deck(self, deck_path, dt, ErrStat, ErrMsg, env_gravi LOGICAL, INTENT(IN), OPTIONAL :: forbid_deck_ambient ! range_files: the caller samples and writes the range graphs of the LINES flag r (CD_AGG_Range_*) LOGICAL, INTENT(IN), OPTIONAL :: range_files + ! range_discard_root: the output root of a run that starts here (see CD_Init_Deck_Aggregate) + CHARACTER(*), INTENT(IN), OPTIONAL :: range_discard_root ! host_orientation: the caller passes the moving-point orientations with every update, ! which a finite-EI line restrained in torsion requires (see CD_Init_Deck_Aggregate) LOGICAL, INTENT(IN), OPTIONAL :: host_orientation @@ -394,7 +396,8 @@ SUBROUTINE CD_AGG_Init_From_Deck(self, deck_path, dt, ErrStat, ErrMsg, env_gravi range_files=range_files, host_orientation=host_orientation, & static_current=static_current, prescribed_motion=prescribed_motion, & static_run=static_run, deck_current=deck_current, & - deck_turbines=deck_turbines, static_deck_current=static_deck_current) + deck_turbines=deck_turbines, static_deck_current=static_deck_current, & + range_discard_root=range_discard_root) IF (PRESENT(external_fluid)) THEN candidate%built_extfluid = external_fluid END IF diff --git a/src/CableDyn_RangeOutput.f90 b/src/CableDyn_RangeOutput.f90 index f43cbf8..6b7210e 100644 --- a/src/CableDyn_RangeOutput.f90 +++ b/src/CableDyn_RangeOutput.f90 @@ -38,7 +38,7 @@ MODULE CableDyn_RangeOutput PUBLIC :: CD_RangeLine, CD_RangeSet PUBLIC :: CD_Range_Setup, CD_Range_End, CD_Range_Needs_Sample, CD_Range_Commit_Sample PUBLIC :: CD_Range_Clearance, CD_Range_TDP_Evaluate, CD_Range_TDP_Reference - PUBLIC :: CD_Range_Write_Files, CD_Range_Discard_Files + PUBLIC :: CD_Range_Write_Files, CD_Range_Discard_Files, CD_Range_Discard_File PUBLIC :: CD_Range_Has_Graph, CD_Range_Mirror_Size, CD_Range_Get_State, CD_Range_Set_State PUBLIC :: CD_Is_TDP_Channel, CD_Parse_TDP_Channel @@ -673,31 +673,44 @@ SUBROUTINE CD_Range_Discard_Files(set, out_root, ErrStat, ErrMsg) CHARACTER(*), INTENT(IN) :: out_root INTEGER, INTENT(OUT) :: ErrStat CHARACTER(*), INTENT(OUT) :: ErrMsg - INTEGER :: il, unit, ios - CHARACTER(LEN(out_root) + 32) :: path - CHARACTER(16) :: id_text + INTEGER :: il ErrStat = CD_RANGE_OK ErrMsg = '' IF (.NOT. set%active) RETURN DO il = 1, SIZE(set%lines) IF (.NOT. set%lines(il)%want_range) CYCLE - WRITE (id_text, '(I0)') set%lines(il)%line_id - path = TRIM(out_root)//'.Line'//TRIM(id_text)//'.range.out' - IF (.NOT. CD_Path_Openable(TRIM(path), .FALSE.)) CYCLE ! no such file - CALL CD_Open_Write(unit, TRIM(path), ios, status='OLD') - IF (ios == 0) CLOSE (unit, STATUS='DELETE', IOSTAT=ios) - IF (ios == 0) THEN - IF (CD_Path_Openable(TRIM(path), .FALSE.)) ios = 1 - END IF - IF (ios /= 0) THEN - ErrStat = CD_RANGE_BADINPUT - ErrMsg = 'CableDyn_RangeOutput: cannot replace the range-graph output file of an earlier run: '//TRIM(path) - RETURN - END IF + CALL CD_Range_Discard_File(out_root, set%lines(il)%line_id, ErrStat, ErrMsg) + IF (ErrStat /= CD_RANGE_OK) RETURN END DO END SUBROUTINE CD_Range_Discard_Files + SUBROUTINE CD_Range_Discard_File(out_root, line_id, ErrStat, ErrMsg) + !! Remove the .Line.range.out file of an earlier run, if there is one. + CHARACTER(*), INTENT(IN) :: out_root + INTEGER, INTENT(IN) :: line_id + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + INTEGER :: unit, ios + CHARACTER(LEN(out_root) + 32) :: path + CHARACTER(16) :: id_text + + ErrStat = CD_RANGE_OK + ErrMsg = '' + WRITE (id_text, '(I0)') line_id + path = TRIM(out_root)//'.Line'//TRIM(id_text)//'.range.out' + IF (.NOT. CD_Path_Openable(TRIM(path), .FALSE.)) RETURN ! no such file + CALL CD_Open_Write(unit, TRIM(path), ios, status='OLD') + IF (ios == 0) CLOSE (unit, STATUS='DELETE', IOSTAT=ios) + IF (ios == 0) THEN + IF (CD_Path_Openable(TRIM(path), .FALSE.)) ios = 1 + END IF + IF (ios /= 0) THEN + ErrStat = CD_RANGE_BADINPUT + ErrMsg = 'CableDyn_RangeOutput: cannot replace the range-graph output file of an earlier run: '//TRIM(path) + END IF + END SUBROUTINE CD_Range_Discard_File + PURE LOGICAL FUNCTION CD_Is_TDP_Channel(lo) RESULT(yes) !! True for a name of the TDP family (lower case), valid or not: "tdp" then a digit. CHARACTER(*), INTENT(IN) :: lo diff --git a/src/openfast/CableDyn_OF.f90 b/src/openfast/CableDyn_OF.f90 index a6b6361..6ffcb2c 100644 --- a/src/openfast/CableDyn_OF.f90 +++ b/src/openfast/CableDyn_OF.f90 @@ -101,7 +101,7 @@ MODULE CableDyn CD_AGG_NumChannels, CD_AGG_ChannelHeader, CD_AGG_EvalChannel, & CD_AGG_GetInitMetadata, CD_AGG_GetInitLine, & CD_AGG_WriteStaticProfile, CD_AGG_Range_Graphs, CD_AGG_Range_Sample, & - CD_AGG_Range_Write, CD_AGG_Range_Discard, CD_AGG_Range_MirrorSize, & + CD_AGG_Range_Write, CD_AGG_Range_MirrorSize, & CD_AGG_Range_Get_State, CD_AGG_Range_Set_State USE CableDyn_OpenFAST_HermiteFMF, ONLY: CD_HFMF_MirrorSize, CD_HFMF_PackMirror, CD_HFMF_UnpackMirror, & CD_HFMF_FrictionMirrorSize, CD_HFMF_BoundaryMirrorSize, & @@ -809,6 +809,13 @@ SUBROUTINE CD_Init(InitInp, u, p, x, xd, z, other, y, m, DTcoupling, InitOut, Er ! The host owns the environment: gravity, water density, and depth override ! whatever the deck declares (MoorDyn-faithful coupled behavior). + ! CableDyn-owned result files. Single-turbine names use p%RootName = + ! .CD; FAST.Farm preserves its established FarmCD root. + IF (nT > 0) THEN + native_root = TRIM(InitInp%RootName) + ELSE + native_root = TRIM(InitInp%RootName)//'.CD' + END IF CALL WrScr( ' Parsing CableDyn input file: '//TRIM(InitInp%FileName) ) IF (nT > 0) THEN ! an allocated-but-empty/too-small PtfmInit means "no initial displacement" @@ -855,7 +862,8 @@ SUBROUTINE CD_Init(InitInp, u, p, x, xd, z, other, y, m, DTcoupling, InitOut, Er env_gravity=REAL(InitInp%g, wp), env_rho_water=REAL(InitInp%rhoW, wp), & env_wtrdpth=REAL(InitInp%WtrDepth, wp), ptfm_init=REAL(ptfm6, wp), & external_fluid=use_extf, run_tmax=REAL(InitInp%Tmax, wp), & - host_orientation=.TRUE., static_current=static_cur, range_files=.TRUE. ) + host_orientation=.TRUE., static_current=static_cur, range_files=.TRUE., & + range_discard_root=TRIM(native_root) ) END IF IF (es /= CD_AGG_OK) THEN ! The slot stays inactive (it is activated only after full success, below), so this and @@ -960,13 +968,6 @@ SUBROUTINE CD_Init(InitInp, u, p, x, xd, z, other, y, m, DTcoupling, InitOut, Er 'calculations; omit dtM or set it to DT for glue-rate solves.' ) END IF END IF - ! CableDyn-owned result files. Single-turbine names use p%RootName = - ! .CD; FAST.Farm preserves its established FarmCD root. - IF (nT > 0) THEN - native_root = TRIM(InitInp%RootName) - ELSE - native_root = TRIM(InitInp%RootName)//'.CD' - END IF ! The static profile records the converged static equilibrium, the state the ! initialization summary reports, so it is written before the SeaState kinematics ! are seeded below: they act from t = 0 and enter the t = 0 output row only. @@ -1384,14 +1385,10 @@ 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 completes. + ! files of an earlier run went when the deck was parsed: 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 ) - IF (es /= CD_AGG_OK) THEN - CALL SetErrStat( ErrID_Fatal, 'CableDyn mooring module: '//TRIM(em), ErrStat, ErrMsg, RoutineName ) - RETURN - END IF CALL sample_range( Inst(id), other, 0.0_DbKi, ErrStat2, ErrMsg2 ) CALL SetErrStat( ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ) IF (ErrStat >= AbortErrLev) RETURN diff --git a/tests/test_openfast_aggregate.f90 b/tests/test_openfast_aggregate.f90 index 8b8482b..4fb5fbf 100644 --- a/tests/test_openfast_aggregate.f90 +++ b/tests/test_openfast_aggregate.f90 @@ -136,6 +136,7 @@ PROGRAM test_openfast_aggregate CALL case_anchor_uplift_channels() CALL case_output_channels() CALL case_range_graph_host() + CALL case_range_discard_at_parse() CALL case_long_init_notes() CALL case_pure_cable_deck() CALL case_cable_load_feedback() @@ -1104,6 +1105,53 @@ SUBROUTINE case_range_graph_host() CALL CD_AGG_End(plain, es, em) END SUBROUTINE case_range_graph_host + SUBROUTINE case_range_discard_at_parse() + !! The range files of an earlier run go once the deck has named them, so an initialisation that + !! fails after the parse leaves none. A rebuild of a run under way passes no root and leaves + !! them, and a deck that does not parse names none: the earlier run's outputs stand whole. + TYPE(CD_AGG_ModuleType) :: a + INTEGER :: es, u, ios + LOGICAL :: there + CHARACTER(512) :: em + + CALL write_mixed_channels_deck('agg_discard.dat', outputs_row='Ten1N1', line_outputs='r', & + extra_option='0.0 TMax', extra_option2='0.1 RangeStart') + ! refused after the parse: RangeStart beyond the host's run time + CALL seed_range_file('agg_discard.Line1.range.out') + CALL seed_range_file('agg_discard.Line2.range.out') + CALL CD_AGG_Init_From_Deck(a, 'agg_discard.dat', 0.05_wp, es, em, run_tmax=0.05_wp, range_files=.TRUE.) + CALL require(es /= CD_AGG_OK, 'range-discard:refused-without-root') + INQUIRE (FILE='agg_discard.Line1.range.out', EXIST=there) + CALL require(there, 'range-discard:no-root-leaves-the-file') + CALL CD_AGG_Init_From_Deck(a, 'agg_discard.dat', 0.05_wp, es, em, run_tmax=0.05_wp, range_files=.TRUE., & + range_discard_root='agg_discard') + CALL require(es /= CD_AGG_OK .AND. INDEX(em, 'RangeStart exceeds TMax') > 0, 'range-discard:refused: '//TRIM(em)) + INQUIRE (FILE='agg_discard.Line1.range.out', EXIST=there) + CALL require(.NOT. there, 'range-discard:line1-removed') + INQUIRE (FILE='agg_discard.Line2.range.out', EXIST=there) + CALL require(.NOT. there, 'range-discard:line2-removed') + ! a deck that does not parse + OPEN (NEWUNIT=u, FILE='agg_discard_bad.dat', STATUS='REPLACE', ACTION='WRITE', IOSTAT=ios) + WRITE (u, '(A)') 'not a deck' + CLOSE (u) + CALL seed_range_file('agg_discard.Line1.range.out') + CALL CD_AGG_Init_From_Deck(a, 'agg_discard_bad.dat', 0.05_wp, es, em, run_tmax=0.05_wp, range_files=.TRUE., & + range_discard_root='agg_discard') + CALL require(es /= CD_AGG_OK, 'range-discard:bad-deck-refused') + INQUIRE (FILE='agg_discard.Line1.range.out', EXIST=there) + CALL require(there, 'range-discard:bad-deck-leaves-the-file') + OPEN (NEWUNIT=u, FILE='agg_discard.Line1.range.out', STATUS='OLD', IOSTAT=ios) + IF (ios == 0) CLOSE (u, STATUS='DELETE') + END SUBROUTINE case_range_discard_at_parse + + SUBROUTINE seed_range_file(path) + CHARACTER(*), INTENT(IN) :: path + INTEGER :: u, ios + OPEN (NEWUNIT=u, FILE=path, STATUS='REPLACE', ACTION='WRITE', IOSTAT=ios) + WRITE (u, '(A)') 'stale' + CLOSE (u) + END SUBROUTINE seed_range_file + SUBROUTINE range_march(a, r0, dt, step, amplitude, label) !! Advance to step's committed time with a small surge and heave of the fairleads about r0, !! scaled by amplitude: the same motion for every instance at amplitude 1. From ceb5d1b749618225c753ea4b6305b3523cf6c472 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Sun, 8 Nov 2026 01:32:41 +0900 Subject: [PATCH 3/6] fix(openfast): keep a body or rod at its start through an early restart A restart inside the first mooring step, which an interval restart now allows, diverged on a deck with Rigid6 bodies or rods. The restored state mirror marks their accelerations committed, while at dtM > DT the uninterrupted run reaches its first step with the start still open and the fluid fields of that step re-solve them. A rebuild before the first commit now reopens the start. At dtM = DT there is no such state: the CalcOutput at t = 0 restores the mirror, which closes it. Signed-off-by: Jae Hoon Seo --- src/CableDyn_OpenFAST_Aggregate.f90 | 10 +++++ src/openfast/CableDyn_OF.f90 | 7 +++- tests/test_openfast_aggregate.f90 | 65 ++++++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/CableDyn_OpenFAST_Aggregate.f90 b/src/CableDyn_OpenFAST_Aggregate.f90 index e10cac4..3fb0d59 100644 --- a/src/CableDyn_OpenFAST_Aggregate.f90 +++ b/src/CableDyn_OpenFAST_Aggregate.f90 @@ -142,6 +142,7 @@ MODULE CableDyn_OpenFAST_Aggregate PUBLIC :: CD_AGG_WriteStaticProfile ! Range graphs of the LINES flag r: the standalone mixed route and the single-turbine OpenFAST ! module sample and write them; the accumulated state rides a host's checkpoint + PUBLIC :: CD_AGG_Reopen_Start PUBLIC :: CD_AGG_Range_Sample, CD_AGG_Range_Write, CD_AGG_Range_Discard, CD_AGG_Range_Graphs, & CD_AGG_Range_MirrorSize, CD_AGG_Range_Get_State, CD_AGG_Range_Set_State PUBLIC :: CD_AGG_GetInitMetadata, CD_AGG_GetInitLine @@ -3333,6 +3334,15 @@ SUBROUTINE CD_AGG_Get_Rod_States(self, states, ErrStat, ErrMsg) IF (self%has_rod) CALL CD_Rod_Get_States(self%rod, states, ErrStat, ErrMsg) END SUBROUTINE CD_AGG_Get_Rod_States + SUBROUTINE CD_AGG_Reopen_Start(self) + !! Back to the start of the first step after a restored state mirror, which marks the object + !! accelerations committed: a state from before the first commit keeps its start open, as the + !! run it continues does, so the fluid fields of the first step re-solve them + !! (objects_committed). Until then they stay those of the mirror. + TYPE(CD_AGG_ModuleType), INTENT(INOUT) :: self + self%objects_committed = .FALSE. + END SUBROUTINE CD_AGG_Reopen_Start + SUBROUTINE CD_AGG_Set_Rod_States(self, states, ErrStat, ErrMsg) !! Restore the rod state and immediately scatter its end-point kinematics !! into the EI=0 system used by restart-time output and the next advance. diff --git a/src/openfast/CableDyn_OF.f90 b/src/openfast/CableDyn_OF.f90 index 6ffcb2c..ab561cd 100644 --- a/src/openfast/CableDyn_OF.f90 +++ b/src/openfast/CableDyn_OF.f90 @@ -101,7 +101,7 @@ MODULE CableDyn CD_AGG_NumChannels, CD_AGG_ChannelHeader, CD_AGG_EvalChannel, & CD_AGG_GetInitMetadata, CD_AGG_GetInitLine, & CD_AGG_WriteStaticProfile, CD_AGG_Range_Graphs, CD_AGG_Range_Sample, & - CD_AGG_Range_Write, CD_AGG_Range_MirrorSize, & + CD_AGG_Range_Write, CD_AGG_Range_MirrorSize, CD_AGG_Reopen_Start, & CD_AGG_Range_Get_State, CD_AGG_Range_Set_State USE CableDyn_OpenFAST_HermiteFMF, ONLY: CD_HFMF_MirrorSize, CD_HFMF_PackMirror, CD_HFMF_UnpackMirror, & CD_HFMF_FrictionMirrorSize, CD_HFMF_BoundaryMirrorSize, & @@ -1759,6 +1759,11 @@ SUBROUTINE rebuild_from_checkpoint( t, u1, p, x, other, m, ErrStat, ErrMsg ) RETURN END IF END IF + ! A restart inside the first mooring step: no commit yet, so the bodies and rods are still + ! at their start, and the fluid fields of the first step re-solve their accelerations. At + ! dtM = DT there is no such state: the CalcOutput at t = 0 restores the mirror, which + ! closes the start. + IF (Inst(id)%nsuper > 1_IntKi .AND. other%t_commit <= 0.0_DbKi) CALL CD_AGG_Reopen_Start( Inst(id)%agg ) ! 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 diff --git a/tests/test_openfast_aggregate.f90 b/tests/test_openfast_aggregate.f90 index 4fb5fbf..6535f3e 100644 --- a/tests/test_openfast_aggregate.f90 +++ b/tests/test_openfast_aggregate.f90 @@ -38,7 +38,7 @@ PROGRAM test_openfast_aggregate CD_AGG_NCtrlChans, CD_AGG_Apply_LineControl, & CD_AGG_Refresh_PointMesh, CD_AGG_HasRigid6, CD_AGG_HasRod, & CD_AGG_Rigid6_MirrorSize, CD_AGG_Get_Rigid6_States, & - CD_AGG_Set_Rigid6_States, CD_AGG_Rod_MirrorSize, & + CD_AGG_Set_Rigid6_States, CD_AGG_Reopen_Start, CD_AGG_Rod_MirrorSize, & CD_AGG_Get_Rod_States, CD_AGG_Set_Rod_States, & CD_AGG_Range_Sample, CD_AGG_Range_Write, CD_AGG_Range_Discard, & CD_AGG_Range_Graphs, CD_AGG_Range_MirrorSize, CD_AGG_Range_Get_State, & @@ -137,6 +137,7 @@ PROGRAM test_openfast_aggregate CALL case_output_channels() CALL case_range_graph_host() CALL case_range_discard_at_parse() + CALL case_reopen_start() CALL case_long_init_notes() CALL case_pure_cable_deck() CALL case_cable_load_feedback() @@ -1144,6 +1145,68 @@ SUBROUTINE case_range_discard_at_parse() IF (ios == 0) CLOSE (u, STATUS='DELETE') END SUBROUTINE case_range_discard_at_parse + SUBROUTINE case_reopen_start() + !! A state mirror restored from before the first commit keeps the body's start open: the host + !! fluid fields of the first step re-solve the start accelerations, as in the run that was + !! never restored (a checkpoint inside the first mooring step). Without CD_AGG_Reopen_Start + !! they do not. + REAL(wp), PARAMETER :: DT = 0.005_wp + TYPE(CD_AGG_ModuleType) :: a, b, c + REAL(wp) :: r0(3, 1), vel(3, 1), acc(3, 1), lds(3, 1) + REAL(wp), ALLOCATABLE :: sa(:), sb(:), sc(:), fv(:, :), fa(:, :), fw(:) + INTEGER :: es, n, ia, nf + LOGICAL :: cv, st + CHARACTER(512) :: em + + CALL write_coupled_rigid6_deck('agg_reopen.dat', 1.0_wp, 0.5_wp, .FALSE.) + CALL CD_AGG_Init_From_Deck(a, 'agg_reopen.dat', DT, es, em, external_fluid=.TRUE.) + CALL require(es == CD_AGG_OK, 'reopen:init: '//TRIM(em)) + IF (es /= CD_AGG_OK) RETURN + CALL CD_AGG_Init_From_Deck(b, 'agg_reopen.dat', DT, es, em, external_fluid=.TRUE.) + CALL CD_AGG_Init_From_Deck(c, 'agg_reopen.dat', DT, es, em, external_fluid=.TRUE.) + n = CD_AGG_Rigid6_MirrorSize(a) + nf = CD_AGG_NFluidNodes(a, es, em) + ALLOCATE (fv(3, nf), fa(3, nf), fw(nf)) + fv = 0.0_wp + fv(1, :) = 0.4_wp + fa = 0.0_wp + fa(1, :) = 0.3_wp + fw = 0.0_wp + ALLOCATE (sa(n), sb(n), sc(n)) + CALL CD_AGG_Get_Rigid6_States(b, sb, es, em) + CALL CD_AGG_Set_Rigid6_States(b, sb, es, em) + CALL CD_AGG_Reopen_Start(b) + CALL CD_AGG_Get_Rigid6_States(c, sc, es, em) + CALL CD_AGG_Set_Rigid6_States(c, sc, es, em) + ! the first step: new fields, then an advance that moves the fairlead + CALL CD_AGG_SetFluidFields(a, fv, fa, fw, es, em) + CALL require(es == CD_AGG_OK, 'reopen:fields-a: '//TRIM(em)) + CALL CD_AGG_SetFluidFields(b, fv, fa, fw, es, em) + CALL require(es == CD_AGG_OK, 'reopen:fields-b: '//TRIM(em)) + CALL CD_AGG_SetFluidFields(c, fv, fa, fw, es, em) + CALL require(es == CD_AGG_OK, 'reopen:fields-c: '//TRIM(em)) + CALL CD_AGG_GetMovingPointMesh(a, r0, vel, acc, lds, es, em) + r0(1, 1) = r0(1, 1) + 0.01_wp + vel = 0.0_wp + vel(1, 1) = 0.2_wp + acc = 0.0_wp + acc(1, 1) = 1.0_wp + CALL CD_AGG_Step_Moving(a, DT, r0, vel, acc, cv, st, ia, es, em, t_committed=DT) + CALL require(es == CD_AGG_OK, 'reopen:step-a: '//TRIM(em)) + CALL CD_AGG_Step_Moving(b, DT, r0, vel, acc, cv, st, ia, es, em, t_committed=DT) + CALL require(es == CD_AGG_OK, 'reopen:step-b: '//TRIM(em)) + CALL CD_AGG_Step_Moving(c, DT, r0, vel, acc, cv, st, ia, es, em, t_committed=DT) + CALL require(es == CD_AGG_OK, 'reopen:step-c: '//TRIM(em)) + CALL CD_AGG_Get_Rigid6_States(a, sa, es, em) + CALL CD_AGG_Get_Rigid6_States(b, sb, es, em) + CALL CD_AGG_Get_Rigid6_States(c, sc, es, em) + CALL require(nan_max_abs(sb - sa) <= 0.0_wp, 'reopen:restored-equals-never-restored') + CALL require(nan_max_abs(sc - sa) > 0.0_wp, 'reopen:closed-start-differs') + CALL CD_AGG_End(a, es, em) + CALL CD_AGG_End(b, es, em) + CALL CD_AGG_End(c, es, em) + END SUBROUTINE case_reopen_start + SUBROUTINE seed_range_file(path) CHARACTER(*), INTENT(IN) :: path INTEGER :: u, ios From 42c562293f69f3f708935c0af48441633fbdbe19 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Sun, 8 Nov 2026 01:59:24 +0900 Subject: [PATCH 4/6] fix(openfast): keep the module driver's orientation in double precision The driver rounded the rotation matrix of a prescribed motion to ReKi before it stored it in the mesh, whose orientation field is double precision on every build. In a single-precision build the matrix is then no longer orthogonal to CableDyn's tolerance, and a rotating motion stopped at the first step. Signed-off-by: Jae Hoon Seo --- src/openfast/CableDyn_OF_Driver.f90 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openfast/CableDyn_OF_Driver.f90 b/src/openfast/CableDyn_OF_Driver.f90 index ec4c051..4c3b2ab 100644 --- a/src/openfast/CableDyn_OF_Driver.f90 +++ b/src/openfast/CableDyn_OF_Driver.f90 @@ -349,7 +349,7 @@ SUBROUTINE set_mesh_pose(uu, r6, v6, a6) uu%CoupledKinematics(1)%TranslationDisp(:, ip) = REAL(rr, ReKi) uu%CoupledKinematics(1)%TranslationVel(:, ip) = REAL(vv, ReKi) uu%CoupledKinematics(1)%TranslationAcc(:, ip) = REAL(aa, ReKi) - uu%CoupledKinematics(1)%Orientation(:, :, ip) = REAL(dcm, ReKi) + uu%CoupledKinematics(1)%Orientation(:, :, ip) = dcm uu%CoupledKinematics(1)%RotationVel(:, ip) = REAL(omg, ReKi) uu%CoupledKinematics(1)%RotationAcc(:, ip) = REAL(alp, ReKi) END DO From 21d7a2ab401ac5c4ac59f531d339f0ce9936386c Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Sun, 8 Nov 2026 02:08:16 +0900 Subject: [PATCH 5/6] docs(outputs): wrap the paragraph on earlier range files Signed-off-by: Jae Hoon Seo --- doc/outputs.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/outputs.rst b/doc/outputs.rst index f0d847a..42f0090 100644 --- a/doc/outputs.rst +++ b/doc/outputs.rst @@ -395,9 +395,10 @@ the end of a run that completes, whenever the host ends it: at ``TMax``, at a st 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 as soon as its deck is parsed, and a run that stops on an error after that, in any module, leaves none. A deck that does not parse, or a module that stops -before CableDyn starts, leaves every output of the earlier run as it was. 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. +before CableDyn starts, leaves every output of the earlier run as it was. 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 From 912762a662a8fd5772af4449eab707cb8e918689 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Sun, 8 Nov 2026 02:28:28 +0900 Subject: [PATCH 6/6] test(openfast): discard earlier range graphs when the statics fail The discard at the parse was tested against a refusal after the parse. A body far too heavy for its lines now fails the static solve itself, by name, and the test requires the files gone. Signed-off-by: Jae Hoon Seo --- tests/test_openfast_aggregate.f90 | 39 +++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/tests/test_openfast_aggregate.f90 b/tests/test_openfast_aggregate.f90 index 6535f3e..0704472 100644 --- a/tests/test_openfast_aggregate.f90 +++ b/tests/test_openfast_aggregate.f90 @@ -1131,6 +1131,17 @@ SUBROUTINE case_range_discard_at_parse() CALL require(.NOT. there, 'range-discard:line1-removed') INQUIRE (FILE='agg_discard.Line2.range.out', EXIST=there) CALL require(.NOT. there, 'range-discard:line2-removed') + ! a static solve that fails: the deck parsed, the equilibrium is not found + CALL write_coupled_rigid6_deck('agg_discard_st.dat', 0.0_wp, 0.0_wp, .FALSE., overloaded=.TRUE.) + CALL seed_range_file('agg_discard.Line1.range.out') + CALL seed_range_file('agg_discard.Line2.range.out') + CALL CD_AGG_Init_From_Deck(a, 'agg_discard_st.dat', 0.05_wp, es, em, run_tmax=0.05_wp, range_files=.TRUE., & + range_discard_root='agg_discard') + CALL require(es /= CD_AGG_OK .AND. INDEX(em, 'equilibrium not found') > 0, 'range-discard:statics-fail: '//TRIM(em)) + INQUIRE (FILE='agg_discard.Line1.range.out', EXIST=there) + CALL require(.NOT. there, 'range-discard:statics-fail-line1-removed') + INQUIRE (FILE='agg_discard.Line2.range.out', EXIST=there) + CALL require(.NOT. there, 'range-discard:statics-fail-line2-removed') ! a deck that does not parse OPEN (NEWUNIT=u, FILE='agg_discard_bad.dat', STATUS='REPLACE', ACTION='WRITE', IOSTAT=ios) WRITE (u, '(A)') 'not a deck' @@ -1911,17 +1922,21 @@ SUBROUTINE case_coupled_point3_fluid() DEALLOCATE (xyz, fv, fa, wl) END SUBROUTINE case_coupled_point3_fluid - SUBROUTINE write_coupled_rigid6_deck(path, cda, ca, extra_free, deck_pose) + SUBROUTINE write_coupled_rigid6_deck(path, cda, ca, extra_free, deck_pose, overloaded) !! A submerged Rigid6 body hung between a Fixed anchor and a Coupled fairlead by two !! chain lines (still water). cda/ca parametrise the body fluid coefficients (0/0 is the !! supported pure-inertia+buoyancy+restoring case); extra_free adds a stray Free point to !! exercise the scope guard; deck_pose starts the body at its deck pose (bodyIC deck) - !! instead of its static equilibrium. + !! instead of its static equilibrium; overloaded flags both lines r and makes the body far too + !! heavy for them, so that the static equilibrium is not found. CHARACTER(*), INTENT(IN) :: path REAL(wp), INTENT(IN) :: cda, ca LOGICAL, INTENT(IN) :: extra_free - LOGICAL, INTENT(IN), OPTIONAL :: deck_pose + LOGICAL, INTENT(IN), OPTIONAL :: deck_pose, overloaded INTEGER :: u, ios + LOGICAL :: heavy + heavy = .FALSE. + IF (PRESENT(overloaded)) heavy = overloaded OPEN (NEWUNIT=u, FILE=path, STATUS='REPLACE', ACTION='WRITE', IOSTAT=ios) WRITE (u, '(A)') 'coupled rigid6 body deck' WRITE (u, '(A)') '--- LINE TYPES ---' @@ -1932,8 +1947,13 @@ SUBROUTINE write_coupled_rigid6_deck(path, cda, ca, extra_free, deck_pose) WRITE (u, '(A)') 'ID Type X Y Z Rot1 Rot2 Rot3 Mass Vol C33 C44 C55 CdA Ca Ixx Iyy Izz' WRITE (u, '(A)') '(-) (-) (m) (m) (m) (deg) (deg) (deg) (kg) (m3) (N/m) (Nm/rad) (Nm/rad) (m2) (-) '// & '(kgm2) (kgm2) (kgm2)' - WRITE (u, '(A,F6.3,1X,F6.3,A)') '1 Rigid6 0.0 0.0 -4.0 0.0 0.0 0.0 100.0 0.05 0.0 100.0 100.0 ', & - cda, ca, ' 20.0 20.0 20.0' + IF (heavy) THEN + WRITE (u, '(A,F6.3,1X,F6.3,A)') '1 Rigid6 0.0 0.0 -4.0 0.0 0.0 0.0 1.0e7 0.05 0.0 100.0 100.0 ', & + cda, ca, ' 20.0 20.0 20.0' + ELSE + WRITE (u, '(A,F6.3,1X,F6.3,A)') '1 Rigid6 0.0 0.0 -4.0 0.0 0.0 0.0 100.0 0.05 0.0 100.0 100.0 ', & + cda, ca, ' 20.0 20.0 20.0' + END IF WRITE (u, '(A)') '--- POINTS ---' WRITE (u, '(A)') 'ID Type X Y Z Mass Vol CdA Ca' WRITE (u, '(A)') '(-) (-) (m) (m) (m) (kg) (m3) (m2) (-)' @@ -1944,8 +1964,13 @@ SUBROUTINE write_coupled_rigid6_deck(path, cda, ca, extra_free, deck_pose) WRITE (u, '(A)') '--- LINES ---' WRITE (u, '(A)') 'ID NodeA NodeB Outputs' WRITE (u, '(A)') '(-) (-) (-) (-)' - WRITE (u, '(A)') '1 2 1 -' - WRITE (u, '(A)') '2 3 2 -' + IF (heavy) THEN + WRITE (u, '(A)') '1 2 1 r' + WRITE (u, '(A)') '2 3 2 r' + ELSE + WRITE (u, '(A)') '1 2 1 -' + WRITE (u, '(A)') '2 3 2 -' + END IF WRITE (u, '(A)') '--- SECTIONS ---' WRITE (u, '(A)') 'LineID LineType Length NumSegs' WRITE (u, '(A)') '(-) (-) (m) (-)'