Skip to content

Masks

Land/ocean masks with automatic stencil dispatch for Arakawa C-grids.

Grid Mask

finitevolx.Mask2D

Bases: Module

Unified Arakawa C-grid mask for a 2-D Cartesian domain.

Stores binary masks on all five Arakawa C-grid staggerings (cell centre h, x-face u, y-face v, xy-corner lenient xy_corner, xy-corner strict xy_corner_strict), boundary-type flags for the corner cells, irregular-boundary indices, a 4-level land/coast classification, directional stencil capability, and optional sponge/bathymetry arrays.

Construct via one of the factory class-methods:

  • :meth:from_mask — from a binary h-grid mask array.
  • :meth:from_center — from a float field at cell centres (NaN = dry).
  • :meth:from_u_face — from a float field at u-faces, with a mode= choice of inversion strategy.
  • :meth:from_v_face — from a float field at v-faces, with a mode= choice of inversion strategy.
  • :meth:from_corner — from a float field at xy-corners (vertices), with a mode= choice of inversion strategy.
  • :meth:from_dimensions — all-ocean domain of given size.

Parameters:

Name Type Description Default
h Bool[Array, 'Ny Nx']

Wet mask at cell centres (T-points), where tracer-like quantities (temperature, salinity, SSH, pressure) live. True = wet / ocean, False = dry / land. This is the canonical mask that the other staggered masks are derived from.

required
u Bool[Array, 'Ny Nx']

Wet mask at x-faces (the west/east boundaries of each cell), where the zonal velocity u lives. u[j, i] is the east face of h[j, i] (positive half-step / NE convention). Wet iff both of the zonally adjacent cell centres are wet — i.e. flow through the face is physically meaningful.

required
v Bool[Array, 'Ny Nx']

Wet mask at y-faces (the south/north boundaries of each cell), where the meridional velocity v lives. v[j, i] is the north face of h[j, i]. Wet iff both of the meridionally adjacent cell centres are wet.

required
xy_corner Bool[Array, 'Ny Nx']

Lenient wet mask at xy-corners (vertices). True wherever at least one of the four surrounding cell centres is wet — useful for detecting any ocean presence at a corner.

required
xy_corner_strict Bool[Array, 'Ny Nx']

Strict wet mask at xy-corners (vertices). True only where all four surrounding cell centres are wet — useful for quantities (e.g. relative vorticity) that require the full 4-point horizontal stencil to be inside the fluid.

required
not_h Bool[Array, 'Ny Nx']

Logical inverses of the corresponding masks.

required
not_u Bool[Array, 'Ny Nx']

Logical inverses of the corresponding masks.

required
not_v Bool[Array, 'Ny Nx']

Logical inverses of the corresponding masks.

required
not_xy_corner Bool[Array, 'Ny Nx']

Logical inverses of the corresponding masks.

required
not_xy_corner_strict Bool[Array, 'Ny Nx']

Logical inverses of the corresponding masks.

required
xy_corner_y_wall Bool[Array, 'Ny Nx']

xy-corner cells on a y-direction wall: wet xy_corner cell where at least one y-adjacent v-face is dry.

required
xy_corner_x_wall Bool[Array, 'Ny Nx']

xy-corner cells on an x-direction wall: wet xy_corner cell where at least one x-adjacent u-face is dry.

required
xy_corner_convex Bool[Array, 'Ny Nx']

xy-corner cells at convex corners (on both x- and y-walls).

required
xy_corner_valid Bool[Array, 'Ny Nx']

Interior xy-corner cells: wet xy_corner with all 4 adjacent faces wet.

required
xy_corner_strict_irrbound_rows Int[Array, 'Nirr']

Row (j) indices of irregular-boundary xy_corner_strict cells in the interior [1:-1, 1:-1]: dry corner cells that neighbour at least one wet corner cell in their 3×3 neighbourhood.

required
xy_corner_strict_irrbound_cols Int[Array, 'Nirr']

Column (i) indices paired with xy_corner_strict_irrbound_rows.

required
classification Int[Array, 'Ny Nx']

4-level integer classification: 0 = land, 1 = coast, 2 = near-coast, 3 = open ocean.

required
stencil_capability StencilCapability2D

Directional contiguous-wet-cell counts on the h-grid.

required
sponge Float[Array, 'Ny Nx']

Sponge-layer weight: 0 at domain walls, 1 in the interior. All-ones when no sponge width is requested.

required
k_bottom Array or None

Optional 2-D array of vertical sea-floor indices (3-D domains).

required
Source code in finitevolx/_src/mask/cartesian.py
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
class Mask2D(eqx.Module):
    """Unified Arakawa C-grid mask for a 2-D Cartesian domain.

    Stores binary masks on all five Arakawa C-grid staggerings (cell
    centre ``h``, x-face ``u``, y-face ``v``, xy-corner lenient
    ``xy_corner``, xy-corner strict ``xy_corner_strict``), boundary-type
    flags for the corner cells, irregular-boundary indices, a 4-level
    land/coast classification, directional stencil capability, and
    optional sponge/bathymetry arrays.

    Construct via one of the factory class-methods:

    * :meth:`from_mask`       — from a binary h-grid mask array.
    * :meth:`from_center`     — from a float field at cell centres
      (NaN = dry).
    * :meth:`from_u_face`     — from a float field at u-faces, with a
      ``mode=`` choice of inversion strategy.
    * :meth:`from_v_face`     — from a float field at v-faces, with a
      ``mode=`` choice of inversion strategy.
    * :meth:`from_corner`     — from a float field at xy-corners
      (vertices), with a ``mode=`` choice of inversion strategy.
    * :meth:`from_dimensions` — all-ocean domain of given size.

    Parameters
    ----------
    h : Bool[Array, "Ny Nx"]
        Wet mask at cell centres (T-points), where tracer-like quantities
        (temperature, salinity, SSH, pressure) live.  ``True`` = wet /
        ocean, ``False`` = dry / land.  This is the canonical mask that
        the other staggered masks are derived from.
    u : Bool[Array, "Ny Nx"]
        Wet mask at x-faces (the west/east boundaries of each cell), where
        the zonal velocity ``u`` lives.  ``u[j, i]`` is the *east* face of
        ``h[j, i]`` (positive half-step / NE convention).  Wet iff *both*
        of the zonally adjacent cell centres are wet — i.e. flow through
        the face is physically meaningful.
    v : Bool[Array, "Ny Nx"]
        Wet mask at y-faces (the south/north boundaries of each cell), where
        the meridional velocity ``v`` lives.  ``v[j, i]`` is the *north*
        face of ``h[j, i]``.  Wet iff *both* of the meridionally adjacent
        cell centres are wet.
    xy_corner : Bool[Array, "Ny Nx"]
        Lenient wet mask at xy-corners (vertices).  ``True`` wherever
        *at least one* of the four surrounding cell centres is wet —
        useful for detecting any ocean presence at a corner.
    xy_corner_strict : Bool[Array, "Ny Nx"]
        Strict wet mask at xy-corners (vertices).  ``True`` only where
        *all four* surrounding cell centres are wet — useful for
        quantities (e.g. relative vorticity) that require the full
        4-point horizontal stencil to be inside the fluid.
    not_h, not_u, not_v, not_xy_corner, not_xy_corner_strict : Bool[Array, "Ny Nx"]
        Logical inverses of the corresponding masks.
    xy_corner_y_wall : Bool[Array, "Ny Nx"]
        xy-corner cells on a y-direction wall: wet xy_corner cell where
        at least one y-adjacent v-face is dry.
    xy_corner_x_wall : Bool[Array, "Ny Nx"]
        xy-corner cells on an x-direction wall: wet xy_corner cell where
        at least one x-adjacent u-face is dry.
    xy_corner_convex : Bool[Array, "Ny Nx"]
        xy-corner cells at convex corners (on both x- and y-walls).
    xy_corner_valid : Bool[Array, "Ny Nx"]
        Interior xy-corner cells: wet ``xy_corner`` with all 4 adjacent
        faces wet.
    xy_corner_strict_irrbound_rows : Int[Array, "Nirr"]
        Row (j) indices of irregular-boundary ``xy_corner_strict`` cells
        in the interior ``[1:-1, 1:-1]``: dry corner cells that
        neighbour at least one wet corner cell in their 3×3 neighbourhood.
    xy_corner_strict_irrbound_cols : Int[Array, "Nirr"]
        Column (i) indices paired with ``xy_corner_strict_irrbound_rows``.
    classification : Int[Array, "Ny Nx"]
        4-level integer classification: 0 = land, 1 = coast, 2 = near-coast,
        3 = open ocean.
    stencil_capability : StencilCapability2D
        Directional contiguous-wet-cell counts on the h-grid.
    sponge : Float[Array, "Ny Nx"]
        Sponge-layer weight: 0 at domain walls, 1 in the interior.
        All-ones when no sponge width is requested.
    k_bottom : Array or None
        Optional 2-D array of vertical sea-floor indices (3-D domains).
    """

    # ── staggered masks ───────────────────────────────────────────────────────
    h: Bool[Array, "Ny Nx"]
    u: Bool[Array, "Ny Nx"]
    v: Bool[Array, "Ny Nx"]
    xy_corner: Bool[Array, "Ny Nx"]
    xy_corner_strict: Bool[Array, "Ny Nx"]

    # ── inverted masks ────────────────────────────────────────────────────────
    not_h: Bool[Array, "Ny Nx"]
    not_u: Bool[Array, "Ny Nx"]
    not_v: Bool[Array, "Ny Nx"]
    not_xy_corner: Bool[Array, "Ny Nx"]
    not_xy_corner_strict: Bool[Array, "Ny Nx"]

    # ── corner boundary classification ────────────────────────────────────────
    xy_corner_y_wall: Bool[Array, "Ny Nx"]
    xy_corner_x_wall: Bool[Array, "Ny Nx"]
    xy_corner_convex: Bool[Array, "Ny Nx"]
    xy_corner_valid: Bool[Array, "Ny Nx"]

    # ── irregular boundary indices (dynamic shape — do not use inside jit) ───
    xy_corner_strict_irrbound_rows: Int[Array, Nirr]
    xy_corner_strict_irrbound_cols: Int[Array, Nirr]

    # ── land/coast classification ─────────────────────────────────────────────
    classification: Int[Array, "Ny Nx"]

    # ── stencil capability ────────────────────────────────────────────────────
    stencil_capability: StencilCapability2D

    # ── optional arrays ───────────────────────────────────────────────────────
    sponge: Float[Array, "Ny Nx"]
    k_bottom: Array | None

    # ── Boolean accessors for land/coast classification ───────────────────────

    @property
    def ind_land(self) -> Bool[Array, "Ny Nx"]:
        """Boolean mask: land cells (classification == 0)."""
        return self.classification == 0

    @property
    def ind_coast(self) -> Bool[Array, "Ny Nx"]:
        """Boolean mask: coast cells (classification == 1)."""
        return self.classification == 1

    @property
    def ind_near_coast(self) -> Bool[Array, "Ny Nx"]:
        """Boolean mask: near-coast cells (classification == 2)."""
        return self.classification == 2

    @property
    def ind_ocean(self) -> Bool[Array, "Ny Nx"]:
        """Boolean mask: open-ocean cells (classification == 3)."""
        return self.classification == 3

    @property
    def ind_boundary(self) -> Bool[Array, "Ny Nx"]:
        """Boolean mask: outermost domain-boundary ring."""
        Ny, Nx = self.h.shape
        bnd = jnp.zeros((Ny, Nx), dtype=bool)
        bnd = bnd.at[0, :].set(True)
        bnd = bnd.at[-1, :].set(True)
        bnd = bnd.at[:, 0].set(True)
        bnd = bnd.at[:, -1].set(True)
        return bnd

    # ── adaptive stencil masks ────────────────────────────────────────────────

    def get_adaptive_masks(
        self,
        direction: str = "x",
        source: str = "h",
        stencil_sizes: tp.Sequence[int] = (2, 4, 6, 8, 10),
    ) -> dict[int, Bool[Array, "Ny Nx"]]:
        """Per-point adaptive stencil-size masks for 2-D operators.

        For each stencil size *s* in ``stencil_sizes``, returns a boolean mask
        that is ``True`` at cells where a symmetric stencil of half-width
        *s//2* is fully supported by contiguous wet neighbours.

        These masks are useful for any operator that wants to fall back
        to a narrower stencil near coastlines or irregular boundaries.
        Common consumers include:

        * **WENO reconstruction**: size 2 → upwind1, 4 → WENO3, 6 → WENO5,
          8 → WENO7, 10 → WENO9 (half-widths 1 / 2 / 3 / 4 / 5).
        * **Higher-order upwind advection** (e.g. 3rd / 5th-order upwind).
        * **Higher-order centred finite-difference** operators (4th / 6th /
          8th-order Laplacians, gradients, etc.).
        * **Higher-order interpolation** kernels (cubic / quintic
          T↔face / T↔corner interpolations).

        The returned masks are **mutually exclusive** hierarchical tiers: the
        mask for size *s* is ``True`` only where *s* is the *largest* usable
        stencil.

        Parameters
        ----------
        direction : {'x', 'y'}
            Stencil direction.
        source : {'h', 'u', 'v', 'xy_corner', 'xy_corner_strict'}
            Source grid whose stencil capability to use.
        stencil_sizes : sequence of int
            Ordered candidate stencil sizes (even integers).

        Returns
        -------
        dict[int, Bool[Array, "Ny Nx"]]
            Mapping from stencil size to its mutually-exclusive boolean mask.
        """
        sc = self._stencil_capability_for(source)
        if direction == "x":
            cnt_pos, cnt_neg = sc.x_pos, sc.x_neg
        elif direction == "y":
            cnt_pos, cnt_neg = sc.y_pos, sc.y_neg
        else:
            raise ValueError(f"direction must be 'x' or 'y', got {direction!r}")

        # Maximum usable stencil size at each point
        max_s = jnp.zeros(self.h.shape, dtype=jnp.int32)
        for s in sorted(stencil_sizes):
            hw = s // 2
            can_use = (cnt_pos >= hw) & (cnt_neg >= hw)
            max_s = jnp.where(can_use, s, max_s)

        # Mutually-exclusive masks
        return {s: (max_s == s) for s in stencil_sizes}

    def _stencil_capability_for(self, source: str) -> StencilCapability2D:
        """Return a :class:`StencilCapability2D` for the given source grid.

        For non-``'h'`` sources the capability is re-computed from the stored
        staggered mask.  This should **not** be called inside a JIT-compiled
        function for non-h sources (numpy conversion required).

        Parameters
        ----------
        source : {'h', 'u', 'v', 'xy_corner', 'xy_corner_strict'}
        """
        grid_map = {
            "h": self.h,
            "u": self.u,
            "v": self.v,
            "xy_corner": self.xy_corner,
            "xy_corner_strict": self.xy_corner_strict,
        }
        if source not in grid_map:
            raise ValueError(
                f"source must be one of {list(grid_map)!r}, got {source!r}"
            )
        if source == "h":
            return self.stencil_capability
        return StencilCapability2D.from_mask(grid_map[source])

    # ── factory class-methods ─────────────────────────────────────────────────

    @classmethod
    def from_mask(
        cls,
        mask_hgrid: np.ndarray | Bool[Array, "Ny Nx"],
        sponge_width: int | None = None,
        k_bottom: Array | None = None,
    ) -> Mask2D:
        """Construct from a binary h-grid (cell-centre) mask.

        All intermediate computations use numpy/scipy for efficiency.
        Stored arrays are converted to JAX at the end.

        Parameters
        ----------
        mask_hgrid : array-like [Ny, Nx]
            Binary wet (1 / True) / dry (0 / False) mask at cell centres.
        sponge_width : int, optional
            Width (in grid cells) of the linear sponge ramp.  ``None``
            produces an all-ones sponge (no damping).
        k_bottom : array-like [Ny, Nx], optional
            Vertical sea-floor indices for 3-D domains.

        Returns
        -------
        Mask2D

        Notes
        -----
        The four staggered masks are derived from ``mask_hgrid`` via
        :func:`pool_bool` with the following kernel/threshold pairs
        (trailing-pad direction → positive half-step convention):

        ==================== ========== =========  ====================================
        Mask                 Kernel     Threshold  Semantics
        ==================== ========== =========  ====================================
        ``u`` (east face)    ``(1, 2)`` ``3/4``    ``h[j, i] AND h[j, i+1]``
        ``v`` (north face)   ``(2, 1)`` ``3/4``    ``h[j, i] AND h[j+1, i]``
        ``xy_corner``        ``(2, 2)`` ``1/8``    ≥ 1 of 4 NE-corner h-cells wet
        ``xy_corner_strict`` ``(2, 2)`` ``7/8``    all 4 NE-corner h-cells wet
        ==================== ========== =========  ====================================

        The land/coast classification uses two successive
        :func:`dilate_mask` passes on ``~mask_hgrid``; the stencil
        capability is built via :meth:`StencilCapability2D.from_mask`.
        """
        h_np = np.asarray(mask_hgrid, dtype=bool)
        Ny, Nx = h_np.shape
        hf = h_np.astype(np.float32)

        # ── staggered masks ───────────────────────────────────────────────
        # All staggered masks use the trailing-pad (positive half-step)
        # convention, matching the grid module's same-index rule:
        # U[j, i] at east face (i+1/2), V[j, i] at north face (j+1/2),
        # X[j, i] at NE corner (i+1/2, j+1/2).

        # u[j, i] = (h[j, i] + h[j, i+1]) / 2 > 3/4  (east face = U-point)
        u_np = pool_bool(hf, kernel=(1, 2), threshold=3.0 / 4.0, direction="trailing")
        # v[j, i] = (h[j, i] + h[j+1, i]) / 2 > 3/4  (north face = V-point)
        v_np = pool_bool(hf, kernel=(2, 1), threshold=3.0 / 4.0, direction="trailing")
        # xy_corner[j, i]: at least 1 of 4 NE-corner h-cells wet  (lenient)
        xy_corner_np = pool_bool(
            hf, kernel=(2, 2), threshold=1.0 / 8.0, direction="trailing"
        )
        # xy_corner_strict[j, i]: all 4 NE-corner h-cells wet     (strict)
        xy_corner_strict_np = pool_bool(
            hf, kernel=(2, 2), threshold=7.0 / 8.0, direction="trailing"
        )

        # ── corner boundary classification ────────────────────────────────
        # For xy_corner[j, i] at NE corner of h[j, i], the 4 incident
        # velocity faces are:
        #   u[j,   i] = east face of h[j,i]    → south of corner (vertical)
        #   u[j+1, i] = east face of h[j+1,i]  → north of corner (vertical)
        #   v[j, i  ] = north face of h[j,i]   → west of corner  (horizontal)
        #   v[j, i+1] = north face of h[j,i+1] → east of corner  (horizontal)
        # u_north and v_east are the +1-shifted neighbours; trailing-side
        # zero-pad supplies the implicit boundary face beyond the array.
        u_north = np.pad(u_np[1:, :], ((0, 1), (0, 0)))  # u[j+1, i]
        v_east = np.pad(v_np[:, 1:], ((0, 0), (0, 1)))  # v[j, i+1]

        # y-wall (vertical wall): one of the two u-faces (vertical lines) dry
        xy_corner_y_wall = xy_corner_np & (~u_np | ~u_north)
        # x-wall (horizontal wall): one of the two v-faces (horizontal lines) dry
        xy_corner_x_wall = xy_corner_np & (~v_np | ~v_east)
        # convex corner: both walls present
        xy_corner_convex = xy_corner_y_wall & xy_corner_x_wall
        # valid interior corner: all 4 adjacent faces wet
        xy_corner_valid = xy_corner_np & u_np & u_north & v_np & v_east

        # ── irregular xy_corner_strict boundary indices ───────────────────
        # Dry xy_corner_strict cells in [1:-1, 1:-1] with >=1 wet
        # xy_corner_strict cell in their 3x3 neighbourhood.
        psif = xy_corner_strict_np.astype(np.float32)
        if Ny >= 3 and Nx >= 3:
            pool3 = np.zeros((Ny - 2, Nx - 2), dtype=np.float32)
            for di in range(3):
                for dj in range(3):
                    pool3 += psif[di : Ny - 2 + di, dj : Nx - 2 + dj]
            pool3 /= 9.0
            irrbound = (~xy_corner_strict_np[1:-1, 1:-1]) & (pool3 > 1.0 / 18.0)
            rows, cols = np.where(irrbound)
            # Map back from interior slice to full-array coordinates.
            rows = rows + 1
            cols = cols + 1
        else:
            rows = np.empty(0, dtype=np.int32)
            cols = np.empty(0, dtype=np.int32)

        # ── land / coast classification ───────────────────────────────────
        # 0 = land, 1 = coast (ocean adj. to land), 2 = near-coast, 3 = ocean
        land = ~h_np
        land_d1 = dilate_mask(land)
        coast = h_np & land_d1  # first ring of ocean
        land_d2 = dilate_mask(land_d1)
        near_coast = h_np & land_d2 & ~coast  # second ring
        open_ocean = h_np & ~land_d2  # interior ocean

        classification = np.zeros((Ny, Nx), dtype=np.int32)
        classification[coast] = 1
        classification[near_coast] = 2
        classification[open_ocean] = 3

        # ── stencil capability ────────────────────────────────────────────
        sc = StencilCapability2D.from_mask(h_np)

        # ── sponge layer ──────────────────────────────────────────────────
        if sponge_width is None or sponge_width == 0:
            sponge_np = np.ones((Ny, Nx), dtype=np.float32)
        else:
            if sponge_width < 0:
                raise ValueError(
                    f"sponge_width must be non-negative; got {sponge_width!r}"
                )
            sponge_np = make_sponge((Ny, Nx), sponge_width)

        return cls(
            h=jnp.asarray(h_np),
            u=jnp.asarray(u_np),
            v=jnp.asarray(v_np),
            xy_corner=jnp.asarray(xy_corner_np),
            xy_corner_strict=jnp.asarray(xy_corner_strict_np),
            not_h=jnp.asarray(~h_np),
            not_u=jnp.asarray(~u_np),
            not_v=jnp.asarray(~v_np),
            not_xy_corner=jnp.asarray(~xy_corner_np),
            not_xy_corner_strict=jnp.asarray(~xy_corner_strict_np),
            xy_corner_y_wall=jnp.asarray(xy_corner_y_wall),
            xy_corner_x_wall=jnp.asarray(xy_corner_x_wall),
            xy_corner_convex=jnp.asarray(xy_corner_convex),
            xy_corner_valid=jnp.asarray(xy_corner_valid),
            xy_corner_strict_irrbound_rows=jnp.asarray(rows.astype(np.int32)),
            xy_corner_strict_irrbound_cols=jnp.asarray(cols.astype(np.int32)),
            classification=jnp.asarray(classification),
            stencil_capability=sc,
            sponge=jnp.asarray(sponge_np),
            k_bottom=jnp.asarray(k_bottom) if k_bottom is not None else None,
        )

    @classmethod
    def from_center(
        cls,
        field: Float[Array, "Ny Nx"],
        sponge_width: int | None = None,
        k_bottom: Array | None = None,
    ) -> Mask2D:
        """Construct from a field at cell centres (T-points).

        ``NaN`` values are treated as dry; finite values as wet.  This
        is the natural constructor for any tracer-like quantity (SSH,
        temperature, salinity, pressure, …) that lives at cell centres.

        Parameters
        ----------
        field : array-like [Ny, Nx]
            Float field at cell centres; ``NaN`` marks dry cells.
        sponge_width : int, optional
            Sponge layer width.  See :meth:`from_mask`.
        k_bottom : array-like, optional
            Sea-floor indices.  See :meth:`from_mask`.

        Returns
        -------
        Mask2D
        """
        h_mask = np.isfinite(np.asarray(field))
        return cls.from_mask(h_mask, sponge_width=sponge_width, k_bottom=k_bottom)

    @classmethod
    def from_u_face(
        cls,
        field: Float[Array, "Ny Nx"],
        mode: str = "permissive",
        sponge_width: int | None = None,
        k_bottom: Array | None = None,
    ) -> Mask2D:
        """Construct from a field at u-faces, deriving the h-grid mask.

        ``NaN`` values are treated as dry u-faces.  The h-grid mask is
        then inferred via :func:`h_from_pooled` with kernel ``(1, 2)``
        (trailing-pad direction, matching the grid's positive-half-step
        convention) and the requested ``mode``; the remaining staggered
        masks are produced by :meth:`from_mask`.

        Parameters
        ----------
        field : array-like [Ny, Nx]
            Float field at u-faces; ``NaN`` marks dry faces.
        mode : {'permissive', 'conservative'}
            Inversion strategy.  See :meth:`Mask1D.from_u_face` for
            details.
        sponge_width : int, optional
            Sponge layer width.  See :meth:`from_mask`.
        k_bottom : array-like, optional
            Sea-floor indices.  See :meth:`from_mask`.

        Returns
        -------
        Mask2D
        """
        u_mask = np.isfinite(np.asarray(field))
        h_mask = h_from_pooled(u_mask, kernel=(1, 2), mode=mode, direction="trailing")
        return cls.from_mask(h_mask, sponge_width=sponge_width, k_bottom=k_bottom)

    @classmethod
    def from_v_face(
        cls,
        field: Float[Array, "Ny Nx"],
        mode: str = "permissive",
        sponge_width: int | None = None,
        k_bottom: Array | None = None,
    ) -> Mask2D:
        """Construct from a field at v-faces, deriving the h-grid mask.

        ``NaN`` values are treated as dry v-faces.  The h-grid mask is
        then inferred via :func:`h_from_pooled` with kernel ``(2, 1)``
        (trailing-pad direction) and the requested ``mode``.

        Parameters
        ----------
        field : array-like [Ny, Nx]
            Float field at v-faces; ``NaN`` marks dry faces.
        mode : {'permissive', 'conservative'}
            Inversion strategy.  See :meth:`Mask1D.from_u_face` for
            details.
        sponge_width : int, optional
            Sponge layer width.  See :meth:`from_mask`.
        k_bottom : array-like, optional
            Sea-floor indices.  See :meth:`from_mask`.

        Returns
        -------
        Mask2D
        """
        v_mask = np.isfinite(np.asarray(field))
        h_mask = h_from_pooled(v_mask, kernel=(2, 1), mode=mode, direction="trailing")
        return cls.from_mask(h_mask, sponge_width=sponge_width, k_bottom=k_bottom)

    @classmethod
    def from_corner(
        cls,
        field: Float[Array, "Ny Nx"],
        mode: str = "permissive",
        sponge_width: int | None = None,
        k_bottom: Array | None = None,
    ) -> Mask2D:
        """Construct from a field at xy-corners (vertices).

        ``NaN`` values are treated as dry corners.  The h-grid mask is
        then inferred via :func:`h_from_pooled` with kernel ``(2, 2)``
        (trailing-pad direction) and the requested ``mode``.  This is
        the natural constructor for vertex-stored quantities such as
        relative vorticity.

        Parameters
        ----------
        field : array-like [Ny, Nx]
            Float field at xy-corners; ``NaN`` marks dry corners.
        mode : {'permissive', 'conservative'}
            Inversion strategy.  See :meth:`Mask1D.from_u_face` for
            details.
        sponge_width : int, optional
            Sponge layer width.  See :meth:`from_mask`.
        k_bottom : array-like, optional
            Sea-floor indices.  See :meth:`from_mask`.

        Returns
        -------
        Mask2D
        """
        c_mask = np.isfinite(np.asarray(field))
        h_mask = h_from_pooled(c_mask, kernel=(2, 2), mode=mode, direction="trailing")
        return cls.from_mask(h_mask, sponge_width=sponge_width, k_bottom=k_bottom)

    @classmethod
    def from_dimensions(
        cls,
        ny: int,
        nx: int,
        sponge_width: int | None = None,
    ) -> Mask2D:
        """Construct an all-ocean domain of given shape.

        Parameters
        ----------
        ny, nx : int
            Total grid dimensions (including ghost cells).
        sponge_width : int, optional
            Sponge layer width.  See :meth:`from_mask`.

        Returns
        -------
        Mask2D
        """
        return cls.from_mask(np.ones((ny, nx), dtype=bool), sponge_width=sponge_width)

ind_boundary property

Boolean mask: outermost domain-boundary ring.

ind_coast property

Boolean mask: coast cells (classification == 1).

ind_land property

Boolean mask: land cells (classification == 0).

ind_near_coast property

Boolean mask: near-coast cells (classification == 2).

ind_ocean property

Boolean mask: open-ocean cells (classification == 3).

from_center(field, sponge_width=None, k_bottom=None) classmethod

Construct from a field at cell centres (T-points).

NaN values are treated as dry; finite values as wet. This is the natural constructor for any tracer-like quantity (SSH, temperature, salinity, pressure, …) that lives at cell centres.

Parameters:

Name Type Description Default
field array - like[Ny, Nx]

Float field at cell centres; NaN marks dry cells.

required
sponge_width int

Sponge layer width. See :meth:from_mask.

None
k_bottom array - like

Sea-floor indices. See :meth:from_mask.

None

Returns:

Type Description
Mask2D
Source code in finitevolx/_src/mask/cartesian.py
@classmethod
def from_center(
    cls,
    field: Float[Array, "Ny Nx"],
    sponge_width: int | None = None,
    k_bottom: Array | None = None,
) -> Mask2D:
    """Construct from a field at cell centres (T-points).

    ``NaN`` values are treated as dry; finite values as wet.  This
    is the natural constructor for any tracer-like quantity (SSH,
    temperature, salinity, pressure, …) that lives at cell centres.

    Parameters
    ----------
    field : array-like [Ny, Nx]
        Float field at cell centres; ``NaN`` marks dry cells.
    sponge_width : int, optional
        Sponge layer width.  See :meth:`from_mask`.
    k_bottom : array-like, optional
        Sea-floor indices.  See :meth:`from_mask`.

    Returns
    -------
    Mask2D
    """
    h_mask = np.isfinite(np.asarray(field))
    return cls.from_mask(h_mask, sponge_width=sponge_width, k_bottom=k_bottom)

from_corner(field, mode='permissive', sponge_width=None, k_bottom=None) classmethod

Construct from a field at xy-corners (vertices).

NaN values are treated as dry corners. The h-grid mask is then inferred via :func:h_from_pooled with kernel (2, 2) (trailing-pad direction) and the requested mode. This is the natural constructor for vertex-stored quantities such as relative vorticity.

Parameters:

Name Type Description Default
field array - like[Ny, Nx]

Float field at xy-corners; NaN marks dry corners.

required
mode ('permissive', 'conservative')

Inversion strategy. See :meth:Mask1D.from_u_face for details.

'permissive'
sponge_width int

Sponge layer width. See :meth:from_mask.

None
k_bottom array - like

Sea-floor indices. See :meth:from_mask.

None

Returns:

Type Description
Mask2D
Source code in finitevolx/_src/mask/cartesian.py
@classmethod
def from_corner(
    cls,
    field: Float[Array, "Ny Nx"],
    mode: str = "permissive",
    sponge_width: int | None = None,
    k_bottom: Array | None = None,
) -> Mask2D:
    """Construct from a field at xy-corners (vertices).

    ``NaN`` values are treated as dry corners.  The h-grid mask is
    then inferred via :func:`h_from_pooled` with kernel ``(2, 2)``
    (trailing-pad direction) and the requested ``mode``.  This is
    the natural constructor for vertex-stored quantities such as
    relative vorticity.

    Parameters
    ----------
    field : array-like [Ny, Nx]
        Float field at xy-corners; ``NaN`` marks dry corners.
    mode : {'permissive', 'conservative'}
        Inversion strategy.  See :meth:`Mask1D.from_u_face` for
        details.
    sponge_width : int, optional
        Sponge layer width.  See :meth:`from_mask`.
    k_bottom : array-like, optional
        Sea-floor indices.  See :meth:`from_mask`.

    Returns
    -------
    Mask2D
    """
    c_mask = np.isfinite(np.asarray(field))
    h_mask = h_from_pooled(c_mask, kernel=(2, 2), mode=mode, direction="trailing")
    return cls.from_mask(h_mask, sponge_width=sponge_width, k_bottom=k_bottom)

from_dimensions(ny, nx, sponge_width=None) classmethod

Construct an all-ocean domain of given shape.

Parameters:

Name Type Description Default
ny int

Total grid dimensions (including ghost cells).

required
nx int

Total grid dimensions (including ghost cells).

required
sponge_width int

Sponge layer width. See :meth:from_mask.

None

Returns:

Type Description
Mask2D
Source code in finitevolx/_src/mask/cartesian.py
@classmethod
def from_dimensions(
    cls,
    ny: int,
    nx: int,
    sponge_width: int | None = None,
) -> Mask2D:
    """Construct an all-ocean domain of given shape.

    Parameters
    ----------
    ny, nx : int
        Total grid dimensions (including ghost cells).
    sponge_width : int, optional
        Sponge layer width.  See :meth:`from_mask`.

    Returns
    -------
    Mask2D
    """
    return cls.from_mask(np.ones((ny, nx), dtype=bool), sponge_width=sponge_width)

from_mask(mask_hgrid, sponge_width=None, k_bottom=None) classmethod

Construct from a binary h-grid (cell-centre) mask.

All intermediate computations use numpy/scipy for efficiency. Stored arrays are converted to JAX at the end.

Parameters:

Name Type Description Default
mask_hgrid array - like[Ny, Nx]

Binary wet (1 / True) / dry (0 / False) mask at cell centres.

required
sponge_width int

Width (in grid cells) of the linear sponge ramp. None produces an all-ones sponge (no damping).

None
k_bottom array - like[Ny, Nx]

Vertical sea-floor indices for 3-D domains.

None

Returns:

Type Description
Mask2D
Notes

The four staggered masks are derived from mask_hgrid via :func:pool_bool with the following kernel/threshold pairs (trailing-pad direction → positive half-step convention):

==================== ========== ========= ==================================== Mask Kernel Threshold Semantics ==================== ========== ========= ==================================== u (east face) (1, 2) 3/4 h[j, i] AND h[j, i+1] v (north face) (2, 1) 3/4 h[j, i] AND h[j+1, i] xy_corner (2, 2) 1/8 ≥ 1 of 4 NE-corner h-cells wet xy_corner_strict (2, 2) 7/8 all 4 NE-corner h-cells wet ==================== ========== ========= ====================================

The land/coast classification uses two successive :func:dilate_mask passes on ~mask_hgrid; the stencil capability is built via :meth:StencilCapability2D.from_mask.

Source code in finitevolx/_src/mask/cartesian.py
@classmethod
def from_mask(
    cls,
    mask_hgrid: np.ndarray | Bool[Array, "Ny Nx"],
    sponge_width: int | None = None,
    k_bottom: Array | None = None,
) -> Mask2D:
    """Construct from a binary h-grid (cell-centre) mask.

    All intermediate computations use numpy/scipy for efficiency.
    Stored arrays are converted to JAX at the end.

    Parameters
    ----------
    mask_hgrid : array-like [Ny, Nx]
        Binary wet (1 / True) / dry (0 / False) mask at cell centres.
    sponge_width : int, optional
        Width (in grid cells) of the linear sponge ramp.  ``None``
        produces an all-ones sponge (no damping).
    k_bottom : array-like [Ny, Nx], optional
        Vertical sea-floor indices for 3-D domains.

    Returns
    -------
    Mask2D

    Notes
    -----
    The four staggered masks are derived from ``mask_hgrid`` via
    :func:`pool_bool` with the following kernel/threshold pairs
    (trailing-pad direction → positive half-step convention):

    ==================== ========== =========  ====================================
    Mask                 Kernel     Threshold  Semantics
    ==================== ========== =========  ====================================
    ``u`` (east face)    ``(1, 2)`` ``3/4``    ``h[j, i] AND h[j, i+1]``
    ``v`` (north face)   ``(2, 1)`` ``3/4``    ``h[j, i] AND h[j+1, i]``
    ``xy_corner``        ``(2, 2)`` ``1/8``    ≥ 1 of 4 NE-corner h-cells wet
    ``xy_corner_strict`` ``(2, 2)`` ``7/8``    all 4 NE-corner h-cells wet
    ==================== ========== =========  ====================================

    The land/coast classification uses two successive
    :func:`dilate_mask` passes on ``~mask_hgrid``; the stencil
    capability is built via :meth:`StencilCapability2D.from_mask`.
    """
    h_np = np.asarray(mask_hgrid, dtype=bool)
    Ny, Nx = h_np.shape
    hf = h_np.astype(np.float32)

    # ── staggered masks ───────────────────────────────────────────────
    # All staggered masks use the trailing-pad (positive half-step)
    # convention, matching the grid module's same-index rule:
    # U[j, i] at east face (i+1/2), V[j, i] at north face (j+1/2),
    # X[j, i] at NE corner (i+1/2, j+1/2).

    # u[j, i] = (h[j, i] + h[j, i+1]) / 2 > 3/4  (east face = U-point)
    u_np = pool_bool(hf, kernel=(1, 2), threshold=3.0 / 4.0, direction="trailing")
    # v[j, i] = (h[j, i] + h[j+1, i]) / 2 > 3/4  (north face = V-point)
    v_np = pool_bool(hf, kernel=(2, 1), threshold=3.0 / 4.0, direction="trailing")
    # xy_corner[j, i]: at least 1 of 4 NE-corner h-cells wet  (lenient)
    xy_corner_np = pool_bool(
        hf, kernel=(2, 2), threshold=1.0 / 8.0, direction="trailing"
    )
    # xy_corner_strict[j, i]: all 4 NE-corner h-cells wet     (strict)
    xy_corner_strict_np = pool_bool(
        hf, kernel=(2, 2), threshold=7.0 / 8.0, direction="trailing"
    )

    # ── corner boundary classification ────────────────────────────────
    # For xy_corner[j, i] at NE corner of h[j, i], the 4 incident
    # velocity faces are:
    #   u[j,   i] = east face of h[j,i]    → south of corner (vertical)
    #   u[j+1, i] = east face of h[j+1,i]  → north of corner (vertical)
    #   v[j, i  ] = north face of h[j,i]   → west of corner  (horizontal)
    #   v[j, i+1] = north face of h[j,i+1] → east of corner  (horizontal)
    # u_north and v_east are the +1-shifted neighbours; trailing-side
    # zero-pad supplies the implicit boundary face beyond the array.
    u_north = np.pad(u_np[1:, :], ((0, 1), (0, 0)))  # u[j+1, i]
    v_east = np.pad(v_np[:, 1:], ((0, 0), (0, 1)))  # v[j, i+1]

    # y-wall (vertical wall): one of the two u-faces (vertical lines) dry
    xy_corner_y_wall = xy_corner_np & (~u_np | ~u_north)
    # x-wall (horizontal wall): one of the two v-faces (horizontal lines) dry
    xy_corner_x_wall = xy_corner_np & (~v_np | ~v_east)
    # convex corner: both walls present
    xy_corner_convex = xy_corner_y_wall & xy_corner_x_wall
    # valid interior corner: all 4 adjacent faces wet
    xy_corner_valid = xy_corner_np & u_np & u_north & v_np & v_east

    # ── irregular xy_corner_strict boundary indices ───────────────────
    # Dry xy_corner_strict cells in [1:-1, 1:-1] with >=1 wet
    # xy_corner_strict cell in their 3x3 neighbourhood.
    psif = xy_corner_strict_np.astype(np.float32)
    if Ny >= 3 and Nx >= 3:
        pool3 = np.zeros((Ny - 2, Nx - 2), dtype=np.float32)
        for di in range(3):
            for dj in range(3):
                pool3 += psif[di : Ny - 2 + di, dj : Nx - 2 + dj]
        pool3 /= 9.0
        irrbound = (~xy_corner_strict_np[1:-1, 1:-1]) & (pool3 > 1.0 / 18.0)
        rows, cols = np.where(irrbound)
        # Map back from interior slice to full-array coordinates.
        rows = rows + 1
        cols = cols + 1
    else:
        rows = np.empty(0, dtype=np.int32)
        cols = np.empty(0, dtype=np.int32)

    # ── land / coast classification ───────────────────────────────────
    # 0 = land, 1 = coast (ocean adj. to land), 2 = near-coast, 3 = ocean
    land = ~h_np
    land_d1 = dilate_mask(land)
    coast = h_np & land_d1  # first ring of ocean
    land_d2 = dilate_mask(land_d1)
    near_coast = h_np & land_d2 & ~coast  # second ring
    open_ocean = h_np & ~land_d2  # interior ocean

    classification = np.zeros((Ny, Nx), dtype=np.int32)
    classification[coast] = 1
    classification[near_coast] = 2
    classification[open_ocean] = 3

    # ── stencil capability ────────────────────────────────────────────
    sc = StencilCapability2D.from_mask(h_np)

    # ── sponge layer ──────────────────────────────────────────────────
    if sponge_width is None or sponge_width == 0:
        sponge_np = np.ones((Ny, Nx), dtype=np.float32)
    else:
        if sponge_width < 0:
            raise ValueError(
                f"sponge_width must be non-negative; got {sponge_width!r}"
            )
        sponge_np = make_sponge((Ny, Nx), sponge_width)

    return cls(
        h=jnp.asarray(h_np),
        u=jnp.asarray(u_np),
        v=jnp.asarray(v_np),
        xy_corner=jnp.asarray(xy_corner_np),
        xy_corner_strict=jnp.asarray(xy_corner_strict_np),
        not_h=jnp.asarray(~h_np),
        not_u=jnp.asarray(~u_np),
        not_v=jnp.asarray(~v_np),
        not_xy_corner=jnp.asarray(~xy_corner_np),
        not_xy_corner_strict=jnp.asarray(~xy_corner_strict_np),
        xy_corner_y_wall=jnp.asarray(xy_corner_y_wall),
        xy_corner_x_wall=jnp.asarray(xy_corner_x_wall),
        xy_corner_convex=jnp.asarray(xy_corner_convex),
        xy_corner_valid=jnp.asarray(xy_corner_valid),
        xy_corner_strict_irrbound_rows=jnp.asarray(rows.astype(np.int32)),
        xy_corner_strict_irrbound_cols=jnp.asarray(cols.astype(np.int32)),
        classification=jnp.asarray(classification),
        stencil_capability=sc,
        sponge=jnp.asarray(sponge_np),
        k_bottom=jnp.asarray(k_bottom) if k_bottom is not None else None,
    )

from_u_face(field, mode='permissive', sponge_width=None, k_bottom=None) classmethod

Construct from a field at u-faces, deriving the h-grid mask.

NaN values are treated as dry u-faces. The h-grid mask is then inferred via :func:h_from_pooled with kernel (1, 2) (trailing-pad direction, matching the grid's positive-half-step convention) and the requested mode; the remaining staggered masks are produced by :meth:from_mask.

Parameters:

Name Type Description Default
field array - like[Ny, Nx]

Float field at u-faces; NaN marks dry faces.

required
mode ('permissive', 'conservative')

Inversion strategy. See :meth:Mask1D.from_u_face for details.

'permissive'
sponge_width int

Sponge layer width. See :meth:from_mask.

None
k_bottom array - like

Sea-floor indices. See :meth:from_mask.

None

Returns:

Type Description
Mask2D
Source code in finitevolx/_src/mask/cartesian.py
@classmethod
def from_u_face(
    cls,
    field: Float[Array, "Ny Nx"],
    mode: str = "permissive",
    sponge_width: int | None = None,
    k_bottom: Array | None = None,
) -> Mask2D:
    """Construct from a field at u-faces, deriving the h-grid mask.

    ``NaN`` values are treated as dry u-faces.  The h-grid mask is
    then inferred via :func:`h_from_pooled` with kernel ``(1, 2)``
    (trailing-pad direction, matching the grid's positive-half-step
    convention) and the requested ``mode``; the remaining staggered
    masks are produced by :meth:`from_mask`.

    Parameters
    ----------
    field : array-like [Ny, Nx]
        Float field at u-faces; ``NaN`` marks dry faces.
    mode : {'permissive', 'conservative'}
        Inversion strategy.  See :meth:`Mask1D.from_u_face` for
        details.
    sponge_width : int, optional
        Sponge layer width.  See :meth:`from_mask`.
    k_bottom : array-like, optional
        Sea-floor indices.  See :meth:`from_mask`.

    Returns
    -------
    Mask2D
    """
    u_mask = np.isfinite(np.asarray(field))
    h_mask = h_from_pooled(u_mask, kernel=(1, 2), mode=mode, direction="trailing")
    return cls.from_mask(h_mask, sponge_width=sponge_width, k_bottom=k_bottom)

from_v_face(field, mode='permissive', sponge_width=None, k_bottom=None) classmethod

Construct from a field at v-faces, deriving the h-grid mask.

NaN values are treated as dry v-faces. The h-grid mask is then inferred via :func:h_from_pooled with kernel (2, 1) (trailing-pad direction) and the requested mode.

Parameters:

Name Type Description Default
field array - like[Ny, Nx]

Float field at v-faces; NaN marks dry faces.

required
mode ('permissive', 'conservative')

Inversion strategy. See :meth:Mask1D.from_u_face for details.

'permissive'
sponge_width int

Sponge layer width. See :meth:from_mask.

None
k_bottom array - like

Sea-floor indices. See :meth:from_mask.

None

Returns:

Type Description
Mask2D
Source code in finitevolx/_src/mask/cartesian.py
@classmethod
def from_v_face(
    cls,
    field: Float[Array, "Ny Nx"],
    mode: str = "permissive",
    sponge_width: int | None = None,
    k_bottom: Array | None = None,
) -> Mask2D:
    """Construct from a field at v-faces, deriving the h-grid mask.

    ``NaN`` values are treated as dry v-faces.  The h-grid mask is
    then inferred via :func:`h_from_pooled` with kernel ``(2, 1)``
    (trailing-pad direction) and the requested ``mode``.

    Parameters
    ----------
    field : array-like [Ny, Nx]
        Float field at v-faces; ``NaN`` marks dry faces.
    mode : {'permissive', 'conservative'}
        Inversion strategy.  See :meth:`Mask1D.from_u_face` for
        details.
    sponge_width : int, optional
        Sponge layer width.  See :meth:`from_mask`.
    k_bottom : array-like, optional
        Sea-floor indices.  See :meth:`from_mask`.

    Returns
    -------
    Mask2D
    """
    v_mask = np.isfinite(np.asarray(field))
    h_mask = h_from_pooled(v_mask, kernel=(2, 1), mode=mode, direction="trailing")
    return cls.from_mask(h_mask, sponge_width=sponge_width, k_bottom=k_bottom)

get_adaptive_masks(direction='x', source='h', stencil_sizes=(2, 4, 6, 8, 10))

Per-point adaptive stencil-size masks for 2-D operators.

For each stencil size s in stencil_sizes, returns a boolean mask that is True at cells where a symmetric stencil of half-width s//2 is fully supported by contiguous wet neighbours.

These masks are useful for any operator that wants to fall back to a narrower stencil near coastlines or irregular boundaries. Common consumers include:

  • WENO reconstruction: size 2 → upwind1, 4 → WENO3, 6 → WENO5, 8 → WENO7, 10 → WENO9 (half-widths 1 / 2 / 3 / 4 / 5).
  • Higher-order upwind advection (e.g. 3rd / 5th-order upwind).
  • Higher-order centred finite-difference operators (4th / 6th / 8th-order Laplacians, gradients, etc.).
  • Higher-order interpolation kernels (cubic / quintic T↔face / T↔corner interpolations).

The returned masks are mutually exclusive hierarchical tiers: the mask for size s is True only where s is the largest usable stencil.

Parameters:

Name Type Description Default
direction ('x', 'y')

Stencil direction.

'x'
source ('h', 'u', 'v', 'xy_corner', 'xy_corner_strict')

Source grid whose stencil capability to use.

'h'
stencil_sizes sequence of int

Ordered candidate stencil sizes (even integers).

(2, 4, 6, 8, 10)

Returns:

Type Description
dict[int, Bool[Array, 'Ny Nx']]

Mapping from stencil size to its mutually-exclusive boolean mask.

Source code in finitevolx/_src/mask/cartesian.py
def get_adaptive_masks(
    self,
    direction: str = "x",
    source: str = "h",
    stencil_sizes: tp.Sequence[int] = (2, 4, 6, 8, 10),
) -> dict[int, Bool[Array, "Ny Nx"]]:
    """Per-point adaptive stencil-size masks for 2-D operators.

    For each stencil size *s* in ``stencil_sizes``, returns a boolean mask
    that is ``True`` at cells where a symmetric stencil of half-width
    *s//2* is fully supported by contiguous wet neighbours.

    These masks are useful for any operator that wants to fall back
    to a narrower stencil near coastlines or irregular boundaries.
    Common consumers include:

    * **WENO reconstruction**: size 2 → upwind1, 4 → WENO3, 6 → WENO5,
      8 → WENO7, 10 → WENO9 (half-widths 1 / 2 / 3 / 4 / 5).
    * **Higher-order upwind advection** (e.g. 3rd / 5th-order upwind).
    * **Higher-order centred finite-difference** operators (4th / 6th /
      8th-order Laplacians, gradients, etc.).
    * **Higher-order interpolation** kernels (cubic / quintic
      T↔face / T↔corner interpolations).

    The returned masks are **mutually exclusive** hierarchical tiers: the
    mask for size *s* is ``True`` only where *s* is the *largest* usable
    stencil.

    Parameters
    ----------
    direction : {'x', 'y'}
        Stencil direction.
    source : {'h', 'u', 'v', 'xy_corner', 'xy_corner_strict'}
        Source grid whose stencil capability to use.
    stencil_sizes : sequence of int
        Ordered candidate stencil sizes (even integers).

    Returns
    -------
    dict[int, Bool[Array, "Ny Nx"]]
        Mapping from stencil size to its mutually-exclusive boolean mask.
    """
    sc = self._stencil_capability_for(source)
    if direction == "x":
        cnt_pos, cnt_neg = sc.x_pos, sc.x_neg
    elif direction == "y":
        cnt_pos, cnt_neg = sc.y_pos, sc.y_neg
    else:
        raise ValueError(f"direction must be 'x' or 'y', got {direction!r}")

    # Maximum usable stencil size at each point
    max_s = jnp.zeros(self.h.shape, dtype=jnp.int32)
    for s in sorted(stencil_sizes):
        hw = s // 2
        can_use = (cnt_pos >= hw) & (cnt_neg >= hw)
        max_s = jnp.where(can_use, s, max_s)

    # Mutually-exclusive masks
    return {s: (max_s == s) for s in stencil_sizes}

Stencil Capability

finitevolx.StencilCapability2D

Bases: Module

Directional count of contiguous wet neighbours for each 2-D grid cell.

At each cell (j, i), stores the number of consecutive wet cells (including the cell itself) reachable before hitting a dry cell or the domain edge.

Parameters:

Name Type Description Default
x_pos Int[Array, 'Ny Nx']

Count in the +x direction.

required
x_neg Int[Array, 'Ny Nx']

Count in the −x direction.

required
y_pos Int[Array, 'Ny Nx']

Count in the +y direction.

required
y_neg Int[Array, 'Ny Nx']

Count in the −y direction.

required
Source code in finitevolx/_src/mask/base.py
class StencilCapability2D(eqx.Module):
    """Directional count of contiguous wet neighbours for each 2-D grid cell.

    At each cell ``(j, i)``, stores the number of consecutive wet cells
    (including the cell itself) reachable before hitting a dry cell or
    the domain edge.

    Parameters
    ----------
    x_pos : Int[Array, "Ny Nx"]
        Count in the +x direction.
    x_neg : Int[Array, "Ny Nx"]
        Count in the −x direction.
    y_pos : Int[Array, "Ny Nx"]
        Count in the +y direction.
    y_neg : Int[Array, "Ny Nx"]
        Count in the −y direction.
    """

    x_pos: Int[Array, "Ny Nx"]
    x_neg: Int[Array, "Ny Nx"]
    y_pos: Int[Array, "Ny Nx"]
    y_neg: Int[Array, "Ny Nx"]

    @classmethod
    def from_mask(cls, h: np.ndarray | Bool[Array, "Ny Nx"]) -> StencilCapability2D:
        """Build stencil capability from a 2-D wet/dry mask.

        Construction uses numpy; stored arrays are JAX int32.

        Parameters
        ----------
        h : array-like [Ny, Nx] bool
            Wet (True) / dry (False) mask.

        Returns
        -------
        StencilCapability2D
        """
        h_np = np.asarray(h, dtype=bool)
        return cls(
            x_pos=jnp.asarray(count_contiguous(h_np, axis=1, forward=True)),
            x_neg=jnp.asarray(count_contiguous(h_np, axis=1, forward=False)),
            y_pos=jnp.asarray(count_contiguous(h_np, axis=0, forward=True)),
            y_neg=jnp.asarray(count_contiguous(h_np, axis=0, forward=False)),
        )

from_mask(h) classmethod

Build stencil capability from a 2-D wet/dry mask.

Construction uses numpy; stored arrays are JAX int32.

Parameters:

Name Type Description Default
h array-like [Ny, Nx] bool

Wet (True) / dry (False) mask.

required

Returns:

Type Description
StencilCapability2D
Source code in finitevolx/_src/mask/base.py
@classmethod
def from_mask(cls, h: np.ndarray | Bool[Array, "Ny Nx"]) -> StencilCapability2D:
    """Build stencil capability from a 2-D wet/dry mask.

    Construction uses numpy; stored arrays are JAX int32.

    Parameters
    ----------
    h : array-like [Ny, Nx] bool
        Wet (True) / dry (False) mask.

    Returns
    -------
    StencilCapability2D
    """
    h_np = np.asarray(h, dtype=bool)
    return cls(
        x_pos=jnp.asarray(count_contiguous(h_np, axis=1, forward=True)),
        x_neg=jnp.asarray(count_contiguous(h_np, axis=1, forward=False)),
        y_pos=jnp.asarray(count_contiguous(h_np, axis=0, forward=True)),
        y_neg=jnp.asarray(count_contiguous(h_np, axis=0, forward=False)),
    )