Coverage for C:\src\imod-python\imod\wq\btn.py: 86%

44 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-08 14:15 +0200

1import jinja2 

2import numpy as np 

3import scipy.ndimage 

4 

5from imod.wq.pkgbase import Package 

6 

7 

8class BasicTransport(Package): 

9 """ 

10 Handles basic tasks that are required by the entire transport model. Among 

11 these tasks are definition of the problem, specification of the boundary and 

12 initial conditions, determination of the stepsize, preparation of mass 

13 balance information, and printout of the simulation results. 

14 

15 Parameters 

16 ---------- 

17 icbund: xr.DataArray of int 

18 is an integer array specifying the boundary condition type (inactive, 

19 constant-concentration, or active) for every model cell. For 

20 multi-species simulation, ICBUND defines the boundary condition type 

21 shared by all species. Note that different species are allowed to have 

22 different constant-concentration conditions through an option in the 

23 Source and Sink Mixing Package. 

24 ICBUND=0, the cell is an inactive concentration cell for all species. 

25 Note that no-flow or "dry" cells are automatically converted into 

26 inactive concentration cells. Furthermore, active cells in terms of flow 

27 can be treated as inactive concentration cells to minimize the area 

28 needed for transport simulation, as long as the solute transport is 

29 insignificant near those cells. 

30 ICBUND<0, the cell is a constant-concentration cell for all species. The 

31 starting concentration of each species remains the same at the cell 

32 throughout the simulation. (To define different constantconcentration 

33 conditions for different species at the same cell location, refer to the 

34 Sink/Source Mixing Package.) Also note that unless explicitly defined as 

35 a constant-concentration cell, a constant-head cell in the flow model is 

36 not treated as a constantconcentration cell. 

37 If ICBUND>0, the cell is an active (variable) concentration cell where 

38 the concentration value will be calculated. 

39 starting_concentration: float or xr.DataArray of floats 

40 is the starting concentration (initial condition) at the beginning of 

41 the simulation (unit: ML-3) (SCONC). For multispecies simulation, the 

42 starting concentration must be specified for all species, one species at 

43 a time. 

44 porosity: float, optional 

45 is the "effective" porosity of the porous medium in a single porosity 

46 system (PRSITY). 

47 Default value is 0.35. 

48 n_species: int, optional 

49 is the total number of chemical species included in the current 

50 simulation (NCOMP). For single-species simulation, set n_species = 1. 

51 Default value is 1. 

52 inactive_concentration: float, optional 

53 is the value for indicating an inactive concentration cell (ICBUND=0) 

54 (CINACT). Even if it is not anticipated to have inactive cells in the 

55 model, a value for inactive_concentration still must be submitted. 

56 Default value is 1.0e30 

57 minimum_active_thickness: float, optional 

58 is the minimum saturated thickness in a cell (THKMIN), expressed as the 

59 decimal fraction of the model layer thickness, below which the cell is 

60 considered inactive. 

61 Default value is 0.01 (i.e., 1% of the model layer thickness). 

62 """ 

63 

64 _pkg_id = "btn" 

65 

66 _mapping = (("icbund", "icbund"), ("dz", "thickness"), ("prsity", "porosity")) 

67 

68 _template = jinja2.Template( 

69 "[btn]\n" 

70 " ncomp = {{n_species}}\n" # Number of components 

71 " mcomp = {{n_species}}\n" # Number of mobile components 

72 " thkmin = {{minimum_active_thickness}}\n" 

73 " cinact = {{inactive_concentration}}\n" 

74 " {%- for species, layerdict in starting_concentration.items() %}\n" 

75 " {%- for layer, value in layerdict.items() %}\n" 

76 " sconc_t{{species}}_l{{layer}} = {{value}}\n" 

77 " {%- endfor -%}\n" 

78 " {%- endfor -%}\n" 

79 " {%- for name, dictname in mapping -%}\n" 

80 " {%- for layer, value in dicts[dictname].items() %}\n" 

81 " {{name}}_l{{layer}} = {{value}}\n" 

82 " {%- endfor -%}\n" 

83 " {%- endfor -%}\n" 

84 ) 

85 

86 def __init__( 

87 self, 

88 icbund, 

89 starting_concentration, 

90 porosity=0.35, 

91 n_species=1, 

92 inactive_concentration=1.0e30, 

93 minimum_active_thickness=0.01, 

94 ): 

95 super().__init__() 

96 self["icbund"] = icbund 

97 self["starting_concentration"] = starting_concentration 

98 self["porosity"] = porosity 

99 self["n_species"] = n_species 

100 self["inactive_concentration"] = inactive_concentration 

101 self["minimum_active_thickness"] = minimum_active_thickness 

102 

103 def _render(self, directory, nlayer): 

104 """ 

105 Renders part of [btn] section that does not depend on time, 

106 and can be inferred without checking the BoundaryConditions. 

107 

108 Parameters 

109 ---------- 

110 directory : str 

111 thickness : xr.DataArray 

112 Taken from BasicFlow 

113 

114 Returns 

115 ------- 

116 rendered : str 

117 """ 

118 d = {} 

119 dicts = {} 

120 d["mapping"] = self._mapping 

121 # Starting concentration also includes a species, and can't be written 

122 # in the same way as the other variables; _T? in the runfile 

123 if "species" in self.dataset["starting_concentration"].coords: 

124 starting_concentration = {} 

125 

126 for i, species in enumerate( 

127 self.dataset["starting_concentration"]["species"].values 

128 ): 

129 da = self.dataset["starting_concentration"].sel(species=species) 

130 starting_concentration[i + 1] = self._compose_values_layer( 

131 "starting_concentration", directory, nlayer=nlayer, da=da 

132 ) 

133 

134 d["starting_concentration"] = starting_concentration 

135 else: 

136 d["starting_concentration"] = { 

137 1: self._compose_values_layer( 

138 "starting_concentration", directory, nlayer=nlayer 

139 ) 

140 } 

141 

142 # Collect which entries are complex (multi-dim) 

143 data_vars = [t[1] for t in self._mapping] 

144 for varname in self.dataset.data_vars.keys(): 

145 if varname == "starting_concentration": 

146 continue # skip it, as mentioned above 

147 if varname in data_vars: # multi-dim entry 

148 dicts[varname] = self._compose_values_layer( 

149 varname, directory, nlayer=nlayer 

150 ) 

151 else: # simple entry, just get the scalar value 

152 d[varname] = self.dataset[varname].values 

153 

154 # Add these from the outside, thickness from BasicFlow 

155 # layer_type from LayerPropertyFlow 

156 dicts["thickness"] = self._compose_values_layer( 

157 "thickness", directory, nlayer=nlayer, da=self.dataset.thickness 

158 ) 

159 d["dicts"] = dicts 

160 return self._template.render(d) 

161 

162 def _pkgcheck(self, ibound=None): 

163 to_check = [ 

164 "starting_concentration", 

165 "porosity", 

166 "n_species", 

167 "minimum_active_thickness", 

168 ] 

169 self._check_positive(to_check) 

170 

171 active_cells = self.dataset["icbund"] != 0 

172 if (active_cells & np.isnan(self.dataset["starting_concentration"])).any(): 

173 raise ValueError( 

174 f"Active cells in icbund may not have a nan value in starting_concentration in {self}" 

175 ) 

176 

177 _, nlabels = scipy.ndimage.label(active_cells.values) 

178 

179 

180# if nlabels > 1: 

181# raise ValueError( 

182# f"{nlabels} disconnected model domain detected in the icbund in {self}" 

183# )