Skip to content

Conversation

@danieljvickers
Copy link
Member

@danieljvickers danieljvickers commented Sep 26, 2025

User description

Description

This PR adds a 1st-order moving immersed boundary method, which will need to be iterated upon. This implimented will require some additional work via adding rotations, higher-order time stepping, and performance optimizations. But it puts in the framework for moving immersed boundaries that can be built upon. I would like to move forward with merging, and then going back to optimize the code, since the current functionality completely captures the scope of a new feature. The code still performs optimally when the new feature is disabled.

This PR also refactors the patch frame work to separate the notions of ICPP and IB patches, which allows IB patches to be moved into a common directory.

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

Scope

  • This PR comprises a set of related changes with a common goal

If you cannot check the above box, please split your PR into multiple PRs that each have a common goal.

How Has This Been Tested?

Please describe the tests that you ran to verify your changes.
Provide instructions so we can reproduce.
Please also list any relevant details for your test configuration

  • I verified this version by running multiple tests of immersed boundaries moving through a stationary and moving fluid, and creating vortexes. A quiver plot of a circle traveling through a stationary fluid on a 50x50 grid can be viewed at: https://youtu.be/7App5aeE4rI. I will soon add a new example case that contains this case file run. View below:

Test Configuration:

import json
import math

Mu = 1.84e-05
gam_a = 1.4

# Configuring case dictionary
print(
    json.dumps(
        {
            # Logistics
            "run_time_info": "T",
            # Computational Domain Parameters
            # For these computations, the cylinder is placed at the (0,0,0)
            # domain origin.
            # axial direction
            "x_domain%beg": 0.0e00,
            "x_domain%end": 6.0e-03,
            # r direction
            "y_domain%beg": 0.0e00,
            "y_domain%end": 6.0e-03,
            "cyl_coord": "F",
            "m": 50,
            "n": 50,
            "p": 0,
            "dt": 6.0e-5,
            "t_step_start": 0,
            "t_step_stop": 1000,
            "t_step_save": 100,
            # Simulation Algorithm Parameters
            # Only one patches are necessary, the air tube
            "num_patches": 1,
            # Use the 5 equation model
            "model_eqns": 2,
            "alt_soundspeed": "F",
            # One fluids: air
            "num_fluids": 1,
            # time step
            "mpp_lim": "F",
            # Correct errors when computing speed of sound
            "mixture_err": "T",
            # Use TVD RK3 for time marching
            "time_stepper": 3,
            # Use WENO5
            "weno_order": 5,
            "weno_eps": 1.0e-16,
            "weno_Re_flux": "T",
            "weno_avg": "T",
            "avg_state": 2,
            "mapped_weno": "T",
            "null_weights": "F",
            "mp_weno": "T",
            "riemann_solver": 2,
            "wave_speeds": 1,
            # We use ghost-cell
            "bc_x%beg": -3,
            "bc_x%end": -3,
            "bc_y%beg": -3,
            "bc_y%end": -3,
            # Set IB to True and add 1 patch
            "ib": "T",
            "num_ibs": 1,
            "viscous": "T",
            # Formatted Database Files Structure Parameters
            "format": 1,
            "precision": 2,
            "prim_vars_wrt": "T",
            "E_wrt": "T",
            "parallel_io": "T",
            # Patch: Constant Tube filled with air
            # Specify the cylindrical air tube grid geometry
            "patch_icpp(1)%geometry": 3,
            "patch_icpp(1)%x_centroid": 3.0e-03,
            # Uniform medium density, centroid is at the center of the domain
            "patch_icpp(1)%y_centroid": 3.0e-03,
            "patch_icpp(1)%length_x": 6.0e-03,
            "patch_icpp(1)%length_y": 6.0e-03,
            # Specify the patch primitive variables
            "patch_icpp(1)%vel(1)": 0.00e00,
            "patch_icpp(1)%vel(2)": 0.0e00,
            "patch_icpp(1)%pres": 1.0e00,
            "patch_icpp(1)%alpha_rho(1)": 1.0e00,
            "patch_icpp(1)%alpha(1)": 1.0e00,
            # Patch: Cylinder Immersed Boundary
            "patch_ib(1)%geometry": 2,
            "patch_ib(1)%x_centroid": 4.5e-03,
            "patch_ib(1)%y_centroid": 3.0e-03,
            "patch_ib(1)%radius": 0.2e-03,
            "patch_ib(1)%slip": "F",
            "patch_ib(1)%moving_ibm": 1,
            "patch_ib(1)%vel(1)": -0.05e00,
            "patch_ib(1)%vel(2)": 0.0,
            "patch_ib(1)%vel(3)": 0.0,
            # Fluids Physical Parameters
            "fluid_pp(1)%gamma": 1.0e00 / (gam_a - 1.0e00),  # 2.50(Not 1.40)
            "fluid_pp(1)%pi_inf": 0,
            "fluid_pp(1)%Re(1)": 2500000,
        }
    )
)
  • What computers and compilers did you use to test this:

I tested this locally on GNU compilers with and without MPI enabled. I also tested on Wingtip with NVHPC compilers with and without MPI and on GPUs.

Checklist

  • I have added comments for the new code
  • I added Doxygen docstrings to the new code
  • I have made corresponding changes to the documentation (docs/)
  • I have added regression tests to the test suite so that people can verify in the future that the feature is behaving as expected
  • I have added example cases in examples/ that demonstrate my new feature performing as expected.
    They run to completion and demonstrate "interesting physics"
  • I ran ./mfc.sh format before committing my code
  • New and existing tests pass locally with my changes, including with GPU capability enabled (both NVIDIA hardware with NVHPC compilers and AMD hardware with CRAY compilers) and disabled
  • This PR does not introduce any repeated code (it follows the DRY principle)
  • I cannot think of a way to condense this code and reduce any introduced additional line count

If your code changes any code source files (anything in src/simulation)

To make sure the code is performing as expected on GPU devices, I have:

  • Checked that the code compiles using NVHPC compilers
  • Checked that the code compiles using CRAY compilers
  • Ran the code on either V100, A100, or H100 GPUs and ensured the new feature performed as expected (the GPU results match the CPU results)
  • Ran the code on MI200+ GPUs and ensure the new features performed as expected (the GPU results match the CPU results)
  • Enclosed the new feature via nvtx ranges so that they can be identified in profiles
  • Ran a Nsight Systems profile using ./mfc.sh run XXXX --gpu -t simulation --nsys, and have attached the output file (.nsys-rep) and plain text results to this PR
  • Ran a Rocprof Systems profile using ./mfc.sh run XXXX --gpu -t simulation --rsys --hip-trace, and have attached the output file and plain text results to this PR.

PR Type

Enhancement


Description

• Implements first-order moving immersed boundary method (MIBM) with velocity-based boundary conditions
• Refactors patch framework to separate ICPP and IB patches into dedicated modules (m_icpp_patches and m_ib_patches)
• Adds moving boundary support with moving_ibm flag and velocity parameters in patch data structures
• Integrates moving boundary updates in main simulation time-stepping loop via s_update_mib function
• Enhances MPI support for broadcasting moving boundary parameters across processes
• Updates build configuration to exclude IB-specific modules from post-process builds
• Adds analytical function support in case generation toolchain


Diagram Walkthrough

flowchart LR
  A["Original m_patches module"] --> B["m_icpp_patches module"]
  A --> C["m_ib_patches module"]
  C --> D["Moving IBM functionality"]
  D --> E["Velocity-based boundaries"]
  D --> F["Time-stepping integration"]
  B --> G["ICPP geometry handling"]
  C --> H["IB geometry handling"]
  E --> I["MPI parameter broadcasting"]
Loading

File Walkthrough

Relevant files
Enhancement
13 files
m_icpp_patches.fpp
Refactor patches module to separate ICPP and IB functionality

src/pre_process/m_icpp_patches.fpp

• Renamed module from m_patches to m_icpp_patches and refactored to
separate ICPP and IB patch handling
• Removed IB patch processing
logic and parameters from all geometry subroutines
• Added s_icpp_
prefix to all geometry subroutines (sphere, circle, rectangle, etc.)

Simplified subroutine signatures by removing optional IB-related
parameters

+188/-761
m_ibm.fpp
Implement moving immersed boundary method functionality   

src/simulation/m_ibm.fpp

• Added moving immersed boundary (MIBM) support with velocity-based
boundary conditions
• Implemented s_propagate_mib and s_update_mib
functions for moving boundary updates
• Added
moving_immersed_boundary_flag to track if any boundaries are moving

Enhanced ghost point allocation with buffer scaling for dynamic
boundaries

+106/-4 
m_mpi_common.fpp
Add MPI integer reduction and fix compilation structure   

src/common/m_mpi_common.fpp

• Added s_mpi_allreduce_integer_sum function for integer reduction
operations
• Fixed conditional compilation structure for MPI
initialization
• Consolidated QBMM variable handling across
pre-process and simulation modules

+33/-22 
m_initial_condition.fpp
Update initial condition to use separated patch modules   

src/pre_process/m_initial_condition.fpp

• Updated to use separate m_ib_patches and m_icpp_patches modules

Modified patch application to call both IB and ICPP patch functions
separately

+5/-4     
m_mpi_proxy.fpp
Add MPI support for moving boundary parameters                     

src/simulation/m_mpi_proxy.fpp

• Added MPI broadcasting for new IB patch parameters: moving_ibm and
vel

+2/-1     
p_main.fpp
Integrate moving boundary updates in main simulation loop

src/simulation/p_main.fpp

• Added moving immersed boundary update call in main time-stepping
loop
• Integrated s_update_mib function when
moving_immersed_boundary_flag is active

+5/-0     
m_global_parameters.fpp
Initialize moving boundary parameters in global setup       

src/pre_process/m_global_parameters.fpp

• Added initialization of moving immersed boundary parameters
(moving_ibm, vel) with default values

+6/-0     
m_derived_types.fpp
Add moving boundary data structures to IB patch type         

src/common/m_derived_types.fpp

• Added moving_ibm integer flag and vel velocity array to
ib_patch_parameters type

+5/-0     
m_checker.fpp
Add placeholder for moving IBM validation                               

src/pre_process/m_checker.fpp

• Added placeholder s_check_moving_IBM subroutine for future
validation

+5/-1     
m_start_up.fpp
Update startup module imports for separated patches           

src/pre_process/m_start_up.fpp

• Updated module imports to use separated m_ib_patches and
m_icpp_patches

+3/-1     
case.py
Enhance case generation to support analytical functions   

toolchain/mfc/case.py

• Modified FPP generation to include pre-processing includes in
simulation builds
• Added support for common subroutines to access
@:analytical function

+8/-6     
case_dicts.py
Add moving boundary parameters to case dictionaries           

toolchain/mfc/run/case_dicts.py

• Added moving_ibm parameter and vel velocity components to IB patch
parameter dictionaries
• Extended parameter support for both
pre-process and simulation modules

+10/-2   
m_ib_patches.fpp
New immersed boundary patches module implementation           

src/common/m_ib_patches.fpp

• Creates a new module m_ib_patches dedicated to immersed boundary
(IB) patch handling
• Implements geometry-specific IB patch
subroutines for circles, rectangles, spheres, cuboids, cylinders,
airfoils, and STL models
• Adds coordinate conversion utilities for
cylindrical to cartesian and spherical coordinates
• Includes levelset
computation functionality for various geometric shapes

+1040/-0
Formatting
2 files
case.fpp
Minor formatting change in case include file                         

src/common/include/case.fpp

• Added empty line in analytical macro definition

+1/-0     
run.py
Minor formatting change in run module                                       

toolchain/mfc/run/run.py

• Added trailing whitespace in run function

+1/-1     
Configuration changes
2 files
CMakeLists.txt
Update build configuration for separated modules                 

CMakeLists.txt

• Added exclusion of m_compute_levelset.fpp and m_ib_patches.fpp from
post_process builds
• Added stdc++ library linking for SILO builds

+8/-1     
settings.json
Disable Fortran language server in VS Code settings           

.vscode/settings.json

• Disabled fortls language server by setting fortran.fortls.disabled
to true

+1/-1     
Additional files
6 files
1dHardcodedIC.fpp [link]   
2dHardcodedIC.fpp [link]   
3dHardcodedIC.fpp [link]   
ExtrusionHardcodedIC.fpp [link]   
m_compute_levelset.fpp [link]   
m_model.fpp [link]   

…ndaries with moving imersed boundary variables
…r what is applied each loop. Wrote a seaparate function and working on passing everything in
…ib patch definitions. This is a precommit before I start separating the two files
@qodo-merge-pro
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

Allocation of ghost/inner point arrays now uses globally summed counts with a 1.2 factor; if ranks have imbalanced distributions, indexing using local num_gps/num_inner_gps may overrun allocations or leave unused capacity. Verify consistency between allocated sizes and downstream array indexing and ensure no out-of-bounds accesses on ranks with larger local counts.

$:GPU_UPDATE(host='[ib_markers%sf]')

! find the number of ghost points and set them to be the maximum total across ranks
call s_find_num_ghost_points(num_gps, num_inner_gps)
call s_mpi_allreduce_integer_sum(num_gps, max_num_gps)
call s_mpi_allreduce_integer_sum(num_inner_gps, max_num_inner_gps)

$:GPU_UPDATE(device='[num_gps, num_inner_gps]')
@:ALLOCATE(ghost_points(1:int(max_num_gps * 1.2)))
@:ALLOCATE(inner_points(1:int(max_num_inner_gps * 1.2)))

$:GPU_ENTER_DATA(copyin='[ghost_points,inner_points]')
Behavior Change

Ghost velocity for non-slip case now depends on patch_ib%moving_ibm and uses a uniform patch velocity vector; previously defaulted to zero. Confirm this applies correct wall velocities for all ghost points (including rotating/axis-aligned cases) and that units/time-step coupling are correct.

    vel_norm_IP = sum(vel_IP*norm)*norm
    vel_g = vel_IP - vel_norm_IP
else
    if (patch_ib(patch_id)%moving_ibm == 0) then
        ! we know the object is not moving if moving_ibm is 0 (false)
        vel_g = 0._wp
    else
        do q = 1, 3
            ! if mibm is 1 or 2, then the boundary may be moving
            vel_g(q) = patch_ib(patch_id)%vel(q)
        end do
    end if
end if
API Refactor Risk

Renamed many routines to s_icpp_* and removed IB handling from ICPP paths while introducing m_ib_patches. Ensure all call sites were updated (e.g., s_apply_icpp_patches vs s_apply_domain_patches) and there’s no duplicate or missing logic for IB vs ICPP, especially for STL/model and smoothing flags.

contains

    impure subroutine s_apply_icpp_patches(patch_id_fp, q_prim_vf)

        type(scalar_field), dimension(1:sys_size), intent(inout) :: q_prim_vf
        integer, dimension(0:m, 0:m, 0:m), intent(inout) :: patch_id_fp

        integer :: i

        !  3D Patch Geometries
        if (p > 0) then

            do i = 1, num_patches

                if (proc_rank == 0) then
                    print *, 'Processing patch', i
                end if

                !> ICPP Patches
                !> @{
                ! Spherical patch
                if (patch_icpp(i)%geometry == 8) then
                    call s_icpp_sphere(i, patch_id_fp, q_prim_vf)
                    ! Cuboidal patch
                elseif (patch_icpp(i)%geometry == 9) then
                    call s_icpp_cuboid(i, patch_id_fp, q_prim_vf)
                    ! Cylindrical patch
                elseif (patch_icpp(i)%geometry == 10) then
                    call s_icpp_cylinder(i, patch_id_fp, q_prim_vf)
                    ! Swept plane patch
                elseif (patch_icpp(i)%geometry == 11) then
                    call s_icpp_sweep_plane(i, patch_id_fp, q_prim_vf)
                    ! Ellipsoidal patch
                elseif (patch_icpp(i)%geometry == 12) then
                    call s_icpp_ellipsoid(i, patch_id_fp, q_prim_vf)
                    ! Spherical harmonic patch
                elseif (patch_icpp(i)%geometry == 14) then
                    call s_icpp_spherical_harmonic(i, patch_id_fp, q_prim_vf)
                    ! 3D Modified circular patch
                elseif (patch_icpp(i)%geometry == 19) then
                    call s_icpp_3dvarcircle(i, patch_id_fp, q_prim_vf)
                    ! 3D STL patch
                elseif (patch_icpp(i)%geometry == 21) then
                    call s_icpp_model(i, patch_id_fp, q_prim_vf)
                end if
            end do

Copy link
Contributor

Choose a reason for hiding this comment

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

High-level Suggestion

The moving immersed boundary feature is inefficient because it recomputes all boundary data structures from scratch at every time step. This should be optimized to only update data for cells affected by the boundary's movement. [High-level, importance: 9]

Solution Walkthrough:

Before:

subroutine s_update_mib(...)
    // Clear all existing IB markers on the entire grid
    ib_markers%sf = 0

    // Update position of all moving boundaries
    for i = 1 to num_ibs:
        call s_propagate_mib(i)

    // Re-apply all IB patches to the entire grid from scratch
    call s_apply_ib_patches(ib_markers%sf, ...)

    // Recalculate all ghost points and their properties from scratch
    call s_find_num_ghost_points(...)
    call s_find_ghost_points(...)
    call s_compute_image_points(...)
    call s_compute_interpolation_coeffs(...)
end subroutine

After:

subroutine s_update_mib(...)
    // For each moving boundary, identify the old and new regions
    old_region = get_region(boundary_at_t)
    new_region = get_region(boundary_at_t_plus_dt)
    affected_region = union(old_region, new_region)

    // Update IB markers only in the affected region
    update_ib_markers_locally(affected_region)

    // Identify ghost points that are added, removed, or need updates
    // in the vicinity of the affected region.
    affected_ghost_points = find_affected_ghost_points(affected_region)

    // Recompute properties only for the affected ghost points
    for each gp in affected_ghost_points:
        recompute_image_point(gp)
        recompute_interpolation_coeffs(gp)
end subroutine

Comment on lines +376 to 392
if (((x_cc(i) - x_centroid)**2 &
+ (y_cc(j) - y_centroid)**2 <= radius**2 &
.and. &
patch_icpp(patch_id)%alter_patch(patch_id_fp(i, j, 0))) &
.or. &
patch_id_fp(i, j, 0) == smooth_patch_id) &
then

patch_id_fp(i, j, 0) = patch_id
else
if (((x_cc(i) - x_centroid)**2 &
+ (y_cc(j) - y_centroid)**2 <= radius**2 &
.and. &
patch_icpp(patch_id)%alter_patch(patch_id_fp(i, j, 0))) &
.or. &
(.not. present(ib_flag) .and. patch_id_fp(i, j, 0) == smooth_patch_id)) &
then

call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
eta, q_prim_vf, patch_id_fp)

@:analytical()
if (patch_icpp(patch_id)%hcid /= dflt_int) then
@:Hardcoded2D()
end if

end if
end if
end do
end do
@:HardcodedDellacation()

end subroutine s_circle

!! @param patch_id is the patch identifier
!! @param patch_id_fp Array to track patch ids
!! @param q_prim_vf Array of primitive variables
!! @param ib True if this patch is an immersed boundary
subroutine s_airfoil(patch_id, patch_id_fp, q_prim_vf, ib_flag)

integer, intent(in) :: patch_id
integer, dimension(0:m, 0:n, 0:p), intent(inout) :: patch_id_fp
type(scalar_field), dimension(1:sys_size), intent(inout) :: q_prim_vf
logical, optional, intent(in) :: ib_flag

real(wp) :: x0, y0, f, x_act, y_act, ca_in, pa, ma, ta, theta
real(wp) :: xa, yt, xu, yu, xl, yl, xc, yc, dycdxc, sin_c, cos_c
integer :: i, j, k
integer :: Np1, Np2

if (.not. present(ib_flag)) return
x0 = patch_ib(patch_id)%x_centroid
y0 = patch_ib(patch_id)%y_centroid
ca_in = patch_ib(patch_id)%c
pa = patch_ib(patch_id)%p
ma = patch_ib(patch_id)%m
ta = patch_ib(patch_id)%t
theta = pi*patch_ib(patch_id)%theta/180._wp

Np1 = int((pa*ca_in/dx)*20)
Np2 = int(((ca_in - pa*ca_in)/dx)*20)
Np = Np1 + Np2 + 1

allocate (airfoil_grid_u(1:Np))
allocate (airfoil_grid_l(1:Np))

airfoil_grid_u(1)%x = x0
airfoil_grid_u(1)%y = y0

airfoil_grid_l(1)%x = x0
airfoil_grid_l(1)%y = y0

eta = 1._wp

do i = 1, Np1 + Np2 - 1
if (i <= Np1) then
xc = x0 + i*(pa*ca_in/Np1)
xa = (xc - x0)/ca_in
yc = (ma/pa**2)*(2*pa*xa - xa**2)
dycdxc = (2*ma/pa**2)*(pa - xa)
else
xc = x0 + pa*ca_in + (i - Np1)*((ca_in - pa*ca_in)/Np2)
xa = (xc - x0)/ca_in
yc = (ma/(1 - pa)**2)*(1 - 2*pa + 2*pa*xa - xa**2)
dycdxc = (2*ma/(1 - pa)**2)*(pa - xa)
end if

yt = (5._wp*ta)*(0.2969_wp*xa**0.5_wp - 0.126_wp*xa - 0.3516_wp*xa**2._wp + 0.2843_wp*xa**3 - 0.1015_wp*xa**4)
sin_c = dycdxc/(1 + dycdxc**2)**0.5_wp
cos_c = 1/(1 + dycdxc**2)**0.5_wp

xu = xa - yt*sin_c
yu = yc + yt*cos_c

xl = xa + yt*sin_c
yl = yc - yt*cos_c

xu = xu*ca_in + x0
yu = yu*ca_in + y0

xl = xl*ca_in + x0
yl = yl*ca_in + y0

airfoil_grid_u(i + 1)%x = xu
airfoil_grid_u(i + 1)%y = yu

airfoil_grid_l(i + 1)%x = xl
airfoil_grid_l(i + 1)%y = yl

end do

airfoil_grid_u(Np)%x = x0 + ca_in
airfoil_grid_u(Np)%y = y0

airfoil_grid_l(Np)%x = x0 + ca_in
airfoil_grid_l(Np)%y = y0

do j = 0, n
do i = 0, m

if (.not. f_is_default(patch_ib(patch_id)%theta)) then
x_act = (x_cc(i) - x0)*cos(theta) - (y_cc(j) - y0)*sin(theta) + x0
y_act = (x_cc(i) - x0)*sin(theta) + (y_cc(j) - y0)*cos(theta) + y0
else
x_act = x_cc(i)
y_act = y_cc(j)
end if
call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
eta, q_prim_vf, patch_id_fp)

if (x_act >= x0 .and. x_act <= x0 + ca_in) then
xa = (x_act - x0)/ca_in
if (xa <= pa) then
yc = (ma/pa**2)*(2*pa*xa - xa**2)
dycdxc = (2*ma/pa**2)*(pa - xa)
else
yc = (ma/(1 - pa)**2)*(1 - 2*pa + 2*pa*xa - xa**2)
dycdxc = (2*ma/(1 - pa)**2)*(pa - xa)
@:analytical()
if (patch_icpp(patch_id)%hcid /= dflt_int) then
@:Hardcoded2D()
end if
if (y_act >= y0) then
k = 1
do while (airfoil_grid_u(k)%x < x_act)
k = k + 1
end do
if (f_approx_equal(airfoil_grid_u(k)%x, x_act)) then
if (y_act <= airfoil_grid_u(k)%y) then
!!IB
!call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
!eta, q_prim_vf, patch_id_fp)
patch_id_fp(i, j, 0) = patch_id
end if
else
f = (airfoil_grid_u(k)%x - x_act)/(airfoil_grid_u(k)%x - airfoil_grid_u(k - 1)%x)
if (y_act <= ((1._wp - f)*airfoil_grid_u(k)%y + f*airfoil_grid_u(k - 1)%y)) then
!!IB
!call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
!eta, q_prim_vf, patch_id_fp)
patch_id_fp(i, j, 0) = patch_id
end if
end if
else
k = 1
do while (airfoil_grid_l(k)%x < x_act)
k = k + 1
end do
if (f_approx_equal(airfoil_grid_l(k)%x, x_act)) then
if (y_act >= airfoil_grid_l(k)%y) then
!!IB
!call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
!eta, q_prim_vf, patch_id_fp)
patch_id_fp(i, j, 0) = patch_id
end if
else
f = (airfoil_grid_l(k)%x - x_act)/(airfoil_grid_l(k)%x - airfoil_grid_l(k - 1)%x)

if (y_act >= ((1._wp - f)*airfoil_grid_l(k)%y + f*airfoil_grid_l(k - 1)%y)) then
!!IB
!call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
!eta, q_prim_vf, patch_id_fp)
patch_id_fp(i, j, 0) = patch_id
end if
end if
end if
end if
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Restore the missing logic to update the patch_id_fp array within the s_icpp_circle subroutine to ensure correct patch identification. [possible issue, importance: 8]

Suggested change
if (((x_cc(i) - x_centroid)**2 &
+ (y_cc(j) - y_centroid)**2 <= radius**2 &
.and. &
patch_icpp(patch_id)%alter_patch(patch_id_fp(i, j, 0))) &
.or. &
patch_id_fp(i, j, 0) == smooth_patch_id) &
then
patch_id_fp(i, j, 0) = patch_id
else
if (((x_cc(i) - x_centroid)**2 &
+ (y_cc(j) - y_centroid)**2 <= radius**2 &
.and. &
patch_icpp(patch_id)%alter_patch(patch_id_fp(i, j, 0))) &
.or. &
(.not. present(ib_flag) .and. patch_id_fp(i, j, 0) == smooth_patch_id)) &
then
call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
eta, q_prim_vf, patch_id_fp)
@:analytical()
if (patch_icpp(patch_id)%hcid /= dflt_int) then
@:Hardcoded2D()
end if
end if
end if
end do
end do
@:HardcodedDellacation()
end subroutine s_circle
!! @param patch_id is the patch identifier
!! @param patch_id_fp Array to track patch ids
!! @param q_prim_vf Array of primitive variables
!! @param ib True if this patch is an immersed boundary
subroutine s_airfoil(patch_id, patch_id_fp, q_prim_vf, ib_flag)
integer, intent(in) :: patch_id
integer, dimension(0:m, 0:n, 0:p), intent(inout) :: patch_id_fp
type(scalar_field), dimension(1:sys_size), intent(inout) :: q_prim_vf
logical, optional, intent(in) :: ib_flag
real(wp) :: x0, y0, f, x_act, y_act, ca_in, pa, ma, ta, theta
real(wp) :: xa, yt, xu, yu, xl, yl, xc, yc, dycdxc, sin_c, cos_c
integer :: i, j, k
integer :: Np1, Np2
if (.not. present(ib_flag)) return
x0 = patch_ib(patch_id)%x_centroid
y0 = patch_ib(patch_id)%y_centroid
ca_in = patch_ib(patch_id)%c
pa = patch_ib(patch_id)%p
ma = patch_ib(patch_id)%m
ta = patch_ib(patch_id)%t
theta = pi*patch_ib(patch_id)%theta/180._wp
Np1 = int((pa*ca_in/dx)*20)
Np2 = int(((ca_in - pa*ca_in)/dx)*20)
Np = Np1 + Np2 + 1
allocate (airfoil_grid_u(1:Np))
allocate (airfoil_grid_l(1:Np))
airfoil_grid_u(1)%x = x0
airfoil_grid_u(1)%y = y0
airfoil_grid_l(1)%x = x0
airfoil_grid_l(1)%y = y0
eta = 1._wp
do i = 1, Np1 + Np2 - 1
if (i <= Np1) then
xc = x0 + i*(pa*ca_in/Np1)
xa = (xc - x0)/ca_in
yc = (ma/pa**2)*(2*pa*xa - xa**2)
dycdxc = (2*ma/pa**2)*(pa - xa)
else
xc = x0 + pa*ca_in + (i - Np1)*((ca_in - pa*ca_in)/Np2)
xa = (xc - x0)/ca_in
yc = (ma/(1 - pa)**2)*(1 - 2*pa + 2*pa*xa - xa**2)
dycdxc = (2*ma/(1 - pa)**2)*(pa - xa)
end if
yt = (5._wp*ta)*(0.2969_wp*xa**0.5_wp - 0.126_wp*xa - 0.3516_wp*xa**2._wp + 0.2843_wp*xa**3 - 0.1015_wp*xa**4)
sin_c = dycdxc/(1 + dycdxc**2)**0.5_wp
cos_c = 1/(1 + dycdxc**2)**0.5_wp
xu = xa - yt*sin_c
yu = yc + yt*cos_c
xl = xa + yt*sin_c
yl = yc - yt*cos_c
xu = xu*ca_in + x0
yu = yu*ca_in + y0
xl = xl*ca_in + x0
yl = yl*ca_in + y0
airfoil_grid_u(i + 1)%x = xu
airfoil_grid_u(i + 1)%y = yu
airfoil_grid_l(i + 1)%x = xl
airfoil_grid_l(i + 1)%y = yl
end do
airfoil_grid_u(Np)%x = x0 + ca_in
airfoil_grid_u(Np)%y = y0
airfoil_grid_l(Np)%x = x0 + ca_in
airfoil_grid_l(Np)%y = y0
do j = 0, n
do i = 0, m
if (.not. f_is_default(patch_ib(patch_id)%theta)) then
x_act = (x_cc(i) - x0)*cos(theta) - (y_cc(j) - y0)*sin(theta) + x0
y_act = (x_cc(i) - x0)*sin(theta) + (y_cc(j) - y0)*cos(theta) + y0
else
x_act = x_cc(i)
y_act = y_cc(j)
end if
call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
eta, q_prim_vf, patch_id_fp)
if (x_act >= x0 .and. x_act <= x0 + ca_in) then
xa = (x_act - x0)/ca_in
if (xa <= pa) then
yc = (ma/pa**2)*(2*pa*xa - xa**2)
dycdxc = (2*ma/pa**2)*(pa - xa)
else
yc = (ma/(1 - pa)**2)*(1 - 2*pa + 2*pa*xa - xa**2)
dycdxc = (2*ma/(1 - pa)**2)*(pa - xa)
@:analytical()
if (patch_icpp(patch_id)%hcid /= dflt_int) then
@:Hardcoded2D()
end if
if (y_act >= y0) then
k = 1
do while (airfoil_grid_u(k)%x < x_act)
k = k + 1
end do
if (f_approx_equal(airfoil_grid_u(k)%x, x_act)) then
if (y_act <= airfoil_grid_u(k)%y) then
!!IB
!call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
!eta, q_prim_vf, patch_id_fp)
patch_id_fp(i, j, 0) = patch_id
end if
else
f = (airfoil_grid_u(k)%x - x_act)/(airfoil_grid_u(k)%x - airfoil_grid_u(k - 1)%x)
if (y_act <= ((1._wp - f)*airfoil_grid_u(k)%y + f*airfoil_grid_u(k - 1)%y)) then
!!IB
!call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
!eta, q_prim_vf, patch_id_fp)
patch_id_fp(i, j, 0) = patch_id
end if
end if
else
k = 1
do while (airfoil_grid_l(k)%x < x_act)
k = k + 1
end do
if (f_approx_equal(airfoil_grid_l(k)%x, x_act)) then
if (y_act >= airfoil_grid_l(k)%y) then
!!IB
!call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
!eta, q_prim_vf, patch_id_fp)
patch_id_fp(i, j, 0) = patch_id
end if
else
f = (airfoil_grid_l(k)%x - x_act)/(airfoil_grid_l(k)%x - airfoil_grid_l(k - 1)%x)
if (y_act >= ((1._wp - f)*airfoil_grid_l(k)%y + f*airfoil_grid_l(k - 1)%y)) then
!!IB
!call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
!eta, q_prim_vf, patch_id_fp)
patch_id_fp(i, j, 0) = patch_id
end if
end if
end if
end if
if (((x_cc(i) - x_centroid)**2 &
+ (y_cc(j) - y_centroid)**2 <= radius**2 &
.and. &
patch_icpp(patch_id)%alter_patch(patch_id_fp(i, j, 0))) &
.or. &
patch_id_fp(i, j, 0) == smooth_patch_id) &
then
call s_assign_patch_primitive_variables(patch_id, i, j, 0, &
eta, q_prim_vf, patch_id_fp)
@:analytical()
if (patch_icpp(patch_id)%hcid /= dflt_int) then
@:Hardcoded2D()
end if
! Updating the patch identities bookkeeping variable
if (1._wp - eta < sgm_eps) patch_id_fp(i, j, 0) = patch_id
end if

Comment on lines +1659 to +1671
if (patch_icpp(patch_id)%smoothen) then
if (eta > patch_icpp(patch_id)%model_threshold) then
eta = 1._wp
end if
else
if (patch_icpp(patch_id)%smoothen) then
if (eta > patch_icpp(patch_id)%model_threshold) then
eta = 1._wp
end if
if (eta > patch_icpp(patch_id)%model_threshold) then
eta = 1._wp
else
if (eta > patch_icpp(patch_id)%model_threshold) then
eta = 1._wp
else
eta = 0._wp
end if
eta = 0._wp
end if
call s_assign_patch_primitive_variables(patch_id, i, j, k, &
eta, q_prim_vf, patch_id_fp)

! Note: Should probably use *eta* to compute primitive variables
! if defining them analytically.
@:analytical()
end if
call s_assign_patch_primitive_variables(patch_id, i, j, k, &
eta, q_prim_vf, patch_id_fp)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Correct the logic for handling smoothed and non-smoothed STL patches to ensure the eta value is calculated correctly based on the smoothen flag. [possible issue, importance: 8]

Suggested change
if (patch_icpp(patch_id)%smoothen) then
if (eta > patch_icpp(patch_id)%model_threshold) then
eta = 1._wp
end if
else
if (patch_icpp(patch_id)%smoothen) then
if (eta > patch_icpp(patch_id)%model_threshold) then
eta = 1._wp
end if
if (eta > patch_icpp(patch_id)%model_threshold) then
eta = 1._wp
else
if (eta > patch_icpp(patch_id)%model_threshold) then
eta = 1._wp
else
eta = 0._wp
end if
eta = 0._wp
end if
call s_assign_patch_primitive_variables(patch_id, i, j, k, &
eta, q_prim_vf, patch_id_fp)
! Note: Should probably use *eta* to compute primitive variables
! if defining them analytically.
@:analytical()
end if
call s_assign_patch_primitive_variables(patch_id, i, j, k, &
eta, q_prim_vf, patch_id_fp)
if (.not. patch_icpp(patch_id)%smoothen) then
if (eta > patch_icpp(patch_id)%model_threshold) then
eta = 1._wp
else
eta = 0._wp
end if
end if
call s_assign_patch_primitive_variables(patch_id, i, j, k, &
eta, q_prim_vf, patch_id_fp)

Comment on lines +911 to +921
! start by using euler's method naiively, but eventually incorporate more sophistocation
if (patch_ib(patch_id)%moving_ibm .eq. 1) then
! this continues with euler's method, which is obviously not that great and we need to add acceleration
do i = 1, 3
patch_ib(patch_id)%vel(i) = patch_ib(patch_id)%vel(i) + 0.0*dt ! TODO :: ADD EXTERNAL FORCES HERE
end do
patch_ib(patch_id)%x_centroid = patch_ib(patch_id)%x_centroid + patch_ib(patch_id)%vel(1)*dt
patch_ib(patch_id)%y_centroid = patch_ib(patch_id)%y_centroid + patch_ib(patch_id)%vel(2)*dt
patch_ib(patch_id)%z_centroid = patch_ib(patch_id)%z_centroid + patch_ib(patch_id)%vel(3)*dt
end if
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: The velocity update logic is currently a no-op as it adds zero. Replace the placeholder 0.0 with the correct acceleration term or remove the line to reflect that velocity is constant until acceleration is implemented. [possible issue, importance: 5]

Suggested change
! start by using euler's method naiively, but eventually incorporate more sophistocation
if (patch_ib(patch_id)%moving_ibm .eq. 1) then
! this continues with euler's method, which is obviously not that great and we need to add acceleration
do i = 1, 3
patch_ib(patch_id)%vel(i) = patch_ib(patch_id)%vel(i) + 0.0*dt ! TODO :: ADD EXTERNAL FORCES HERE
end do
patch_ib(patch_id)%x_centroid = patch_ib(patch_id)%x_centroid + patch_ib(patch_id)%vel(1)*dt
patch_ib(patch_id)%y_centroid = patch_ib(patch_id)%y_centroid + patch_ib(patch_id)%vel(2)*dt
patch_ib(patch_id)%z_centroid = patch_ib(patch_id)%z_centroid + patch_ib(patch_id)%vel(3)*dt
end if
! start by using euler's method naiively, but eventually incorporate more sophistocation
if (patch_ib(patch_id)%moving_ibm .eq. 1) then
! this continues with euler's method, which is obviously not that great and we need to add acceleration
! TODO :: ADD EXTERNAL FORCES HERE to update velocity, e.g.:
! do i = 1, 3
! patch_ib(patch_id)%vel(i) = patch_ib(patch_id)%vel(i) + acceleration(i)*dt
! end do
patch_ib(patch_id)%x_centroid = patch_ib(patch_id)%x_centroid + patch_ib(patch_id)%vel(1)*dt
patch_ib(patch_id)%y_centroid = patch_ib(patch_id)%y_centroid + patch_ib(patch_id)%vel(2)*dt
patch_ib(patch_id)%z_centroid = patch_ib(patch_id)%z_centroid + patch_ib(patch_id)%vel(3)*dt
end if

bound = p + buff_size - 1
end if

if (f_approx_equal(norm(dim), 0._wp)) then
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Prevent a potential out-of-bounds array access by adding an index < bound check to the do while loop condition, ensuring s_cc(index + 1) is accessed safely. [possible issue, importance: 8]

@sbryngelson
Copy link
Member

haven't looked at all details yet but sniff test seems like this is quite good. i have a separate question i'll ask on slack

@codecov
Copy link

codecov bot commented Sep 26, 2025

Codecov Report

❌ Patch coverage is 41.18705% with 327 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.74%. Comparing base (eb61616) to head (e2c5f12).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/common/m_ib_patches.fpp 43.15% 193 Missing and 27 partials ⚠️
src/pre_process/m_icpp_patches.fpp 38.63% 70 Missing and 11 partials ⚠️
src/simulation/m_ibm.fpp 4.00% 24 Missing ⚠️
src/simulation/p_main.fpp 0.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1004      +/-   ##
==========================================
- Coverage   41.91%   41.74%   -0.18%     
==========================================
  Files          69       70       +1     
  Lines       19783    20002     +219     
  Branches     2473     2483      +10     
==========================================
+ Hits         8293     8349      +56     
- Misses       9952    10121     +169     
+ Partials     1538     1532       -6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@danieljvickers
Copy link
Member Author

This was accidentally submitted as a debug branch. A new PR has been created that has all of these changes.

@danieljvickers danieljvickers deleted the resolve_mibm_error_temp branch October 17, 2025 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants