Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/documentation/case.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,8 @@ This is enabled by adding ``'elliptic_smoothing': "T",`` and ``'elliptic_smoothi
| `num_particle_clouds` | Integer | Number of particle bed specifications to generate immersed boundary patches from |
| `ib_neighborhood_radius` | Integer | Parameter that controls the neighborhood size for IB detection. |
| `many_ib_patch_parallelism` | Logical | Parallelize over IB patches instead of grid cells (better for many small patches). |
| `ib_second_order_vel` | Logical | Extrapolate the ghost-point velocity linearly through the boundary intercept. |
| `ib_ip_min_dist` | Real | Minimum boundary-to-image-point distance, in local cell widths, for `ib_second_order_vel`. |
| `geometry` | Integer | Geometry configuration of the patch.|
| `x[y,z]_centroid` | Real | Centroid of the applied geometry in the [x,y,z]-direction. |
| `length_x[y,z]` | Real | Length, if applicable, in the [x,y,z]-direction. |
Expand Down Expand Up @@ -450,6 +452,10 @@ Additional details on this specification can be found in [NACA airfoil](https://

- `ib_coefficient_of_friction` is the coefficient of friction used in IB collisions.

- `ib_second_order_vel` replaces the default ghost-point velocity, which imposes the wall velocity at the ghost point, with the linear extrapolation of Mittal et al. (2008) through the boundary intercept and replaces the interpolation scheme with a bilinear weight interpolation.

- `ib_ip_min_dist` sets \f$d_{IP} = \max(d_{GP}, \text{ib\_ip\_min\_dist}\,\Delta)\f$, with \f$\Delta\f$ the smallest local cell width, so image points of near-wall ghost points interpolate from fluid cells. The default 0 is the pure mirror (\f$d_{IP} = d_{GP}\f$). About \f$\sqrt{2}\f$ in 2D (\f$\sqrt{3}\f$ in 3D) keeps the stencil off a locally planar wall.

- `ib_neighborhood_radius` controls the size of the neighborhood size. A value of $r$ indicates that any given rank is aware of IBs up to $r$ ranks away. This value defaults to 0, which leaves the radius unset so that it is selected automatically. This parameter is required to strong-scale a case when IBs eventually grow to be larger than one full processor domain wide.

#### Particle Clouds
Expand Down
21 changes: 21 additions & 0 deletions docs/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,27 @@ @article{Mittal05
doi = {10.1146/annurev.fluid.37.061903.175743}
}

% second order IBM
@article{Mittal08,
author = {R. Mittal and H. Dong and M. Bozkurttas and F. M. Najjar and A. Vargas and A. von Loebbecke},
title = {A versatile sharp interface immersed boundary method for incompressible flows with complex boundaries},
journal = {Journal of Computational Physics},
volume = {227},
number = {10},
pages = {4825--4852},
year = {2008}
}

@article{Chaudhuri11,
author = {A. Chaudhuri and A. Hadjadj and A. Chinnayya},
title = {On the use of immersed boundary methods for shock/obstacle interactions},
journal = {Journal of Computational Physics},
volume = {230},
number = {5},
pages = {1731--1748},
year = {2011}
}

% --- Low Mach corrections ---
@article{Thornber08,
Expand Down
119 changes: 119 additions & 0 deletions examples/2D_ibm_second_order_airfoil/case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""
NACA 0012 at M = 0.3, alpha = 2 deg, 200 cells per chord: the 2D_ibm_airfoil_surface_pressure example
switched to a viscous no-slip wall (Re = 1000 per chord), with frequent snapshots for time averaging.
Env: RE, T_STOP, T_SAVE, SECOND_ORDER=1 (PR branch only: second-order IB velocities).
"""

import json
import math
import os

Ma = 0.3
alpha_deg = 2.0
gamma = 1.4
rho_inf, U_inf, chord = 1.0, 1.0, 1.0
Re = float(os.environ.get("RE", 1000.0))
P_inf = rho_inf * U_inf**2 / (gamma * Ma**2)

case = {
# --- Output ---
"run_time_info": "T",
"format": 2,
"precision": 2,
"parallel_io": "T",
"prim_vars_wrt": "T",
# --- Domain: pre-stretch extents; the stretching maps them outward ---
"x_domain%beg": -3.0,
"x_domain%end": 4.0,
"y_domain%beg": -3.0,
"y_domain%end": 3.0,
"m": 1399,
"n": 1199,
"p": 0,
"cyl_coord": "F",
"stretch_x": "T",
"a_x": 15.0,
"x_a": -0.8,
"x_b": 1.8,
"loops_x": 2,
"stretch_y": "T",
"a_y": 15.0,
"y_a": -0.7,
"y_b": 0.7,
"loops_y": 2,
# --- Time stepping ---
"cfl_adap_dt": "T",
"cfl_target": 0.5,
"n_start": 0,
"t_save": 0.2,
"t_stop": 6.0,
# --- Numerics ---
"num_patches": 1,
"num_fluids": 1,
"model_eqns": 2,
"alt_soundspeed": "F",
"mpp_lim": "F",
"mixture_err": "T",
"time_stepper": 3,
"weno_order": 5,
"weno_eps": 1.0e-10,
"weno_Re_flux": "F",
"weno_avg": "T",
"avg_state": 2,
"mapped_weno": "T",
"null_weights": "F",
"mp_weno": "F",
"riemann_solver": 2,
"low_Mach": 2,
"wave_speeds": 1,
"viscous": "T",
"fd_order": 4,
# --- Uniform freestream ---
"patch_icpp(1)%geometry": 3,
"patch_icpp(1)%x_centroid": 0.0,
"patch_icpp(1)%y_centroid": 0.0,
"patch_icpp(1)%length_x": 1.0e3,
"patch_icpp(1)%length_y": 1.0e3,
"patch_icpp(1)%vel(1)": U_inf,
"patch_icpp(1)%vel(2)": 0.0,
"patch_icpp(1)%pres": P_inf,
"patch_icpp(1)%alpha_rho(1)": rho_inf,
"patch_icpp(1)%alpha(1)": 1.0,
"fluid_pp(1)%gamma": 1.0 / (gamma - 1.0),
"fluid_pp(1)%eos": "ideal_gas",
"fluid_pp(1)%Re(1)": Re,
# --- Characteristic far-field boundaries ---
"bc_x%beg": -7,
"bc_x%grcbc_in": "T",
"bc_x%vel_in(1)": U_inf,
"bc_x%vel_in(2)": 0.0,
"bc_x%pres_in": P_inf,
"bc_x%alpha_rho_in(1)": rho_inf,
"bc_x%alpha_in(1)": 1.0,
"bc_x%end": -8,
"bc_x%grcbc_out": "T",
"bc_x%pres_out": P_inf,
"bc_y%beg": -9,
"bc_y%end": -9,
# --- IB: NACA 0012 (m must be > 0, so a negligible camber stands in for 0) ---
"ib": "T",
"num_ibs": 1,
"patch_ib(1)%geometry": 4,
"patch_ib(1)%x_centroid": 0.0,
"patch_ib(1)%y_centroid": 0.0,
"patch_ib(1)%airfoil_id": 1,
"patch_ib(1)%angles(3)": -math.radians(alpha_deg),
"patch_ib(1)%slip": "F",
"patch_ib(1)%moving_ibm": 0,
"ib_airfoil(1)%c": chord,
"ib_airfoil(1)%t": 0.12,
"ib_airfoil(1)%p": 0.4,
"ib_airfoil(1)%m": 1.0e-9,
}

# the parameters for second-order IBM
case.update({"ib_second_order_vel": "T", "ib_ip_min_dist": 1.5})


print(json.dumps(case, indent=4))
1 change: 1 addition & 0 deletions src/common/m_derived_types.fpp
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ module m_derived_types
type ghost_point
integer, dimension(3) :: loc !< Physical location of the ghost point
real(wp), dimension(3) :: ip_loc !< Physical location of the image point
real(wp) :: ip_dist !< Distance from the boundary intercept to the image point
integer, dimension(3) :: ip_grid !< Top left grid point of IP
real(wp), dimension(2, 2, 2) :: interp_coeffs !< Interpolation Coefficients of image point
logical :: interp_valid !< .false. if every image point stencil cell lies inside an IB
Expand Down
2 changes: 2 additions & 0 deletions src/simulation/m_global_parameters.fpp
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,8 @@ contains
ib_force_wrt = .false.
ib_force_stride = 1
many_ib_patch_parallelism = .false.
ib_second_order_vel = .false.
ib_ip_min_dist = 0._wp
! Bubble modeling (sim-specific)
bubble_model = 1
Expand Down
130 changes: 75 additions & 55 deletions src/simulation/m_ibm.fpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ module m_ibm
implicit none

private :: s_compute_image_points, s_compute_interpolation_coeffs, s_interpolate_image_point, s_find_ghost_points, &
& s_find_num_ghost_points, s_compute_ghost_point_pressure, s_compute_ghost_point_velocity
& s_find_num_ghost_points, s_compute_ghost_point_pressure, s_compute_ghost_point_velocity, s_interpolation_weights
; public :: s_initialize_ibm_module, s_ibm_setup, s_ibm_correct_state, s_finalize_ibm_module, s_report_ibm_surface

!> Ghost points at which the reacting-surface Newton solve did not reach its tolerance, so the point fell back to a chemically
Expand Down Expand Up @@ -85,6 +85,7 @@ contains
@:ACC_SETUP_SFs(corrected_gps)

$:GPU_ENTER_DATA(copyin='[num_gps]')
$:GPU_UPDATE(device='[ib_second_order_vel, ib_ip_min_dist]')

if (collision_model > 0) call s_initialize_collisions_module()

Expand Down Expand Up @@ -193,7 +194,7 @@ contains
real(wp), intent(in) :: rho, pres_IP
real(wp), intent(out) :: pres_GP

pres_GP = pres_IP/min(max(1._wp - 2._wp*abs(gp%levelset) &
pres_GP = pres_IP/min(max(1._wp - (abs(gp%levelset) + gp%ip_dist) &
& *rho/pres_IP*dot_product(patch_ib(gp_patch_id)%force/patch_ib(gp_patch_id)%mass, &
& gp%levelset_norm), 5.e-1_wp), 2._wp)

Expand Down Expand Up @@ -257,6 +258,9 @@ contains
if (buf > 0._wp) vel_GP = vel_GP + v_blow_eff*norm/buf
end if

! extrapolate linearly through the boundary intercept, Mittal et al. (2008)
if (ib_second_order_vel) vel_GP = vel_GP - abs(gp%levelset)/max(gp%ip_dist, sgm_eps)*(vel_IP - vel_GP)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use linear-exact interpolation for second-order velocity

For an image point that is not symmetrically placed among its stencil centers, vel_IP comes from s_compute_interpolation_coeffs, whose normalized 1/dist**2 weights at lines 1017-1028 do not reproduce even a linear velocity field. This new no-slip extrapolation therefore propagates an O(h) image-velocity error into the ghost value and cannot provide the advertised second-order surface velocity; use bilinear/trilinear linear-exact weights or an equivalent correction. The added Circle -> second order vel goldens use only one n=49 grid, so they cannot verify the claimed convergence order.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Evaluate rotating wall velocity at the boundary intercept

When ib_second_order_vel is used on a rotating no-slip moving IB (angular_vel != 0), the value in vel_GP before this line is the rigid-body velocity at the ghost center because radial_vector is built from physical_loc at lines 485-492. This extrapolation treats that value as u_BI, so it enforces Ω×(x_GP-C) at the intercept instead of Ω×(x_BI-C), leaving an O(h) tangential slip; build the rotation velocity from the boundary-intercept location before extrapolating. The registered second-order test leaves the body static, so this path is unverified.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.


end subroutine s_compute_ghost_point_velocity

!> Update the conservative variables at the ghost points
Expand Down Expand Up @@ -706,6 +710,7 @@ contains
impure subroutine s_compute_image_points()

real(wp) :: dist
real(wp) :: min_cell_width
real(wp), dimension(3) :: norm
real(wp), dimension(3) :: physical_loc
real(wp) :: temp_loc
Expand All @@ -721,8 +726,8 @@ contains

bounds_error = .false.

$:GPU_PARALLEL_LOOP(private='[q, gp, i, j, k, physical_loc, patch_id, dist, norm, dim, bound, dir, index, temp_loc, &
& s_cc]', copy='[bounds_error]', present='[ghost_points]')
$:GPU_PARALLEL_LOOP(private='[q, gp, i, j, k, physical_loc, patch_id, dist, min_cell_width, norm, dim, bound, dir, index, &
& temp_loc, s_cc]', copy='[bounds_error]', present='[ghost_points]')
do q = 1, num_gps
gp = ghost_points(q)
i = gp%loc(1)
Expand All @@ -740,7 +745,14 @@ contains
patch_id = gp%ib_patch_id
dist = abs(real(gp%levelset, kind=wp))
norm(:) = gp%levelset_norm
ghost_points(q)%ip_loc(:) = physical_loc(:) + 2*dist*norm(:)
ghost_points(q)%ip_dist = dist
! keep the image point stencil off the wall
if (ib_second_order_vel) then
min_cell_width = min(dx(i), dy(j))
if (p > 0) min_cell_width = min(min_cell_width, dz(k))
ghost_points(q)%ip_dist = max(dist, ib_ip_min_dist*min_cell_width)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Carry the image distance through surface reconstruction

When ib_ip_min_dist*Δ exceeds abs(levelset) in a chemistry run with a prescribed-temperature or reacting IB, this assignment makes the gas-side image-point distance differ from the ghost-point distance, but s_ibm_correct_state still passes abs(gp%levelset) to the surface-flux solve at line 407 and s_blend_ghost_state extrapolates temperature and species with a unit distance ratio at lines 2204-2206. The resulting flux and ghost state no longer recover the requested wall state; use gp%ip_dist for IP-to-wall gradients and d_GP/d_IP for the ghost extrapolation, including the Stefan velocity. The registered second-order cases do not enable chemistry, so this accepted combination is currently unverified.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

end if
ghost_points(q)%ip_loc(:) = physical_loc(:) + (dist + ghost_points(q)%ip_dist)*norm(:)

! Find the closest grid point to the image point
do dim = 1, num_dims
Expand Down Expand Up @@ -931,16 +943,12 @@ contains
subroutine s_compute_interpolation_coeffs()

real(wp), dimension(2, 2, 2) :: dist
real(wp), dimension(2, 2, 2) :: alpha
real(wp), dimension(2, 2, 2) :: fluid_mask
real(wp), dimension(2, 2, 2) :: interp_coeffs
real(wp) :: buf
real(wp), dimension(2, 2, 2) :: eta
type(ghost_point) :: gp
integer :: q, i, j, k, ii, jj, kk !< Grid indexes and iterators
logical :: is_cell_center

$:GPU_PARALLEL_LOOP(private='[q, i, j, k, ii, jj, kk, dist, buf, gp, interp_coeffs, eta, alpha, is_cell_center]', &
& present='[ghost_points]')
$:GPU_PARALLEL_LOOP(private='[q, i, j, k, ii, jj, kk, dist, gp, interp_coeffs, fluid_mask]', present='[ghost_points]')
do q = 1, num_gps
gp = ghost_points(q)
! Get the interpolation points
Expand All @@ -954,7 +962,6 @@ contains

! get the distance to a cell in each direction
dist = 0._wp
buf = 1._wp
do ii = 0, 1
do jj = 0, 1
if (p == 0) then
Expand All @@ -969,51 +976,17 @@ contains
end do
end do

! check if we are arbitrarily close to a cell center
interp_coeffs = 0._wp
is_cell_center = .false.
check_is_cell_center: do ii = 0, 1
! flag the fluid cells of the stencil
fluid_mask = 0._wp
do kk = 0, min(p, 1)
do jj = 0, 1
! A coincident solid cell leaves the point invalid: falling through to the eta
! branch with dist = 0 would give alpha*eta = 0*Inf = NaN.
if (dist(ii + 1, jj + 1, 1) <= 1.e-16_wp) then
if (ib_markers%sf(i + ii, j + jj, k) == 0) interp_coeffs(ii + 1, jj + 1, 1) = 1._wp
is_cell_center = .true.
exit check_is_cell_center
else
if (p /= 0) then
if (dist(ii + 1, jj + 1, 2) <= 1.e-16_wp) then
if (ib_markers%sf(i + ii, j + jj, k + 1) == 0) interp_coeffs(ii + 1, jj + 1, 2) = 1._wp
is_cell_center = .true.
exit check_is_cell_center
end if
end if
end if
do ii = 0, 1
if (ib_markers%sf(i + ii, j + jj, k + kk) == 0) fluid_mask(ii + 1, jj + 1, kk + 1) = 1._wp
end do
end do
end do check_is_cell_center

if (.not. is_cell_center) then
! if we are not arbitrarily close, interpolate
alpha = 1._wp
if (ib_markers%sf(i, j, k) /= 0) alpha(1, 1, 1) = 0._wp
if (ib_markers%sf(i + 1, j, k) /= 0) alpha(2, 1, 1) = 0._wp
if (ib_markers%sf(i, j + 1, k) /= 0) alpha(1, 2, 1) = 0._wp
if (ib_markers%sf(i + 1, j + 1, k) /= 0) alpha(2, 2, 1) = 0._wp

if (p == 0) then
eta(:,:,1) = 1._wp/dist(:,:,1)**2
buf = sum(alpha(:,:,1)*eta(:,:,1))
if (buf > 0._wp) interp_coeffs(:,:,1) = alpha(:,:,1)*eta(:,:,1)/buf
else
if (ib_markers%sf(i, j, k + 1) /= 0) alpha(1, 1, 2) = 0._wp
if (ib_markers%sf(i + 1, j, k + 1) /= 0) alpha(2, 1, 2) = 0._wp
if (ib_markers%sf(i, j + 1, k + 1) /= 0) alpha(1, 2, 2) = 0._wp
if (ib_markers%sf(i + 1, j + 1, k + 1) /= 0) alpha(2, 2, 2) = 0._wp
eta = 1._wp/dist**2
buf = sum(alpha*eta)
if (buf > 0._wp) interp_coeffs = alpha*eta/buf
end if
end if
end do

call s_interpolation_weights(gp, i, j, k, dist, fluid_mask, interp_coeffs)

! An image point buried in a neighboring IB gets no weights at all: its ghost point is
! averaged from its own neighbors instead, in s_ibm_correct_state.
Expand All @@ -1025,6 +998,53 @@ contains

end subroutine s_compute_interpolation_coeffs

!> Fluid-masked image-point weights: bilinear for second-order IBM, else inverse distance squared
subroutine s_interpolation_weights(gp, i, j, k, dist, fluid_mask, interp_coeffs)

$:GPU_ROUTINE(parallelism='[seq]')

type(ghost_point), intent(in) :: gp
integer, intent(in) :: i, j, k !< lower corner of the stencil
real(wp), dimension(2, 2, 2), intent(in) :: dist, fluid_mask
real(wp), dimension(2, 2, 2), intent(out) :: interp_coeffs
real(wp), dimension(2, 2, 2) :: weights
real(wp), dimension(2, 3) :: axis_weight !< (stencil side, dimension) linear weights
real(wp) :: weight_sum
integer :: ii, jj, kk, num_z_layers

num_z_layers = min(p, 1) + 1

if (ib_second_order_vel) then
! bilinear weights, Mittal et al. (2008)
axis_weight(2, 1) = (gp%ip_loc(1) - x_cc(i))/(x_cc(i + 1) - x_cc(i))
axis_weight(2, 2) = (gp%ip_loc(2) - y_cc(j))/(y_cc(j + 1) - y_cc(j))
axis_weight(2, 3) = 0._wp
if (p /= 0) axis_weight(2, 3) = (gp%ip_loc(3) - z_cc(k))/(z_cc(k + 1) - z_cc(k))
axis_weight(2,:) = min(max(axis_weight(2,:), 0._wp), 1._wp) ! clamp to the cell edges
axis_weight(1,:) = 1._wp - axis_weight(2,:)
do kk = 1, num_z_layers
do jj = 1, 2
do ii = 1, 2
weights(ii, jj, kk) = axis_weight(ii, 1)*axis_weight(jj, 2)*axis_weight(kk, 3)
end do
end do
end do
else
! inverse distance squared, Chaudhuri et al. JCP (2011)
if (minval(dist(:,:,1:num_z_layers)) <= 1.e-16_wp) then
weights(:,:,1:num_z_layers) = merge(1._wp, 0._wp, dist(:,:,1:num_z_layers) <= 1.e-16_wp)
else
weights(:,:,1:num_z_layers) = 1._wp/dist(:,:,1:num_z_layers)**2
end if
end if

interp_coeffs = 0._wp
weights(:,:,1:num_z_layers) = fluid_mask(:,:,1:num_z_layers)*weights(:,:,1:num_z_layers)
weight_sum = sum(weights(:,:,1:num_z_layers))
if (weight_sum > 0._wp) interp_coeffs(:,:,1:num_z_layers) = weights(:,:,1:num_z_layers)/weight_sum

end subroutine s_interpolation_weights

!> Interpolate primitive variables to a ghost point's image point using bilinear or trilinear interpolation
subroutine s_interpolate_image_point(q_prim_vf, gp, alpha_rho_IP, alpha_IP, pres_IP, vel_IP, c_IP, r_IP, v_IP, pb_IP, mv_IP, &
& nmom_IP, pb_in, mv_in, presb_IP, massv_IP, Ys_IP)
Expand Down
Loading
Loading