Skip to content

Levelset refactor#1123

Open
danieljvickers wants to merge 88 commits intoMFlowCode:masterfrom
danieljvickers:levelset-refactor
Open

Levelset refactor#1123
danieljvickers wants to merge 88 commits intoMFlowCode:masterfrom
danieljvickers:levelset-refactor

Conversation

@danieljvickers
Copy link
Member

@danieljvickers danieljvickers commented Feb 4, 2026

User description

Description

This PR is a significant refactor of the levelset code. It contains

  1. The removal of IB markers and levelset values from pre-processing. Now all values are computed in simulation
  2. The deletion of the levelset and levelset norm global arrays. The are now tied to ghost points. The levelset code is now parallelized over ghost points instead of grid cells. This reduces the total amount of memory that needs to be allocated for levelsets by an order of magnitude in simple cases, and by several orders of magnitude when using multiple immersed boundaries.
  3. I HAVE DEFERRED GPU STLs TO A SEPARATE PR FOR SIMPLICITY BECAUSE THE SCOPE HAS SIGNIFICANTLY CREEPED ON THIS FEATURE AND IT IS HINDERING DEBUGGING: GPU compute of STL IB levelst values. This is done by loading STL models into global memory, and they are computed separately for IB markers and levelset values. Then each separate loop was parallelized.
  4. Arbitrary rotation of STL models which is compatible with the same local coordiante system as all other IBs

Fixes #1011

How Has This Been Tested?

This currently passes all tests on MacOS and Ubuntu operating systms using the GNU compiler.

PR Type

Enhancement, Refactor

Description

  • Removed levelset computation from preprocessing: IB markers and levelset values are no longer computed during pre-processing; all computations now occur during simulation runtime

  • Eliminated global levelset arrays: Replaced global levelset and levelset_norm arrays with local storage in the ghost_point derived type, reducing memory allocation by orders of magnitude

  • Parallelized levelset computation over ghost points: Refactored levelset code to parallelize over ghost points instead of grid cells, improving memory efficiency and GPU performance

  • GPU-accelerated STL model processing: Implemented GPU compute for STL IB markers and levelset values by loading STL models into global memory with separate parallelized loops

  • New m_compute_levelset module: Created dedicated module for all levelset computations with geometry-specific functions (circles, rectangles, ellipses, spheres, cylinders, airfoils, STL models)

  • Added STL model configuration parameters: Extended IB patch parameters to support model_filepath, model_spc, model_threshold, and transformation parameters (model_translate, model_rotate)

  • Simplified data I/O: Removed levelset and levelset norm from preprocessing output; added dedicated IB data output subroutines for simulation runtime

  • Updated test infrastructure: Adjusted grid resolution for circle test cases and updated golden test files to reflect removal of pre-computed IB markers

Diagram Walkthrough

flowchart LR
  A["Preprocessing<br/>Input Data"] -->|"No levelset<br/>computation"| B["Simulation<br/>Initialization"]
  B -->|"s_instantiate_STL_models"| C["Load STL Models<br/>to Global Memory"]
  C -->|"GPU compute"| D["Ghost Point<br/>Levelset Storage"]
  D -->|"s_apply_levelset"| E["IBM Updates<br/>During Simulation"]
  E -->|"s_write_ib_data_file"| F["Output IB Data<br/>at Runtime"]
Loading

File Walkthrough

Relevant files
Refactor
8 files
m_ib_patches.fpp
Refactor levelset computation from preprocessing to simulation runtime

src/simulation/m_ib_patches.fpp

  • Removed m_compute_levelset module dependency and levelset computation
    calls from s_apply_ib_patches
  • Added new s_instantiate_STL_models() subroutine to load and preprocess
    STL models during initialization
  • Refactored s_ib_model() to use pre-instantiated models from global
    models array instead of computing levelsets inline
  • Removed local eta variable declarations and replaced with local scope
    usage in geometry-specific functions
  • Added offset variable to airfoil subroutines for cleaner centroid
    offset handling
+165/-234
m_data_output.fpp
Remove levelset data output from preprocessing                     

src/pre_process/m_data_output.fpp

  • Removed ib_markers, levelset, and levelset_norm parameters from data
    output subroutine signatures
  • Removed file I/O operations for writing IB markers, levelset, and
    levelset norm data
  • Simplified abstract interface and implementation for
    s_write_abstract_data_files()
+6/-252 
m_ibm.fpp
Refactor IBM module to use ghost-point-based levelsets     

src/simulation/m_ibm.fpp

  • Removed global levelset and levelset_norm arrays and replaced with
    ghost-point-local storage
  • Added call to s_instantiate_STL_models() during IBM setup
  • Replaced s_apply_ib_patches() calls to remove levelset parameters
  • Changed s_compute_image_points() to use levelset values from
    ghost_point structure instead of global arrays
  • Updated s_update_mib() to call new s_apply_levelset() instead of
    computing levelsets inline
  • Removed patch_id_fp array allocation
+38/-44 
m_start_up.fpp
Remove levelset restart file I/O from simulation startup 

src/simulation/m_start_up.fpp

  • Removed code that reads IB markers, levelset, and levelset norm data
    from restart files
  • Removed airfoil grid data reading from preprocessing
  • Added call to s_write_ib_data_file() after IBM setup during
    initialization
  • Simplified s_read_ic_data_files() call signature by removing IB marker
    parameter
+6/-212 
m_start_up.fpp
Remove levelset handling from preprocessing startup           

src/pre_process/m_start_up.fpp

  • Removed m_ib_patches module import
  • Removed ib_markers, levelset, and levelset_norm variable declarations
    and allocations
  • Removed IB marker reading from preprocessing startup
  • Simplified s_read_ic_data_files() interface by removing IB marker
    parameter
+6/-70   
m_mpi_common.fpp
Simplify MPI data initialization for IBM                                 

src/common/m_mpi_common.fpp

  • Removed levelset and levelset_norm parameters from
    s_initialize_mpi_data() subroutine
  • Removed MPI type creation for levelset and levelset norm data
    structures
  • Removed airfoil grid MPI I/O setup code
  • Simplified MPI data initialization for IB markers only
+2/-66   
m_initial_condition.fpp
Remove IBM-related variables from preprocessing                   

src/pre_process/m_initial_condition.fpp

  • Removed m_ib_patches module import
  • Removed ib_markers, levelset, and levelset_norm variable declarations
  • Removed IB patch allocation and initialization code
  • Removed s_apply_ib_patches() call from preprocessing
+1/-31   
m_time_steppers.fpp
Remove levelset parameters from immersed boundary update 

src/simulation/m_time_steppers.fpp

  • Simplified the s_update_mib subroutine call by removing levelset and
    levelset_norm parameters
  • Reflects the refactoring where levelset values are now computed in
    simulation rather than passed as arguments
+1/-1     
Enhancement
5 files
m_compute_levelset.fpp
New module for ghost-point-based levelset computation       

src/simulation/m_compute_levelset.fpp

  • New module created to handle all levelset computations for immersed
    boundaries
  • Implements s_apply_levelset() subroutine that computes levelsets for
    ghost points instead of grid cells
  • Contains geometry-specific levelset functions for circles, rectangles,
    ellipses, spheres, cylinders, airfoils, and STL models
  • Levelset and normal vector computations now tied to ghost_point
    derived type instead of global arrays
  • Parallelized over ghost points using GPU macros for improved memory
    efficiency
+723/-0 
m_data_output.fpp
Add dedicated IB data output subroutines                                 

src/simulation/m_data_output.fpp

  • Added new subroutines s_write_serial_ib_data() and
    s_write_parallel_ib_data() for IB data output
  • Created s_write_ib_data_file() wrapper to handle both serial and
    parallel I/O
  • Removed levelset and levelset norm output from main data writing
    routines
  • Simplified MPI data initialization calls by removing levelset
    parameters
+96/-21 
m_global_parameters.fpp
Add default initialization for IB patch parameters             

src/simulation/m_global_parameters.fpp

  • Added initialization loop for patch_ib() array with default values for
    all IB patch parameters
  • Sets proper defaults for STL model transformation parameters (scale,
    translate, rotate)
  • Initializes rotation matrices and moving boundary parameters
+41/-0   
m_derived_types.fpp
Add derived types for model storage and ghost point levelsets

src/common/m_derived_types.fpp

  • Added new t_model_array derived type to store STL model data with
    boundary vertices and interpolation information
  • Extended ghost_point derived type with levelset and levelset_norm
    fields for local storage
  • Removed global levelset array dependencies by moving data to ghost
    point structure
+13/-0   
case_dicts.py
Add STL model and transformation parameters to IB configuration

toolchain/mfc/run/case_dicts.py

  • Added three new immersed boundary patch parameters: model_filepath,
    model_spc, and model_threshold
  • Added three new model transformation parameters: model_translate and
    model_rotate for each spatial direction
  • Commented out model_scale parameter for potential future use
+6/-1     
Tests
7 files
cases.py
Adjust grid resolution for circle test cases                         

toolchain/mfc/test/cases.py

  • Updated Circle test case to use n=49 grid resolution for improved
    precision
  • Added comment explaining machine-level precision sensitivity for
    circular geometries
  • Minor formatting adjustment to boundary condition test setup
+6/-2     
golden.txt
Remove IB markers from golden test output                               

tests/7FA04E95/golden.txt

  • Removed the ib_markers output line from golden test data
  • Reflects the refactoring where IB markers are no longer pre-computed
    and stored globally
+1/-2     
golden.txt
Remove IB markers output from test golden file                     

tests/5600D63B/golden.txt

  • Removed the last line containing D/ib_markers.00.dat with all zero
    values
  • Kept the D/cons.5.00.000050.dat line with updated values
  • Test golden file updated to reflect removal of IB markers from output
+1/-2     
golden-metadata.txt
Update test metadata with new build environment details   

tests/7F70E665/golden-metadata.txt

  • Updated timestamp from 2025-01-20 to 2026-02-03
  • Changed Git commit hash and branch information
  • Updated CMake version and compiler information (AppleClang to GNU)
  • Changed system from macOS (Apple M1 Pro) to Linux (Intel i7-12700K)
  • Reordered build configuration sections (post_process moved to top)
  • Added OpenMP : OFF configuration line
  • Expanded CPU information with detailed lscpu output
+84/-41 
golden-metadata.txt
Update test metadata with new build environment details   

tests/F60D6594/golden-metadata.txt

  • Updated timestamp from 2025-01-20 to 2026-02-03
  • Changed Git commit hash and branch information
  • Updated CMake version and compiler information (AppleClang to GNU)
  • Changed system from macOS (Apple M1 Pro) to Linux (Intel i7-12700K)
  • Reordered build configuration sections
  • Added OpenMP : OFF configuration line
  • Expanded CPU information with detailed lscpu output
+84/-41 
golden-metadata.txt
Update test metadata with new build environment details   

tests/4F5A5E32/golden-metadata.txt

  • Updated timestamp from 2025-01-20 to 2026-02-03
  • Changed Git commit hash and branch information
  • Updated CMake version and compiler information (AppleClang to GNU)
  • Changed system from macOS (Apple M1 Pro) to Linux (Intel i7-12700K)
  • Reordered build configuration sections
  • Added OpenMP : OFF configuration line
  • Expanded CPU information with detailed lscpu output
+81/-38 
golden-metadata.txt
Update test metadata with new build environment details   

tests/8D8F6424/golden-metadata.txt

  • Updated timestamp from 2025-01-20 to 2026-02-03
  • Changed Git commit hash and branch information
  • Updated CMake version and compiler information (AppleClang to GNU)
  • Changed system from macOS (Apple M1 Pro) to Linux (Intel i7-12700K)
  • Reordered build configuration sections
  • Added OpenMP : OFF configuration line
  • Expanded CPU information with detailed lscpu output
+81/-38 
Miscellaneous
3 files
golden-metadata.txt
Update golden test metadata and build environment               

tests/B0CE19C5/golden-metadata.txt

  • Updated test metadata with new generation timestamp and git commit
    hash
  • Changed build system configuration details (CMake version, compiler
    paths, system information)
  • Updated CPU information from macOS to Linux system specifications
+93/-102
golden-metadata.txt
Update golden test metadata and build environment               

tests/7DCE34B4/golden-metadata.txt

  • Updated test metadata with new generation timestamp and git commit
    hash
  • Changed build system configuration details (CMake version, compiler
    paths, system information)
  • Updated CPU information from macOS to Linux system specifications
+90/-99 
golden-metadata.txt
Update golden test metadata and build environment               

tests/6171E9D4/golden-metadata.txt

  • Updated test metadata with new generation timestamp and git commit
    hash
  • Changed build system configuration details (CMake version, compiler
    paths, system information)
  • Updated CPU information from macOS M1 Pro to Linux x86_64 system
    specifications
+84/-41 
Additional files
63 files
m_compute_levelset.fpp +0/-625 
m_model.fpp +0/-1     
m_global_parameters.fpp +0/-10   
m_icpp_patches.fpp +0/-6     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-3     
golden.txt +0/-1     
golden.txt +0/-1     
golden-metadata.txt +78/-35 
golden.txt +10/-11 
golden.txt +0/-1     
golden.txt +1/-2     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +1/-2     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +10/-11 
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +10/-11 
golden-metadata.txt +81/-38 
golden.txt +14/-15 
golden-metadata.txt +81/-38 
golden.txt +14/-15 
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +18/-19 
golden.txt +12/-13 
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +18/-19 
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +1/-2     
golden.txt +0/-1     
golden.txt +12/-13 
golden-metadata.txt +81/-38 
golden.txt +10/-11 
golden.txt +1/-2     
golden.txt +1/-2     
golden.txt +0/-1     
golden-metadata.txt +81/-38 
golden.txt +14/-15 
golden.txt +1/-2     
golden.txt +1/-2     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +0/-1     
golden.txt +14/-15 
golden.txt +1/-2     

Summary by CodeRabbit

  • New Features

    • STL/model-based immersed-boundary support with on-demand model instantiation
    • New dedicated immersed-boundary data export routine
  • Refactor

    • Simplified IB/level‑set interfaces and internal plumbing
    • Switched to a model-driven IB workflow and removed legacy per-point level‑set outputs
  • Bug Fixes

    • Improved circle geometry precision by adjusting default sampling
  • Tests

    • Updated golden outputs to reflect IB/model changes and removed legacy marker data

CodeAnt-AI Description

Compute immersed-boundary levelsets at runtime and remove levelset file I/O

What Changed

  • Levelset and levelset-normal values are no longer precomputed and stored globally; they are computed on-demand per ghost point during simulation so restart and pre-process files no longer include levelset or levelset_norm data.
  • STL models are loaded and instantiated once during IBM setup and reused for per-ghost-point levelset checks; model interpolation and boundary detection run at setup and levelset queries use those in-memory models.
  • IB marker output and restart behavior separated: IB marker files continue to be written, but levelset/levelset_norm read/write paths and related allocations were removed from pre-process and restart I/O.
  • New per-ghost-point levelset routines support circle, rectangle, airfoil (2D/3D), ellipse, sphere, cuboid, cylinder, and STL geometries; image-point computation and ghost-point handling now use the ghost-point's levelset and normal values.
  • Memory usage reduced for levelset storage (no large global levelset arrays) and IB setup now computes centroid offsets after markers are generated; IB data is written at time 0 during startup.

Impact

✅ Smaller memory footprint for levelset data
✅ No restart dependency on precomputed levelset files
✅ STL models available at runtime for accurate ghost-point geometry checks

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

danieljvickers and others added 30 commits December 12, 2025 13:48
@danieljvickers
Copy link
Member Author

Would I be able to get approval for benchmark on this?

@danieljvickers
Copy link
Member Author

I don't see the benchmarks running and I also don't see it listed as a check to perform anymore. Was there something that modifies when benchmark is run in the toolchain rework?

Copy link
Member

@sbryngelson sbryngelson left a comment

Choose a reason for hiding this comment

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

Triggering benchmark CI

Copy link
Member

@sbryngelson sbryngelson left a comment

Choose a reason for hiding this comment

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

Approve to trigger benchmark CI

Copy link
Member

@sbryngelson sbryngelson left a comment

Choose a reason for hiding this comment

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

Approve to trigger benchmark CI

@sbryngelson sbryngelson self-requested a review February 12, 2026 18:38
sbryngelson
sbryngelson previously approved these changes Feb 12, 2026
@sbryngelson
Copy link
Member

understood, i see the issue and will fix. in the meantime can you look and comment on the remaining more targeted AI reviewer comments?

@danieljvickers
Copy link
Member Author

Yeah, I saw you working on it. Didn't meant to ping you if you saw it immediately.

I will do that until the runners are resolved.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 12, 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:XXL This PR changes 1000+ lines, ignoring generated files and removed size:XXL This PR changes 1000+ lines, ignoring generated files labels Feb 12, 2026
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 12, 2026

CodeAnt AI Incremental review completed.

@sbryngelson
Copy link
Member

i tuned the reviewers a lot. i'm glad to see it actually did something useful. runners should work now modulo their regular fickleness, though i did add something to make them a bit less fickle.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 13, 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:XXL This PR changes 1000+ lines, ignoring generated files and removed size:XXL This PR changes 1000+ lines, ignoring generated files labels Feb 13, 2026
Comment on lines +1048 to +1066
subroutine s_write_serial_ib_data(time_step)

integer, intent(in) :: time_step
character(LEN=path_len + 2*name_len) :: file_path
character(LEN=path_len + 2*name_len) :: t_step_dir

! Creating or overwriting the time-step root directory
write (t_step_dir, '(A,I0,A,I0)') trim(case_dir)//'/p_all'
write (t_step_dir, '(a,i0,a,i0)') trim(case_dir)//'/p_all/p', &
proc_rank, '/', time_step
write (file_path, '(A,I0,A)') trim(t_step_dir)//'/ib.dat'

open (2, FILE=trim(file_path), &
FORM='unformatted', &
STATUS='new')

write (2) ib_markers%sf(0:m, 0:n, 0:p); close (2)

end subroutine
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Remove the redundant and malformed write statement in s_write_serial_ib_data to prevent a runtime formatting error. [possible issue, importance: 7]

Suggested change
subroutine s_write_serial_ib_data(time_step)
integer, intent(in) :: time_step
character(LEN=path_len + 2*name_len) :: file_path
character(LEN=path_len + 2*name_len) :: t_step_dir
! Creating or overwriting the time-step root directory
write (t_step_dir, '(A,I0,A,I0)') trim(case_dir)//'/p_all'
write (t_step_dir, '(a,i0,a,i0)') trim(case_dir)//'/p_all/p', &
proc_rank, '/', time_step
write (file_path, '(A,I0,A)') trim(t_step_dir)//'/ib.dat'
open (2, FILE=trim(file_path), &
FORM='unformatted', &
STATUS='new')
write (2) ib_markers%sf(0:m, 0:n, 0:p); close (2)
end subroutine
subroutine s_write_serial_ib_data(time_step)
integer, intent(in) :: time_step
character(LEN=path_len + 2*name_len) :: file_path
character(LEN=path_len + 2*name_len) :: t_step_dir
! Creating or overwriting the time-step root directory
write (t_step_dir, '(a,i0,a,i0)') trim(case_dir)//'/p_all/p', &
proc_rank, '/', time_step
write (file_path, '(A,I0,A)') trim(t_step_dir)//'/ib.dat'
open (2, FILE=trim(file_path), &
FORM='unformatted', &
STATUS='new')
write (2) ib_markers%sf(0:m, 0:n, 0:p); close (2)
end subroutine


! compute the levelset with the quadratic equation [ -B + sqrt(B^2 - 4AC) ] / 2A
gp%levelset = -0.5_wp*(-quadratic_coeffs(2) + sqrt(quadratic_coeffs(2)**2._wp - 4._wp*quadratic_coeffs(1)*quadratic_coeffs(3)))/quadratic_coeffs(1)

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Guard the quadratic discriminant in the ellipse levelset computation against small negative values due to roundoff to prevent NaNs from the square root. [custom_rule]

Severity Level: Minor ⚠️

Suggested change
gp%levelset = -0.5_wp*(-quadratic_coeffs(2) + sqrt(max(quadratic_coeffs(2)**2._wp - 4._wp*quadratic_coeffs(1)*quadratic_coeffs(3), 0._wp)))/quadratic_coeffs(1)
Why it matters? ⭐

The discriminant can be slightly negative due to roundoff even when mathematically non-negative, causing sqrt() to produce NaN. Wrapping the discriminant with max(..., 0._wp) prevents taking sqrt of a tiny negative value and resolves a numerical stability issue (precision/roundoff) highlighted by the project's review priorities.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/simulation/m_compute_levelset.fpp
**Line:** 434:434
**Comment:**
	*Custom Rule: Guard the quadratic discriminant in the ellipse levelset computation against small negative values due to roundoff to prevent NaNs from the square root.

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.
👍 | 👎

boundary(2) = 0.5_wp*length(3)
dist_sides_vec = (/0, 0, 1/)
dist_surface_vec = (/1, 1, 0/)
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: Add a fallback branch in the cylinder handling to return a safe default when all cylinder lengths are effectively zero, avoiding use of uninitialized direction vectors. [custom_rule]

Severity Level: Minor ⚠️

Suggested change
end if
else
gp%levelset = 0._wp
gp%levelset_norm = 0._wp
return
Why it matters? ⭐

The existing code leaves dist_sides_vec/dist_surface_vec uninitialized when all length components are (near) zero, which can lead to undefined behavior later in the routine. Adding an explicit else branch that sets a safe default levelset and normal and returns prevents use of uninitialized direction vectors and fixes a correctness/memory safety issue. This directly addresses the PR review priorities for correctness and memory (avoid using uninitialized data) from the MFC review rules.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/simulation/m_compute_levelset.fpp
**Line:** 618:618
**Comment:**
	*Custom Rule: Add a fallback branch in the cylinder handling to return a safe default when all cylinder lengths are effectively zero, avoiding use of uninitialized direction vectors.

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.
👍 | 👎

Comment on lines +359 to +361
min_dist = side_dists(1)
idx = 1

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: In the rectangle levelset computation, the distances to the right and top sides have reversed sign compared to the cuboid and sphere definitions, so points both inside and outside on those sides can yield the same (negative) levelset sign, breaking inside/outside classification; redefine those distances so that the sign convention is consistent with other geometries. [logic error]

Severity Level: Critical 🚨
- ⚠️ Inconsistent levelset sign only for rectangular immersed boundaries.
- ⚠️ Downstream code assuming sign uniformity misclassifies rectangle regions.
- ⚠️ Rectangular IB test cases may show asymmetric behavior.
Suggested change
min_dist = side_dists(1)
idx = 1
side_dists(2) = xy_local(1) - top_right(1)
side_dists(3) = bottom_left(2) - xy_local(2)
side_dists(4) = xy_local(2) - top_right(2)
Steps of Reproduction ✅
1. Configure a 2D simulation that uses a rectangular immersed boundary (geometry id 3 in
`patch_ib(:)%geometry`), so that `s_apply_levelset` in
`src/simulation/m_compute_levelset.fpp:28-97` is called with ghost points whose
`gps(i)%ib_patch_id` refers to that rectangle.

2. For a ghost point inside the rectangle but closer to the right side than the left
(e.g., `xy_local(1) > 0`), call `s_rectangle_levelset` directly (lines 315-385) or via
`s_apply_levelset`'s 2D branch (lines 61-77) so execution reaches the block at lines
358-367.

3. At lines 358-361, observe that `side_dists(2) = top_right(1) - xy_local(1)` is positive
for interior points near the right side, while `side_dists(1) = bottom_left(1) -
xy_local(1)` is negative for comparable interior points near the left side; `gp%levelset`
is assigned `side_dists(idx)` at line 369.

4. Compare with `s_cuboid_levelset` in the same file (lines 437-535), where distances are
defined as `Left - x` and `x - Right`, giving `gp%levelset < 0` inside and `> 0` outside
for all faces; in contrast, for rectangles the current formulas yield interior points with
different signs depending on whether they are near left or right/top sides, so sign cannot
consistently encode inside vs outside.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/simulation/m_compute_levelset.fpp
**Line:** 359:361
**Comment:**
	*Logic Error: In the rectangle levelset computation, the distances to the right and top sides have reversed sign compared to the cuboid and sphere definitions, so points both inside and outside on those sides can yield the same (negative) levelset sign, breaking inside/outside classification; redefine those distances so that the sign convention is consistent with other geometries.

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.
👍 | 👎

Comment on lines +395 to +433

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
integer :: ib_patch_id !< patch ID

ib_patch_id = gp%ib_patch_id
i = gp%loc(1)
j = gp%loc(2)

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

xy_local = [x_cc(i) - center(1), y_cc(j) - center(2), 0._wp]
xy_local = matmul(inverse_rotation, xy_local)

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 transformation method
normal_vector = normal_vector/sqrt(dot_product(normal_vector, normal_vector)) ! normalize the vector
gp%levelset_norm = 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
gp%levelset = -0.5_wp*(-quadratic_coeffs(2) + sqrt(quadratic_coeffs(2)**2._wp - 4._wp*quadratic_coeffs(1)*quadratic_coeffs(3)))/quadratic_coeffs(1)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: In the ellipse levelset, the quadratic discriminant is used directly inside a sqrt without guarding against small negative values caused by floating-point roundoff, which can produce NaNs and crash or corrupt the levelset when a point lies close to the ellipse boundary; clamping the discriminant to be non-negative before sqrt avoids this. [possible bug]

Severity Level: Critical 🚨
- ❌ NaN levelset values possible for ellipse ghost points near boundary.
- ⚠️ Can destabilize or crash runs with elliptical boundaries.
- ⚠️ Hard-to-reproduce failures at specific grid/geometry alignments.
Suggested change
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
integer :: ib_patch_id !< patch ID
ib_patch_id = gp%ib_patch_id
i = gp%loc(1)
j = gp%loc(2)
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
xy_local = [x_cc(i) - center(1), y_cc(j) - center(2), 0._wp]
xy_local = matmul(inverse_rotation, xy_local)
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 transformation method
normal_vector = normal_vector/sqrt(dot_product(normal_vector, normal_vector)) ! normalize the vector
gp%levelset_norm = 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
gp%levelset = -0.5_wp*(-quadratic_coeffs(2) + sqrt(quadratic_coeffs(2)**2._wp - 4._wp*quadratic_coeffs(1)*quadratic_coeffs(3)))/quadratic_coeffs(1)
real(wp) :: disc
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
integer :: ib_patch_id !< patch ID
ib_patch_id = gp%ib_patch_id
i = gp%loc(1)
j = gp%loc(2)
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
xy_local = [x_cc(i) - center(1), y_cc(j) - center(2), 0._wp]
xy_local = matmul(inverse_rotation, xy_local)
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 transformation method
normal_vector = normal_vector/sqrt(dot_product(normal_vector, normal_vector)) ! normalize the vector
gp%levelset_norm = 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
disc = quadratic_coeffs(2)**2._wp - 4._wp*quadratic_coeffs(1)*quadratic_coeffs(3)
disc = max(disc, 0._wp)
! compute the levelset with the quadratic equation [ -B + sqrt(B^2 - 4AC) ] / 2A
gp%levelset = -0.5_wp*(-quadratic_coeffs(2) + sqrt(disc))/quadratic_coeffs(1)
Steps of Reproduction ✅
1. Configure a 2D simulation using an elliptical immersed boundary (geometry id 6) so that
`s_apply_levelset` in `src/simulation/m_compute_levelset.fpp:28-97` enters the 2D branch
(lines 61-77) and calls `s_ellipse_levelset` for ghost points on that patch.

2. Select a ghost point whose local coordinates place it numerically very close to the
ellipse boundary (i.e., `xy_local` at lines 419-420 such that the exact mathematical
discriminant `B^2 - 4AC` for the quadratic constructed at lines 428-430 is zero).

3. Due to floating-point roundoff in the computations of `quadratic_coeffs(1:3)` (lines
428-430), the actual value of `quadratic_coeffs(2)**2._wp -
4._wp*quadratic_coeffs(1)*quadratic_coeffs(3)` at line 433 can become a small negative
number (e.g. `-1e-15_wp`) instead of exactly zero.

4. When `sqrt` is evaluated on this negative discriminant at line 433, the Fortran runtime
produces a NaN for the square root, which propagates into `gp%levelset` and then into any
later use of that ghost point's levelset (e.g., interpolation or forcing), potentially
destabilizing the simulation.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/simulation/m_compute_levelset.fpp
**Line:** 395:433
**Comment:**
	*Possible Bug: In the ellipse levelset, the quadratic discriminant is used directly inside a sqrt without guarding against small negative values caused by floating-point roundoff, which can produce NaNs and crash or corrupt the levelset when a point lies close to the ellipse boundary; clamping the discriminant to be non-negative before sqrt avoids this.

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.
👍 | 👎


if (dist_side < abs(dist_surface)) then
! if the closest edge is flat
gp%levelset = -dist_side
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: For the cylinder, when the closest feature is a flat end cap, the levelset is always set to the negative of the axial distance, so points outside the cylinder along its axis incorrectly get negative levelset values instead of positive, which breaks the expected signed-distance behavior and inside/outside logic; the sign should depend on whether the point is between the end caps or beyond them. [logic error]

Severity Level: Critical 🚨
- ⚠️ Mis-signed levelset near cylindrical end caps along axis.
- ⚠️ Outside axial points classified as inside in cylinder cases.
- ⚠️ Can distort immersed-boundary forcing near cylinder ends.
Suggested change
gp%levelset = -dist_side
if (side_pos >= boundary(1) .and. side_pos <= boundary(2)) then
! point lies between the end caps (inside along the axis)
gp%levelset = -dist_side
else
! point lies outside the end caps along the axis
gp%levelset = dist_side
end if
Steps of Reproduction ✅
1. Configure a 3D simulation with a cylindrical immersed boundary (geometry id 10) so that
`s_apply_levelset` in `src/simulation/m_compute_levelset.fpp:28-97` is called with ghost
points belonging to that patch (branch at lines 48-52 calls `s_cylinder_levelset`).

2. Choose a ghost point located along the cylinder axis but just beyond one end cap in
local coordinates (e.g., `xyz_local` such that `side_pos > boundary(2)` at lines 604-617
and radial position within the cylinder radius so `dist_surface` is strongly negative).

3. When `s_cylinder_levelset` executes for this point (lines 571-646), `dist_side` at
lines 624-627 will be small and positive, `abs(dist_surface)` at lines 628-629 will be
larger, so the `if (dist_side < abs(dist_surface))` condition at line 631 is true and the
flat-cap branch is taken.

4. At line 633, `gp%levelset` is set to `-dist_side`, a negative value, even though the
point lies outside the cylinder volume along its axis, whereas other geometry routines in
this module (e.g., `s_sphere_levelset` at lines 537-569 and `s_cuboid_levelset` at lines
437-535) return negative values inside and positive outside; this makes axial exterior
points indistinguishable from interior points based on sign.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/simulation/m_compute_levelset.fpp
**Line:** 633:633
**Comment:**
	*Logic Error: For the cylinder, when the closest feature is a flat end cap, the levelset is always set to the negative of the axial distance, so points outside the cylinder along its axis incorrectly get negative levelset values instead of positive, which breaks the expected signed-distance behavior and inside/outside logic; the sign should depend on whether the point is between the end caps or beyond them.

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.
👍 | 👎

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 13, 2026

CodeAnt AI Incremental review completed.

@sbryngelson
Copy link
Member

the merge conflicts should be trivial

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

Labels

Review effort 4/5 size:XXL This PR changes 1000+ lines, ignoring generated files

Development

Successfully merging this pull request may close these issues.

Remove Levelset and Levelset Norm Arrays

3 participants