← Explainer · Markdown · Source on GitHub

# Cut-Cell Geometry Support for GPU Multigrid

> Architecture update (2026-09-16): Fine metrics are borrowed through the solver geometry contract; coarse geometry owns distinct SDF samples. Pressure halos and immutable boundary descriptors are separated from velocity state/BC uploads. Geometry changes invalidate weights, diagonals and connectivity. See [runtime contracts](/docs/RUNTIME_CONTRACTS) for the supported API; older sketches below retain their historical context.

**Status:** ✅ **Implemented and Tested**  
**Date Created:** November 17, 2025  
**Last Reviewed:** 2026-09-16; original implementation record dated November 17, 2025

**Related:** `GPU_MULTIGRID_DESIGN.md`, `MOVING_GEOMETRY_DESIGN.md`

**Current contract (2026-09-26):** the implemented level plan, coarse geometry rebuilds, transfers (volume-weighted
restriction, injection prolongation; no trilinear interpolation) and tests are in the
[Multigrid README](/src/PolyCfd.Gpu/Multigrid/README). `PcgMgSolverGpu` now lives in `src/PolyCfd.Gpu/Multigrid/`
and the V-cycle in `MultigridCycle`; the file names, the `VCycle()` task and the `--mg` flag below are historical.

## Current implementation notes (2026-09-16)

- Face apertures use corner-based fractions and cell volumes use a fitted plane,
  with a centre-based fallback for sub-cell features; the original centre-sample
  sketches below are historical.
- `SdfGeometryDescriptor.FromStl` supports stationary STL geometry. Rotating STL
  cases use `CachedSdf` / `RotatedCachedSdf` and rebuild coarse metrics from that
  SDF on each update; they do not automatically fall back to Jacobi-PCG.
- `CoarseThickeningMode.ConstantPhysical` keeps the finest physical offset at
  each coarse level and is now the default; `SameCellFraction` remains available.
- `MultigridSymmetryTests` check the weighted V-cycle. `FanHierarchyTests` cover
  six frozen blade angles, including the first two adaptive steps. Its compatible
  test RHS does not establish moving-source compatibility: the evolving fan also
  needs per-region RHS compatibility and the rigid wall-flux source. See
  [MOVING_FAN_VALIDATION.md](https://github.com/hankbeasley/polycfd/blob/18102808d7066ed97d8908d66755a68df8d0516e/docs/MOVING_FAN_VALIDATION.md).
- (2026-09-25, SOL-01) The finest-level PCG projects every connected pressure
  region without a fixed-pressure boundary (`PressureComponentsGpu`, owned by the
  solver) out of the recursive residual after each update; coarse levels project
  nothing. Fine cut-cell geometry commonly has more regions than its coarse levels
  (a 12-cell fan pocket at 128³ is merged at 64³); the fine projection handles them.
  See [pressure nullspaces](/docs/PHYSICS_GUIDE#pcg-and-multigrid).
- Timing gates live in [PERFORMANCE_VALIDATION.md](/docs/PERFORMANCE_VALIDATION).
  The test counts, speedup ratios, and task estimates below are dated records,
  not current suite counts or performance guarantees.

## Original implementation record

**Implementation Progress:**
- ✅ Phase 1: Core abstraction (`IGeometryDescriptor`, `SdfGeometryDescriptor`) complete
- ✅ Phase 2: Coarse geometry construction in MG hierarchy complete
- ✅ Phase 3: Geometry-aware transfer operators (restriction/prolongation) complete
- ✅ Phase 3: Explicit solid cell masking in `PressureOperatorGpu` complete
- ✅ Phase 4: Integration testing complete - all tests passing

**Test Results:**
- 6/6 unit tests passing (`GeometryDescriptorTests`)
- 3/3 integration tests passing (`CutCellMultigridTests`)
  - Sphere obstacle: MG converges successfully
  - Box obstacle: MG 1.5-3x faster than Jacobi-PCG  
  - Multi-level geometry: Maintains geometric fidelity across resolutions

---

## 1. Overview

This document tracks the design and implementation of cut-cell geometry support for the GPU geometric multigrid (GMG) solver. The goal is to enable the `PcgMgSolverGpu` to handle interior obstacles (cut-cell geometries) while maintaining geometric fidelity and multigrid efficiency across coarse levels.

### Current State

✅ **What Works:**
- GPU multigrid solver for uniform grids (no cut-cells)
- Cut-cell geometry on CPU via `CutCellBuilder` (SDF-based)
- GPU operators (`PressureOperatorGpu`, `DivGradGpu`) respect geometry metrics at finest level
- V-cycle with restriction, prolongation, Jacobi smoothing

❌ **What's Missing:**
- Coarse-level geometry construction for cut-cells
- Geometry-aware restriction/prolongation (skip solid cells)
- Architecture to rebuild geometry at multiple resolutions

---

## 2. Design Principles

### 2.1 Geometric Fidelity via SDF Re-evaluation

**Decision:** Use the **original signed distance function (SDF)** to rebuild geometry at each multigrid level, rather than averaging fine-level metrics.

**Rationale:**
- **Correctness:** A sphere obstacle remains a sphere at all resolutions; averaging volumes/apertures distorts geometry
- **Accurate apertures:** Face apertures computed from actual SDF values at coarse face centers, not averaged from fine faces
- **Proper normals:** Interface normals computed from SDF gradients at coarse resolution
- **Standard practice:** Rediscretization is the canonical approach for embedded-boundary multigrid (e.g., Chombo, EB-AMR)

**Alternative rejected:**
- Volume/area/aperture averaging: geometrically incorrect, causes convergence issues

### 2.2 Geometry Abstraction Layer

**Decision:** Introduce `IGeometryDescriptor` interface to decouple geometry definition from grid resolution.

**Purpose:**
- Enable geometry rebuilding at arbitrary resolutions (for MG levels)
- Support multiple geometry sources (SDF primitives, composed shapes, future: STL)
- Cleanly separate "geometry definition" from "discretized geometry" (`CellGeometry`)

**Interface:**
```csharp
public interface IGeometryDescriptor
{
    /// <summary>Build discretized geometry for given grid resolution</summary>
    CellGeometry Build(GridSize size, float dx, float dy, float dz);
    
    /// <summary>Optional: get underlying SDF if available</summary>
    Func<float, float, float, float>? GetSdf();
}
```

**Implementations:**
- `SdfGeometryDescriptor`: Wraps a primitive SDF (sphere, box, plane, etc.)
- Planned, not yet implemented: `CompositeGeometryDescriptor` (combine multiple SDFs via union/intersection/difference)
- Future: `StlGeometryDescriptor` (mesh-based, requires SDF reconstruction or alternate coarsening strategy). STL meshes currently enter through `MeshSdfGpu` / `SdfBuilder` rather than a dedicated descriptor.

### 2.3 Multigrid Scope Limitation

**Decision:** Only support MG+cut-cells for geometries backed by `IGeometryDescriptor` (SDF-based).

**For geometries without descriptor:**
- Fall back to finest-level-only (no coarsening), or
- Disable MG entirely, use Jacobi-PCG

**Rationale:**
- Keeps implementation honest and maintainable
- STL/precomputed geometries require different coarsening strategies (deferred to future work)
- SDF-based geometries cover most validation cases (sphere, box, channel obstacles)

### 2.4 Rediscretization Strategy

**Decision:** Use operator rediscretization at each level (not Galerkin coarsening).

**Approach:**
- Each coarse level: call `CutCellBuilder.Build()` with original SDF
- Coarse operator computed from coarse geometry metrics (matrix-free)
- No need to assemble `R·A·P` or store coarse matrices

**Benefits:**
- Consistent with matrix-free design
- Preserves geometric semantics
- Simpler implementation than Galerkin

### 2.5 Solid Cell Handling in Operators

**Critical detail:** The pressure operator and divergence operator must explicitly handle solid cells.

**CPU Implementation:**
- `DivGrad.Divergence()`: Checks `CellClassification[idx] == CellClass.Solid`, sets `div = 0` and skips computation
- `PressureOperator.Apply()`: Checks `CellClassification[idx] == CellClass.Solid`, sets `result = 0` to zero out solid rows
- `PressureOperator.GetDiagonal()`: Returns `0.0f` for solid cells (masked from system)

**GPU Implementation:**
- `DivGradGpu.DivergenceKernel()`: Currently does NOT check for solid cells (relies on zero volume/aperture)
- `PressureOperatorGpu.LaplacianKernel()`: Currently does NOT check for solid cells

**Consequence:** Solid cells naturally get zero contribution via `Vol=0` and `Alpha=0`, but explicit masking is more robust.

**Design decision:** Add explicit solid cell masking to GPU kernels for consistency and numerical safety.

### 2.6 Cut-Face Boundary Conditions

**Key insight:** Cut-face projection is **separate** from the pressure solve and multigrid.

**Where it happens:**
- In `TimeIntegrator.AdvanceOneStep()`:
  1. **After diffusion/sources** (line 127): `CutFaceProjector.ProjectCutFaces()` enforces interior obstacle BCs on velocity
  2. Velocity field enters pressure projection with BCs already satisfied
  3. Pressure solve does NOT need to know about cut-face BCs (already baked into velocity)

**What it does:**
- **NoSlip mode**: Sets velocity to zero at faces with `alpha < 0.999` or adjacent to solid cells
- **FreeSlip mode**: Removes normal component (u·n = 0), preserves tangential

**Multigrid implication:**
- MG operates only on pressure (scalar field)
- No velocity projection needed in MG cycle
- Solid cells handled via zero volume/area/aperture in operator
- Cut-face BCs are "pre-applied" to velocity before pressure solve

---

## 3. Architecture Changes

### 3.1 New Components  

#### `IGeometryDescriptor` (Core)
**Location:** `src/PolyCfd.Core/Geometry/IGeometryDescriptor.cs`

```csharp
public interface IGeometryDescriptor
{
    CellGeometry Build(GridSize size, float dx, float dy, float dz);
    Func<float, float, float, float>? GetSdf();
}
```

#### `SdfGeometryDescriptor` (Core)
**Location:** `src/PolyCfd.Core/Geometry/SdfGeometryDescriptor.cs`

```csharp
public sealed class SdfGeometryDescriptor : IGeometryDescriptor
{
    private readonly Func<float, float, float, float> _sdf;
    private readonly CutCellBuilder.BuildOptions _options;
    
    public SdfGeometryDescriptor(
        Func<float, float, float, float> sdf,
        CutCellBuilder.BuildOptions? options = null)
    {
        _sdf = sdf;
        _options = options ?? new CutCellBuilder.BuildOptions();
    }
    
    public CellGeometry Build(GridSize size, float dx, float dy, float dz)
    {
        return CutCellBuilder.Build(size, dx, dy, dz, _sdf, _options);
    }
    
    public Func<float, float, float, float>? GetSdf() => _sdf;
}
```

#### `CompositeGeometryDescriptor` (Core) — planned, not yet implemented
**Intended location:** `src/PolyCfd.Core/Geometry/CompositeGeometryDescriptor.cs`

Combines multiple SDFs using CSG operations (union, intersection, difference).

### 3.2 Modified Components

#### `GridBlock` (Core)
**Changes:**
- Add optional `IGeometryDescriptor? GeometryDescriptor { get; }` property
- Constructor accepts descriptor instead of pre-built `CellGeometry`
- Build geometry lazily or on-demand

**Signature:**
```csharp
public GridBlock(
    GridSize size, 
    float dx, float dy, float dz, 
    IGeometryDescriptor? geometryDescriptor = null)
{
    // Build geometry if descriptor provided
    Geometry = geometryDescriptor?.Build(size, dx, dy, dz);
    GeometryDescriptor = geometryDescriptor;
    
    // ... rest of initialization
}
```

**Backward compatibility:** Existing code that creates `GridBlock` with no geometry continues to work (trivial uniform geometry).

#### `GpuMgHierarchy` (GPU)
**Changes:**
- Constructor accepts `IGeometryDescriptor?` from finest block
- `BuildCoarseBlock()` uses descriptor to rebuild geometry at coarse resolution

**Modified logic:**
```csharp
private GridBlock BuildCoarseBlock(
    GridBlock fineBlock, 
    IGeometryDescriptor? geometryDescriptor,
    int coarseNx, int coarseNy, int coarseNz)
{
    var coarseSize = new GridSize(coarseNx, coarseNy, coarseNz);
    float coarseDx = fineBlock.Metrics.Dx * fineBlock.Size.Nx / coarseNx;
    float coarseDy = fineBlock.Metrics.Dy * fineBlock.Size.Ny / coarseNy;
    float coarseDz = fineBlock.Metrics.Dz * fineBlock.Size.Nz / coarseNz;
    
    // Rebuild geometry at coarse resolution if descriptor available
    return new GridBlock(coarseSize, coarseDx, coarseDy, coarseDz, geometryDescriptor);
}
```

#### `GpuMgLevel` (GPU)
**Changes:**
- Store `DeviceBuffer<byte> CellClass` for cell classifications
- Used by restriction/prolongation to skip solid cells

#### `MultigridKernels` (GPU)
**Changes:**
- Add `cellClass` parameter to `Restrict()` and `Prolongate()`
- Skip solid cells in restriction averaging
- Skip solid cells in prolongation injection

#### `DivGradGpu` (GPU) - Solid Cell Masking
**Changes:**
- Add explicit solid cell check in `DivergenceKernel()`
- Set `div = 0` for solid cells
- Requires passing `CellClass` buffer to kernel

#### `PressureOperatorGpu` (GPU) - Solid Cell Masking
**Changes:**
- Add explicit solid cell check in `LaplacianKernel()`
- Set `ApInterior = 0` for solid cells
- Requires passing `CellClass` buffer to kernel

---

## 4. Implementation Plan

### Phase 1: Core Abstraction (Foundation)
**Status:** ✅ **Completed**

**Tasks:**
1. ✅ Design doc created
2. ✅ Create `IGeometryDescriptor` interface
3. ✅ Implement `SdfGeometryDescriptor`
4. ✅ Add `GeometryDescriptor` property to `GridBlock`
5. ✅ Update `GridBlock` constructor to accept descriptor

**Estimated effort:** 2-3 hours

**Validation:**
- Unit test: `SdfGeometryDescriptor` builds geometry identical to direct `CutCellBuilder.Build()`
- Ensure `GridBlock` backward compatibility (no descriptor = trivial geometry)

### Phase 2: Multigrid Coarse Geometry (Core Feature)
**Status:** ✅ **Completed**

**Tasks:**
1. ✅ Modify `GpuMgHierarchy.BuildCoarseBlock()` to use `IGeometryDescriptor`
2. ✅ Thread descriptor through hierarchy constructor
3. ✅ Add `DeviceBuffer<byte> CellClass` to `GpuMgLevel`
4. ✅ Upload cell classifications to device for each level

**Estimated effort:** 3-4 hours

**Validation:**
- Unit test: 16³ → 8³ → 4³ hierarchy with sphere obstacle
  - Verify each level has correct geometry (cell classifications, apertures)
  - Check coarse sphere remains centered and circular (not averaged/distorted)

### Phase 3: Geometry-Aware Transfer Ops (Kernel Updates)
**Status:** ✅ **Completed** (Solid cell masking in operators deferred)

**Tasks:**
1. ✅ Add `cellClass` parameter to `RestrictionKernel`
2. ✅ Skip solid cells in restriction averaging
3. ✅ Add `cellClass` parameter to `ProlongationKernel`
4. ✅ Skip solid cells in prolongation injection
5. ✅ Update `MultigridKernels.Restrict()` and `Prolongate()` signatures
6. ⏳ Add solid cell masking to `DivGradGpu.DivergenceKernel()` (deferred - implicit masking sufficient for now)
7. ⏳ Add solid cell masking to `PressureOperatorGpu.LaplacianKernel()` (deferred - implicit masking sufficient for now)

**Estimated effort:** 3-4 hours (updated for operator masking)

**Notes:** 
- Explicit solid cell masking in `DivGradGpu` and `PressureOperatorGpu` deferred to Phase 4 after initial testing
- Current implicit masking via zero volume/aperture should be sufficient for initial validation
- If numerical issues arise during testing, explicit masking can be added

**Validation:**
- Kernel test: Restrict residual from 8³ to 4³ with central solid block
  - Verify solid cells ignored in averaging
  - Check coarse residual only reflects fluid cells
- Kernel test: Prolongate correction from 4³ to 8³
  - Verify solid cells receive no correction

### Phase 4: Integration & Testing (Full MG Cycle)
**Historical planning status:** originally not started; implementation and tests
subsequently completed (see the current notes and dated summary). The checklist
below preserves the original proposed tasks.

**Tasks:**
1. [ ] Update `PcgMgSolverGpu.VCycle()` to pass cell classifications
2. [ ] Create validation case: channel with planar obstacle
3. [ ] Run GPU MG solver with cut-cell geometry
4. [ ] Compare convergence vs Jacobi-PCG

**Estimated effort:** 4-5 hours

**Validation:**
- Integration test: 16³ channel with thin plane at x=0.5
  - MG should converge (no crash, finite iterations)
  - Solution respects geometry (zero pressure gradient across solid)
- Performance test: 32³ sphere in cavity
  - MG converges faster than Jacobi-PCG
  - Final divergence ≈ same as Jacobi-PCG (correctness check)

### Phase 5: Composite Geometries (Enhancement)
**Status:** Not Started (Optional)

**Tasks:**
1. [ ] Implement `CompositeGeometryDescriptor` with CSG operations
2. [ ] Add SDF union/intersection/difference helpers
3. [ ] Test with complex geometries (multiple spheres, box + sphere, etc.)

**Estimated effort:** 3-4 hours

**Validation:**
- Validation case: two spheres at different locations
- Validation case: hollow box (difference of two boxes)

---

## 5. Design Decisions Log

### Decision 1: SDF Re-evaluation vs Averaging (Nov 17, 2025)
**Question:** Should coarse geometry be built by averaging fine-level metrics or re-evaluating the SDF?

**Chosen:** SDF re-evaluation (rediscretization)

**Rationale:**
- Averaging distorts geometry (sphere becomes blocky aggregate)
- Apertures computed from averaged face SDFs ≠ apertures from actual SDF at coarse faces
- Rediscretization is standard for embedded-boundary MG
- Simpler implementation (reuse `CutCellBuilder.Build()`)

**Alternatives considered:**
- Volume-weighted averaging: rejected due to geometric incorrectness
- Galerkin coarsening (`R·A·P`): rejected due to matrix-free design, complexity

### Decision 2: Geometry Abstraction (Nov 17, 2025)
**Question:** How to enable geometry rebuilding at multiple resolutions?

**Chosen:** `IGeometryDescriptor` interface

**Rationale:**
- Decouples geometry definition from discretization
- Enables multi-resolution support for MG
- Extensible to non-SDF geometries (future STL, meshes)
- Clean separation of concerns

**Alternatives considered:**
- Store `Func<float,float,float,float>` on `GridBlock`: too rigid, couples to SDF
- Store pre-built `CellGeometry` only: prevents rebuilding at coarse resolutions
- Pass SDF through MG hierarchy directly: tight coupling, not extensible

### Decision 3: MG Scope for Non-SDF Geometries (Nov 17, 2025)
**Question:** What to do with STL/precomputed geometries that lack an SDF?

**Chosen:** Disable MG coarsening for geometries without `IGeometryDescriptor`

**Options:**
1. Finest-level-only MG (no coarsening, just smoothing)
2. Fall back to Jacobi-PCG
3. Require explicit coarse geometry stack (user-provided)

**Chosen:** Option 2 (fall back to Jacobi-PCG) for simplicity

**Future work:** SDF reconstruction from STL meshes, or mesh coarsening strategies

---

## 6. Testing Strategy

### Unit Tests
**Location:** `test/PolyCfd.Tests/Unit/`

1. **SdfGeometryDescriptor**:
   - Build geometry at 8³, verify vs direct `CutCellBuilder.Build()`
   - Rebuild same descriptor at 16³, verify resolution independence

2. **Coarse Geometry Correctness**:
   - Sphere at center: 16³ → 8³ → 4³
   - Verify cell classifications match actual SDF
   - Verify apertures at coarse faces ≈ analytical sphere apertures

3. **Restriction with Solid Cells**:
   - 8³ grid with 2³ solid block at center
   - Random residual in fluid cells, zero in solid
   - Restrict to 4³, verify only fluid cells contribute

4. **Prolongation with Solid Cells**:
   - 4³ coarse correction (non-zero in fluid)
   - Prolongate to 8³, verify solid cells unchanged

### Integration Tests
**Location:** `test/PolyCfd.Gpu.Tests/Multigrid/`

1. **MG Convergence with Cut-Cells**:
   - 16³ grid with sphere (radius 0.15)
   - Random RHS, zero initial guess
   - PCG-MG should converge to same tolerance as Jacobi-PCG
   - Check iteration count (MG should be fewer)

2. **Channel with Obstacle**:
   - 24×16×16 channel with vertical plane at x=0.5
   - Pressure driven flow setup
   - Verify pressure field is smooth, no penetration

### Validation Tests
**Location:** `validation/PolyCfd.Validation/`

1. **Cavity with Sphere**:
   - 32³ lid-driven cavity with sphere obstacle at center
   - GPU backend with `--mg` flag
   - Compare vs CPU solver, check divergence and velocity profiles

2. **Taylor-Green with Cut-Cells** (future):
   - Add obstacles to TG vortex test
   - Verify vortex evolution with embedded boundaries

---

## 7. Performance Expectations

### Baseline (Uniform Grid, no cut-cells)
- 16³: MG converges in ~7 iterations vs Jacobi ~39 iterations (5.5× speedup)
- 32³: MG converges in ~8 iterations vs Jacobi ~60+ iterations (7.5× speedup)

### Expected with Cut-Cells
- **Iteration count:** Similar to uniform grid (7-10 iterations for MG)
- **Per-iteration cost:** Slightly higher due to:
  - Coarse geometry build (one-time, negligible amortized)
  - Cell classification checks in restriction/prolongation (minimal branching)
- **Overall speedup vs Jacobi-PCG:** 4-6× (slightly lower than uniform due to geometry complexity)

### Degradation Scenarios
- Very fine cut-cells (many slivers): coarsening may create more cut cells at coarse levels
- High solid fraction (>50%): fewer fluid cells at coarse levels, potential for premature coarsening termination

**Mitigation:**
- Sliver agglomeration at finest level reduces pathological coarse cells
- Stop coarsening when fluid cell count drops below threshold (e.g., 64 cells)

---

## 8. Known Limitations

### 8.1 STL/Mesh Geometries
STL meshes are supported through SDF reconstruction: `SdfGeometryDescriptor.FromStl`
for static geometry and GPU cached SDFs for rotation. A dedicated
`StlGeometryDescriptor` class is unnecessary for those paths. This does not provide
arbitrary non-SDF mesh coarsening.

### 8.2 Moving Geometries
Rotating geometry is implemented. The integrator updates projection and multigrid
metrics using the end-of-step rotated SDF; coarse geometry and diagonals are
rebuilt for that shape. Connectivity may change, so pressure-component
compatibility is refreshed as part of moving projection. Rebuild cost is included
in solver-step performance measurements. Moving viscous-wall coefficients remain
separate work.

### 8.3 Anisotropic Coarsening
**Issue:** 2× coarsening in all directions may be suboptimal for thin channels with obstacles.

**Current behavior:** Semi-coarsening not implemented.

**Future solution:** Directional coarsening based on geometry aspect ratio.

---

## 9. Critical Implementation Details

### 9.1 Solid Cell Masking in Operators

**CPU Behavior (reference implementation):**

```csharp
// DivGrad.Divergence()
if (geom != null && geom.CellClassification[cellIdx] == CellClass.Solid)
{
    div[cellIdx] = 0.0f;
    continue;  // Skip computation entirely
}

// PressureOperator.Apply()
if (_block.Geometry != null && _block.Geometry.CellClassification[idx] == Geometry.CellClass.Solid)
{
    v = 0.0f; // solid rows inert
}
yInterior[idx] = v;
```

**GPU Implementation Required:**

```csharp
// In DivGradGpu.DivergenceKernel
if (cellClass != null && cellClass[cellIdx] == 1) // 1 = CellClass.Solid
{
    divInterior[cellIdx] = 0.0f;
    return;
}

// In PressureOperatorGpu.LaplacianKernel
float divergence = /* ... compute ... */;
if (cellClass != null && cellClass[cellIdx] == 1)
{
    ApInterior[cellIdx] = 0.0f;
}
else
{
    ApInterior[cellIdx] = -divergence;
}
```

**Rationale:**
- Explicit masking is more robust than relying on zero volume/aperture
- Prevents numerical issues from near-zero denominators
- Matches CPU reference implementation behavior
- Required for multigrid correctness (solid cells must not participate in residual)

### 9.2 Cut-Face Projection is Not Part of MG

**Common misconception:** Cut-face projection happens during pressure solve.

**Actual flow:**
1. Time integrator advances velocity via advection/diffusion
2. **Cut-face projection applied to velocity** (sets u=0 at interior obstacles)
3. Velocity enters pressure projection step
4. Pressure solve (with MG) operates on scalar pressure field
5. Gradient of pressure corrects velocity
6. Velocity BCs re-applied (domain boundaries only)

**Implication for MG:**
- MG only needs to handle pressure field (no velocity)
- Solid cells are passive (zero contribution via operator masking)
- No special "cut-face BC" in MG cycle itself
- Cut-face effects enter via divergence of velocity (already projected)

### 9.3 Volume/Aperture Zero Handling

**Current approach (implicit):**
- Solid cells have `Vol = 0`, `Alpha = 0`, `Area = 0`
- Operators naturally get zero contribution from solid cells
- Works but can create numerical instabilities (0/0 divisions)

**Enhanced approach (explicit + implicit):**
- Keep zero geometry metrics (implicit masking)
- **Add explicit classification checks** (robust masking)
- Best of both worlds: geometrically correct + numerically safe

### 9.4 Coarse Geometry Consistency

**Critical requirement:** Coarse geometry must come from **same SDF** as fine geometry.

**Example - 16³ → 8³ coarsening with sphere (radius 0.15):**

**Correct:**
```csharp
// Fine level
var fineGeom = CutCellBuilder.Build(fineSize, dx, dy, dz, sphereSDF, options);

// Coarse level
var coarseGeom = CutCellBuilder.Build(coarseSize, 2*dx, 2*dy, 2*dz, sphereSDF, options);
// ^ Same SDF function, different grid spacing
```

**Incorrect (don't do this):**
```csharp
// Averaging fine geometry - WRONG!
var coarseGeom = AverageFineCellVolumes(fineGeom, ...);  // Distorts sphere
```

**Why correct approach works:**
- Sphere equation unchanged: `|r| - 0.15`
- Coarse grid samples sphere at different locations
- Apertures computed from actual SDF at coarse face centers
- Normals computed from actual SDF gradients at coarse cells
- Geometry remains faithful to analytical obstacle

---

## 10. Open Questions

### Q1: Coarsening Strategy for Complex Geometries
**Context:** Multiple disconnected obstacles, thin features.

**Options:**
1. Always coarsen 2× in all directions (current)
2. Adaptive coarsening: stop if geometry becomes degenerate
3. Selective coarsening: coarsen only in directions without thin features

**Decision needed:** After testing with validation cases.

### Q2: Nullspace Handling on Coarse Levels
**Context:** Finest level projects out mean for periodic BCs. What about coarse levels?

**Resolved (2026-09-25):** only the finest-level PCG residual is projected, per connected
region and after every update; coarse levels project nothing. See the current
implementation notes above.

### Q3: Should GPU Operators Always Check CellClass?
**Context:** CPU operators always check solid cells. GPU could rely on zero metrics alone.

**Options:**
1. Always check `CellClass` buffer (safe, explicit)
2. Only check when geometry present (optimization)
3. Never check, rely on zero metrics (current, risky)

**Recommendation:** Option 2 - check when geometry present, skip check for uniform grids.

### Q4: CellClass Buffer Size at Each Level
**Context:** Each MG level needs cell classifications on device.

**Options:**
1. Upload full array (Nx×Ny×Nz bytes per level)
2. Use bitpacked representation (Nx×Ny×Nz bits / 8)
3. Sparse indexing (only store cut/solid cell indices)

**Recommendation:** Option 1 for simplicity (memory cost is minimal: 64³ = 256KB).

---

## 11. Next Steps (Priority Order)

1. ✅ **Complete design doc** (this document)
2. ✅ **Review CPU implementation** (cut-face projection, solid cell masking)
3. ✅ **Implement Phase 1** (Core Abstraction):
   - ✅ `IGeometryDescriptor` interface
   - ✅ `SdfGeometryDescriptor` implementation
   - ✅ Update `GridBlock` constructor
4. ✅ **Implement Phase 2** (Coarse Geometry):
   - ✅ Modify `GpuMgHierarchy.BuildCoarseBlock()`
   - ✅ Add cell classification device buffers
5. ✅ **Implement Phase 3** (Kernel Updates):
   - ✅ Geometry-aware restriction/prolongation
   - ⏳ Operator solid cell masking (deferred to Phase 4)
6. [ ] **Implement Phase 4** (Integration & Testing):
   - [ ] Modify `ChannelWithObstacle.cs` to use `SdfGeometryDescriptor` when creating GridBlock
   - [ ] Run validation case with GPU + MG + cut-cell geometry
   - [ ] Compare convergence vs Jacobi-PCG
   - [ ] If numerical issues occur, add explicit solid cell masking to operators
7. [ ] **Update Documentation**:
   - [ ] Add usage examples to `API.md`
   - [ ] Update `GEOMETRY_PROGRESS.md` with MG status

**Implementation Notes (November 17, 2025):**
- Phases 1-3 completed successfully
- All code compiles without errors
- Solid cell masking in `DivGradGpu` and `PressureOperatorGpu` deferred to Phase 4
  - Current implicit masking via zero volume/aperture should be sufficient
  - If numerical stability issues arise during testing, explicit masking can be added
- Next step: Create test case that uses `SdfGeometryDescriptor` to enable MG with cut-cells

---

## 11. References

- **Embedded Boundary Methods:**
  - Johansen & Colella (1998), "A Cartesian Grid Embedded Boundary Method"
  - Schwartz et al. (2006), "A Cartesian Grid Multigrid Method for the Poisson Equation with Variable Coefficients"
  - Chombo library documentation (LBL)

- **Related PolyCFD Docs:**
  - `GPU_MULTIGRID_DESIGN.md` - GPU MG implementation for uniform grids
  - `GEOMETRY_PROGRESS.md` - Cut-cell geometry implementation status
  - `design.md` - Overall architecture and solver design

---

**Document Version:** 2.0  
**Last Updated:** November 17, 2025  
**Status:** ✅ **Implementation Complete - All Tests Passing**

## Implementation Summary

### Completed Work (November 17, 2025)

**Phase 1-3 Implementation:**
- ✅ Core geometry descriptor abstraction (`IGeometryDescriptor`, `SdfGeometryDescriptor`)
- ✅ Multi-resolution geometry support in `GridBlock` and `GpuMgHierarchy`
- ✅ Geometry-aware multigrid transfer operators (restriction/prolongation)
- ✅ Explicit solid cell masking in `PressureOperatorGpu.LaplacianKernel()`
- ✅ Cell classification upload to GPU for all MG levels

**Phase 4 Testing:**
- ✅ 6 unit tests created and passing (`GeometryDescriptorTests`)
  - Geometry descriptor correctness
  - Multi-resolution rebuilding
  - GridBlock integration
- ✅ 3 integration tests created and passing (`CutCellMultigridTests`)
  - Sphere obstacle convergence
  - Box obstacle: 1.5-3x MG speedup vs Jacobi
  - Multi-level geometry preservation (32³→16³→8³→4³)

**Key Findings:**
1. **Explicit operator masking required**: Initial tests failed with NaN due to implicit masking. Adding explicit `cellClass` check in `PressureOperatorGpu.LaplacianKernel()` resolved the issue.
2. **Transfer operators work correctly**: Restriction and prolongation properly skip solid cells.
3. **MG efficiency maintained**: Even with cut-cells, MG provides 1.5-3x iteration reduction vs Jacobi-PCG.
4. **Geometric fidelity preserved**: SDF re-evaluation at coarse levels maintains obstacle shape correctly.

**Files Created/Modified:**
- `src/PolyCfd.Core/Geometry/IGeometryDescriptor.cs` (new)
- `src/PolyCfd.Core/Geometry/SdfGeometryDescriptor.cs` (new)
- `src/PolyCfd.Core/Core/GridBlock.cs` (modified)
- `src/PolyCfd.Gpu/Multigrid/GpuMgHierarchy.cs` (modified)
- `src/PolyCfd.Gpu/Multigrid/GpuMgLevel.cs` (modified)
- `src/PolyCfd.Gpu/Multigrid/MultigridKernels.cs` (modified)
- `src/PolyCfd.Gpu/PressureOperatorGpu.cs` (modified)
- `src/PolyCfd.Gpu/PcgMgSolverGpu.cs` (modified)
- `test/PolyCfd.Tests/Unit/GeometryDescriptorTests.cs` (new)
- `test/PolyCfd.Gpu.Tests/Multigrid/CutCellMultigridTests.cs` (new)

**Next Steps for Production Use:**
1. Update validation cases (e.g., `ChannelWithObstacle.cs`) to use `SdfGeometryDescriptor` for MG support
2. Add performance benchmarks comparing CPU vs GPU MG with cut-cells
3. Test with more complex geometries (multiple obstacles, thin features)
4. Consider adding explicit masking to `DivGradGpu` if needed for time integration stability