Source code for imod.prepare.cleanup

"""Cleanup utilities"""

from enum import Enum
from typing import Optional

import pandas as pd
import xarray as xr

from imod.mf6.utilities.mask import mask_arrays
from imod.prepare.wells import locate_wells, validate_well_columnnames
from imod.schemata import scalar_None
from imod.typing import GridDataArray


class AlignLevelsMode(Enum):
    TOPDOWN = 0
    BOTTOMUP = 1


def align_nodata(grids: dict[str, xr.DataArray]) -> dict[str, xr.DataArray]:
    return mask_arrays(grids)


def align_interface_levels(
    top: GridDataArray,
    bottom: GridDataArray,
    method: AlignLevelsMode = AlignLevelsMode.TOPDOWN,
) -> tuple[GridDataArray, GridDataArray]:
    to_align = top < bottom

    match method:
        case AlignLevelsMode.BOTTOMUP:
            return top.where(~to_align, bottom), bottom
        case AlignLevelsMode.TOPDOWN:
            return top, bottom.where(~to_align, top)
        case _:
            raise TypeError(f"Unmatched case for method, got {method}")


def _cleanup_robin_boundary(
    idomain: GridDataArray, grids: dict[str, GridDataArray]
) -> dict[str, GridDataArray]:
    """Cleanup robin boundary condition (i.e. bc with conductance)"""
    active = idomain == 1
    # Deactivate conductance cells outside active domain; this nodata
    # inconsistency will be aligned in the final call to align_nodata
    conductance = grids["conductance"].where(active)
    concentration = grids["concentration"]
    # Make conductance cells with erronous values inactive
    grids["conductance"] = conductance.where(conductance > 0.0)
    # Clip negative concentration cells to 0.0
    if (concentration is not None) and not scalar_None(concentration):
        grids["concentration"] = concentration.clip(min=0.0)
    else:
        grids.pop("concentration")

    # Align nodata
    return align_nodata(grids)


[docs] def cleanup_riv( idomain: GridDataArray, bottom: GridDataArray, stage: GridDataArray, conductance: GridDataArray, bottom_elevation: GridDataArray, concentration: Optional[GridDataArray] = None, ) -> dict[str, GridDataArray]: """ Clean up river data, fixes some common mistakes causing ValidationErrors by doing the following: - Cells where conductance <= 0 are deactivated. - Cells where concentration < 0 are set to 0.0. - Cells outside active domain (idomain==1) are removed. - Align NoData: If one variable has an inactive cell in one cell, ensure this cell is deactivated for all variables. - River bottom elevations below model bottom of a layer are set to model bottom of that layer. - River bottom elevations which exceed river stage are lowered to river stage. Parameters ---------- idomain: xarray.DataArray | xugrid.UgridDataArray MODFLOW 6 model domain. idomain==1 is considered active domain. bottom: xarray.DataArray | xugrid.UgridDataArray Grid with model bottoms stage: xarray.DataArray | xugrid.UgridDataArray Grid with river stages conductance: xarray.DataArray | xugrid.UgridDataArray Grid with conductances bottom_elevation: xarray.DataArray | xugrid.UgridDataArray Grid with river bottom elevations concentration: xarray.DataArray | xugrid.UgridDataArray, optional Optional grid with concentrations Returns ------- dict[str, xarray.DataArray | xugrid.UgridDataArray] Dict of cleaned up grids. Has keys: "stage", "conductance", "bottom_elevation", "concentration". """ # Output dict output_dict = { "stage": stage, "conductance": conductance, "bottom_elevation": bottom_elevation, "concentration": concentration, } output_dict = _cleanup_robin_boundary(idomain, output_dict) if (output_dict["stage"] < bottom).any(): raise ValueError( "River stage below bottom of model layer, cannot fix this. " "Probably rivers are assigned to the wrong layer, you can reallocate " "river data to model layers with: " "``imod.prepare.topsystem.allocate_riv_cells``." ) # Ensure bottom elevation above model bottom output_dict["bottom_elevation"], _ = align_interface_levels( output_dict["bottom_elevation"], bottom, AlignLevelsMode.BOTTOMUP ) # Ensure stage above bottom_elevation output_dict["stage"], output_dict["bottom_elevation"] = align_interface_levels( output_dict["stage"], output_dict["bottom_elevation"], AlignLevelsMode.TOPDOWN ) return output_dict
[docs] def cleanup_drn( idomain: GridDataArray, elevation: GridDataArray, conductance: GridDataArray, concentration: Optional[GridDataArray] = None, ) -> dict[str, GridDataArray]: """ Clean up drain data, fixes some common mistakes causing ValidationErrors by doing the following: - Cells where conductance <= 0 are deactivated. - Cells where concentration < 0 are set to 0.0. - Cells outside active domain (idomain==1) are removed. - Align NoData: If one variable has an inactive cell in one cell, ensure this cell is deactivated for all variables. Parameters ---------- idomain: xarray.DataArray | xugrid.UgridDataArray MODFLOW 6 model domain. idomain==1 is considered active domain. elevation: xarray.DataArray | xugrid.UgridDataArray Grid with drain elevations conductance: xarray.DataArray | xugrid.UgridDataArray Grid with conductances concentration: xarray.DataArray | xugrid.UgridDataArray, optional Optional grid with concentrations Returns ------- dict[str, xarray.DataArray | xugrid.UgridDataArray] Dict of cleaned up grids. Has keys: "elevation", "conductance", "concentration". """ # Output dict output_dict = { "elevation": elevation, "conductance": conductance, "concentration": concentration, } return _cleanup_robin_boundary(idomain, output_dict)
[docs] def cleanup_ghb( idomain: GridDataArray, head: GridDataArray, conductance: GridDataArray, concentration: Optional[GridDataArray] = None, ) -> dict[str, GridDataArray]: """ Clean up general head boundary data, fixes some common mistakes causing ValidationErrors by doing the following: - Cells where conductance <= 0 are deactivated. - Cells where concentration < 0 are set to 0.0. - Cells outside active domain (idomain==1) are removed. - Align NoData: If one variable has an inactive cell in one cell, ensure this cell is deactivated for all variables. Parameters ---------- idomain: xarray.DataArray | xugrid.UgridDataArray MODFLOW 6 model domain. idomain==1 is considered active domain. head: xarray.DataArray | xugrid.UgridDataArray Grid with heads conductance: xarray.DataArray | xugrid.UgridDataArray Grid with conductances concentration: xarray.DataArray | xugrid.UgridDataArray, optional Optional grid with concentrations Returns ------- dict[str, xarray.DataArray | xugrid.UgridDataArray] Dict of cleaned up grids. Has keys: "head", "conductance", "concentration". """ # Output dict output_dict = { "head": head, "conductance": conductance, "concentration": concentration, } return _cleanup_robin_boundary(idomain, output_dict)
def _locate_wells_in_bounds( wells: pd.DataFrame, top: GridDataArray, bottom: GridDataArray ) -> tuple[pd.DataFrame, pd.Series, pd.Series]: """ Locate wells in model bounds, wells outside bounds are dropped. Returned dataframes and series have well "id" as index. Returns ------- wells_in_bounds: pd.DataFrame wells in model boundaries. Has "id" as index. xy_top_series: pd.Series model top at well xy location. Has "id" as index. xy_base_series: pd.Series model base at well xy location. Has "id" as index. """ id_in_bounds, xy_top, xy_bottom, _ = locate_wells( wells, top, bottom, validate=False ) xy_base_model = xy_bottom.isel(layer=-1, drop=True) # Assign id as coordinates xy_top = xy_top.assign_coords(id=("index", id_in_bounds)) xy_base_model = xy_base_model.assign_coords(id=("index", id_in_bounds)) # Create pandas dataframes/series with "id" as index. xy_top_series = xy_top.to_dataframe(name="top").set_index("id")["top"] xy_base_series = xy_base_model.to_dataframe(name="bottom").set_index("id")["bottom"] wells_in_bounds = wells.set_index("id").loc[id_in_bounds] return wells_in_bounds, xy_top_series, xy_base_series def _clip_filter_screen_to_surface_level( cleaned_wells: pd.DataFrame, xy_top_series: pd.Series ) -> pd.DataFrame: cleaned_wells["screen_top"] = cleaned_wells["screen_top"].clip(upper=xy_top_series) return cleaned_wells def _drop_wells_below_model_base( cleaned_wells: pd.DataFrame, xy_base_series: pd.Series ) -> pd.DataFrame: is_below_base = cleaned_wells["screen_top"] >= xy_base_series return cleaned_wells.loc[is_below_base] def _clip_filter_bottom_to_model_base( cleaned_wells: pd.DataFrame, xy_base_series: pd.Series ) -> pd.DataFrame: cleaned_wells["screen_bottom"] = cleaned_wells["screen_bottom"].clip( lower=xy_base_series ) return cleaned_wells def _set_inverted_filters_to_point_filters(cleaned_wells: pd.DataFrame) -> pd.DataFrame: # Convert all filters where screen bottom exceeds screen top to # point filters cleaned_wells["screen_bottom"] = cleaned_wells["screen_bottom"].clip( upper=cleaned_wells["screen_top"] ) return cleaned_wells def _set_ultrathin_filters_to_point_filters( cleaned_wells: pd.DataFrame, minimum_thickness: float ) -> pd.DataFrame: not_ultrathin_layer = ( cleaned_wells["screen_top"] - cleaned_wells["screen_bottom"] ) > minimum_thickness cleaned_wells["screen_bottom"] = cleaned_wells["screen_bottom"].where( not_ultrathin_layer, cleaned_wells["screen_top"] ) return cleaned_wells
[docs] def cleanup_wel( wells: pd.DataFrame, top: GridDataArray, bottom: GridDataArray, minimum_thickness: float = 0.05, ) -> pd.DataFrame: """ Clean up dataframe with wells, fixes some common mistakes in the following order: 1. Wells outside grid bounds are dropped 2. Filters above surface level are set to surface level 3. Drop wells with filters entirely below base 4. Clip filter screen_bottom to model base 5. Clip filter screen_bottom to screen_top 6. Well filters thinner than minimum thickness are made point filters Parameters ---------- wells: pandas.Dataframe Dataframe with wells to be cleaned up. Requires columns ``"x", "y", "id", "screen_top", "screen_bottom"`` top: xarray.DataArray | xugrid.UgridDataArray Grid with model top bottom: xarray.DataArray | xugrid.UgridDataArray Grid with model bottoms minimum_thickness: float Minimum thickness, filter thinner than this thickness are set to point filters Returns ------- pandas.DataFrame Cleaned well dataframe. """ validate_well_columnnames( wells, names={"x", "y", "id", "screen_top", "screen_bottom"} ) cleaned_wells, xy_top_series, xy_base_series = _locate_wells_in_bounds( wells, top, bottom ) cleaned_wells = _clip_filter_screen_to_surface_level(cleaned_wells, xy_top_series) cleaned_wells = _drop_wells_below_model_base(cleaned_wells, xy_base_series) cleaned_wells = _clip_filter_bottom_to_model_base(cleaned_wells, xy_base_series) cleaned_wells = _set_inverted_filters_to_point_filters(cleaned_wells) cleaned_wells = _set_ultrathin_filters_to_point_filters( cleaned_wells, minimum_thickness ) return cleaned_wells