Skip to content

[com1]: stopping option for implicit friction; sfcChangeCum output file - #1343

Open
PaulaSp3 wants to merge 9 commits into
masterfrom
PS_stoppingImplicitFric
Open

PaulaSp3 wants to merge 9 commits into
masterfrom
PS_stoppingImplicitFric

Conversation

@PaulaSp3

@PaulaSp3 PaulaSp3 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor
  • when adaptSfcStop = 1 -> stopping occurs when friction is implicit, when velocity magnitude <= 1.2 * minimum velocity value
  • added accumulated sfc change output file
  • when the flags are switched on, in every time step adaptedDEM, sfcChange, entrDepth are modified according to stoppping, entrainmnet, detrainment, only if the one cell of sfcChange exceeds the threshold (default 0.1 m) the normals of the DEM are updated

standardTests are ident, except avaHofSnowGlideTest

PR Checklist

Please confirm before requesting review:

  • I ran pytest locally without fails
  • I added/updated tests where needed
  • I updated documentation where needed

Confirm before the final merge/rebase into master

  • Commits are sensibly squashed and rebased onto latest master
  • Standardtest run without difference (with recompiled cython code)

@PaulaSp3
PaulaSp3 requested review from awirb and fso42 October 14, 2026 08:59
@PaulaSp3 PaulaSp3 self-assigned this Oct 14, 2026
@qltysh

qltysh Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Analysis for project AvaFrame

❌ 4 blocking issues (6 total)

Tool Category Rule Count
black Style Incorrect formatting, autoformat by running qlty fmt. 3 ❌
ruff Lint f-string without any placeholders 1 ❌
qlty Structure Function with high complexity (count = 19): mergePartArea 1
qlty Structure Deeply nested control flow (level = 4) 1

@qltysh one-click actions:

  • Auto-fix formatting (qlty fmt && git push)

@qltysh

qltysh Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Qlty


Coverage Impact

⬆️ Merging this pull request will increase total coverage on master by 0.3%.

Modified Components (1)

RatingComponent% Diff
Coverage rating: C Coverage rating: C
com1DFA94.3%

Modified Files with Diff Coverage (3)

RatingFile% DiffUncovered Line #s
Coverage rating: B Coverage rating: B
avaframe/com1DFA/com1DFA.py92.9%2858-3208
Coverage rating: D Coverage rating: C
avaframe/com1DFA/particleTools.py100.0%
Coverage rating: A Coverage rating: A
avaframe/out3Plot/plotUtils.py100.0%
Total95.6%
🤖 Increase coverage with AI coding...
In the `PS_stoppingImplicitFric` branch, add test coverage for this new code:

- `avaframe/com1DFA/com1DFA.py` -- Line 2858-3208

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

if adaptStop or adaptDet or adaptEnt:
dem, fields = adaptDEM(dem, fields, cfg)
# adapt DEM considering erosion and deposition every time step
# (normals and area of the DEM are only updated if the surface changed more than thresholdAdaptSfc)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is this condition used so that normals are computed less often? what about a condition that these are only recomputed for the cells that are affected?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes,
I've also tried to only update normals and area in affected area. But I'm not sure if this is worth it since the code becomes much more complicated.

(with an extra function like:)

def updateNormalAndAreaMesh(dem, changedMask, num, maxFraction=0.5):
    """ Recompute normals and cell area only in the cells of changedMask and in their neighbour cells
    changedMask can contain any number of separate patches (connected regions of True cells). Each patch is
    computed on its own cutout of the DEM (bounding box of the patch grown by 2 cells: 1 ring of neighbour
    cells to update and 1 ring of cells providing the elevations needed for their normals) and only the cells
    of the patch and their neighbour cells are copied back. Cutouts of different patches may overlap.
    If the cutouts together cover more than maxFraction of the DEM,
    normals and area are recomputed for the full DEM.

    Parameters
    ----------
    dem: dict
        dem dict with header, rasterData (already updated elevation), Nx, Ny, Nz, areaRaster
    changedMask: 2D boolean numpy array
        True where normals and area are updated (together with the direct neighbour cells of these cells),
    num: int
        chose between 4, 6 or 8 (using then 4, 6 or 8 triangles) or
        1 to use the simple cross product method (with the diagonals)
    maxFraction: float
        if the cutouts together are larger than this fraction of the DEM, do a full recompute

    Returns
    -------
    dem: dict
        dem dict with updated Nx, Ny, Nz, areaRaster and outOfDEM
    """
    if not np.any(changedMask):
        return dem

    z = dem['rasterData']
    n, m = z.shape
    rows = np.flatnonzero(np.any(changedMask, axis=1))
    cols = np.flatnonzero(np.any(changedMask, axis=0))
    # box: bounding box of all changed cells grown by 2 cells (1 ring of neighbour cells to update and
    # 1 ring of cells whose elevation is needed for the normals of these neighbours), clipped to the DEM
    br0 = max(rows[0] - 2, 0)
    br1 = min(rows[-1] + 2, n - 1) + 1
    bc0 = max(cols[0] - 2, 0)
    bc1 = min(cols[-1] + 2, m - 1) + 1
    box = (slice(br0, br1), slice(bc0, bc1))
    # every connected (8-neighbourhood) region of changed cells is one patch
    labels, _ = ndimage.label(changedMask[box], structure=np.ones((3, 3), dtype=bool))
    patchSlices = ndimage.find_objects(labels)

    # cutout of each patch: bounding box of the patch grown by 2 cells, clipped to the DEM
    # (indices relative to box)
    nb, mb = labels.shape
    cutouts = []
    nCellsCutouts = 0
    for sl in patchSlices:
        r0 = max(sl[0].start - 2, 0)
        r1 = min(sl[0].stop + 2, nb)
        c0 = max(sl[1].start - 2, 0)
        c1 = min(sl[1].stop + 2, mb)
        cutouts.append((slice(r0, r1), slice(c0, c1)))
        nCellsCutouts += (r1 - r0) * (c1 - c0)

    if nCellsCutouts > maxFraction * n * m:
        dem = geoTrans.getNormalMesh(dem, num=num)
        dem = getAreaMesh(dem, num)
        return dem

    # copy arrays to not modify arrays that might be referenced elsewhere
    keys = ['Nx', 'Ny', 'Nz', 'areaRaster']
    newArrays = {key: np.array(dem[key], dtype=float, copy=True) for key in keys}
    if 'outOfDEM' in dem:
        outOfDEM = dem['outOfDEM'].reshape(n, m).copy()
    else:
        outOfDEM = np.isnan(z)
    zBox = z[box]
    for label, cut in enumerate(cutouts, start=1):
        # compute normals and area on the cutout of this patch; the outer ring of the cutout has wrong
        # (border) values, but it is never written unless it coincides with the real DEM border, where it is
        # correct. The cutout contains the current elevation of all cells (also of other patches), so cells
        # that are updated by several patches get the same (correct) value
        zSub = zBox[cut]
        nSub, mSub = zSub.shape
        subDem = {'header': {'nrows': nSub, 'ncols': mSub, 'cellsize': dem['header']['cellsize']},
                  'rasterData': zSub}
        subDem = geoTrans.getNormalMesh(subDem, num=num)
        subDem = getAreaMesh(subDem, num)
        # cells to update: cells of this patch and all their (8) neighbour cells
        patchPad = np.pad(labels[cut] == label, 1)
        writeMask = np.zeros((nSub, mSub), dtype=bool)
        for di in range(3):
            for dj in range(3):
                writeMask |= patchPad[di:di + nSub, dj:dj + mSub]
        for key in keys:
            newArrays[key][box][cut][writeMask] = subDem[key][writeMask]
        outOfDEM[box][cut][writeMask] = np.isnan(zSub)[writeMask]

    dem.update(newArrays)
    dem['outOfDEM'] = outOfDEM.flatten()
    return dem

Comment thread avaframe/com1DFA/com1DFA.py Resolved
Comment thread avaframe/com1DFA/DFAfunctionsCython.pyx Resolved
Comment thread avaframe/tests/test_com1DFA.py Resolved
Comment thread avaframe/com1DFA/com1DFA.py Resolved
Comment thread avaframe/com1DFA/DFAfunctionsCython.pyx Resolved
@PaulaSp3
PaulaSp3 requested review from awirb and fso42 November 9, 2026 16:37

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants