Skip to content

Commit 6e4d90c

Browse files
committed
Record immersed-boundary forces and kinematics every time step
`ib_state_wrt` writes the force, torque and kinematic state of each immersed boundary only at snapshot intervals, so the force history is sampled at the field-output cadence. That is far too coarse to compare a transient load against an experiment or a reference computation, or to drive a reduced-order model from a force signal: typical cases here write a field every few hundred steps. Write one record per time step to `D/ib<id>_forces.dat` (time step, time, force, torque, velocity, angular velocity, angles, centroid), under the existing `ib_state_wrt` flag. Records are buffered per rank and flushed in batches rather than opened per body per step: opening a file per body per step is a metadata operation per step on a parallel filesystem and does not scale, and a particle bed of a thousand bodies would issue on the order of a hundred million of them over a long run. Each buffered row carries its own global body id, so a body changing owner mid-run needs no special handling, and `ib_force_stride` subsamples runs long enough for the record itself to become large. Claude-Session: https://claude.ai/code/session_01HMJ7cycfo7kTFSFq5yhHLG
1 parent 33ad77a commit 6e4d90c

8 files changed

Lines changed: 94 additions & 3 deletions

File tree

docs/documentation/case.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,7 @@ To restart the simulation from $k$-th time step, see @ref running "Restarting Ca
728728
| `alpha_wrt(i)` | Logical | Add the volume fraction of fluid $i$ to the database |
729729
| `gamma_wrt` | Logical | Add the specific heat ratio function to the database |
730730
| `heat_ratio_wrt` | Logical | Add the specific heat ratio to the database |
731+
| `ib_force_stride` | Integer | Stride, in time steps, of the per-step immersed-boundary force record (default 1) |
731732
| `ib_state_wrt` | Logical | Parameter to handle writing IB state on saves and outputting the state as a point mesh to SILO files. |
732733
| `pi_inf_wrt` | Logical | Add the liquid stiffness function to the database |
733734
| `pres_inf_wrt` | Logical | Add the liquid stiffness to the formatted database |
@@ -795,7 +796,7 @@ If `file_per_process` is true, then pre_process, simulation, and post_process mu
795796

796797
- `probe_wrt` activates the output of state variables at coordinates specified by `probe(i)%[x;y,z]`.
797798

798-
- `ib_state_wrt` is used to trigger post-processing of the IB state to be written out as a point mesh in the SILO files. When no IBs are moving, it also triggers force and torque calculation so that those values may be written to the output state files.
799+
- `ib_state_wrt` is used to trigger post-processing of the IB state to be written out as a point mesh in the SILO files. When no IBs are moving, it also triggers force and torque calculation so that those values may be written to the output state files. It also records one line per time step in `D/ib<id>_forces.dat` for each immersed boundary (time step, time, force, torque, velocity, angular velocity, angles, centroid). Records are buffered and written in batches rather than opened per step, and `ib_force_stride` writes only every N-th step for runs long enough that the record itself becomes large.
799800

800801
- `output_partial_domain` activates the output of part of the domain specified by `[x,y,z]_output%%beg` and `[x,y,z]_output%%end`.
801802
This is useful for large domains where only a portion of the domain is of interest.

src/simulation/m_data_output.fpp

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ module m_data_output
2626
private
2727
public :: s_initialize_data_output_module, s_open_run_time_information_file, s_open_com_files, s_open_probe_files, &
2828
& s_write_run_time_information, s_write_data_files, s_write_serial_data_files, s_write_parallel_data_files, &
29-
& s_write_ib_data_file, s_write_com_files, s_write_probe_files, s_write_ib_state_file, s_close_run_time_information_file, &
30-
& s_close_com_files, s_close_probe_files, s_finalize_data_output_module
29+
& s_write_ib_data_file, s_write_com_files, s_write_probe_files, s_write_ib_state_file, s_write_ib_force_files, &
30+
& s_flush_ib_force_files, s_close_run_time_information_file, s_close_com_files, s_close_probe_files, &
31+
& s_finalize_data_output_module
3132

3233
real(wp), public, allocatable, dimension(:,:) :: c_mass
3334
$:GPU_DECLARE(create='[c_mass]')
@@ -42,6 +43,11 @@ module m_data_output
4243

4344
type(scalar_field), allocatable, dimension(:) :: q_cons_temp_ds
4445

46+
!> Buffered immersed-boundary force records: (id, t_step, time, force, torque, vel, angular_vel, angles, centroid)
47+
integer, parameter :: ib_force_buf_len = 1024
48+
real(wp), dimension(21, ib_force_buf_len) :: ib_force_buf
49+
integer :: ib_force_buf_n = 0
50+
4551
contains
4652

4753
!> Write data files. Dispatch subroutine that replaces procedure pointer.
@@ -1100,6 +1106,80 @@ contains
11001106

11011107
end subroutine s_write_serial_ib_state
11021108

1109+
!> Record the force, torque and kinematic state of every owned immersed boundary for this time step.
1110+
!!
1111+
!! Rows are accumulated in a rank-local buffer and flushed to D/ib<id>_forces.dat in batches, because opening
1112+
!! a file per body per step is a metadata operation per step on a parallel filesystem and does not scale --
1113+
!! a particle bed of a thousand bodies would issue a hundred million of them over a long run. Each buffered
1114+
!! row carries its own global body id, so a body changing owner mid-run needs no special handling: the old
1115+
!! owner's pending rows still reach the right file. `ib_force_stride` subsamples very long runs.
1116+
impure subroutine s_write_ib_force_files(t_step)
1117+
1118+
integer, intent(in) :: t_step
1119+
integer :: i, ib_idx, n_write
1120+
1121+
if (mod(t_step, max(ib_force_stride, 1)) /= 0) return
1122+
1123+
n_write = num_local_ibs
1124+
if (num_procs == 1) n_write = num_ibs
1125+
if (n_write == 0) return ! ranks holding no body have nothing to record
1126+
1127+
$:GPU_UPDATE(host='[patch_ib(1:num_ibs)]')
1128+
1129+
do i = 1, n_write
1130+
ib_idx = i
1131+
if (num_procs > 1) ib_idx = local_ib_patch_ids(i)
1132+
if (ib_force_buf_n == ib_force_buf_len) call s_flush_ib_force_files()
1133+
ib_force_buf_n = ib_force_buf_n + 1
1134+
ib_force_buf(1, ib_force_buf_n) = real(max(patch_ib(ib_idx)%gbl_patch_id, ib_idx), wp)
1135+
ib_force_buf(2, ib_force_buf_n) = real(t_step, wp)
1136+
ib_force_buf(3, ib_force_buf_n) = mytime
1137+
ib_force_buf(4:6,ib_force_buf_n) = patch_ib(ib_idx)%force(1:3)
1138+
ib_force_buf(7:9,ib_force_buf_n) = patch_ib(ib_idx)%torque(1:3)
1139+
ib_force_buf(10:12,ib_force_buf_n) = patch_ib(ib_idx)%vel(1:3)
1140+
ib_force_buf(13:15,ib_force_buf_n) = patch_ib(ib_idx)%angular_vel(1:3)
1141+
ib_force_buf(16:18,ib_force_buf_n) = patch_ib(ib_idx)%angles(1:3)
1142+
ib_force_buf(19, ib_force_buf_n) = patch_ib(ib_idx)%x_centroid
1143+
ib_force_buf(20, ib_force_buf_n) = patch_ib(ib_idx)%y_centroid
1144+
ib_force_buf(21, ib_force_buf_n) = patch_ib(ib_idx)%z_centroid
1145+
end do
1146+
1147+
end subroutine s_write_ib_force_files
1148+
1149+
!> Write every buffered immersed-boundary force record and empty the buffer
1150+
impure subroutine s_flush_ib_force_files()
1151+
1152+
character(LEN=path_len + 2*name_len) :: file_loc
1153+
integer :: i, j, ib_id, file_unit
1154+
logical :: file_exist
1155+
1156+
do i = 1, ib_force_buf_n
1157+
ib_id = nint(ib_force_buf(1, i))
1158+
if (ib_id < 0) cycle ! already written as part of an earlier body's pass
1159+
1160+
write (file_loc, '(A,I0,A)') '/D/ib', ib_id, '_forces.dat'
1161+
file_loc = trim(case_dir) // trim(file_loc)
1162+
inquire (file=trim(file_loc), exist=file_exist)
1163+
if (file_exist) then
1164+
open (newunit=file_unit, file=trim(file_loc), form='formatted', status='old', position='append')
1165+
else
1166+
open (newunit=file_unit, file=trim(file_loc), form='formatted', status='new')
1167+
write (file_unit, '(A)') '# t_step time Fx Fy Fz Tx Ty Tz vx vy vz wx wy wz ax ay az xc yc zc'
1168+
end if
1169+
1170+
do j = i, ib_force_buf_n ! all rows for this body, in time order
1171+
if (nint(ib_force_buf(1, j)) /= ib_id) cycle
1172+
write (file_unit, '(I10,19ES18.10E3)') nint(ib_force_buf(2, j)), ib_force_buf(3:21,j)
1173+
ib_force_buf(1, j) = -1._wp
1174+
end do
1175+
1176+
close (file_unit)
1177+
end do
1178+
1179+
ib_force_buf_n = 0
1180+
1181+
end subroutine s_flush_ib_force_files
1182+
11031183
!> @brief Writes IB state records to restart_data/ib_state.dat. Must be called only on rank 0.
11041184
impure subroutine s_write_ib_state_file(time_step)
11051185

src/simulation/m_global_parameters.fpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ contains
512512
collision_time = dflt_real
513513
ib_coefficient_of_friction = dflt_real
514514
ib_state_wrt = .false.
515+
ib_force_stride = 1
515516
many_ib_patch_parallelism = .false.
516517
517518
! Bubble modeling (sim-specific)

src/simulation/m_start_up.fpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,8 @@ contains
11091109
!> Finalize and deallocate all simulation sub-modules in reverse initialization order
11101110
impure subroutine s_finalize_modules
11111111

1112+
if (ib .and. ib_state_wrt) call s_flush_ib_force_files() ! write whatever is still buffered
1113+
11121114
if (model_eqns == model_eqns_6eq) call s_report_pressure_relaxation()
11131115

11141116
call s_finalize_time_steppers_module()

src/simulation/m_time_steppers.fpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,8 @@ contains
477477
call s_compute_derived_variables(t_step, q_cons_ts(1)%vf, q_prim_ts1, q_prim_ts2)
478478
end if
479479

480+
if (ib_state_wrt) call s_write_ib_force_files(t_step)
481+
480482
if (cfl_dt) then
481483
if (mytime >= t_stop) return
482484
else

toolchain/mfc/case_validator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,8 @@ def check_ibm(self):
759759
)
760760
self.prohibit(not ib and num_ibs > 0, "num_ibs is set, but ib is not enabled")
761761
self.prohibit(ib_state_wrt and not ib, "ib_state_wrt requires ib to be enabled")
762+
ib_force_stride = self.get("ib_force_stride", 1) or 1
763+
self.prohibit(ib_force_stride < 1, "ib_force_stride must be >= 1")
762764
self.prohibit(many_ib_patch_parallelism and not ib, "many_ib_patch_parallelism requires ib to be enabled")
763765

764766
for i in range(1, num_particle_clouds + 1):

toolchain/mfc/params/definitions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,7 @@ def _load():
677677
# Output
678678
_r("precision", INT, {"output"})
679679
_r("format", INT, {"output"})
680+
_r("ib_force_stride", INT, {"output", "ib"})
680681
for n in ["parallel_io", "file_per_process", "run_time_info", "prim_vars_wrt", "cons_vars_wrt", "fft_wrt", "ib_state_wrt"]:
681682
_r(n, LOG, {"output"})
682683
for n in [
@@ -1343,6 +1344,7 @@ def _decl(targets: set, *names: str) -> None:
13431344
"prim_vars_wrt",
13441345
"fd_order",
13451346
"ib_state_wrt",
1347+
"ib_force_stride",
13461348
"avg_state",
13471349
"alt_soundspeed",
13481350
"mixture_err",

toolchain/mfc/params/descriptions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
"cf_wrt": "Write color function field",
137137
# Immersed boundaries
138138
"ib": "Enable immersed boundary method",
139+
"ib_force_stride": "Write the per-step immersed-boundary force record every N steps (default 1)",
139140
"num_ibs": "Number of immersed boundary patches",
140141
"num_stl_models": "Number of STL/OBJ model entries in the stl_models array",
141142
"num_particle_clouds": "Number of particle bed specifications to generate immersed boundary patches from",

0 commit comments

Comments
 (0)