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/doc/outputs.rst b/doc/outputs.rst index c66e9e6..42f0090 100644 --- a/doc/outputs.rst +++ b/doc/outputs.rst @@ -393,10 +393,12 @@ 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`` -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. +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. 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/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..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 @@ -296,7 +297,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 +333,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 +397,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 @@ -3330,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/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 36a03d2..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_Discard, 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, & @@ -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. @@ -806,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" @@ -852,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 @@ -957,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. @@ -1270,13 +1274,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 +1319,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 @@ -1371,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 @@ -1518,9 +1528,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 +1567,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 +1646,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 +1687,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 +1715,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 +1734,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,13 +1751,19 @@ 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 ) 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 @@ -1819,6 +1842,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 +1879,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 +1888,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 +1972,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 +2058,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 +2721,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 +2968,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 +4859,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_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 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) diff --git a/tests/test_openfast_aggregate.f90 b/tests/test_openfast_aggregate.f90 index 8b8482b..0704472 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, & @@ -136,6 +136,8 @@ 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_reopen_start() CALL case_long_init_notes() CALL case_pure_cable_deck() CALL case_cable_load_feedback() @@ -1104,6 +1106,126 @@ 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 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' + 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 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 + 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. @@ -1800,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 ---' @@ -1821,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) (-)' @@ -1833,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) (-)'