Coverage for C:\src\imod-python\imod\mf6\validation.py: 100%

1"""

2 This module contains specific validation utilities for Modflow 6.

3"""

4

5 from typing import cast

6

7 import numpy as np

8

9 from imod.mf6.statusinfo import NestedStatusInfo, StatusInfo, StatusInfoBase

10 from imod.schemata import DimsSchema, NoDataComparisonSchema, ValidationError

11 from imod.typing import GridDataArray

12

13 PKG_DIMS_SCHEMA = (

14 DimsSchema("layer", "y", "x")

15 | DimsSchema("layer", "{face_dim}")

16 | DimsSchema("layer")

17 | DimsSchema()

18)

19

20 BOUNDARY_DIMS_SCHEMA = (

21 DimsSchema("time", "layer", "y", "x")

22 | DimsSchema("layer", "y", "x")

23 | DimsSchema("time", "layer", "{face_dim}")

24 | DimsSchema("layer", "{face_dim}")

25 # Layer dim not necessary, as long as there is a layer coordinate present.

26 | DimsSchema("time", "y", "x")

27 | DimsSchema("y", "x")

28 | DimsSchema("time", "{face_dim}")

29 | DimsSchema("{face_dim}")

30)

31

32 CONC_DIMS_SCHEMA = (

33 DimsSchema("species", "time", "layer", "y", "x")

34 | DimsSchema("species", "layer", "y", "x")

35 | DimsSchema("species", "time", "layer", "{face_dim}")

36 | DimsSchema("species", "layer", "{face_dim}")

37 # Layer dim not necessary, as long as there is a layer coordinate present.

38 | DimsSchema("species", "time", "y", "x")

39 | DimsSchema("species", "y", "x")

40 | DimsSchema("species", "time", "{face_dim}")

41 | DimsSchema("species", "{face_dim}")

42)

43

44

45 class DisBottomSchema(NoDataComparisonSchema):

46 """

47 Custom schema for the bottoms as these require some additional logic,

48 because of how Modflow 6 computes cell thicknesses.

49 """

50

51 def validate(self, obj: GridDataArray, **kwargs) -> None:

52 other_obj = kwargs[self.other]

53

54 active = self.is_other_notnull(other_obj)

55 bottom = obj

56

57 # Only check for multi-layered models

58 if bottom.coords["layer"].size > 1:

59 # Check if zero thicknesses occur in active cells. The difference across

60 # layers is a "negative thickness"

61 thickness = bottom.diff(dim="layer") * -1.0

62 if (thickness.where(active.isel(layer=slice(1, None))) <= 0.0).any():

63 raise ValidationError("found thickness <= 0.0")

64

65 # To compute thicknesses properly, Modflow 6 requires bottom data in the

66 # layer above the active cell in question.

67 overlaying_top_inactive = cast(GridDataArray, np.isnan(bottom)).shift(

68 layer=1, fill_value=False

69 )

70 if (overlaying_top_inactive & active).any():

71 raise ValidationError("inactive bottom above active cell")

72

73

74 def validation_pkg_error_message(pkg_errors):

75 messages = []

76 for var, var_errors in pkg_errors.items():

77 messages.append(f"* {var}")

78 messages.extend(f"\t- {error}" for error in var_errors)

79 return "\n" + "\n".join(messages)

80

81

82 def pkg_errors_to_status_info(

83 pkg_name: str, pkg_errors: dict[str, list[ValidationError]]

84) -> StatusInfoBase:

85 pkg_status_info = NestedStatusInfo(f"{pkg_name} package")

86 for var_name, var_errors in pkg_errors.items():

87 var_status_info = StatusInfo(var_name)

88 for var_error in var_errors:

89 var_status_info.add_error(str(var_error))

90 pkg_status_info.add(var_status_info)

91

92 return pkg_status_info