Skip to content

Conversation

@danieljvickers
Copy link
Member

@danieljvickers danieljvickers commented Jan 15, 2026

User description

User description

Description

Adds a numeric method of computing the centroid of IBs where the center of mass is not well posed. In particular, I added hooks for STL and airfiol IBs.

This PR also includes support for parameterized IB motion. It allows one to analytically describe the velocity and rotation rate of any IB experiencing 1-way coupling.

Both of these features have been tested with IB's the move and rotate using the new analytic parameterization. I have also tested with an airfoil rotating about the correct center of mass. Videos available in my slides.

Fixes #1105
Fixes #1106

In addition to these issues being fixed, I also resolved a separate bug that is related in getting the 2-way force calculation to work with multi-fluid flow. I also included some minor refactoring to clean up the time stepper subroutine.

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Scope

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

That goal was enabling the ability to run a pitching airfoil case, which oscillates in time.

How Has This Been Tested?

  • I ran a pitching airfoil case which oscillates in time at a fixed frequency about the new correct center of mass
  • I ran a case of a cylinder moving in a circular pattern to "stir" a fluid

The best configuration can be summarized with this test case file for the pitching airfoil:

import json
import math

Mu = 1.84e-05
gam_a = 1.4
gam_b = 1.1

total_time = 5.0 
num_time_steps = 2000
num_saves = 500
dt = total_time / float(num_time_steps)
steps_per_save = int(num_time_steps / num_saves)

# Configuring case dictionary
print(
    json.dumps(
        {
            # Logistics
            "run_time_info": "T",
            # Computational Domain Parameters
            # axial direction
            "x_domain%beg": 0.0e00,
            "x_domain%end": 6.0,
            # r direction
            "y_domain%beg": 0.0e00,
            "y_domain%end": 3.0,
            "cyl_coord": "F",
            "m": 400,
            "n": 200,
            "p": 0,
            "dt": dt,
            "t_step_start": 0,
            "t_step_stop": num_time_steps,
            "t_step_save": steps_per_save,
            # Simulation Algorithm Parameters
            # Only one patches are necessary, the air tube
            "num_patches": 1,
            # Use the 5 equation model
            "model_eqns": 2,
            # 6 equations model does not need the K \div(u) term
            "alt_soundspeed": "F",
            # One fluids: air
            "num_fluids": 2,
            # time step
            "mpp_lim": "F",
            # Correct errors when computing speed of sound
            "mixture_err": "T",
            # Use TVD RK3 for time marching
            "time_stepper": 3,
            # Reconstruct the primitive variables to minimize spurious
            # Use WENO5
            "weno_order": 5,
            "weno_eps": 1.0e-16,
            "weno_Re_flux": "T",
            "weno_avg": "T",
            "avg_state": 2,
            "viscous": "T",
            # Use the mapped WENO weights to maintain monotinicity
            "mapped_weno": "T",
            "null_weights": "F",
            "mp_weno": "T",
            # Use the HLLC  Riemann solver
            "riemann_solver": 2,
            "wave_speeds": 1,
            # We use reflective boundary conditions at octant edges and
            # non-reflective boundary conditions at the domain edges
            "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,
            # 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.0,
            # Uniform medium density, centroid is at the center of the domain
            "patch_icpp(1)%y_centroid": 1.5,
            "patch_icpp(1)%length_x": 6.0,
            "patch_icpp(1)%length_y": 3.0,
            # Specify the patch primitive variables
            "patch_icpp(1)%vel(1)": 3.0e00,
            "patch_icpp(1)%vel(2)": 0.0e00,
            "patch_icpp(1)%pres": 1.0e00,
            "patch_icpp(1)%alpha_rho(1)": 0.8e00,
            "patch_icpp(1)%alpha(1)": 0.8e00,
            "patch_icpp(1)%alpha_rho(2)": 0.2e00,
            "patch_icpp(1)%alpha(2)": 0.2e00,
            # Patch: Airfoil Immersed Boundary
            "patch_ib(1)%geometry": 4,
            "patch_ib(1)%x_centroid": 1.5,
            "patch_ib(1)%y_centroid": 1.5,
            "patch_ib(1)%c": 1.0,
            "patch_ib(1)%t": 0.15,
            "patch_ib(1)%p": 0.4,
            "patch_ib(1)%m": 0.02,
            "patch_ib(1)%angular_vel(3)": "-10.0 * 2.0 * pi * sin(2.0 * pi * t) * pi / 180.",
            "patch_ib(1)%angles(3)": -5.0 * math.pi / 180.,
            "patch_ib(1)%moving_ibm": 1,
            "patch_ib(1)%slip": "F",
            # Fluids Physical Parameters
            # Use the same stiffness as the air bubble
            "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)": 1000000,
            "fluid_pp(2)%gamma": 1.0e00 / (gam_b - 1.0e00),  # 2.50(Not 1.40)
            "fluid_pp(2)%pi_inf": 0,
            "fluid_pp(2)%Re(1)": 1000000,
        }
    )
)

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.
  • Ran my code using various numbers of different GPUs (1, 2, and 8, for example) in parallel and made sure that the results scale similarly to what happens if you run without the new code/feature

PR Type

Enhancement

Description

  • Add ellipse immersed boundary patch geometry with levelset computation

  • Implement centroid offset calculation for complex IB shapes (airfoils, STL)

  • Support analytical parameterization of moving IB velocities and rotation rates

  • Enable 1-way coupling with analytically-defined IB motion in time-stepping

Diagram Walkthrough

flowchart LR
  A["IB Patch Definition"] -->|"geometry=6"| B["Ellipse Levelset"]
  A -->|"centroid_offset"| C["Centroid Computation"]
  C -->|"airfoil/STL"| D["Center of Mass Calculation"]
  D -->|"offset vector"| E["Levelset Adjustment"]
  F["Analytical Parameters"] -->|"vel, angular_vel"| G["1-Way Coupling"]
  G -->|"time-stepping"| H["IB Motion Update"]
Loading

File Walkthrough

Relevant files
Enhancement
9 files
m_compute_levelset.fpp
Add ellipse levelset computation and centroid offset         
+60/-1   
m_derived_types.fpp
Add centroid offset field to IB patch type                             
+1/-0     
m_ib_patches.fpp
Add ellipse IB patch initialization and marker setup         
+47/-0   
m_check_ib_patches.fpp
Add validation for ellipse IB patch geometry                         
+2/-0     
m_global_parameters.fpp
Initialize centroid offset to zero for all patches             
+1/-0     
m_ibm.fpp
Add centroid offset computation and 1-way coupling support
+56/-1   
m_time_steppers.fpp
Integrate analytical velocity parameterization for 1-way coupling
+5/-1     
case.py
Add code generation for analytical moving IB velocities   
+91/-3   
case_dicts.py
Mark IB velocity and angular velocity as analytically-parameterizable
+2/-4     
Documentation
1 files
case.py
Add example case configuration for ellipse IB simulation 
+97/-0   

CodeAnt-AI Description

Support analytic motion for moving immersed boundaries and correct centroid for airfoils/STL patches

What Changed

  • Input files can provide analytic expressions for a moving immersed boundary's translational and angular velocity; those expressions are evaluated at runtime so 1-way coupled IBs follow user-defined motions.
  • For airfoil and STL-like patch geometries, the code computes the true center-of-mass from marker cells, applies a centroid offset, and uses the corrected centroid when placing and sampling IB markers so shapes rotate and translate about the correct point.
  • The case-generation tool now recognizes and emits Fortran stubs for analytic moving-IB parameters, and default values initialize centroid offsets to zero when not needed.

Impact

✅ Correct airfoil pitch center during rotation
✅ Predictable user-defined IB motions for 1-way coupling
✅ Fewer simulation setup errors for odd-shaped IBs

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • New Features

    • Ellipse-shaped immersed-boundary support.
    • Analytical support for moving immersed-boundary initial/analytic conditions.
  • Improvements

    • Centroid-offset initialization and handling for immersed-boundary patches.
    • Consolidated moving-IBM propagation for consistent motion and state updates.
    • Enhanced per-fluid force and viscous handling for more accurate multi-fluid interactions.
  • Bug Fixes

    • Duplicate ellipse level-set implementation introduced (needs removal).

✏️ Tip: You can customize this high-level summary in your review settings.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Jan 15, 2026

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 15, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds per-patch centroid_offset storage and computation; applies centroid offsets to airfoil levelsets; introduces ellipse geometry and an exported s_ellipse_levelset (duplicate implementation present); routes moving-IB analytic generation through the toolchain; centralizes moving-IBM propagation in the time-step with per-fluid force handling.

Changes

Cohort / File(s) Summary
Data Type Extensions
src/common/m_derived_types.fpp
Added real(wp), dimension(1:3) :: centroid_offset to ib_patch_parameters.
Compute Levelset
src/common/m_compute_levelset.fpp
Exported and implemented s_ellipse_levelset(ib_patch_id, levelset, levelset_norm); computes ellipse quadratic levelset and rotated normals; applied centroid_offset adjustments in s_airfoil_levelset and s_3D_airfoil_levelset. Note: file contains a duplicated s_ellipse_levelset implementation.
IB Patch Geometry
src/common/m_ib_patches.fpp
Subtract patch_ib(... )%centroid_offset in s_ib_airfoil and s_ib_3D_airfoil; added geometry code 6 dispatch and internal s_ib_ellipse(patch_id, ib_markers_sf) for GPU-parallel ellipse marking.
Initialization Defaults
src/pre_process/m_global_parameters.fpp
Initialize patch_ib(i)%centroid_offset(:) = 0._wp for default IB patches.
Centroid Computation & Setup
src/simulation/m_ibm.fpp
Added internal s_compute_centroid_offset(ib_marker) and call from s_ibm_setup; accumulates per-cell mass*pos, performs MPI Allreduce, sets patch_ib%centroid_offset, updates centroids, and rotates offset into local IB coords.
IB Force & Multi-fluid
src/simulation/m_ibm.fpp
Modified s_compute_ib_forces signature to accept fluid_pp; reworked to iterate per-fluid for pressure/viscous contributions and updated GPU parallel loop lists and viscous force computations.
Time-Stepping / MIBM
src/simulation/m_time_steppers.fpp
Added case.fpp include and new s_propagate_immersed_boundaries(s) to centralize MIB propagation (one-way analytic MIB + two-way coupling logic).
Case Macro
src/common/include/case.fpp
Added mib_analytical() macro definition for moving-IB analytic motion.
Toolchain: Case Generation
toolchain/mfc/case.py
Added MIBM_ANALYTIC_VARS; refactored analytic FPP generation into __get_analytic_ic_fpp and new __get_analytic_mib_fpp; added __is_mib_analytical detection and integrated MIB analytic section into sim FPP.
Toolchain: Case Dicts
toolchain/mfc/run/case_dicts.py
In simulation FPP, changed patch_ib(... )%vel and %angular_vel to ParamType.REAL.analytic(); removed PRE_PROCESS analytic generation for those fields.
Examples & Minor Edits
examples/2D_ibm_ellipse/case.py, examples/*/case.py
Small comment/text updates and removals of some local constants in multiple example case files (non-functional in most, but several removals may cause undefined-name errors in examples).

Sequence Diagram(s)

sequenceDiagram
    participant TS as "TimeStepper"
    participant Prop as "s_propagate_immersed_boundaries(s)"
    participant Analytic as "Analytic (mib_analytical / Case)"
    participant IBM as "s_compute_ib_forces / s_compute_centroid_offset"
    participant Patch as "patch_ib[]"
    participant MPI as "MPI Allreduce"

    TS->>Prop: call s_propagate_immersed_boundaries(s)
    Prop->>Analytic: if moving_ibm == 1 → evaluate analytic motion
    Analytic->>Patch: write velocities/angles to patch_ib fields
    Prop->>IBM: if two-way → call s_compute_ib_forces and s_compute_centroid_offset
    IBM->>Patch: accumulate per-cell mass*pos and counts
    IBM->>MPI: Allreduce mass*pos and counts
    MPI-->>IBM: reduced totals
    IBM->>Patch: compute center_of_mass, set patch_ib%centroid_offset (rotate to local frame)
    Prop->>TS: update IB states for timestep
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • sbryngelson
  • wilfonba

Poem

🐰 I hopped through patches, counted mass with care,

Drew ellipses crisp in the morning air,
I nudged the centroid so rotations spin true,
Timesteps hum gently as boundaries move through,
Carrots for coders — the rabbit's work is through!

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive The raw summary indicates a duplicate definition of s_ellipse_levelset and removal of variable initializations (eps, mu) from example files that may not be directly related to the stated objectives. Clarify whether the duplicate s_ellipse_levelset definition is intentional, and verify the variable removals from example files are necessary for the PR objectives or represent unintended changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Analytic mibm velocities and airfoil centroid' clearly summarizes the two main changes: analytic MIBM (moving immersed boundary) velocities and correction of airfoil centroid, which are the primary objectives of the PR.
Description check ✅ Passed The PR description comprehensively covers all required template sections including objectives, testing details, type of change, scope, and checklist items. It provides clear motivation, linked issues (#1105, #1106), and specific test case configurations.
Linked Issues check ✅ Passed The code changes directly address both linked issues: #1105 (parametrized 1-way MIBM velocities via analytical expressions evaluated at runtime) and #1106 (centroid offset computation for airfoils/STL shapes enabling correct rotation and torque calculations).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai bot added the size:L This PR changes 100-499 lines, ignoring generated files label Jan 15, 2026
@qodo-code-review
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

1106 - Partially compliant

Compliant requirements:

  • Fix the "centroid" used for airfoils so it is the true centroid/center of mass rather than the airfoil tip.
  • Ensure rotations in 1-way coupling occur about the center of mass.

Non-compliant requirements:

  • Ensure torque calculations in 2-way coupling use the correct centroid/center of mass reference.

Requires further human verification:

  • Works for 2D and 3D airfoils across all environments.
  • Ensure torque calculations in 2-way coupling use the correct centroid/center of mass reference.

1105 - Partially compliant

Compliant requirements:

  • Support parametrizing 1-way MIBM motion (velocity and rotation rate) as a function of time in the case file.
  • Provide a toolchain/codegen addition that injects analytic functions into a case file (similar to analytic patches/ICs).
  • Apply the prescribed motion inside the time-step update logic using a conditional statement.

Non-compliant requirements:

  • ""

Requires further human verification:

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

Possible Issue

s_compute_centroid_offset appears to be called during s_ibm_setup before ib_markers%sf is allocated/populated (and before the IB patch marker routines run), yet it iterates over ib_markers%sf(i,j,k) to find cells. This risks invalid memory access or always computing a zero-count COM depending on initialization order.

! do all set up for moving immersed boundaries
moving_immersed_boundary_flag = .false.
do i = 1, num_ibs
    if (patch_ib(i)%moving_ibm /= 0) then
        moving_immersed_boundary_flag = .true.
    end if
    call s_update_ib_rotation_matrix(i)
    call s_compute_centroid_offset(i)
end do
$:GPU_ENTER_DATA(copyin='[patch_ib]')

! Allocating the patch identities bookkeeping variable
allocate (patch_id_fp(0:m, 0:n, 0:p))

$:GPU_UPDATE(device='[ib_markers%sf]')
$:GPU_UPDATE(device='[levelset%sf]')
$:GPU_UPDATE(device='[levelset_norm%sf]')

! Get neighboring IB variables from other processors
call s_populate_ib_buffers()

$: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)

! set the size of the ghost point arrays to be the amount of points total, plus a factor of 2 buffer
$:GPU_UPDATE(device='[num_gps, num_inner_gps]')
@:ALLOCATE(ghost_points(1:int((max_num_gps + max_num_inner_gps) * 2.0)))
@:ALLOCATE(inner_points(1:int((max_num_gps + max_num_inner_gps) * 2.0)))

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

call s_find_ghost_points(ghost_points, inner_points)
$:GPU_UPDATE(device='[ghost_points, inner_points]')

call s_compute_image_points(ghost_points, levelset, levelset_norm)
$:GPU_UPDATE(device='[ghost_points]')
Possible Issue

The MPI reduction logic computes global sums into center_of_mass and global cell count into num_cells, but then divides by num_cells_local (local count) instead of the global count. This will yield incorrect centroids in parallel runs. Also verify 2D handling: the accumulation uses [x_cc(i), y_cc(j), 0] but still loops over k; in 2D this may overweight if p spans more than a single plane.

!> Computes the center of mass for IB patch types where we are unable to determine their center of mass analytically.
!> These patches include things like NACA airfoils and STL models
subroutine s_compute_centroid_offset(ib_marker)

    integer, intent(in) :: ib_marker

    integer :: i, j, k, num_cells, num_cells_local
    real(wp), dimension(1:3) :: center_of_mass

    ! Offset only needs to be computes for specific geometries
    if (patch_ib(ib_marker)%geometry == 4 .or. &
        patch_ib(ib_marker)%geometry == 5 .or. &
        patch_ib(ib_marker)%geometry == 11 .or. &
        patch_ib(ib_marker)%geometry == 12) then

        center_of_mass = [0._wp, 0._wp, 0._wp]
        num_cells_local = 0

        ! get the summed mass distribution and number of cells to divide by
        do i = 0, m
            do j = 0, n
                do k = 0, p
                    if (ib_markers%sf(i, j, k) == ib_marker) then
                      num_cells_local = num_cells_local + 1
                        center_of_mass = center_of_mass + [x_cc(i), y_cc(j), 0._wp]
                        if (num_dims == 3) center_of_mass(3) = center_of_mass(3) + z_cc(k)
                    end if
                end do
            end do
        end do

        ! reduce the mass contribution over all MPI ranks and compute COM
        ! print *, "Before reduction ", center_of_mass, num_cells_local
        call s_mpi_allreduce_sum(center_of_mass(1), center_of_mass(1))
        call s_mpi_allreduce_sum(center_of_mass(2), center_of_mass(2))
        call s_mpi_allreduce_sum(center_of_mass(3), center_of_mass(3))
        call s_mpi_allreduce_integer_sum(num_cells_local, num_cells)
        center_of_mass =  center_of_mass / real(num_cells_local, wp)

        ! assign the centroid offset as a vector pointing from the true COM to the "centroid" in the input file and replace the current centroid
        patch_ib(ib_marker)%centroid_offset = [patch_ib(ib_marker)%x_centroid, patch_ib(ib_marker)%y_centroid, patch_ib(ib_marker)%z_centroid] &
            - center_of_mass
        patch_ib(ib_marker)%x_centroid = center_of_mass(1)
        patch_ib(ib_marker)%y_centroid = center_of_mass(2)
        patch_ib(ib_marker)%z_centroid = center_of_mass(3)

        ! rotate the centroid offset back into the local coords of the IB
        patch_ib(ib_marker)%centroid_offset = matmul(patch_ib(ib_marker)%rotation_matrix_inverse, patch_ib(ib_marker)%centroid_offset)
    else
        patch_ib(ib_marker)%centroid_offset(:) = [0._wp, 0._wp, 0._wp]
    end if
Possible Issue

s_ellipse_levelset only assigns levelset/levelset_norm when a point is inside the ellipse; outside points are left untouched, which can leave uninitialized/stale values in the levelset fields and break downstream logic that assumes the full field is defined. Consider explicitly setting an outside value and normal (or at least levelset) for all cells.

subroutine s_ellipse_levelset(ib_patch_id, levelset, levelset_norm)

    type(levelset_field), intent(INOUT), optional :: levelset
    type(levelset_norm_field), intent(INOUT), optional :: levelset_norm

    integer, intent(in) :: ib_patch_id
    real(wp) :: ellipse_coeffs(2) ! a and b in the ellipse equation
    real(wp) :: quadratic_coeffs(3) ! A, B, C in the quadratic equation to compute levelset

    real(wp) :: length_x, length_y
    real(wp), dimension(1:3) :: xy_local, normal_vector !< x and y coordinates in local IB frame
    real(wp), dimension(2) :: center !< x and y coordinates in local IB frame
    real(wp), dimension(1:3, 1:3) :: rotation, inverse_rotation

    integer :: i, j, k !< Loop index variables
    integer :: idx !< Shortest path direction indicator

    length_x = patch_ib(ib_patch_id)%length_x
    length_y = patch_ib(ib_patch_id)%length_y
    center(1) = patch_ib(ib_patch_id)%x_centroid
    center(2) = patch_ib(ib_patch_id)%y_centroid
    inverse_rotation(:, :) = patch_ib(ib_patch_id)%rotation_matrix_inverse(:, :)
    rotation(:, :) = patch_ib(ib_patch_id)%rotation_matrix(:, :)

    ellipse_coeffs(1) = 0.5_wp * length_x
    ellipse_coeffs(2) = 0.5_wp * length_y

    $:GPU_PARALLEL_LOOP(private='[i,j,k,idx,quadratic_coeffs,xy_local,normal_vector]', &
              & copyin='[ib_patch_id,center,ellipse_coeffs,inverse_rotation,rotation]', collapse=2)
    do i = 0, m
        do j = 0, n
            xy_local = [x_cc(i) - center(1), y_cc(j) - center(2), 0._wp]
            xy_local = matmul(inverse_rotation, xy_local)

            ! we will get NaNs in the levelset if we compute this outside the ellipse
            if ((xy_local(1)/ellipse_coeffs(1))**2 + (xy_local(2)/ellipse_coeffs(2))**2 <= 1._wp) then

                normal_vector = xy_local
                normal_vector(2) = normal_vector(2) * (ellipse_coeffs(1)/ellipse_coeffs(2))**2._wp ! get the normal direction via the coordinate transofmration method
                normal_vector = normal_vector / sqrt(dot_product(normal_vector, normal_vector)) ! normalize the vector
                levelset_norm%sf(i, j, 0, ib_patch_id, :) = matmul(rotation, normal_vector) ! save after rotating the vector to the global frame

                ! use the normal vector to set up the quadratic equation for the levelset, using A, B, and C in indices 1, 2, and 3
                quadratic_coeffs(1) = (normal_vector(1)/ellipse_coeffs(1))**2 + (normal_vector(2)/ellipse_coeffs(2))**2
                quadratic_coeffs(2) = 2._wp * ((xy_local(1)*normal_vector(1)/(ellipse_coeffs(1)**2)) + (xy_local(2)*normal_vector(2)/(ellipse_coeffs(2)**2)))
                quadratic_coeffs(3) = (xy_local(1)/ellipse_coeffs(1))**2._wp + (xy_local(2)/ellipse_coeffs(2))**2._wp - 1._wp

                ! compute the levelset with the quadratic equation [ -B + sqrt(B^2 - 4AC) ] / 2A
                levelset%sf(i, j, 0, ib_patch_id) = -0.5_wp * (-quadratic_coeffs(2) + sqrt(quadratic_coeffs(2)**2._wp - 4._wp * quadratic_coeffs(1)*quadratic_coeffs(3))) / quadratic_coeffs(1)
            end if
        end do
    end do
    $:END_GPU_PARALLEL_LOOP()

end subroutine s_ellipse_levelset

do i = 0, m
xyz_local = [x_cc(i) - center(1), y_cc(j) - center(2), z_cc(l) - center(3)] ! get coordinate frame centered on IB
xyz_local = matmul(inverse_rotation, xyz_local) ! rotate the frame into the IB's coordinates
xyz_local = xyz_local - patch_ib(patch_id)%centroid_offset ! airfoils are a patch that require a centroid offset
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 3D airfoil routine subtracts patch_ib(patch_id)%centroid_offset from xyz_local after applying inverse_rotation; if centroid_offset is expressed in the global/model frame this subtraction is done in the wrong basis. Rotate the centroid offset into the IB/local frame with the same inverse_rotation before subtracting to ensure the offset is applied consistently in the same coordinate system. [logic error]

Severity Level: Critical 🚨
- ❌ 3D airfoil marker placement incorrect in s_ib_3D_airfoil.
- ⚠️ 3D pitching/rotating airfoil simulations show wrong geometry.
Suggested change
xyz_local = xyz_local - patch_ib(patch_id)%centroid_offset ! airfoils are a patch that require a centroid offset
xyz_local = xyz_local - matmul(inverse_rotation, patch_ib(patch_id)%centroid_offset) ! rotate centroid_offset into IB/local frame before subtracting
Steps of Reproduction ✅
1. Prepare a 3D case with an airfoil patch (patch_ib(... )%geometry == 11) so
s_apply_ib_patches calls s_ib_3D_airfoil during marker generation.

2. Set patch_ib(patch_id)%centroid_offset to a non-zero vector and set non-zero rotation
angles so rotation_matrix_inverse is non-trivial; run to the IB marker generation step.

3. In s_ib_3D_airfoil the code computes xyz_local and applies inverse_rotation at
src/common/m_ib_patches.fpp:435-436, then subtracts patch_ib(patch_id)%centroid_offset at
line 437 without rotating that offset into the local frame.

4. As a result the z/y/x cell-inclusion tests (for example the z_min/z_max check at the
surrounding block) and the later assignments to ib_markers_sf(i,j,l) are done with an
inconsistent offset and the 3D airfoil appears shifted. Reproduces when centroid_offset ≠
0 and rotation ≠ identity.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/common/m_ib_patches.fpp
**Line:** 437:437
**Comment:**
	*Logic Error: The 3D airfoil routine subtracts `patch_ib(patch_id)%centroid_offset` from `xyz_local` after applying `inverse_rotation`; if `centroid_offset` is expressed in the global/model frame this subtraction is done in the wrong basis. Rotate the centroid offset into the IB/local frame with the same `inverse_rotation` before subtracting to ensure the offset is applied consistently in the same coordinate system.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

call s_mpi_allreduce_sum(center_of_mass(2), center_of_mass(2))
call s_mpi_allreduce_sum(center_of_mass(3), center_of_mass(3))
call s_mpi_allreduce_integer_sum(num_cells_local, num_cells)
center_of_mass = center_of_mass / real(num_cells_local, wp)
Copy link
Contributor

@codeant-ai codeant-ai bot Jan 15, 2026

Choose a reason for hiding this comment

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

Suggestion: Logic error: the code divides the reduced (global) sum of coordinates by the local rank cell count (num_cells_local) instead of the global cell count (num_cells), which yields an incorrect centroid and can cause a division-by-zero when the local rank has zero cells; change the divisor to the globally reduced num_cells and guard against zero. [logic error]

Severity Level: Critical 🚨
- ❌ Centroid miscomputed for airfoil/STL IBs (s_compute_centroid_offset)
- ❌ Possible runtime crash on ranks with zero local IB cells
- ⚠️ Affects s_ibm_setup and subsequent IB motion calculations
- ⚠️ Breaks pitching-airfoil test cases (user-reported)
Suggested change
center_of_mass = center_of_mass / real(num_cells_local, wp)
if (num_cells == 0) then
! No cells globally for this IB -- leave offsets at zero and exit safely
patch_ib(ib_marker)%centroid_offset(:) = [0._wp, 0._wp, 0._wp]
return
end if
center_of_mass = center_of_mass / real(num_cells, wp)
Steps of Reproduction ✅
1. Build and run the code with MPI (>=2 ranks) and a case that registers an IB of geometry
type handled here (geometry == 4/5/11/12). The initialization path runs s_ibm_setup
(src/simulation/m_ibm.fpp: around lines 93-105) which calls s_compute_centroid_offset.

2. Ensure the IB lies entirely within one MPI rank's subdomain such that other ranks have
zero cells for that IB. During s_compute_centroid_offset (subroutine start at
src/simulation/m_ibm.fpp: ~1086) each rank counts local cells into num_cells_local and
sums coordinates into center_of_mass.

3. The code calls s_mpi_allreduce_integer_sum(num_cells_local, num_cells) (line 1120)
which returns global num_cells, but then divides by the local num_cells_local at line
1121. If the local rank had num_cells_local == 0, this division triggers a divide-by-zero
on that rank causing a crash; otherwise the centroid is computed incorrectly (using local
count instead of global).

4. Observe either a floating-point exception / program abort on ranks with zero local
cells or an incorrect centroid value being written to patch_ib(...)%x_centroid etc.
Confirm by inspecting outputs written after s_compute_centroid_offset or by adding a debug
print near lines 1115-1121.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/simulation/m_ibm.fpp
**Line:** 1121:1121
**Comment:**
	*Logic Error: Logic error: the code divides the reduced (global) sum of coordinates by the local rank cell count (`num_cells_local`) instead of the global cell count (`num_cells`), which yields an incorrect centroid and can cause a division-by-zero when the local rank has zero cells; change the divisor to the globally reduced `num_cells` and guard against zero.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Copy link
Member Author

Choose a reason for hiding this comment

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

This comment actually doesn't stink

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Jan 15, 2026

CodeAnt AI finished reviewing your PR.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/pre_process/m_check_ib_patches.fpp (1)

65-70: Add validation for ellipse geometry and update error message to include geometry 6.

The ellipse patch (geometry == 6) branch lacks parameter validation that exists for all other geometry types. Additionally, the error message on line 70 must be updated to include geometry 6 in the list of valid geometries to match the explicit handling at line 65.

🔧 Suggested fixes
  1. Update the error message to include geometry 6:
                 call s_prohibit_abort("Invalid IB patch", &
                                       "patch_ib("//trim(iStr)//")%geometry must be "// &
-                                      "2-4, 8-10, 11 or 12.")
+                                      "2-6, 8-10, 11 or 12.")
  1. Add a validation subroutine for ellipse geometry and call it:
else if (patch_ib(i)%geometry == 6) then
    call s_check_ellipse_ib_patch_geometry(i)

Then implement s_check_ellipse_ib_patch_geometry to validate centroid and radii parameters similarly to the circle patch validation.

src/simulation/m_time_steppers.fpp (1)

629-643: The @:mib_analytical() macro at line 631 is undefined and will cause a preprocessing error.

The macro is invoked for 1-way coupling (moving_ibm == 1) but is not defined in case.fpp, macros.fpp, or any other .fpp file in the codebase. Only an empty analytical() macro (without the mib_ prefix) exists in case.fpp.

The macro must be properly defined to specify how analytic velocities are applied to the IB patches in 1-way coupling mode. Add the implementation to case.fpp or the appropriate macro file.

🤖 Fix all issues with AI agents
In `@examples/2D_ibm_ellipse/case.py`:
- Around line 84-90: Update the misleading comment "Cylinder Immersed Boundary"
to describe the actual geometry being configured (an ellipse) where the patch
keys like "patch_ib(1)%geometry" are set to 6; change the comment near the block
that sets "patch_ib(1)%geometry", "patch_ib(1)%x_centroid",
"patch_ib(1)%y_centroid", "patch_ib(1)%length_x", "patch_ib(1)%length_y", and
"patch_ib(1)%slip" so it accurately states "Ellipse Immersed Boundary" (or
equivalent) to match geometry=6.

In `@src/common/m_compute_levelset.fpp`:
- Line 380: The inline comment on the line assigning normal_vector(2) contains a
typo "transofmration"; update the comment text to "transformation" in the same
statement so it reads "... via the coordinate transformation method" while
leaving the code (normal_vector(2) = normal_vector(2) *
(ellipse_coeffs(1)/ellipse_coeffs(2))**2._wp) and surrounding whitespace
unchanged.

In `@src/common/m_ib_patches.fpp`:
- Around line 789-792: The comment in the m_ib_patches module that reads
"Checking whether the rectangle covers..." is incorrect for the ellipse-handling
subroutine; update the comment to say "ellipse" (and adjust surrounding phrasing
if needed, e.g., "ellipse covers a particular cell") so it accurately reflects
the geometry handled by the subroutine in this file.

In `@src/simulation/m_ibm.fpp`:
- Around line 1115-1122: The center_of_mass calculation divides by the local
count; after calling s_mpi_allreduce_sum(center_of_mass) and
s_mpi_allreduce_integer_sum(num_cells_local, num_cells) replace the division by
real(num_cells_local, wp) with real(num_cells, wp) so the globally reduced
center_of_mass is normalized by the global cell count (ensure you use the
reduced integer num_cells, not num_cells_local, in the division and keep the wp
real-kind conversion).
🧹 Nitpick comments (1)
toolchain/mfc/case.py (1)

223-256: Consider binding pid explicitly to avoid closure capture issue.

The static analyzer flagged that rhs_replace captures the loop variable pid by reference. While this is safe here since the function is called immediately within the same iteration, it's a potential maintenance hazard if the code is refactored.

♻️ Optional fix using default argument binding
         for pid, items in ib_patches.items():
 
             # function that defines how we will replace variable names with
             # values from the case file
-            def rhs_replace(match):
+            def rhs_replace(match, pid=pid):
                 return {
                     'x': 'x_cc(i)', 'y': 'y_cc(j)', 'z': 'z_cc(k)',
                     't': 'mytime',
 
                     'r':     f'patch_ib({pid})%radius',
 
                     'e' : f'{math.e}',
                 }.get(match.group(), match.group())
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 31c938f and 7bd28cd.

📒 Files selected for processing (10)
  • examples/2D_ibm_ellipse/case.py
  • src/common/m_compute_levelset.fpp
  • src/common/m_derived_types.fpp
  • src/common/m_ib_patches.fpp
  • src/pre_process/m_check_ib_patches.fpp
  • src/pre_process/m_global_parameters.fpp
  • src/simulation/m_ibm.fpp
  • src/simulation/m_time_steppers.fpp
  • toolchain/mfc/case.py
  • toolchain/mfc/run/case_dicts.py
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{fpp,f90}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{fpp,f90}: Use 2-space indentation; continuation lines align beneath &
Use lower-case keywords and intrinsics (do, end subroutine, etc.)
Name modules with m_ pattern (e.g., m_transport)
Name public subroutines with s_ pattern (e.g., s_compute_flux)
Name public functions with f
_ pattern
Keep subroutine size ≤ 500 lines, helper subroutines ≤ 150 lines, functions ≤ 100 lines, files ≤ 1000 lines
Limit routine arguments to ≤ 6; use derived-type params struct if more are needed
Forbid goto statements (except in legacy code), COMMON blocks, and save globals
Every argument must have explicit intent; use dimension/allocatable/pointer as appropriate
Call s_mpi_abort() for errors, never use stop or error stop

**/*.{fpp,f90}: Indent 2 spaces; continuation lines align under &
Use lower-case keywords and intrinsics (do, end subroutine, etc.)
Name modules with m_<feature> prefix (e.g., m_transport)
Name public subroutines as s_<verb>_<noun> (e.g., s_compute_flux) and functions as f_<verb>_<noun>
Keep private helpers in the module; avoid nested procedures
Enforce size limits: subroutine ≤ 500 lines, helper ≤ 150, function ≤ 100, module/file ≤ 1000
Limit subroutines to ≤ 6 arguments; otherwise pass a derived-type 'params' struct
Avoid goto statements (except unavoidable legacy); avoid global state (COMMON, save)
Every variable must have intent(in|out|inout) specification and appropriate dimension / allocatable / pointer
Use s_mpi_abort(<msg>) for error termination instead of stop
Use !> style documentation for header comments; follow Doxygen Fortran format with !! @param and `!! `@return tags
Use implicit none statement in all modules
Use private declaration followed by explicit public exports in modules
Use derived types with pointers for encapsulation (e.g., pointer, dimension(:,:,:) => null())
Use pure and elemental attributes for side-effect-free functions; combine them for array ...

Files:

  • src/common/m_derived_types.fpp
  • src/pre_process/m_check_ib_patches.fpp
  • src/simulation/m_ibm.fpp
  • src/simulation/m_time_steppers.fpp
  • src/common/m_compute_levelset.fpp
  • src/common/m_ib_patches.fpp
  • src/pre_process/m_global_parameters.fpp
src/**/*.fpp

📄 CodeRabbit inference engine (.cursor/rules/mfc-agent-rules.mdc)

src/**/*.fpp: Use .fpp file extension for Fypp preprocessed files; CMake transpiles them to .f90
Start module files with Fypp include for macros: #:include 'macros.fpp'
Use the fypp ASSERT macro for validating conditions: @:ASSERT(predicate, message)
Use fypp macro @:ALLOCATE(var1, var2) for device-aware allocation instead of standard Fortran allocate
Use fypp macro @:DEALLOCATE(var1, var2) for device-aware deallocation instead of standard Fortran deallocate

Files:

  • src/common/m_derived_types.fpp
  • src/pre_process/m_check_ib_patches.fpp
  • src/simulation/m_ibm.fpp
  • src/simulation/m_time_steppers.fpp
  • src/common/m_compute_levelset.fpp
  • src/common/m_ib_patches.fpp
  • src/pre_process/m_global_parameters.fpp
src/simulation/**/*.{fpp,f90}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/simulation/**/*.{fpp,f90}: Wrap tight GPU loops with !$acc parallel loop gang vector default(present) reduction(...); add collapse(n) when safe; declare loop-local variables with private(...)
Allocate large GPU arrays with managed memory or move them into persistent !$acc enter data regions at start-up
Avoid stop/error stop inside GPU device code
Ensure GPU code compiles with Cray ftn, NVIDIA nvfortran, GNU gfortran, and Intel ifx/ifort compilers

src/simulation/**/*.{fpp,f90}: Mark GPU-callable helpers with $:GPU_ROUTINE(function_name='...', parallelism='[seq]') immediately after declaration
Do not use OpenACC or OpenMP directives directly; use Fypp macros from src/common/include/parallel_macros.fpp instead
Wrap tight loops with $:GPU_PARALLEL_FOR(private='[...]', copy='[...]') macro; add collapse=n for safe nested loop merging
Declare loop-local variables with private='[...]' in GPU parallel loop macros
Allocate large arrays with managed or move them into a persistent $:GPU_ENTER_DATA(...) region at start-up
Do not place stop or error stop inside device code

Files:

  • src/simulation/m_ibm.fpp
  • src/simulation/m_time_steppers.fpp
🧠 Learnings (7)
📚 Learning: 2025-11-24T21:50:16.713Z
Learnt from: CR
Repo: MFlowCode/MFC PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T21:50:16.713Z
Learning: Applies to src/simulation/**/*.{fpp,f90} : Allocate large GPU arrays with managed memory or move them into persistent !$acc enter data regions at start-up

Applied to files:

  • src/simulation/m_ibm.fpp
📚 Learning: 2025-11-24T21:50:46.909Z
Learnt from: CR
Repo: MFlowCode/MFC PR: 0
File: .cursor/rules/mfc-agent-rules.mdc:0-0
Timestamp: 2025-11-24T21:50:46.909Z
Learning: Applies to src/simulation/**/*.{fpp,f90} : Allocate large arrays with `managed` or move them into a persistent `$:GPU_ENTER_DATA(...)` region at start-up

Applied to files:

  • src/simulation/m_ibm.fpp
📚 Learning: 2025-11-24T21:50:46.909Z
Learnt from: CR
Repo: MFlowCode/MFC PR: 0
File: .cursor/rules/mfc-agent-rules.mdc:0-0
Timestamp: 2025-11-24T21:50:46.909Z
Learning: Applies to src/simulation/**/*.{fpp,f90} : Wrap tight loops with `$:GPU_PARALLEL_FOR(private='[...]', copy='[...]')` macro; add `collapse=n` for safe nested loop merging

Applied to files:

  • src/simulation/m_ibm.fpp
  • src/simulation/m_time_steppers.fpp
📚 Learning: 2025-11-24T21:50:46.909Z
Learnt from: CR
Repo: MFlowCode/MFC PR: 0
File: .cursor/rules/mfc-agent-rules.mdc:0-0
Timestamp: 2025-11-24T21:50:46.909Z
Learning: Applies to src/simulation/**/*.{fpp,f90} : Do not use OpenACC or OpenMP directives directly; use Fypp macros from `src/common/include/parallel_macros.fpp` instead

Applied to files:

  • src/simulation/m_time_steppers.fpp
📚 Learning: 2025-11-24T21:50:16.713Z
Learnt from: CR
Repo: MFlowCode/MFC PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T21:50:16.713Z
Learning: Applies to src/simulation/**/*.{fpp,f90} : Ensure GPU code compiles with Cray ftn, NVIDIA nvfortran, GNU gfortran, and Intel ifx/ifort compilers

Applied to files:

  • src/simulation/m_time_steppers.fpp
📚 Learning: 2025-11-24T21:50:16.713Z
Learnt from: CR
Repo: MFlowCode/MFC PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T21:50:16.713Z
Learning: Applies to src/simulation/**/*.{fpp,f90} : Wrap tight GPU loops with !$acc parallel loop gang vector default(present) reduction(...); add collapse(n) when safe; declare loop-local variables with private(...)

Applied to files:

  • src/simulation/m_time_steppers.fpp
📚 Learning: 2025-11-24T21:50:46.909Z
Learnt from: CR
Repo: MFlowCode/MFC PR: 0
File: .cursor/rules/mfc-agent-rules.mdc:0-0
Timestamp: 2025-11-24T21:50:46.909Z
Learning: Applies to **/*.{fpp,f90} : Use `private` declaration followed by explicit `public` exports in modules

Applied to files:

  • src/common/m_compute_levelset.fpp
🧬 Code graph analysis (3)
src/simulation/m_ibm.fpp (1)
toolchain/mfc/test/cases.py (1)
  • alter_ib (377-422)
examples/2D_ibm_ellipse/case.py (1)
toolchain/mfc/test/cases.py (8)
  • alter_ib (377-422)
  • alter_3d (322-365)
  • alter_2d (293-320)
  • alter_hypoelasticity (590-652)
  • alter_num_fluids (224-291)
  • alter_mixlayer_perturb (686-717)
  • alter_viscosity (788-839)
  • alter_bc_patches (908-955)
toolchain/mfc/case.py (1)
toolchain/mfc/common.py (1)
  • is_number (190-201)
🪛 GitHub Actions: Spell Check
src/common/m_compute_levelset.fpp

[error] 380-380: Typos found. 'transofmration' should be 'transformation'.

🪛 Ruff (0.14.11)
toolchain/mfc/case.py

232-232: Function definition does not bind loop variable pid

(B023)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Code Cleanliness Check
  • GitHub Check: Coverage Test on CodeCov
  • GitHub Check: Github (ubuntu, no-mpi, single, no-debug, false)
  • GitHub Check: Github (ubuntu, mpi, no-debug, false)
  • GitHub Check: Github (macos, mpi, no-debug, false)
  • GitHub Check: Github (macos, mpi, debug, false)
  • GitHub Check: Github (ubuntu, mpi, no-debug, true)
  • GitHub Check: Github (ubuntu, mpi, debug, false)
  • GitHub Check: Github (ubuntu, mpi, debug, true)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Build & Publish
🔇 Additional comments (20)
toolchain/mfc/run/case_dicts.py (1)

368-371: LGTM! Analytic type support for IB velocity parameters.

The change to ParamType.REAL.analytic() for vel and angular_vel correctly enables time-parameterized motion expressions for moving immersed boundaries, consistent with how other analytic initial conditions are handled elsewhere in the codebase.

toolchain/mfc/case.py (5)

15-18: LGTM! Well-structured constant for MIB analytic variables.

The MIBM_ANALYTIC_VARS list correctly enumerates all velocity and angular velocity components that can be analytically defined for moving immersed boundaries.


101-111: LGTM! Proper regex escaping for variable matching.

Good use of re.escape(variable) to handle parentheses in variable names like vel(1). The method correctly mirrors the pattern established by __is_ic_analytical.


350-358: LGTM! Clean refactoring for FPP generation.

The separation of __get_analytic_ic_fpp and __get_analytic_mib_fpp with proper concatenation in __get_sim_fpp ensures both IC and MIB analytic macros are available during simulation compilation.


54-57: LGTM! Proper handling of MIB analytic expressions in input generation.

Correctly extends the analytic placeholder logic to MIB parameters, ensuring string expressions are replaced with 0d0 in the .inp file while the actual expressions are handled via generated Fypp macros.


258-265: No action needed — mytime is properly available in scope.

The generated #:def mib_analytical() macro correctly maps the time variable t to mytime, which is defined as a module-level variable in m_global_parameters.fpp and is initialized and maintained throughout the simulation lifecycle. It is accessible wherever the macro is expanded.

src/common/m_derived_types.fpp (1)

299-299: LGTM! Well-documented centroid offset field.

The centroid_offset field correctly addresses issue #1106 by allowing proper tracking of the center of mass offset for asymmetric immersed boundaries like airfoils. The field type and dimensioning are appropriate.

src/pre_process/m_global_parameters.fpp (1)

565-565: LGTM! Proper default initialization for centroid_offset.

The initialization to 0._wp is consistent with other array initializations in the IB patch defaults and ensures a clean starting state before the actual centroid offset is computed during simulation setup.

examples/2D_ibm_ellipse/case.py (1)

1-97: LGTM - New 2D ellipse example case.

The case file correctly demonstrates the new ellipse IB patch (geometry=6) with appropriate domain parameters, boundary conditions, and fluid properties for viscous flow simulation.

src/simulation/m_time_steppers.fpp (1)

5-6: LGTM - Include case.fpp for analytic MIB macros.

Adding the include enables the @:mib_analytical() macro for prescribing analytic velocities in 1-way MIBM coupling.

src/simulation/m_ibm.fpp (2)

96-104: LGTM - Added centroid offset computation in IBM setup.

The call to s_compute_centroid_offset(i) is correctly placed after the rotation matrix update and before GPU data transfer, enabling proper centroid calculations for airfoil and STL geometries.


1103-1113: Use the more direct if (p > 0) check for consistency with the rest of the codebase. Line 1109 should be if (p > 0) instead of if (num_dims == 3), matching the pattern used elsewhere (e.g., lines 224–227) and avoiding reliance on a global parameter that should inherently correspond to the grid dimensions.

src/common/m_compute_levelset.fpp (4)

26-27: LGTM - Added ellipse levelset to public exports.

The new s_ellipse_levelset subroutine is correctly added to the module's public interface.


97-97: LGTM - Centroid offset adjustment for 2D airfoil levelset.

Correctly subtracts centroid_offset from local coordinates after rotation, aligning the airfoil's geometric center with its true center of mass for proper levelset computation.


193-193: LGTM - Centroid offset adjustment for 3D airfoil levelset.

Consistent with the 2D implementation, correctly applies centroid offset in the local IB frame.


342-396: Resolve the sign discrepancy between the formula comment and implementation on line 390.

The comment on line 389 states to compute [ -B + sqrt(B^2 - 4AC) ] / 2A (the plus root), but the code on line 390 implements -0.5_wp * (-quadratic_coeffs(2) + sqrt(...)) / quadratic_coeffs(1), which mathematically evaluates to (B - sqrt(...)) / (2A) (the minus root with a sign flip). Verify whether this negation is intentional (e.g., for inward-facing signed distance) and update the comment to reflect the actual formula being computed.

Regarding the other points: centroid_offset is correctly omitted since ellipses have a well-defined geometric center (unlike airfoils which require explicit offset correction, as noted in s_airfoil_levelset). Minor style: replace **2._wp exponents on lines 380, 387 with **2 per coding guidelines.

src/common/m_ib_patches.fpp (4)

122-125: LGTM - Added ellipse geometry dispatch in 2D IB patch application.

The new geometry type 6 for ellipse is correctly routed to both the marker function (s_ib_ellipse) and levelset computation (s_ellipse_levelset).


282-282: LGTM - Centroid offset for 2D airfoil patch markers.

Correctly applies centroid offset in the local IB frame, consistent with the levelset computation in m_compute_levelset.fpp.


437-437: LGTM - Centroid offset for 3D airfoil patch markers.

Consistent implementation with the 2D version.


771-810: LGTM - New ellipse IB patch subroutine.

The implementation correctly:

  • Extracts ellipse semi-axes from length_x/2 and length_y/2
  • Transforms coordinates to local IB frame using rotation matrix
  • Uses the standard ellipse equation (x/a)² + (y/b)² ≤ 1 for interior check
  • Follows the same pattern as other 2D patch subroutines

The GPU parallel loop is properly configured with appropriate private variables and data movement.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

6 issues found across 10 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="src/simulation/m_ibm.fpp">

<violation number="1" location="src/simulation/m_ibm.fpp:1121">
P1: After the MPI reductions, the centroid is divided by `num_cells_local` (the pre-reduction local count) instead of the globally reduced `num_cells`, which can produce NaNs or incorrect centers of mass. Divide by the global count returned by `s_mpi_allreduce_integer_sum` (and guard against zero) to compute a valid centroid.</violation>
</file>

<file name="src/pre_process/m_check_ib_patches.fpp">

<violation number="1" location="src/pre_process/m_check_ib_patches.fpp:65">
P2: Geometry 6 IB patches bypass all validation: the new branch just prints “Ellipse Patch” and never enforces required parameters (centroid, radii, dimensionality). This lets malformed ellipse IB definitions pass unchecked and can cause runtime failures when s_ib_ellipse later uses unset data. Replace the print with proper validation mirroring other geometries.</violation>
</file>

<file name="toolchain/mfc/run/case_dicts.py">

<violation number="1" location="toolchain/mfc/run/case_dicts.py:369">
P1: Assigning `ParamType.REAL.analytic()` (which returns a plain dict) directly into `SIMULATION` is inconsistent with other entries that use `ParamType` Enum members. Downstream code that builds `_properties = {k: v.value for k, v in ALL.items()}` will fail with an `AttributeError` because dicts don't have a `.value` attribute. Use `ParamType.REAL` instead to maintain consistency.</violation>

<violation number="2" location="toolchain/mfc/run/case_dicts.py:371">
P1: Same issue as with `vel`: assigning `ParamType.REAL.analytic()` (a plain dict) will cause `AttributeError` when building `_properties`. Use `ParamType.REAL` instead.</violation>
</file>

<file name="src/common/m_compute_levelset.fpp">

<violation number="1" location="src/common/m_compute_levelset.fpp:377">
P1: levelset and normal fields are never set for cells outside the ellipse, leaving undefined geometry data for most of the domain. Add an `else` branch that computes the signed distance/normal outside the ellipse instead of skipping the assignment.</violation>
</file>

<file name="toolchain/mfc/case.py">

<violation number="1" location="toolchain/mfc/case.py:227">
P1: Analytic IB codegen replaces `x`, `y`, `z` with grid-center arrays using undefined indices (`j`, `k`) inside the time-stepper loop, causing compilation failures or wrong values when users reference spatial variables.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/common/m_ib_patches.fpp`:
- Line 781: The inline comment "! Transferring the rectangle's centroid and
length information" is a copy-paste error; update it to refer to the ellipse
instead (e.g., "! Transferring the ellipse's centroid and axes/length
information") so it accurately describes the code handling ellipse centroid and
length/axes values; locate and change this exact comment string in
m_ib_patches.fpp (search for "Transferring the rectangle's centroid") and
replace "rectangle" with "ellipse" (optionally adjust "length" to "axes" for
clarity).
♻️ Duplicate comments (2)
src/common/m_ib_patches.fpp (1)

788-791: Fix copy-paste comment: "rectangle" should be "ellipse".

📝 Proposed fix
-        ! Checking whether the rectangle covers a particular cell in the
+        ! Checking whether the ellipse covers a particular cell in the
         ! domain and verifying whether the current patch has the permission
         ! to write to that cell. If both queries check out, the primitive
         ! variables of the current patch are assigned to this cell.
src/common/m_compute_levelset.fpp (1)

376-391: Level set only computed inside ellipse - exterior cells remain uninitialized.

The condition at line 377 restricts computation to points inside the ellipse. Cells outside will retain their initial values (likely zero from the caller's initialization), which may cause incorrect ghost point interpolation for cells near but outside the ellipse boundary.

Compare to s_circle_levelset (lines 49-62) which computes dist - radius for ALL grid points, providing a valid signed distance everywhere.

🐛 Proposed fix: Compute signed distance for all points

For points outside the ellipse, you could use an algebraic distance approximation:

-                if ((xy_local(1)/ellipse_coeffs(1))**2 + (xy_local(2)/ellipse_coeffs(2))**2 <= 1._wp) then
+                ! Algebraic distance approximation valid everywhere
+                levelset%sf(i, j, 0, ib_patch_id) = sqrt(ellipse_coeffs(1) * ellipse_coeffs(2)) * &
+                    (sqrt((xy_local(1)/ellipse_coeffs(1))**2 + (xy_local(2)/ellipse_coeffs(2))**2) - 1._wp)
 
+                ! Normal vector computation only valid inside/near surface
+                if ((xy_local(1)/ellipse_coeffs(1))**2 + (xy_local(2)/ellipse_coeffs(2))**2 <= 1._wp) then
                     normal_vector = xy_local
                     normal_vector(2) = normal_vector(2)*(ellipse_coeffs(1)/ellipse_coeffs(2))**2._wp
                     normal_vector = normal_vector/sqrt(dot_product(normal_vector, normal_vector))
                     levelset_norm%sf(i, j, 0, ib_patch_id, :) = matmul(rotation, normal_vector)
-
-                    quadratic_coeffs(1) = ...
-                    ...
-                    levelset%sf(i, j, 0, ib_patch_id) = ...
                 end if

Alternatively, compute the exact closest-point-on-ellipse distance for exterior points, though this requires iterative root finding.

🧹 Nitpick comments (2)
src/simulation/m_ibm.fpp (1)

1115-1121: Previous critical bug is fixed, but consider guarding against zero cell count.

The division now correctly uses the globally reduced num_cells instead of num_cells_local. However, if an IB patch has no cells (e.g., misconfigured geometry), num_cells could be zero, causing a division-by-zero crash.

🛡️ Optional: Add zero-guard for robustness
             call s_mpi_allreduce_sum(center_of_mass(1), center_of_mass(1))
             call s_mpi_allreduce_sum(center_of_mass(2), center_of_mass(2))
             call s_mpi_allreduce_sum(center_of_mass(3), center_of_mass(3))
             call s_mpi_allreduce_integer_sum(num_cells_local, num_cells)
+            if (num_cells == 0) then
+                call s_mpi_abort("s_compute_centroid_offset: No cells found for IB patch")
+            end if
             center_of_mass = center_of_mass/real(num_cells, wp)
src/common/m_compute_levelset.fpp (1)

356-357: Remove unused variables k and idx.

These variables are declared but never used in the subroutine.

♻️ Proposed fix
-        integer :: i, j, k !< Loop index variables
-        integer :: idx !< Shortest path direction indicator
+        integer :: i, j !< Loop index variables
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7bd28cd and 541e727.

📒 Files selected for processing (3)
  • src/common/m_compute_levelset.fpp
  • src/common/m_ib_patches.fpp
  • src/simulation/m_ibm.fpp
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{fpp,f90}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{fpp,f90}: Use 2-space indentation; continuation lines align beneath &
Use lower-case keywords and intrinsics (do, end subroutine, etc.)
Name modules with m_ pattern (e.g., m_transport)
Name public subroutines with s_ pattern (e.g., s_compute_flux)
Name public functions with f
_ pattern
Keep subroutine size ≤ 500 lines, helper subroutines ≤ 150 lines, functions ≤ 100 lines, files ≤ 1000 lines
Limit routine arguments to ≤ 6; use derived-type params struct if more are needed
Forbid goto statements (except in legacy code), COMMON blocks, and save globals
Every argument must have explicit intent; use dimension/allocatable/pointer as appropriate
Call s_mpi_abort() for errors, never use stop or error stop

**/*.{fpp,f90}: Indent 2 spaces; continuation lines align under &
Use lower-case keywords and intrinsics (do, end subroutine, etc.)
Name modules with m_<feature> prefix (e.g., m_transport)
Name public subroutines as s_<verb>_<noun> (e.g., s_compute_flux) and functions as f_<verb>_<noun>
Keep private helpers in the module; avoid nested procedures
Enforce size limits: subroutine ≤ 500 lines, helper ≤ 150, function ≤ 100, module/file ≤ 1000
Limit subroutines to ≤ 6 arguments; otherwise pass a derived-type 'params' struct
Avoid goto statements (except unavoidable legacy); avoid global state (COMMON, save)
Every variable must have intent(in|out|inout) specification and appropriate dimension / allocatable / pointer
Use s_mpi_abort(<msg>) for error termination instead of stop
Use !> style documentation for header comments; follow Doxygen Fortran format with !! @param and `!! `@return tags
Use implicit none statement in all modules
Use private declaration followed by explicit public exports in modules
Use derived types with pointers for encapsulation (e.g., pointer, dimension(:,:,:) => null())
Use pure and elemental attributes for side-effect-free functions; combine them for array ...

Files:

  • src/common/m_ib_patches.fpp
  • src/simulation/m_ibm.fpp
  • src/common/m_compute_levelset.fpp
src/**/*.fpp

📄 CodeRabbit inference engine (.cursor/rules/mfc-agent-rules.mdc)

src/**/*.fpp: Use .fpp file extension for Fypp preprocessed files; CMake transpiles them to .f90
Start module files with Fypp include for macros: #:include 'macros.fpp'
Use the fypp ASSERT macro for validating conditions: @:ASSERT(predicate, message)
Use fypp macro @:ALLOCATE(var1, var2) for device-aware allocation instead of standard Fortran allocate
Use fypp macro @:DEALLOCATE(var1, var2) for device-aware deallocation instead of standard Fortran deallocate

Files:

  • src/common/m_ib_patches.fpp
  • src/simulation/m_ibm.fpp
  • src/common/m_compute_levelset.fpp
src/simulation/**/*.{fpp,f90}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/simulation/**/*.{fpp,f90}: Wrap tight GPU loops with !$acc parallel loop gang vector default(present) reduction(...); add collapse(n) when safe; declare loop-local variables with private(...)
Allocate large GPU arrays with managed memory or move them into persistent !$acc enter data regions at start-up
Avoid stop/error stop inside GPU device code
Ensure GPU code compiles with Cray ftn, NVIDIA nvfortran, GNU gfortran, and Intel ifx/ifort compilers

src/simulation/**/*.{fpp,f90}: Mark GPU-callable helpers with $:GPU_ROUTINE(function_name='...', parallelism='[seq]') immediately after declaration
Do not use OpenACC or OpenMP directives directly; use Fypp macros from src/common/include/parallel_macros.fpp instead
Wrap tight loops with $:GPU_PARALLEL_FOR(private='[...]', copy='[...]') macro; add collapse=n for safe nested loop merging
Declare loop-local variables with private='[...]' in GPU parallel loop macros
Allocate large arrays with managed or move them into a persistent $:GPU_ENTER_DATA(...) region at start-up
Do not place stop or error stop inside device code

Files:

  • src/simulation/m_ibm.fpp
🧠 Learnings (4)
📚 Learning: 2025-11-24T21:50:16.713Z
Learnt from: CR
Repo: MFlowCode/MFC PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T21:50:16.713Z
Learning: Applies to src/simulation/**/*.{fpp,f90} : Allocate large GPU arrays with managed memory or move them into persistent !$acc enter data regions at start-up

Applied to files:

  • src/simulation/m_ibm.fpp
📚 Learning: 2025-11-24T21:50:46.909Z
Learnt from: CR
Repo: MFlowCode/MFC PR: 0
File: .cursor/rules/mfc-agent-rules.mdc:0-0
Timestamp: 2025-11-24T21:50:46.909Z
Learning: Applies to src/simulation/**/*.{fpp,f90} : Allocate large arrays with `managed` or move them into a persistent `$:GPU_ENTER_DATA(...)` region at start-up

Applied to files:

  • src/simulation/m_ibm.fpp
📚 Learning: 2025-11-24T21:50:46.909Z
Learnt from: CR
Repo: MFlowCode/MFC PR: 0
File: .cursor/rules/mfc-agent-rules.mdc:0-0
Timestamp: 2025-11-24T21:50:46.909Z
Learning: Applies to src/simulation/**/*.{fpp,f90} : Wrap tight loops with `$:GPU_PARALLEL_FOR(private='[...]', copy='[...]')` macro; add `collapse=n` for safe nested loop merging

Applied to files:

  • src/simulation/m_ibm.fpp
📚 Learning: 2025-11-24T21:50:46.909Z
Learnt from: CR
Repo: MFlowCode/MFC PR: 0
File: .cursor/rules/mfc-agent-rules.mdc:0-0
Timestamp: 2025-11-24T21:50:46.909Z
Learning: Applies to **/*.{fpp,f90} : Use `private` declaration followed by explicit `public` exports in modules

Applied to files:

  • src/common/m_compute_levelset.fpp
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Github (ubuntu, no-mpi, single, no-debug, false)
  • GitHub Check: Github (ubuntu, mpi, debug, false)
  • GitHub Check: Github (ubuntu, mpi, no-debug, false)
  • GitHub Check: Github (ubuntu, mpi, no-debug, true)
  • GitHub Check: Github (ubuntu, mpi, debug, true)
  • GitHub Check: Code Cleanliness Check
  • GitHub Check: Coverage Test on CodeCov
  • GitHub Check: Build & Publish
🔇 Additional comments (8)
src/simulation/m_ibm.fpp (2)

1084-1136: LGTM! Well-structured centroid offset computation.

The subroutine correctly:

  • Targets only geometry types requiring offset computation (airfoils, STL models)
  • Uses MPI reduction for distributed computation
  • Transforms the offset back to local IB coordinates for consistent usage
  • Initializes offset to zero for inapplicable geometries

96-105: LGTM! Correct integration of centroid offset computation.

The call ordering is correct: rotation matrix is computed first (required for the offset rotation at line 1131), and the centroid offset is computed before the GPU data transfer.

src/common/m_compute_levelset.fpp (2)

95-97: LGTM! Correct coordinate frame for centroid offset subtraction.

The centroid_offset is stored in local IB coordinates (transformed at m_ibm.fpp:1131), and here it's correctly subtracted after xy_local has been rotated into the same local frame.


191-193: LGTM! Consistent with 2D airfoil implementation.

src/common/m_ib_patches.fpp (4)

280-282: LGTM! Centroid offset correctly applied in local coordinates.

The centroid_offset is pre-rotated into local IB coordinates during initialization (m_ibm.fpp:1131). Here it's correctly subtracted after xy_local is transformed to the same local frame. Both vectors share the same coordinate basis.


435-437: LGTM! Consistent centroid offset handling for 3D airfoil.


770-809: LGTM! Clean ellipse IB marker implementation.

The subroutine correctly:

  • Extracts ellipse parameters from patch data
  • Transforms coordinates to local IB frame
  • Uses standard ellipse membership test (x/a)² + (y/b)² ≤ 1

122-124: LGTM! Correct dispatch for ellipse geometry.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Jan 16, 2026

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Jan 16, 2026
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Jan 16, 2026

CodeAnt AI Incremental review completed.

@codecov
Copy link

codecov bot commented Jan 16, 2026

Codecov Report

❌ Patch coverage is 68.00000% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.16%. Comparing base (b02e7f2) to head (928ccd0).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/simulation/m_ibm.fpp 70.73% 10 Missing and 2 partials ⚠️
src/simulation/m_time_steppers.fpp 65.51% 5 Missing and 5 partials ⚠️
src/common/m_compute_levelset.fpp 50.00% 1 Missing ⚠️
src/common/m_ib_patches.fpp 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1111      +/-   ##
==========================================
+ Coverage   44.06%   44.16%   +0.10%     
==========================================
  Files          71       71              
  Lines       20379    20412      +33     
  Branches     1989     1992       +3     
==========================================
+ Hits         8980     9015      +35     
+ Misses      10254    10250       -4     
- Partials     1145     1147       +2     

☔ 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.

@codeant-ai codeant-ai bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Jan 16, 2026
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Jan 16, 2026

CodeAnt AI Incremental review completed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/simulation/m_ibm.fpp`:
- Around line 1170-1171: The two variable assignments center_of_mass_local and
num_cells_local in the same if-block have inconsistent indentation causing the
pipeline failure; align the indentation so both statements use the same
indentation level (match the indent used for num_cells_local or
center_of_mass_local) within the function or block in src/simulation/m_ibm.fpp
to ensure consistent formatting.
- Around line 1186-1193: After the MPI reductions (s_mpi_allreduce_sum on
center_of_mass_local and s_mpi_allreduce_integer_sum on num_cells) add a guard
to avoid dividing by zero: check if num_cells == 0 and in that case set
center_of_mass = 0.0_wp (or another defined safe default) and skip the division;
otherwise perform center_of_mass = center_of_mass/real(num_cells, wp). This
change touches the variables center_of_mass, center_of_mass_local,
num_cells_local, num_cells and the reduction sequence (s_mpi_allreduce_sum /
s_mpi_allreduce_integer_sum) so place the conditional immediately after the
reductions and before the division.
♻️ Duplicate comments (1)
src/simulation/m_ibm.fpp (1)

98-107: Add guard against division by zero in s_compute_centroid_offset when no cells are found for the IB patch.

The call placement at line 106 is correct—ib_markers%sf is populated before s_ibm_setup() is invoked (from the restart file at line 440 in m_start_up.fpp). However, line 1192 divides by num_cells without checking if it is zero. If a NACA airfoil or STL geometry has no cells assigned across all ranks, the MPI reduction leaves num_cells = 0, causing NaN values to propagate to center_of_mass.

Add a guard after the MPI reduction:

call s_mpi_allreduce_integer_sum(num_cells_local, num_cells)
if (num_cells > 0) then
    center_of_mass = center_of_mass/real(num_cells, wp)
else
    center_of_mass(:) = [0._wp, 0._wp, 0._wp]
end if

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Jan 17, 2026

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Jan 17, 2026
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Jan 17, 2026

CodeAnt AI Incremental review completed.

@danieljvickers
Copy link
Member Author

Just a note: I know that this passed tests, but I want to make a few more changes before this is merged to clean up some code.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Jan 18, 2026

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Jan 18, 2026
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Jan 18, 2026

CodeAnt AI Incremental review completed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/simulation/m_time_steppers.fpp`:
- Around line 791-804: The code currently calls s_compute_ib_forces(...) inside
the per-IB loop whenever patch_ib(i)%moving_ibm == 2, causing redundant
full-domain force computations; instead, before the do i = 1, num_ibs loop
detect if any patch_ib(:)%moving_ibm == 2 (e.g., a boolean flag set by scanning
patch_ib) and if so call s_compute_ib_forces(q_prim_vf, 1._wp/fluid_pp(1)%Re(1))
once for the stage, then inside the loop remove the s_compute_ib_forces call and
only perform the per-IB updates to patch_ib(i)%vel, patch_ib(i)%angular_vel,
call s_compute_moment_of_inertia(i, ...) and the subsequent division by
patch_ib(i)%moment.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 2 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="src/simulation/m_ibm.fpp">

<violation number="1" location="src/simulation/m_ibm.fpp:1053">
P1: Pressure-gradient accumulation overwrites Y/Z components with the X component, producing incorrect force/torque directions. Each component should accumulate from its own value.</violation>

<violation number="2" location="src/simulation/m_ibm.fpp:1073">
P1: Dynamic viscosity uses an uninitialized index (`l`) and misses the current `i` index, so it reads the wrong cell (undefined in Fortran). This can corrupt viscous force computations.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/simulation/m_ibm.fpp`:
- Around line 1070-1074: The dynamic_viscosity accumulation uses
q_cons_vf(...%sf(j, k, l)) but the loop is over (i, j, k) so the index l is
wrong; update the access to use the correct i index (q_cons_vf(fluid_idx + advxb
- 1)%sf(i, j, k)) so dynamic_viscosity is computed from the current (i,j,k) cell
when summing over fluids and using fluid_pp%Re within the loop in m_ibm.fpp.
- Around line 1049-1059: The y and z components of the pressure-force
accumulator are accidentally using local_force_contribution(1) on the right-hand
side, which mixes axes; in the block that computes local_force_contribution(2)
and, inside the num_dims==3 branch, local_force_contribution(3), change the RHS
to reference the same component being updated (use local_force_contribution(2)
for the y update and local_force_contribution(3) for the z update) so each axis
subtracts its own finite-difference pressure gradient (retain use of
q_prim_vf(E_idx + fluid_idx)%sf(...) and the existing dx, dy, dz and cell_volume
logic).
♻️ Duplicate comments (3)
src/simulation/m_ibm.fpp (2)

98-107: Move centroid-offset computation after IB markers are populated.
s_compute_centroid_offset scans ib_markers%sf, but this call happens before s_apply_ib_patches / s_populate_ib_buffers, so it runs on empty markers and does a full-domain scan per IB with zero counts. Please move it after the markers are populated.

🔧 Suggested move
-            call s_compute_centroid_offset(i)
         call s_populate_ib_buffers()
+        do i = 1, num_ibs
+            call s_compute_centroid_offset(i)
+        end do

1193-1200: Guard against zero cells before dividing.
If num_cells == 0 across all ranks, this divides by zero and propagates NaNs into centroid placement.

🛡️ Suggested guard
             call s_mpi_allreduce_integer_sum(num_cells_local, num_cells)
-            center_of_mass = center_of_mass/real(num_cells, wp)
+            if (num_cells == 0) then
+                patch_ib(ib_marker)%centroid_offset(:) = [0._wp, 0._wp, 0._wp]
+                return
+            end if
+            center_of_mass = center_of_mass/real(num_cells, wp)
src/simulation/m_time_steppers.fpp (1)

772-805: Compute IB forces once per stage, not once per IB.
s_compute_ib_forces already sweeps the full domain for all IBs; calling it inside the per-IB loop repeats that work for every moving_ibm == 2 patch. Please move the call outside the loop and guard it with any(patch_ib(:)%moving_ibm == 2).

♻️ Suggested restructuring
+        if (any(patch_ib(:)%moving_ibm == 2)) then
+            call s_compute_ib_forces(q_prim_vf, fluid_pp)
+        end if
         do i = 1, num_ibs
@@
-                else if (patch_ib(i)%moving_ibm == 2) then ! if we are using two-way coupling, apply force and torque
-                    ! compute the force and torque on the IB from the fluid
-                    call s_compute_ib_forces(q_prim_vf, fluid_pp)
+                else if (patch_ib(i)%moving_ibm == 2) then ! if we are using two-way coupling, apply force and torque

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="src/simulation/m_ibm.fpp">

<violation number="1" location="src/simulation/m_ibm.fpp:1073">
P2: The dynamic viscosity computation indexes q_prim_vf with (j, k, l) even though l is not initialized in this loop. This can read the wrong cell (or an undefined index), producing incorrect forces/torques. Use the current cell indices (i, j, k).</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@danieljvickers
Copy link
Member Author

Ok, I have addressed all AI review comments and finished the basic updates that I also wanted to run in this PR. Once this passes the test suite again, it will be ready to merge

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/simulation/m_ibm.fpp`:
- Around line 1049-1061: The pressure gradient is being computed per-fluid using
q_prim_vf(E_idx + fluid_idx) which is wrong because E_idx is the single shared
pressure index; change the code to compute the pressure gradient using
q_prim_vf(E_idx)%sf(...) only (not E_idx + fluid_idx). Either move the gradient
computation for local_force_contribution(1..3) outside the fluid loop or compute
a local pressure_gradient variable from q_prim_vf(E_idx)%sf(...) and then add
that same contribution to each fluid's local_force_contribution inside the loop;
keep the existing cell_volume and num_dims handling unchanged.
🧹 Nitpick comments (1)
src/simulation/m_ibm.fpp (1)

1028-1028: Remove unused variable vel.

The variable vel is declared but never used in this subroutine.

♻️ Proposed fix
-        real(wp), dimension(1:3) :: local_force_contribution, radial_vector, local_torque_contribution, vel
+        real(wp), dimension(1:3) :: local_force_contribution, radial_vector, local_torque_contribution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review effort 4/5 size:L This PR changes 100-499 lines, ignoring generated files

Development

Successfully merging this pull request may close these issues.

Airfoil Centroid in Wrong Location Parametrized 1-Way MIBM Movement

2 participants