Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #921 Update uzf #867

Merged
merged 23 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/api/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,16 @@ Added
be selected, available in :func:`imod.prepare.DISTRIBUTION_OPTION`.
- :func:`imod.prepare.celltable` supports an optional ``dtype`` argument. This
can be used, for example, to create celltables of float values.

- Budget arrays now contain np.nan for cells where budget variables are not defined.
Based on new budget output a disquisition between active cells but zero flow and
inactive cells can be made. Changes affect:
:func:`imod.mf6.open_cbc`
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor nitpick: Best to put function at start of message. Something like:

Suggested change
- Budget arrays now contain np.nan for cells where budget variables are not defined.
Based on new budget output a disquisition between active cells but zero flow and
inactive cells can be made. Changes affect:
:func:`imod.mf6.open_cbc`
- :func:`imod.mf6.open_cbc` now returns arrays which contain np.nan for cells where budget variables are not defined.
Based on new budget output a disquisition between active cells but zero flow and
inactive cells can be made.

- Package type added to return budget names of CBC-output. New format is
"package type"-"optional package variable"_"package name". E.g. a River package
named ``primary-sys`` will get a budget name ``riv_primary-sys``. An UZF package
with name ``uzf-sys1`` will get a budget name ``uzf-gwrch_uzf-sys1`` for the
groundwater recharge budget from the UZF-CBC. Changes affect:
:func:`imod.mf6.open_cbc`
Copy link
Contributor

Choose a reason for hiding this comment

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

idem


Fixed
~~~~~
Expand Down
23 changes: 18 additions & 5 deletions imod/mf6/out/cbc.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,17 @@ def read_cbc_headers(
imeth6_header = read_imeth6_header(f)
datasize = imeth6_header["nlist"] * (8 + imeth6_header["ndat"] * 8)
header["pos"] = f.tell()
key = imeth6_header["txt2id2"]
# key-format: "package type"-"optional_package_variable"_"package name"
# for river output: riv_sys1
# for uzf output: uzf-gwrch_uzf_sys1
key = header["text"] + "_" + imeth6_header["txt2id2"]
# npf-key can be present multiple times in cases of saved saturation + specific discharge
if header["text"].startswith("data-"):
key = key + "-" + header["text"].replace("data-", "")
key = (
imeth6_header["txt2id2"]
+ "_"
+ header["text"].replace("data-", "")
)
headers[key].append(Imeth6Header(**header, **imeth6_header))
else:
raise ValueError(
Expand Down Expand Up @@ -261,6 +268,7 @@ def read_imeth6_budgets_dense(
size: int,
shape: tuple,
return_variable: str,
indices: np.ndarray | None,
) -> FloatArray:
"""
Read the data for an imeth==6 budget section.
Expand All @@ -282,14 +290,19 @@ def read_imeth6_budgets_dense(
size of the entire model domain
shape: tuple[int, int, int]
Shape (nlayer, nrow, ncolumn) of entire model domain.
return_variable: str
variable name to return from budget table
indices: np.ndarray | None
optional array that contains the indices to map return_variable to model topology

Returns
-------
Three-dimensional array of floats
"""
# Allocates a dense array for the entire domain
out = np.zeros(size, dtype=np.float64)
out = np.full(size, np.nan, dtype=np.float64)
JoerivanEngelen marked this conversation as resolved.
Show resolved Hide resolved
table = read_imeth6_budgets(cbc_path, count, dtype, pos)
id1 = table["id1"] - 1 # Convert to 0 based index
out[id1] = table[return_variable]
if indices is None:
indices = table["id1"] - 1 # Convert to 0 based index
out[indices] = table[return_variable]
return out.reshape(shape)
12 changes: 11 additions & 1 deletion imod/mf6/out/common.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pathlib
from typing import BinaryIO, Union
from typing import Any, BinaryIO, Dict, List, Union

import numpy as np

Expand All @@ -19,3 +19,13 @@ def _to_nan(a: FloatArray, dry_nan: bool) -> FloatArray:
if dry_nan:
a[a == -1e30] = np.nan
return a


def get_first_header_advanced_package(
headers: Dict[str, List[Any]],
) -> Any:
for key, header_list in headers.items():
# multimodels have a gwf-gwf budget for flow-ja-face between domains
if not "flow-ja-face" in key and "gwf_" in key:
return header_list[0]
return None
43 changes: 38 additions & 5 deletions imod/mf6/out/dis.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@
from imod.mf6.utilities.dataset import assign_datetime_coords

from . import cbc
from .common import FilePath, FloatArray, IntArray, _to_nan
from .common import (
FilePath,
FloatArray,
IntArray,
_to_nan,
get_first_header_advanced_package,
)


# Binary Grid File / DIS Grids
Expand Down Expand Up @@ -197,6 +203,7 @@ def open_imeth6_budgets(
grb_content: dict,
header_list: List[cbc.Imeth6Header],
return_variable: str = "budget",
indices: np.ndarray | None = None,
) -> xr.DataArray:
"""
Open the data for an imeth==6 budget section.
Expand All @@ -210,6 +217,8 @@ def open_imeth6_budgets(
cbc_path: str, pathlib.Path
grb_content: dict
header_list: List[Imeth1Header]
return_variable: str
return_id: np.ndarray | None
HendrikKok marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
Expand All @@ -227,7 +236,14 @@ def open_imeth6_budgets(
for i, header in enumerate(header_list):
time[i] = header.totim
a = dask.delayed(cbc.read_imeth6_budgets_dense)(
cbc_path, header.nlist, dtype, header.pos, size, shape, return_variable
cbc_path,
header.nlist,
dtype,
header.pos,
size,
shape,
return_variable,
indices,
)
x = dask.array.from_delayed(a, shape=shape, dtype=np.float64)
dask_list.append(x)
Expand Down Expand Up @@ -410,10 +426,23 @@ def open_cbc(
time_unit: Optional[str] = "d",
) -> Dict[str, xr.DataArray]:
headers = cbc.read_cbc_headers(cbc_path)
indices = None
header_advanced_package = get_first_header_advanced_package(headers)
if header_advanced_package is not None:
# For advanced packages the id2 column of variable gwf contains the MF6 id's.
# Get id's eager from first stress period.
dtype = np.dtype(
[("id1", np.int32), ("id2", np.int32), ("budget", np.float64)]
+ [(name, np.float64) for name in header_advanced_package.auxtxt]
)
table = cbc.read_imeth6_budgets(
cbc_path, header_advanced_package.nlist, dtype, header_advanced_package.pos
)
indices = table["id2"] - 1 # Convert to 0 based index
cbc_content = {}
for key, header_list in headers.items():
# TODO: validate homogeneity of header_list, ndat consistent, nlist consistent etc.
if key == "flow-ja-face":
if key == "flow-ja-face" and isinstance(header_list[0], cbc.Imeth1Header):
if flowja:
flowja, nm = cbc.open_face_budgets_as_flowja(
cbc_path, header_list, grb_content
Expand All @@ -438,11 +467,15 @@ def open_cbc(
for return_variable in header_list[0].auxtxt:
key_aux = header_list[0].txt2id1 + "-" + return_variable
cbc_content[key_aux] = open_imeth6_budgets(
cbc_path, grb_content, header_list, return_variable
cbc_path,
grb_content,
header_list,
return_variable=return_variable,
indices=indices,
)
else:
cbc_content[key] = open_imeth6_budgets(
cbc_path, grb_content, header_list
cbc_path, grb_content, header_list, indices=indices
)
if simulation_start_time is not None:
for cbc_name, cbc_array in cbc_content.items():
Expand Down
43 changes: 38 additions & 5 deletions imod/mf6/out/disv.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
from imod.mf6.utilities.dataset import assign_datetime_coords

from . import cbc
from .common import FilePath, FloatArray, IntArray, _to_nan
from .common import (
FilePath,
FloatArray,
IntArray,
_to_nan,
get_first_header_advanced_package,
)


def _ugrid_iavert_javert(
Expand Down Expand Up @@ -229,6 +235,7 @@ def open_imeth6_budgets(
grb_content: dict,
header_list: List[cbc.Imeth6Header],
return_variable: str = "budget",
return_id: np.ndarray | None = None,
) -> xu.UgridDataArray:
"""
Open the data for an imeth==6 budget section.
Expand All @@ -242,6 +249,8 @@ def open_imeth6_budgets(
cbc_path: str, pathlib.Path
grb_content: dict
header_list: List[Imeth1Header]
return_variable: str
return_id: np.ndarray | None

Returns
-------
Expand All @@ -259,7 +268,14 @@ def open_imeth6_budgets(
for i, header in enumerate(header_list):
time[i] = header.totim
a = dask.delayed(cbc.read_imeth6_budgets_dense)(
cbc_path, header.nlist, dtype, header.pos, size, shape, return_variable
cbc_path,
header.nlist,
dtype,
header.pos,
size,
shape,
return_variable,
return_id,
)
x = dask.array.from_delayed(a, shape=shape, dtype=np.float64)
dask_list.append(x)
Expand Down Expand Up @@ -465,9 +481,22 @@ def open_cbc(
time_unit: Optional[str] = "d",
) -> Dict[str, xu.UgridDataArray]:
headers = cbc.read_cbc_headers(cbc_path)
indices = None
header_advanced_package = get_first_header_advanced_package(headers)
if header_advanced_package is not None:
# For advanced packages the id2 column of variable gwf contains the MF6 id's.
# Get id's eager from first stress period.
dtype = np.dtype(
[("id1", np.int32), ("id2", np.int32), ("budget", np.float64)]
+ [(name, np.float64) for name in header_advanced_package.auxtxt]
)
table = cbc.read_imeth6_budgets(
cbc_path, header_advanced_package.nlist, dtype, header_advanced_package.pos
)
indices = table["id2"] - 1 # Convert to 0 based index
cbc_content = {}
for key, header_list in headers.items():
if key == "flow-ja-face":
if key == "flow-ja-face" and isinstance(header_list[0], cbc.Imeth1Header):
if flowja:
flowja, ij = cbc.open_face_budgets_as_flowja(
cbc_path, header_list, grb_content
Expand All @@ -490,11 +519,15 @@ def open_cbc(
for return_variable in header_list[0].auxtxt:
key_aux = header_list[0].txt2id1 + "-" + return_variable
cbc_content[key_aux] = open_imeth6_budgets(
cbc_path, grb_content, header_list, return_variable
cbc_path,
grb_content,
header_list,
return_variable=return_variable,
return_id=indices,
)
else:
cbc_content[key] = open_imeth6_budgets(
cbc_path, grb_content, header_list
cbc_path, grb_content, header_list, return_id=indices
)

if simulation_start_time is not None:
Expand Down
2 changes: 1 addition & 1 deletion imod/mf6/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ def _merge_and_assign_exchange_budgets(self, cbc: GridDataset) -> GridDataset:
exchange_budgets = cbc[exchange_names].to_array().sum(dim="variable")
cbc = cbc.drop_vars(exchange_names)
# "gwf-gwf" or "gwt-gwt"
exchange_key = exchange_names[0].split("_")[0]
exchange_key = exchange_names[0].split("_")[1]
HendrikKok marked this conversation as resolved.
Show resolved Hide resolved
cbc[exchange_key] = exchange_budgets
return cbc

Expand Down
36 changes: 26 additions & 10 deletions imod/mf6/uzf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from imod.logging import init_log_decorator
from imod.mf6.boundary_condition import AdvancedBoundaryCondition, BoundaryCondition
from imod.mf6.validation import BOUNDARY_DIMS_SCHEMA
from imod.prepare.layer import get_upper_active_grid_cells
from imod.schemata import (
AllInsideNoDataSchema,
AllNoDataSchema,
Expand Down Expand Up @@ -92,6 +93,10 @@ class UnsaturatedZoneFlow(AdvancedBoundaryCondition):
keyword to indicate that UZF flow terms will be written to the file specified with "BUDGET
FILEOUT" in Output Control.
Default is False.
budget_fileout: ({"str"}, optional)
path to output cbc-file for UZF budgets
budgetcsv_fileout: ({"str"}, optional)
path to output csv-file for summed budgets
observations: [Not yet supported.]
Default is None.
water_mover: [Not yet supported.]
Expand Down Expand Up @@ -183,12 +188,12 @@ class UnsaturatedZoneFlow(AdvancedBoundaryCondition):
"theta_sat": [IdentityNoDataSchema("kv_sat"), AllValueSchema(">=", 0.0)],
"theta_init": [IdentityNoDataSchema("kv_sat"), AllValueSchema(">=", 0.0)],
"epsilon": [IdentityNoDataSchema("kv_sat")],
"infiltration_rate": [IdentityNoDataSchema("kv_sat")],
"et_pot": [IdentityNoDataSchema("kv_sat")],
"extinction_depth": [IdentityNoDataSchema("kv_sat")],
"extinction_theta": [IdentityNoDataSchema("kv_sat")],
"root_potential": [IdentityNoDataSchema("kv_sat")],
"root_activity": [IdentityNoDataSchema("kv_sat")],
"infiltration_rate": [IdentityNoDataSchema("stress_period_active")],
HendrikKok marked this conversation as resolved.
Show resolved Hide resolved
"et_pot": [IdentityNoDataSchema("stress_period_active")],
"extinction_depth": [IdentityNoDataSchema("stress_period_active")],
"extinction_theta": [IdentityNoDataSchema("stress_period_active")],
"root_potential": [IdentityNoDataSchema("stress_period_active")],
"root_activity": [IdentityNoDataSchema("stress_period_active")],
}

_package_data = (
Expand Down Expand Up @@ -226,6 +231,8 @@ def __init__(
print_input=False,
print_flows=False,
save_flows=False,
budget_fileout=None,
budgetcsv_fileout=None,
observations=None,
water_mover=None,
timeseries=None,
Expand All @@ -234,6 +241,7 @@ def __init__(
landflag = self._determine_landflag(kv_sat)
iuzno = self._create_uzf_numbers(landflag)
ivertcon = self._determine_vertical_connection(iuzno)
stress_period_active = landflag.where(landflag == 1)

dict_dataset = {
# Package data
Expand All @@ -244,6 +252,7 @@ def __init__(
"theta_init": theta_init,
"epsilon": epsilon,
# Stress period data
"stress_period_active": stress_period_active,
"infiltration_rate": infiltration_rate,
"et_pot": et_pot,
"extinction_depth": extinction_depth,
Expand All @@ -260,6 +269,8 @@ def __init__(
"print_input": print_input,
"print_flows": print_flows,
"save_flows": save_flows,
"budget_fileout": budget_fileout,
"budgetcsv_fileout": budgetcsv_fileout,
"observations": observations,
"water_mover": water_mover,
"timeseries": timeseries,
Expand Down Expand Up @@ -343,16 +354,19 @@ def _check_options(

def _create_uzf_numbers(self, landflag):
"""Create unique UZF ID's. Inactive cells equal 0"""
return np.cumsum(np.ravel(landflag)).reshape(landflag.shape) * landflag
active_nodes = landflag.notnull().astype(np.int8)
return np.nancumsum(active_nodes).reshape(landflag.shape) * active_nodes

def _determine_landflag(self, kv_sat):
return (np.isfinite(kv_sat)).astype(np.int32)
"""returns the landflag for uzf-model. Landflag == 1 for top active UZF-nodes"""
land_nodes = get_upper_active_grid_cells(kv_sat).astype(np.int32)
return land_nodes.where(kv_sat.notnull())

def _determine_vertical_connection(self, uzf_number):
return uzf_number.shift(layer=-1, fill_value=0)

def _package_data_to_sparse(self):
notnull = self.dataset["landflag"].values == 1
notnull = self.dataset["landflag"].notnull().to_numpy()
iuzno = self.dataset["iuzno"].values[notnull]
landflag = self.dataset["landflag"].values[notnull]
ivertcon = self.dataset["ivertcon"].values[notnull]
Expand Down Expand Up @@ -392,7 +406,8 @@ def render(self, directory, pkgname, globaltimes, binary):
d = self._get_options(d, not_options=not_options)
path = directory / pkgname / f"{self._pkg_id}-pkgdata.dat"
d["packagedata"] = path.as_posix()
d["nuzfcells"] = self._max_active_n()
# max uzf-cells for which time period data will be supplied
d["nuzfcells"] = np.count_nonzero(np.isfinite(d["landflag"]))
return self._template.render(d)

def _to_struct_array(self, arrdict, layer):
Expand Down Expand Up @@ -422,6 +437,7 @@ def _to_struct_array(self, arrdict, layer):
def _validate(self, schemata, **kwargs):
# Insert additional kwargs
kwargs["kv_sat"] = self["kv_sat"]
kwargs["stress_period_active"] = self["stress_period_active"]
errors = super()._validate(schemata, **kwargs)

return errors