#! /usr/bin/env python
"""Calculate vector divergence and related quantities at nodes or cells."""
import numpy as np

from landlab.utils.decorators import use_field_name_or_array


@use_field_name_or_array("link")
def calc_flux_div_at_node(grid, unit_flux, out=None):
    """Calculate divergence of link-based fluxes at nodes.

    Given a flux per unit width across each face in the grid, calculate the net
    outflux (or influx, if negative) divided by cell area, at each node (zero
    or "out" value for nodes without cells).

    Parameters
    ----------
    grid : ModelGrid
        A ModelGrid.
    unit_flux : ndarray or field name
        Flux per unit width along links (x number of links).

    Returns
    -------
    ndarray (x number of nodes)
        Flux divergence at nodes.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> from landlab.grid.divergence import calc_flux_div_at_node
    >>> rg = RasterModelGrid((3, 4), xy_spacing=10.0)
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> z[5] = 50.0
    >>> z[6] = 36.0
    >>> lg = rg.calc_grad_at_link(z)  # there are 17 links
    >>> lg
    array([ 0. ,  0. ,  0. ,  0. ,  5. ,  3.6,  0. ,  5. , -1.4, -3.6,  0. ,
           -5. , -3.6,  0. ,  0. ,  0. ,  0. ])
    >>> calc_flux_div_at_node(rg, -lg)
    array([ 0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  1.64,  0.94,  0.  ,  0.  ,
            0.  ,  0.  ,  0.  ])
    >>> rg.set_status_at_node_on_edges(right=rg.BC_NODE_IS_CLOSED)
    >>> rg.set_status_at_node_on_edges(top=rg.BC_NODE_IS_CLOSED)
    >>> unit_flux_at_links = np.zeros(rg.number_of_links)
    >>> unit_flux_at_links[rg.active_links] = -lg[rg.active_links]
    >>> calc_flux_div_at_node(rg, unit_flux_at_links)
    array([ 0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  1.14,  0.22,  0.  ,  0.  ,
            0.  ,  0.  ,  0.  ])
    >>> _ = rg.add_field("neg_grad_at_link", -lg, at="link")
    >>> calc_flux_div_at_node(rg, 'neg_grad_at_link')
    array([ 0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  1.64,  0.94,  0.  ,  0.  ,
            0.  ,  0.  ,  0.  ])


    Notes
    -----
    Performs a numerical flux divergence operation on nodes.

    LLCATS: NINF GRAD
    """
    if unit_flux.size != grid.number_of_links:
        raise ValueError("Parameter unit_flux must be num links " "long")
    if out is None:
        out = grid.zeros(at="node")
    elif out.size != grid.number_of_nodes:
        raise ValueError("output buffer length mismatch with number of nodes")

    out[grid.node_at_cell] = (
        _calc_net_face_flux_at_cell(grid, unit_flux[grid.link_at_face])
        / grid.area_of_cell
    )

    return out


@use_field_name_or_array("link")
def calc_flux_div_at_cell(grid, unit_flux, out=None):
    """Calculate divergence of link-based fluxes at cells.

    This function is very similar to the function *calc_flux_div_at_node*.

    Given a flux per unit width across each cell face in the grid, calculate
    the net outflux (or influx, if negative) divided by cell area, at each
    cell.

    Parameters
    ----------
    grid : ModelGrid
        A ModelGrid.
    unit_flux_at_links_across_faces : ndarray or field name
        Flux per unit width along links at faces (x number of faces) or link
        field.

    Returns
    -------
    ndarray (x number of cells)
        Flux divergence at cells.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> from landlab.grid.divergence import calc_flux_div_at_cell
    >>> rg = RasterModelGrid((3, 4), xy_spacing=10.0)
    >>> import numpy as np
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> z[5] = 50.0
    >>> z[6] = 36.0
    >>> lg = rg.calc_grad_at_link(z)  # there are 17 links
    >>> lg
    array([ 0. ,  0. ,  0. ,  0. ,  5. ,  3.6,  0. ,  5. , -1.4, -3.6,  0. ,
           -5. , -3.6,  0. ,  0. ,  0. ,  0. ])
    >>> fg = lg[rg.link_at_face]  # there are 7 faces
    >>> fg
    array([ 5. ,  3.6,  5. , -1.4, -3.6, -5. , -3.6])
    >>> calc_flux_div_at_cell(rg, -fg)
    array([ 1.64,  0.94])
    >>> rg.set_status_at_node_on_edges(right=rg.BC_NODE_IS_CLOSED)
    >>> rg.set_status_at_node_on_edges(top=rg.BC_NODE_IS_CLOSED)
    >>> unit_flux_at_faces = np.zeros(rg.number_of_faces)
    >>> unit_flux_at_faces[rg.active_faces] = -fg[rg.active_faces]
    >>> calc_flux_div_at_cell(rg, unit_flux_at_faces)
    array([ 1.14,  0.22])
    >>> _ = rg.add_field("neg_grad_at_link", -lg, at="link")
    >>> calc_flux_div_at_cell(rg, 'neg_grad_at_link')
    array([ 1.64,  0.94])

    Notes
    -----
    Performs a numerical flux divergence operation at cells.

    LLCATS: NINF GRAD
    """
    if unit_flux.size not in (grid.number_of_links, grid.number_of_faces):
        raise ValueError(
            "Parameter unit_flux must be num faces long"
            "or the field name for a at_link array"
        )
    if out is None:
        out = grid.zeros(at="cell")
    elif out.size != grid.number_of_cells:
        raise ValueError("output buffer length mismatch with number of cells")

    if unit_flux.size == grid.number_of_links:
        out = (
            _calc_net_face_flux_at_cell(grid, unit_flux[grid.link_at_face])
            / grid.area_of_cell
        )
    elif unit_flux.size == grid.number_of_faces:
        out = _calc_net_face_flux_at_cell(grid, unit_flux) / grid.area_of_cell

    return out


@use_field_name_or_array("link")
def calc_net_flux_at_node(grid, unit_flux_at_links, out=None):
    """Calculate net link fluxes at nodes.

    Given a flux per unit width along each link in the grid, calculate the net
    outflux (or influx, if negative) at each node. Fluxes are treated as zero
    for links that have no faces, and net fluxes are treated as zero for nodes
    that have no cell.

    Parameters
    ----------
    grid : ModelGrid
        A ModelGrid.
    unit_flux_at_links : ndarray or field name
        Flux per unit width associated with links.
    out : ndarray, optional
        Buffer to hold the result.

    Returns
    -------
    ndarray (x number of cells)
        Net flux at nodes.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> rg = RasterModelGrid((3, 4), xy_spacing=10.0)
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> z[5] = 50.0
    >>> z[6] = 36.0
    >>> lg = rg.calc_grad_at_link(z)  # there are 17 links
    >>> lg
    array([ 0. ,  0. ,  0. ,  0. ,  5. ,  3.6,  0. ,  5. , -1.4, -3.6,  0. ,
           -5. , -3.6,  0. ,  0. ,  0. ,  0. ])
    >>> calc_net_flux_at_node(rg, -lg)
    array([   0.,    0.,    0.,    0.,    0.,  164.,   94.,    0.,    0.,
              0.,    0.,    0.])
    >>> rg.set_status_at_node_on_edges(right=rg.BC_NODE_IS_CLOSED)
    >>> rg.set_status_at_node_on_edges(top=rg.BC_NODE_IS_CLOSED)
    >>> unit_flux_at_links = np.zeros(rg.number_of_links)
    >>> unit_flux_at_links[rg.active_links] = -lg[rg.active_links]
    >>> nlfn = calc_net_flux_at_node(rg, unit_flux_at_links)
    >>> np.round(nlfn)
    array([   0.,    0.,    0.,    0.,    0.,  114.,   22.,    0.,    0.,
              0.,    0.,    0.])

    >>> from landlab import HexModelGrid
    >>> hg = HexModelGrid((3, 3), spacing=10.0)
    >>> z = hg.add_zeros("topographic__elevation", at="node", clobber=True)
    >>> z[4] = 50.0
    >>> z[5] = 36.0
    >>> lg = hg.calc_grad_at_link(z)  # there are ? links
    >>> lg
    array([ 0. ,  0. ,  0. ,  5. ,  5. ,  3.6,  3.6,  0. ,  5. , -1.4, -3.6,
            0. , -5. , -5. , -3.6, -3.6,  0. ,  0. ,  0. ])
    >>> nlfn = calc_net_flux_at_node(hg, -lg)
    >>> np.round(nlfn)
    array([   0.,    0.,    0.,    0.,  152.,   96.,    0.,    0.,    0.,    0.])

    Notes
    -----
    This is essentially a line integral for the fluxes along the boundaries of
    each cell. Hence, the resulting output has dimensions of total flux (so,
    if the unit flux happens to be mass per time per face width, the output
    will be in mass per unit time). Because a line integral is undefined where
    there are no cells (i.e., perimeter nodes), the result is given as zeros
    for these nodes. The current algorithm uses fancy indexing (calling
    _calc_net_face_flux_at_cells) and could probably be made faster.

    LLCATS: NINF GRAD
    """
    if out is None:
        out = grid.zeros(at="node")

    out[grid.node_at_cell] = _calc_net_face_flux_at_cell(
        grid, unit_flux_at_links[grid.link_at_face]
    )
    return out


@use_field_name_or_array("face")
def _calc_net_face_flux_at_cell(grid, unit_flux_at_faces, out=None):
    """Calculate net face fluxes at cells.

    Given a flux per unit width across each face in the grid, calculate the net
    outflux (or influx, if negative) at each cell.

    Parameters
    ----------
    grid : ModelGrid
        A ModelGrid.
    unit_flux_at_faces : ndarray or field name
        Flux per unit width associated with faces.
    out : ndarray, optional
        Buffer to hold the result.

    Returns
    -------
    ndarray (x number of cells)
        Net flux at cells.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> rg = RasterModelGrid((3, 4), xy_spacing=10.0)
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> z[5] = 50.0
    >>> z[6] = 36.0
    >>> lg = rg.calc_grad_at_link(z)
    >>> fg = lg[rg.link_at_face]  # there are 7 faces
    >>> fg
    array([ 5. ,  3.6,  5. , -1.4, -3.6, -5. , -3.6])
    >>> _calc_net_face_flux_at_cell(rg, -fg)
    array([ 164.,   94.])
    >>> rg.set_status_at_node_on_edges(right=rg.BC_NODE_IS_CLOSED)
    >>> rg.set_status_at_node_on_edges(top=rg.BC_NODE_IS_CLOSED)
    >>> unit_flux_at_faces = np.zeros(rg.number_of_faces)
    >>> unit_flux_at_faces[rg.active_faces] = -fg[rg.active_faces]
    >>> _calc_net_face_flux_at_cell(rg, unit_flux_at_faces)
    array([ 114.,   22.])

    >>> from landlab import HexModelGrid
    >>> hg = HexModelGrid((3, 3), spacing=10.0)
    >>> z = hg.add_zeros("topographic__elevation", at="node", clobber=True)
    >>> z[4] = 50.0
    >>> z[5] = 36.0
    >>> lg = hg.calc_grad_at_link(z)
    >>> fg = lg[hg.link_at_face]  # there are 11 faces
    >>> fg
    array([ 5. ,  5. ,  3.6,  3.6,  5. , -1.4, -3.6, -5. , -5. , -3.6, -3.6])
    >>> nffc = _calc_net_face_flux_at_cell(hg, -fg)
    >>> np.round(nffc)
    array([ 152.,   96.])

    Notes
    -----
    This is essentially a line integral for the fluxes along the boundaries of
    each cell. Hence, the resulting output has dimensions of total flux (so,
    if the unit flux happens to be mass per time per face width, the output
    will be in mass per unit time).
    """
    if out is None:
        out = grid.empty(at="cell")
    total_flux = unit_flux_at_faces * grid.length_of_face
    out = np.zeros(grid.number_of_cells)
    fac = grid.faces_at_cell
    for c in range(grid.link_dirs_at_node.shape[1]):
        out -= total_flux[fac[:, c]] * grid.link_dirs_at_node[grid.node_at_cell, c]
    return out


@use_field_name_or_array("face")
def _calc_face_flux_divergence_at_cell(grid, unit_flux_at_faces):
    """Calculate divergence of face-based fluxes at cells.

    Given a flux per unit width across each face in the grid, calculate the net
    outflux (or influx, if negative) divided by cell area, at each cell.

    Parameters
    ----------
    grid : ModelGrid
        A ModelGrid.
    unit_flux_at_faces : ndarray or field name
        Flux per unit width associated with faces.

    Returns
    -------
    ndarray (x number of cells)
        Flux divergence at cells.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> rg = RasterModelGrid((3, 4), xy_spacing=10.0)
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> z[5] = 50.0
    >>> z[6] = 36.0
    >>> lg = rg.calc_grad_at_link(z)
    >>> lg[rg.link_at_face]
    array([ 5. ,  3.6,  5. , -1.4, -3.6, -5. , -3.6])
    >>> _calc_face_flux_divergence_at_cell(rg, -lg[rg.link_at_face])
    array([ 1.64,  0.94])
    >>> rg.set_status_at_node_on_edges(right=rg.BC_NODE_IS_CLOSED)
    >>> rg.set_status_at_node_on_edges(top=rg.BC_NODE_IS_CLOSED)
    >>> unit_flux_at_faces = np.zeros(rg.number_of_faces)
    >>> fg = lg[rg.link_at_face]
    >>> unit_flux_at_faces[rg.active_faces] = -fg[rg.active_faces]
    >>> _calc_face_flux_divergence_at_cell(rg, unit_flux_at_faces)
    array([ 1.14,  0.22])

    Notes
    -----
    Performs a numerical flux divergence operation on cells.
    """
    return _calc_net_face_flux_at_cell(grid, unit_flux_at_faces) / grid.area_of_cell


@use_field_name_or_array("face")
def _calc_net_active_face_flux_at_cell(grid, unit_flux_at_faces, out=None):
    """Calculate net face fluxes at cells, ignoring values on inactive faces.

    Given a flux per unit width across each face in the grid, calculate the net
    outflux (or influx, if negative) at each cell. Same as
    `_calc_net_face_flux_at_cell` except that flux values on inactive faces
    are ignored.

    Parameters
    ----------
    grid : ModelGrid
        A ModelGrid.
    unit_flux_at_faces : ndarray or field name (x number of faces)
        Flux per unit width associated with faces.
    out : ndarray, optional
        Buffer to hold the result.

    Returns
    -------
    ndarray (x number of cells)
        Net flux at cells.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> rg = RasterModelGrid((3, 4), xy_spacing=10.0)
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> z[5] = 50.0
    >>> z[6] = 36.0
    >>> fg = rg.calc_grad_at_link(z)[rg.link_at_face]  # there are 7 faces
    >>> fg
    array([ 5. ,  3.6,  5. , -1.4, -3.6, -5. , -3.6])
    >>> _calc_net_active_face_flux_at_cell(rg, -fg)
    array([ 164.,   94.])
    >>> rg.set_status_at_node_on_edges(right=rg.BC_NODE_IS_CLOSED)
    >>> rg.set_status_at_node_on_edges(top=rg.BC_NODE_IS_CLOSED)
    >>> _calc_net_active_face_flux_at_cell(rg, -fg)
    array([ 114.,   22.])

    >>> from landlab import HexModelGrid
    >>> hg = HexModelGrid((3, 3), spacing=10.0)
    >>> z = hg.add_zeros("topographic__elevation", at="node", clobber=True)
    >>> z[4] = 50.0
    >>> z[5] = 36.0
    >>> fg = hg.calc_grad_at_link(z)[hg.link_at_face]  # there are 11 faces
    >>> fg
    array([ 5. ,  5. ,  3.6,  3.6,  5. , -1.4, -3.6, -5. , -5. , -3.6, -3.6])
    >>> nffc = _calc_net_active_face_flux_at_cell(hg, -fg)
    >>> np.round(nffc)
    array([ 152.,   96.])

    Notes
    -----
    This is essentially a line integral for the fluxes along the boundaries of
    each cell. Hence, the resulting output has dimensions of total flux (so,
    if the unit flux happens to be mass per time per face width, the output
    will be in mass per unit time).
    """
    if out is None:
        out = grid.empty(at="cell")
    total_flux = unit_flux_at_faces * grid.length_of_face
    out = np.zeros(grid.number_of_cells)
    fac = grid.faces_at_cell
    for c in range(grid.active_link_dirs_at_node.shape[1]):
        out -= (
            total_flux[fac[:, c]] * grid.active_link_dirs_at_node[grid.node_at_cell, c]
        )
    return out


@use_field_name_or_array("face")
def _calc_active_face_flux_divergence_at_cell(grid, unit_flux_at_faces):
    """Calculate divergence of face-based fluxes at cells, ignoring values on
    inactive faces.

    Given a flux per unit width across each face in the grid, calculate the net
    outflux (or influx, if negative) divided by cell area, at each cell. Same
    as `_calc_face_flux_divergence_at_cell` except that flux values at inactive
    faces are ignored.

    Parameters
    ----------
    grid : ModelGrid
        A ModelGrid.
    unit_flux_at_faces : ndarray or field name (x number of faces)
        Flux per unit width associated with faces.

    Returns
    -------
    ndarray (x number of cells)
        Flux divergence at cells.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> rg = RasterModelGrid((3, 4), xy_spacing=10.0)
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> z[5] = 50.0
    >>> z[6] = 36.0
    >>> fg = rg.calc_grad_at_link(z)[rg.link_at_face]  # there are 7 faces
    >>> fg
    array([ 5. ,  3.6,  5. , -1.4, -3.6, -5. , -3.6])
    >>> _calc_active_face_flux_divergence_at_cell(rg, -fg)
    array([ 1.64,  0.94])
    >>> rg.set_status_at_node_on_edges(right=rg.BC_NODE_IS_CLOSED)
    >>> rg.set_status_at_node_on_edges(top=rg.BC_NODE_IS_CLOSED)
    >>> _calc_active_face_flux_divergence_at_cell(rg, -fg)
    array([ 1.14,  0.22])

    Notes
    -----
    Performs a numerical flux divergence operation on cells.
    """
    return (
        _calc_net_active_face_flux_at_cell(grid, unit_flux_at_faces) / grid.area_of_cell
    )


@use_field_name_or_array("link")
def _calc_net_active_link_flux_at_node(grid, unit_flux_at_links, out=None):
    """Calculate net link fluxes at nodes, ignoring fluxes on inactive links.

    Given a flux per unit width along each link in the grid, calculate the net
    outflux (or influx, if negative) at each node. Fluxes are treated as zero
    for links that have no faces, and net fluxes are treated as zero for nodes
    that have no cell. Same as `_calc_net_link_flux_at_node` except that it
    ignores any flux values on inactive links.

    Parameters
    ----------
    grid : ModelGrid
        A ModelGrid.
    unit_flux_at_links : ndarray or field name (x number of links)
        Flux per unit width associated with links.
    out : ndarray, optional
        Buffer to hold the result.

    Returns
    -------
    ndarray (x number of cells)
        Net flux at nodes.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> rg = RasterModelGrid((3, 4), xy_spacing=10.0)
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> z[5] = 50.0
    >>> z[6] = 36.0
    >>> lg = rg.calc_grad_at_link(z)  # there are 17 links
    >>> lg
    array([ 0. ,  0. ,  0. ,  0. ,  5. ,  3.6,  0. ,  5. , -1.4, -3.6,  0. ,
           -5. , -3.6,  0. ,  0. ,  0. ,  0. ])
    >>> _calc_net_active_link_flux_at_node(rg, -lg)
    array([   0.,    0.,    0.,    0.,    0.,  164.,   94.,    0.,    0.,
              0.,    0.,    0.])
    >>> rg.set_status_at_node_on_edges(right=rg.BC_NODE_IS_CLOSED)
    >>> rg.set_status_at_node_on_edges(top=rg.BC_NODE_IS_CLOSED)
    >>> nlfn = _calc_net_active_link_flux_at_node(rg, -lg)
    >>> np.round(nlfn)
    array([   0.,    0.,    0.,    0.,    0.,  114.,   22.,    0.,    0.,
              0.,    0.,    0.])

    >>> from landlab import HexModelGrid
    >>> hg = HexModelGrid((3, 3), spacing=10.0)
    >>> z = hg.add_zeros("topographic__elevation", at="node", clobber=True)
    >>> z[4] = 50.0
    >>> z[5] = 36.0
    >>> lg = hg.calc_grad_at_link(z)  # there are ? links
    >>> lg
    array([ 0. ,  0. ,  0. ,  5. ,  5. ,  3.6,  3.6,  0. ,  5. , -1.4, -3.6,
            0. , -5. , -5. , -3.6, -3.6,  0. ,  0. ,  0. ])
    >>> nlfn = _calc_net_active_link_flux_at_node(hg, -lg)
    >>> np.round(nlfn)
    array([   0.,    0.,    0.,    0.,  152.,   96.,    0.,    0.,    0.,    0.])

    Notes
    -----
    This is essentially a line integral for the fluxes along the boundaries of
    each cell. Hence, the resulting output has dimensions of total flux (so,
    if the unit flux happens to be mass per time per face width, the output
    will be in mass per unit time). Because a line integral is undefined where
    there are no cells (i.e., perimeter nodes), the result is given as zeros
    for these nodes. The current algorithm uses fancy indexing (calling
    _calc_net_face_flux_at_cells) and could probably be made faster.
    """
    if out is None:
        out = grid.zeros(at="node")

    out[grid.node_at_cell] = _calc_net_active_face_flux_at_cell(
        grid, unit_flux_at_links[grid.link_at_face]
    )
    return out


@use_field_name_or_array("link")
def _calc_active_link_flux_divergence_at_node(grid, unit_flux_at_links, out=None):
    """Calculate divergence of link-based fluxes at nodes, ignoring any fluxes
    at inactive links.

    Given a flux per unit width across each face in the grid, calculate the net
    outflux (or influx, if negative) divided by cell area, at each node (zero
    or "out" value for nodes without cells).

    Parameters
    ----------
    grid : ModelGrid
        A ModelGrid.
    unit_flux_at_links : ndarray or field name (x number of links)
        Flux per unit width associated with links.

    Returns
    -------
    ndarray (x number of nodes)
        Flux divergence at nodes.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> rg = RasterModelGrid((3, 4), xy_spacing=10.0)
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> z[5] = 50.0
    >>> z[6] = 36.0
    >>> lg = rg.calc_grad_at_link(z)  # there are 17 links
    >>> lg
    array([ 0. ,  0. ,  0. ,  0. ,  5. ,  3.6,  0. ,  5. , -1.4, -3.6,  0. ,
           -5. , -3.6,  0. ,  0. ,  0. ,  0. ])
    >>> _calc_active_link_flux_divergence_at_node(rg, -lg)
    array([ 0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  1.64,  0.94,  0.  ,  0.  ,
            0.  ,  0.  ,  0.  ])
    >>> rg.set_status_at_node_on_edges(right=rg.BC_NODE_IS_CLOSED)
    >>> rg.set_status_at_node_on_edges(top=rg.BC_NODE_IS_CLOSED)
    >>> _calc_active_link_flux_divergence_at_node(rg, -lg)
    array([ 0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  1.14,  0.22,  0.  ,  0.  ,
            0.  ,  0.  ,  0.  ])

    Notes
    -----
    Performs a numerical flux divergence operation on nodes.
    """
    if out is None:
        out = grid.zeros(at="node")

    out[grid.node_at_cell] = (
        _calc_net_active_face_flux_at_cell(grid, unit_flux_at_links[grid.link_at_face])
        / grid.area_of_cell
    )
    return out


@use_field_name_or_array("face")
def _calc_net_face_flux_at_node(grid, unit_flux_at_faces, out=None):
    """Calculate net face fluxes at nodes.

    Given a flux per unit width across each face in the grid, calculate the net
    outflux (or influx, if negative) at each node (nodes without cells are
    zero, or unchanged from `out` parameter if provided)

    Parameters
    ----------
    grid : ModelGrid
        A ModelGrid.
    unit_flux_at_faces : ndarray or field name
        Flux per unit width associated with faces.
    out : ndarray, optional
        Buffer to hold the result.

    Returns
    -------
    ndarray (x number of nodes)
        Net flux at nodes.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> rg = RasterModelGrid((3, 4), xy_spacing=10.0)
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> z[5] = 50.0
    >>> z[6] = 36.0
    >>> fg = rg.calc_grad_at_link(z)[rg.link_at_face]  # there are 7 faces
    >>> fg
    array([ 5. ,  3.6,  5. , -1.4, -3.6, -5. , -3.6])
    >>> _calc_net_face_flux_at_node(rg, -fg)
    array([   0.,    0.,    0.,    0.,    0.,  164.,   94.,    0.,    0.,
              0.,    0.,    0.])
    >>> rg.set_status_at_node_on_edges(right=rg.BC_NODE_IS_CLOSED)
    >>> rg.set_status_at_node_on_edges(top=rg.BC_NODE_IS_CLOSED)
    >>> unit_flux_at_faces = np.zeros(rg.number_of_faces)
    >>> unit_flux_at_faces[rg.active_faces] = -fg[rg.active_faces]
    >>> _calc_net_face_flux_at_node(rg, unit_flux_at_faces)
    array([   0.,    0.,    0.,    0.,    0.,  114.,   22.,    0.,    0.,
              0.,    0.,    0.])

    >>> from landlab import HexModelGrid
    >>> hg = HexModelGrid((3, 3), spacing=10.0)
    >>> z = hg.add_zeros("topographic__elevation", at="node", clobber=True)
    >>> z[4] = 50.0
    >>> z[5] = 36.0
    >>> fg = hg.calc_grad_at_link(z)[hg.link_at_face]  # there are 11 faces
    >>> fg
    array([ 5. ,  5. ,  3.6,  3.6,  5. , -1.4, -3.6, -5. , -5. , -3.6, -3.6])
    >>> nffc = _calc_net_face_flux_at_node(hg, -fg)
    >>> np.round(nffc)
    array([   0.,    0.,    0.,    0.,  152.,   96.,    0.,    0.,    0.,    0.])

    Notes
    -----
    Like _calc_net_face_flux_at_cells, this essentially performs a line integral
    for the fluxes along the boundaries of each cell. Nodes without cells are
    either assigned a zero value, or if `out` is provided, they retain their
    previous values.
    """
    if out is None:
        out = grid.zeros(at="node")

    out[grid.node_at_cell] = _calc_net_face_flux_at_cell(grid, unit_flux_at_faces)
    return out


@use_field_name_or_array("face")
def _calc_net_active_face_flux_at_node(grid, unit_flux_at_faces, out=None):
    """Calculate net face fluxes at nodes, ignore inactive faces.

    Given a flux per unit width across each face in the grid, calculate the net
    outflux (or influx, if negative) at each node (nodes without cells are
    zero, or unchanged from `out` parameter if provided). Same as
    `_calc_net_face_flux_at_node` except that it ignores inactive faces.

    Parameters
    ----------
    grid : ModelGrid
        A ModelGrid.
    unit_flux_at_faces : ndarray or field name (x number of faces)
        Flux per unit width associated with faces.
    out : ndarray, optional
        Buffer to hold the result.

    Returns
    -------
    ndarray (x number of nodes)
        Net flux at nodes.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> rg = RasterModelGrid((3, 4), xy_spacing=10.0)
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> z[5] = 50.0
    >>> z[6] = 36.0
    >>> fg = rg.calc_grad_at_link(z)[rg.link_at_face]  # there are 7 faces
    >>> fg
    array([ 5. ,  3.6,  5. , -1.4, -3.6, -5. , -3.6])
    >>> _calc_net_active_face_flux_at_node(rg, -fg)
    array([   0.,    0.,    0.,    0.,    0.,  164.,   94.,    0.,    0.,
              0.,    0.,    0.])
    >>> rg.set_status_at_node_on_edges(right=rg.BC_NODE_IS_CLOSED)
    >>> rg.set_status_at_node_on_edges(top=rg.BC_NODE_IS_CLOSED)
    >>> _calc_net_active_face_flux_at_node(rg, -fg)
    array([   0.,    0.,    0.,    0.,    0.,  114.,   22.,    0.,    0.,
              0.,    0.,    0.])

    >>> from landlab import HexModelGrid
    >>> hg = HexModelGrid((3, 3), spacing=10.0)
    >>> z = hg.add_zeros("topographic__elevation", at="node", clobber=True)
    >>> z[4] = 50.0
    >>> z[5] = 36.0
    >>> fg = hg.calc_grad_at_link(z)[hg.link_at_face]  # there are 11 faces
    >>> fg
    array([ 5. ,  5. ,  3.6,  3.6,  5. , -1.4, -3.6, -5. , -5. , -3.6, -3.6])
    >>> nffc = _calc_net_active_face_flux_at_node(hg, -fg)
    >>> np.round(nffc)
    array([   0.,    0.,    0.,    0.,  152.,   96.,    0.,    0.,    0.,    0.])

    Notes
    -----
    Like _calc_net_face_flux_at_cells, this essentially performs a line integral
    for the fluxes along the boundaries of each cell. Nodes without cells are
    either assigned a zero value, or if `out` is provided, they retain their
    previous values.
    """
    if out is None:
        out = grid.zeros(at="node")

    out[grid.node_at_cell] = _calc_net_active_face_flux_at_cell(
        grid, unit_flux_at_faces
    )
    return out


@use_field_name_or_array("face")
def _calc_active_face_flux_divergence_at_node(grid, unit_flux_at_faces, out=None):
    """Calculate divergence of face-based fluxes at nodes (active faces only).

    Given a flux per unit width across each face in the grid, calculate the net
    outflux (or influx, if negative) divided by cell area, at each node that
    lies within a cell.

    Parameters
    ----------
    grid : ModelGrid
        A ModelGrid.
    unit_flux_at_faces : ndarray or field name (x number of faces)
        Flux per unit width associated with faces.
    out : ndarray (x number of nodes), optional
        Buffer to hold the result.

    Returns
    -------
    ndarray (x number of nodes)
        Flux divergence at nodes.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> rg = RasterModelGrid((3, 4), xy_spacing=10.0)
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> z[5] = 50.0
    >>> z[6] = 36.0
    >>> fg = rg.calc_grad_at_link(z)[rg.link_at_face]  # there are 7 faces
    >>> fg
    array([ 5. ,  3.6,  5. , -1.4, -3.6, -5. , -3.6])
    >>> _calc_active_face_flux_divergence_at_node(rg, -fg)
    array([ 0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  1.64,  0.94,  0.  ,  0.  ,
            0.  ,  0.  ,  0.  ])
    >>> rg.set_status_at_node_on_edges(right=rg.BC_NODE_IS_CLOSED)
    >>> rg.set_status_at_node_on_edges(top=rg.BC_NODE_IS_CLOSED)
    >>> _calc_active_face_flux_divergence_at_node(rg, -fg)
    array([ 0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  1.14,  0.22,  0.  ,  0.  ,
            0.  ,  0.  ,  0.  ])

    Notes
    -----
    Performs a numerical flux divergence operation on cells, and returns the
    result in an array of length equal to the number of nodes. Nodes without
    cells (those on the grid perimeter) are not affected (i.e., their value
    is either zero, or if `out` is given, whatever the prior value in `out`
    was).
    """
    if out is None:
        out = grid.zeros(at="node")
    out[grid.node_at_cell] = (
        _calc_net_active_face_flux_at_cell(grid, unit_flux_at_faces) / grid.area_of_cell
    )
    return out
