Skip to content

Commit 08da506

Browse files
CopilotSierd
andcommitted
Apply non-erodible layer constraint only at domain boundaries
Per feedback, the problem is specifically at downwind boundaries, not throughout the domain. Modified implementation to apply constraint only at domain edges: - Added boundary detection logic in all quadrant solvers - Constrain pickup only for cells at n=0, n=max, s=0, or s=max - Added post-processing for boundary cells with copied pickup values - Updated tests to verify boundary-only constraint behavior - Updated documentation to reflect targeted boundary approach This prevents the issue at downwind boundaries without affecting interior cells. Co-authored-by: Sierd <14054272+Sierd@users.noreply.github.com>
1 parent 223402b commit 08da506

File tree

3 files changed

+77
-25
lines changed

3 files changed

+77
-25
lines changed

SOLUTION_SUMMARY.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ At the downwind boundary:
3030

3131
## Solution Implementation
3232

33-
A dual-layer defensive approach was implemented:
33+
A targeted boundary-focused approach was implemented:
3434

35-
### Layer 1: Prevention in Sweep Solver
35+
### Layer 1: Prevention in Sweep Solver (Boundary-Only)
3636

3737
Modified `aeolis/utils.py`:
3838

@@ -43,13 +43,16 @@ Modified `aeolis/utils.py`:
4343
- `porosity`: Bed porosity
4444

4545
2. Updated all quadrant solvers and generic stencil to:
46-
- Calculate total pickup for each cell
46+
- **Apply constraint ONLY at domain boundaries** (edges of grid)
47+
- Calculate total pickup for each boundary cell
4748
- Convert pickup to bed level change: `dz = total_pickup / (rhog * (1 - porosity))`
4849
- Check if `zb - dz < zne`
4950
- If true, limit pickup to: `max_pickup = (zb - zne) * rhog * (1 - porosity)`
5051
- Scale down pickup proportionally for all fractions
5152

52-
This prevents the solver from calculating excessive pickup in the first place.
53+
3. Added post-processing constraint for boundary cells that receive copied pickup values
54+
55+
This prevents excessive pickup at domain boundaries where the problem occurs.
5356

5457
### Layer 2: Safety Net in Bed Update
5558

aeolis/tests/test_nonerodible.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ class TestNonErodibleLayerConstraint:
1616
non-erodible layer constraint to prevent erosion below zne.
1717
"""
1818

19-
def test_sweep_respects_nonerodible_layer(self):
19+
def test_sweep_respects_nonerodible_layer_at_boundaries(self):
2020
"""
21-
Test that the sweep function limits pickup to prevent bed from
22-
dropping below the non-erodible layer.
21+
Test that the sweep function limits pickup at domain boundaries to prevent bed from
22+
dropping below the non-erodible layer. The constraint is only applied at boundaries.
2323
"""
2424
# Setup a simple 5x5 grid with 1 fraction
2525
ny, nx, nf = 4, 4, 1
@@ -66,15 +66,20 @@ def test_sweep_respects_nonerodible_layer(self):
6666
dz = total_pickup / mass_per_meter
6767
new_zb = zb - dz
6868

69-
# Check that bed level doesn't drop below non-erodible layer
70-
# Allow small numerical tolerance
71-
assert np.all(new_zb >= zne - 1e-6), \
72-
f"Bed level dropped below non-erodible layer. Min new_zb: {new_zb.min()}, zne: {zne.min()}"
69+
# Check that bed level at BOUNDARIES doesn't drop below non-erodible layer
70+
# Define boundary cells (edges of domain)
71+
boundary_mask = np.zeros_like(zb, dtype=bool)
72+
boundary_mask[0, :] = True # Top edge
73+
boundary_mask[-1, :] = True # Bottom edge
74+
boundary_mask[:, 0] = True # Left edge
75+
boundary_mask[:, -1] = True # Right edge
7376

74-
# Check that pickup was limited (should be less than available mass)
75-
max_allowed_pickup = mass_per_meter * (zb - zne).max()
76-
assert np.all(total_pickup <= max_allowed_pickup + 1e-6), \
77-
f"Pickup exceeded maximum allowed. Max pickup: {total_pickup.max()}, max allowed: {max_allowed_pickup}"
77+
# Check only boundary cells
78+
assert np.all(new_zb[boundary_mask] >= zne[boundary_mask] - 1e-6), \
79+
f"Bed level at boundaries dropped below non-erodible layer. Min new_zb: {new_zb[boundary_mask].min()}, zne: {zne[boundary_mask].min()}"
80+
81+
# Interior cells may drop below zne (constraint not applied there)
82+
# This is expected behavior as the problem is specifically at boundaries
7883

7984
def test_sweep_without_nonerodible_layer_backward_compatible(self):
8085
"""

aeolis/utils.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,30 @@ def sweep(Ct, Cu, mass, dt, Ts, ds, dn, us, un, w, zb=None, zne=None, rhog=2650.
582582
# print(np.shape(visited[0,:]==False))
583583
pickup[0,:,0] = pickup[1,:,0].copy()
584584
pickup[-1,:,0] = pickup[-2,:,0].copy()
585+
586+
# Apply non-erodible layer constraint to boundary cells that were set by copying
587+
if zb is not None and zne is not None:
588+
for boundary_row in [0, Ct.shape[0] - 1]:
589+
for s in range(Ct.shape[1]):
590+
# Calculate total pickup for this cell
591+
total_pickup = 0.0
592+
for f in range(nf):
593+
total_pickup += pickup[boundary_row, s, f]
594+
595+
if total_pickup > 0:
596+
# Calculate bed level change
597+
dz = total_pickup / (rhog * (1.0 - porosity))
598+
599+
# Check if bed would drop below non-erodible layer
600+
if zb[boundary_row, s] - dz < zne[boundary_row, s]:
601+
# Limit pickup
602+
max_dz = max(0.0, zb[boundary_row, s] - zne[boundary_row, s])
603+
max_pickup_total = max_dz * rhog * (1.0 - porosity)
604+
605+
if total_pickup > 0.0:
606+
scale_factor = max_pickup_total / total_pickup
607+
for f in range(nf):
608+
pickup[boundary_row, s, f] *= scale_factor
585609

586610
k+=1
587611

@@ -657,8 +681,12 @@ def _solve_quadrant1(Ct, Cu, mass, pickup, dt, Ts, ds, dn, ufs, ufn, w, visited,
657681

658682
Ct[n, s, f] = num_limited / den_limited
659683

660-
# Apply non-erodible layer constraint if zb and zne are provided
661-
if zb is not None and zne is not None:
684+
# Apply non-erodible layer constraint at domain boundaries only
685+
# Check if cell is at a boundary (any edge of domain)
686+
is_boundary = (n == 1 or n == Ct.shape[0] - 1 or
687+
s == 1 or s == Ct.shape[1] - 1)
688+
689+
if zb is not None and zne is not None and is_boundary:
662690
# Calculate total pickup mass for this cell
663691
total_pickup = 0.0
664692
for f in range(nf):
@@ -736,8 +764,12 @@ def _solve_quadrant2(Ct, Cu, mass, pickup, dt, Ts, ds, dn, ufs, ufn, w, visited,
736764

737765
Ct[n, s, f] = num_limited / den_limited
738766

739-
# Apply non-erodible layer constraint if zb and zne are provided
740-
if zb is not None and zne is not None:
767+
# Apply non-erodible layer constraint at domain boundaries only
768+
# Check if cell is at a boundary (edges of domain)
769+
is_boundary = (n == 1 or n == Ct.shape[0] - 1 or
770+
s == 1 or s == Ct.shape[1] - 2)
771+
772+
if zb is not None and zne is not None and is_boundary:
741773
# Calculate total pickup mass for this cell
742774
total_pickup = 0.0
743775
for f in range(nf):
@@ -815,8 +847,12 @@ def _solve_quadrant3(Ct, Cu, mass, pickup, dt, Ts, ds, dn, ufs, ufn, w, visited,
815847

816848
Ct[n, s, f] = num_limited / den_limited
817849

818-
# Apply non-erodible layer constraint if zb and zne are provided
819-
if zb is not None and zne is not None:
850+
# Apply non-erodible layer constraint at domain boundaries only
851+
# Check if cell is at a boundary (edges of domain)
852+
is_boundary = (n == 0 or n == Ct.shape[0] - 2 or
853+
s == 0 or s == Ct.shape[1] - 2)
854+
855+
if zb is not None and zne is not None and is_boundary:
820856
# Calculate total pickup mass for this cell
821857
total_pickup = 0.0
822858
for f in range(nf):
@@ -894,8 +930,12 @@ def _solve_quadrant4(Ct, Cu, mass, pickup, dt, Ts, ds, dn, ufs, ufn, w, visited,
894930

895931
Ct[n, s, f] = num_limited / den_limited
896932

897-
# Apply non-erodible layer constraint if zb and zne are provided
898-
if zb is not None and zne is not None:
933+
# Apply non-erodible layer constraint at domain boundaries only
934+
# Check if cell is at a boundary (edges of domain)
935+
is_boundary = (n == 0 or n == Ct.shape[0] - 2 or
936+
s == 1 or s == Ct.shape[1] - 1)
937+
938+
if zb is not None and zne is not None and is_boundary:
899939
# Calculate total pickup mass for this cell
900940
total_pickup = 0.0
901941
for f in range(nf):
@@ -1004,8 +1044,12 @@ def _solve_generic_stencil(Ct, Cu, mass, pickup, dt, Ts, ds, dn, ufs, ufn, w, vi
10041044

10051045
Ct[n, s, f] = num_lim / den_lim
10061046

1007-
# Apply non-erodible layer constraint if zb and zne are provided
1008-
if zb is not None and zne is not None:
1047+
# Apply non-erodible layer constraint at domain boundaries only
1048+
# Check if cell is at a boundary (edges of domain)
1049+
is_boundary = (n == 0 or n == Ct.shape[0] - 2 or
1050+
s == 1 or s == Ct.shape[1] - 1)
1051+
1052+
if zb is not None and zne is not None and is_boundary:
10091053
# Calculate total pickup mass for this cell
10101054
total_pickup = 0.0
10111055
for f in range(nf):

0 commit comments

Comments
 (0)