Skip to content

Add hemisphere shell particle cloud packing - #1667

Draft
BCKim55 wants to merge 3 commits into
MFlowCode:masterfrom
BCKim55:feature/particle-cloud-hemi-shell
Draft

Add hemisphere shell particle cloud packing#1667
BCKim55 wants to merge 3 commits into
MFlowCode:masterfrom
BCKim55:feature/particle-cloud-hemi-shell

Conversation

@BCKim55

@BCKim55 BCKim55 commented Jul 22, 2026

Copy link
Copy Markdown

Description

Adds a hemisphere-shell particle cloud packing option for immersed-boundary particle clouds.

This introduces particle_cloud(i)%packing_method = 3, which randomly places spherical/circular IBM particles inside a hemisphere-shell region while enforcing:

  • inner and outer shell-radius clearance,
  • hemisphere plane clearance,
  • bounding-box clearance,
  • particle-particle non-overlap using the existing spatial hash approach.

This also adds shell_inner_radius and shell_outer_radius particle cloud parameters. The local verification example was removed from this PR to avoid introducing a new golden test in the same change.

Type of change

  • New feature

Testing

  • ./mfc.sh format
  • ./mfc.sh validate examples/*/case.py
  • ./mfc.sh validate examples/3D_mibm_particle_cloud_hemi_shell/case.py
  • ./mfc.sh run examples/3D_mibm_particle_cloud_hemi_shell/case.py --clean --no-debug

Additional local checks:

  • Verified generated ib_state_0.dat particle positions.
  • Confirmed no shell-boundary violations.
  • Confirmed no particle overlap violations.
  • Confirmed VF 0.2 succeeds with min_spacing=0.02.
  • Confirmed VF 0.3 succeeds when min_spacing=0.0.

Checklist

  • I added or updated tests for new behavior
  • I updated documentation if user-facing behavior changed
GPU changes (expand if you modified src/simulation/)
  • GPU results match CPU results
  • Tested on NVIDIA GPU or AMD GPU

@BCKim55
BCKim55 requested a review from sbryngelson as a code owner July 22, 2026 06:52
@sbryngelson
sbryngelson marked this pull request as draft July 22, 2026 13:04
@wilfonba

wilfonba commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

I don't think this is implemented quite right. The example you added highlights this fact. It defines a hemispherical particle cloud using a cubic domain. I think instead you should add a particle_cloud(i)%geometry variable, and then treat the particle cloud more like the fluid patches are treated. Also, a hemispherical shell is not a packing method in the same way that rejection sampling and lattice placement are packing methods, so using the existing acking_method variable for this isn't the right thing to do. You can place particles in a hemispherical shell using either rejection sampling, lattice placement, or some other packing method that's not implemented yet.

Edit: Your s_particle_cloud_random_hemi_shell routine appears to do rejection sampling, which adds to the confusion of what packing_method means.

@BCKim55

BCKim55 commented Jul 23, 2026

Copy link
Copy Markdown
Author

Thanks Ben, that makes sense. I agree that the hemisphere shell should be treated as a particle-cloud geometry rather than a packing method.

I’ll refactor this so that particle_cloud(i)%packing_method remains responsible for the placement algorithm, e.g. rejection sampling or lattice, and add a separate particle_cloud(i)%geometry option for the cloud shape. Then the current hemisphere-shell implementation will become the rejection-sampling path for geometry = hemisphere shell, while unsupported combinations such as hemisphere-shell lattice packing can error out explicitly for now.

I’ll also remove or adjust the example so it does not imply that a cubic domain defines the hemispherical cloud.

@BCKim55
BCKim55 force-pushed the feature/particle-cloud-hemi-shell branch from 81e9038 to 933f66c Compare July 23, 2026 23:21
Comment thread src/simulation/m_particle_cloud.fpp
Comment thread src/simulation/m_particle_cloud.fpp Outdated
do while (n_placed < particle_cloud(cloud_idx)%num_particles .and. n_attempts < max_attempts)
n_attempts = n_attempts + 1

if (p == 0) then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

num_dims < 3 peferred in general. This likely does not affect things, but num_dims is a case-optimization parameter, and therefore the compiler can optimize this away if you make it a check on num_dims.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated this to use num_dims < 3 instead of checking p == 0.

xdir = rho*cos(phi)
ydir = rho*sin(phi)
u = f_xorshift(seed)
r_shell = ((r_outer**3._wp - r_inner**3._wp)*u + r_inner**3._wp)**(1._wp/3._wp)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems like a lot fo compute for your QOI. You do not use the [xyz]dir parameter at all after computing it. Again, either use it or do not computed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That said, you using a probability distribution function to weight your random seed is correct here. Just you are computing redundant quantities here that seem pointless.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

  1. Thanks for catching that! I completely missed that [xyz]dir wasn't being used later on. I've removed the redundant calculation to save compute.

  2. Thanks for confirming the PDF logic. I've cleaned up all the redundant quantities you mentioned to optimize the compute.

@BCKim55
BCKim55 force-pushed the feature/particle-cloud-hemi-shell branch from 933f66c to 4aff083 Compare July 25, 2026 15:01

@BCKim55 BCKim55 left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the feedback. I updated the hemisphere-shell path to remove the redundant box-bounds logic and rely on the shell radii for the placement region. I also changed the dimensionality checks to use num_dims < 3 and removed the extra direction variables in the 3D sampling path.

Comment thread src/simulation/m_particle_cloud.fpp Outdated
do while (n_placed < particle_cloud(cloud_idx)%num_particles .and. n_attempts < max_attempts)
n_attempts = n_attempts + 1

if (p == 0) then

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated this to use num_dims < 3 instead of checking p == 0.

xdir = rho*cos(phi)
ydir = rho*sin(phi)
u = f_xorshift(seed)
r_shell = ((r_outer**3._wp - r_inner**3._wp)*u + r_inner**3._wp)**(1._wp/3._wp)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

  1. Thanks for catching that! I completely missed that [xyz]dir wasn't being used later on. I've removed the redundant calculation to save compute.

  2. Thanks for confirming the PDF logic. I've cleaned up all the redundant quantities you mentioned to optimize the compute.

@sbryngelson sbryngelson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Request changes

  1. Missing regression coverage. This PR adds the hemisphere-shell execution path but removes the local example and adds no test fixture. The existing particle-cloud case is box-only, so CI never executes geometry = 2. Please add a small deterministic 2D or 3D case that checks generated particle positions for shell/plane clearance and non-overlap.

  2. Documentation was not updated. docs/documentation/case.md does not document geometry, shell_inner_radius, or shell_outer_radius, and its length_[x,y,z] description is inaccurate for geometry = 2, where those extents are ignored. Please update the Particle Clouds section accordingly.

Comment thread toolchain/mfc/case_validator.py Outdated
Comment thread src/simulation/m_particle_cloud.fpp Outdated
@BCKim55
BCKim55 marked this pull request as ready for review July 28, 2026 05:11
@BCKim55

BCKim55 commented Jul 28, 2026

Copy link
Copy Markdown
Author

Hi Spencer, I addressed the requested validation, documentation, and regression coverage changes. CI is now passing, and the PR is ready for another look when you have time.

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 58.76289% with 40 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.04%. Comparing base (83bf84c) to head (2cbc7b9).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
src/simulation/m_particle_cloud.fpp 59.55% 25 Missing and 11 partials ⚠️
src/simulation/m_checker.fpp 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1667      +/-   ##
==========================================
- Coverage   61.04%   61.04%   -0.01%     
==========================================
  Files          83       83              
  Lines       20978    21054      +76     
  Branches     3099     3109      +10     
==========================================
+ Hits        12807    12853      +46     
- Misses       6126     6150      +24     
- Partials     2045     2051       +6     

☔ View full report in Codecov by Harness.
📢 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.

@sbryngelson sbryngelson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the revisions. Documentation and validation are genuinely addressed:

  • case.md now documents geometry, shell_inner_radius, shell_outer_radius, and qualifies length_x[y,z] as box-only.
  • The Python validator and m_checker.fpp now enforce the same condition, and shell_outer_radius > shell_inner_radius + 2*radius is exactly the sampler's feasibility requirement (r_outer > r_inner after the +/- radius insets), so the s_mpi_abort path is no longer reachable from valid input.
  • geometry and both new reals are broadcast in m_mpi_proxy.fpp.
  • The sampling math is right: the 2D sqrt CDF and the 3D cube-root CDF with uniform cos(polar) both give uniform density in the shell.

The test coverage point is not yet addressed, though, and that is the blocking item. Details inline.

}


def test_hemi_shell_random_packing_respects_clearance_and_overlap():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This test does not exercise the code the PR adds. _pack_hemi_shell_3d is a Python reimplementation of s_particle_cloud_random_hemi_shell, and the assertions below check that the Python implementation satisfies the shell and overlap constraints. Nothing here reads from m_particle_cloud.fpp, so no regression in the Fortran sampler can make this test fail.

What I asked for was verification of the generated particle positions. Please parse ib_state_0.dat from an actual run and assert the shell clearance, plane clearance, and pairwise non-overlap on those coordinates. A mirrored implementation cannot serve that purpose no matter how faithful it is.

return False


def _pack_hemi_shell_3d(count, radius, min_spacing, inner_radius, outer_radius, seed_value):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Separately from the point above: this model is not actually equivalent to the Fortran. _is_overlapping keys cells on exact bin tuples, whereas f_cloud_particle_overlaps folds bins through f_bin_hash into hash_size buckets. Collisions cause the Fortran to reject candidates this model accepts, so the two will diverge on particle placement for the same seed.

So even taken as documentation of the algorithm, it is misleading. Another reason to test the real output instead.

cases.append(
define_case_d(
stack,
"IBM -> Particle Cloud -> Hemisphere Shell",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two coverage gaps here.

1. 3D is not covered. This block sits inside if len(dimInfo[0]) == 2 and not viscous:, so CI only ever runs the 2D branch — which is a half-annulus, not a hemisphere. The 3D path (phi/zdir/rho with the cube-root radial CDF) is the one this feature is named for and it never executes. Please add a 3D golden.

2. The box path this PR rewrote has no coverage at all. grep particle_cloud toolchain/mfc/test/cases.py returns only this new case — it is the only particle-cloud golden in the suite. Meanwhile s_particle_cloud_random_box was rewritten here (p == 0 -> num_dims < 3, the inlined overlap loop replaced by f_cloud_particle_overlaps, bin coordinates now computed after acceptance rather than before). That refactor is unverified. Please add a box golden as well, or demonstrate bit-identical output before and after the change on an existing example.

Note also that the goldens hold cons.* fields rather than ib_state. Placement perturbs the flow, so this is a real regression lock, but it does not directly assert the geometric constraints.

end if

if (num_dims < 3) then
geom = 2

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

geometry is overloaded in a way that will bite users. Here particle_cloud(i)%geometry == 2 means hemisphere shell, while two lines down geom = 2 means circle in the patch_ib vocabulary (and geom = 8 means sphere). The same subroutine uses two different meanings of geometry = 2 within six lines.

Please rename the cloud field to something like region_shape or cloud_geometry, or reuse the existing patch geometry codes. As written, a user who reads particle_cloud(1)%geometry = 2 will reasonably expect a circle.

end if

if (num_dims < 3) then
if (ry < particle_cloud(cloud_idx)%y_centroid + particle_cloud(cloud_idx)%radius) cycle

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two things about the region definition.

Orientation is hard-coded and unconfigurable. The flat face is at y_centroid in 2D and z_centroid in 3D, always opening toward +y / +z. That is a reasonable initial restriction, but it needs to be stated in case.md — right now nothing tells a user which way the hemisphere faces.

Nothing bounds the cloud to the domain. Dropping the [xyz]min/max logic was the right call per @danieljvickers, but nothing replaced it: x_centroid +/- shell_outer_radius can extend past x_domain, and s_add_cloud_particle performs no bounds check, so particles land off-grid silently. The validator already has x[y,z]_centroid, shell_outer_radius, and the domain extents, so this is a cheap check to add there.

j = hash_head(slot)
do while (j > 0)
if (num_dims < 3) then
dist_sq = (px - placed(1, j))**2._wp + (py - placed(2, j))**2._wp

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Minor: the extraction turned integer exponents into real ones. The original was (rx - placed(1, j))**2; this is now **2._wp, and min_dist_sq = min_dist**2._wp above is the same. gfortran folds x**2.0 to x*x (I checked at both -O0 and -O2), but that is a compiler courtesy rather than a guarantee, and this is the innermost loop of the rejection sampler. Please use **2.

Also minor, in the same area: s_get_cloud_bin runs twice per accepted particle — once inside f_cloud_particle_overlaps and again at the insertion site on line 227 — and it tests num_dims == 3 while everything else in the module tests num_dims < 3.


- `geometry` selects the cloud region:
- `1` (box) uses `x[y,z]_centroid` and `length_x[y,z]` to define the region.
- `2` (hemisphere shell) uses `x[y,z]_centroid`, `shell_inner_radius`, and `shell_outer_radius` to define the region. Particle centres are sampled between `shell_inner_radius + radius` and `shell_outer_radius - radius`, and the flat hemisphere plane is kept clear by one particle radius.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a clear improvement, but two facts a user needs are still missing:

  • Which way the hemisphere opens. The flat face lies at y_centroid in 2D and z_centroid in 3D, and the filled half is +y / +z. This is not configurable and is not stated anywhere.
  • That the 2D region is a half-annulus, not a hemisphere. Calling both cases "hemisphere shell" is going to confuse people setting up 2D cases.

@sbryngelson
sbryngelson marked this pull request as draft July 29, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants