mf6rtm 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mf6rtm/__init__.py +10 -0
- mf6rtm/__main__.py +4 -0
- mf6rtm/mf6rtm.py +661 -0
- mf6rtm/mup3d.py +609 -0
- mf6rtm/py.typed +0 -0
- mf6rtm/utils.py +418 -0
- mf6rtm-0.1.0.dist-info/LICENSE +28 -0
- mf6rtm-0.1.0.dist-info/METADATA +37 -0
- mf6rtm-0.1.0.dist-info/RECORD +12 -0
- mf6rtm-0.1.0.dist-info/WHEEL +5 -0
- mf6rtm-0.1.0.dist-info/entry_points.txt +2 -0
- mf6rtm-0.1.0.dist-info/top_level.txt +2 -0
mf6rtm/__init__.py
ADDED
mf6rtm/__main__.py
ADDED
mf6rtm/mf6rtm.py
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import os
|
|
3
|
+
import warnings
|
|
4
|
+
warnings.filterwarnings("ignore")
|
|
5
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
6
|
+
from PIL import Image
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import numpy as np
|
|
9
|
+
import flopy
|
|
10
|
+
import phreeqcrm
|
|
11
|
+
import modflowapi
|
|
12
|
+
# from modflowapi.extensions import ApiSimulation
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
# from time import sleep
|
|
15
|
+
from . import utils
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# global variables
|
|
19
|
+
DT_FMT = "%Y-%m-%d %H:%M:%S"
|
|
20
|
+
|
|
21
|
+
time_units_dic = {
|
|
22
|
+
'second': 1,
|
|
23
|
+
'minute': 60,
|
|
24
|
+
'hour': 3600,
|
|
25
|
+
'day': 86400,
|
|
26
|
+
'week': 604800,
|
|
27
|
+
'month': 2628000,
|
|
28
|
+
'year': 31536000
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def prep_to_run(wd):
|
|
34
|
+
'''Prepares the model to run by checking if the model directory contains the necessary files
|
|
35
|
+
and returns the path to the yaml file (phreeqcrm) and the dll file (mf6 api)'''
|
|
36
|
+
# check if wd exists
|
|
37
|
+
assert os.path.exists(wd), f'{wd} not found'
|
|
38
|
+
# check if file starting with libmf6 exists
|
|
39
|
+
dll = [f for f in os.listdir(wd) if f.startswith('libmf6')]
|
|
40
|
+
assert len(dll) == 1, 'libmf6 dll not found in model directory'
|
|
41
|
+
assert os.path.exists(os.path.join(wd, 'mf6rtm.yaml')), 'mf6rtm.yaml not found in model directory'
|
|
42
|
+
|
|
43
|
+
nam = [f for f in os.listdir(wd) if f.endswith('.nam')]
|
|
44
|
+
assert 'mfsim.nam' in nam, 'mfsim.nam file not found in model directory'
|
|
45
|
+
assert 'gwf.nam' in nam, 'gwf.nam file not found in model directory'
|
|
46
|
+
dll = os.path.join(wd, 'libmf6')
|
|
47
|
+
yamlfile = os.path.join(wd, 'mf6rtm.yaml')
|
|
48
|
+
|
|
49
|
+
return yamlfile, dll
|
|
50
|
+
|
|
51
|
+
def solve(wd, reactive=True):
|
|
52
|
+
'''Wrapper to prepare and call solve functions
|
|
53
|
+
'''
|
|
54
|
+
mf6rtm = initialize_interfaces(wd)
|
|
55
|
+
if not reactive:
|
|
56
|
+
mf6rtm._set_reactive(reactive)
|
|
57
|
+
success = mf6rtm._solve()
|
|
58
|
+
return success
|
|
59
|
+
|
|
60
|
+
def initialize_interfaces(wd):
|
|
61
|
+
'''Function to initialize the interfaces for modflowapi and phreeqcrm and returns the mf6rtm object
|
|
62
|
+
'''
|
|
63
|
+
yamlfile, dll = prep_to_run(wd)
|
|
64
|
+
mf6api = Mf6API(wd, dll)
|
|
65
|
+
phreeqcrm = PhreeqcBMI(yamlfile)
|
|
66
|
+
mf6rtm = Mf6RTM(wd, mf6api, phreeqcrm)
|
|
67
|
+
return mf6rtm
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_mf6_dis(sim):
|
|
71
|
+
'''Function to extract dis from modflow6 sim object
|
|
72
|
+
'''
|
|
73
|
+
dis = sim.get_model(sim.model_names[0]).dis
|
|
74
|
+
nlay = dis.nlay.get_data()
|
|
75
|
+
nrow = dis.nrow.get_data()
|
|
76
|
+
ncol = dis.ncol.get_data()
|
|
77
|
+
return nlay, nrow, ncol
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def calc_nxyz_from_dis(sim):
|
|
81
|
+
'''Function to calculate number of cells from dis object
|
|
82
|
+
'''
|
|
83
|
+
nlay, nrow, ncol = get_mf6_dis(sim)
|
|
84
|
+
return nlay*nrow*ncol
|
|
85
|
+
|
|
86
|
+
def get_mf6_disv(sim):
|
|
87
|
+
#TODO: implement this function
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
def determine_grid_type(sim):
|
|
91
|
+
'''Function to determine the grid type of the model
|
|
92
|
+
'''
|
|
93
|
+
# get the grid type
|
|
94
|
+
mf6 = sim.get_model(sim.model_names[0])
|
|
95
|
+
distype = mf6.get_grid_type().name
|
|
96
|
+
return distype
|
|
97
|
+
|
|
98
|
+
class PhreeqcBMI(phreeqcrm.BMIPhreeqcRM):
|
|
99
|
+
|
|
100
|
+
def __init__(self, yaml="mf6rtm.yaml"):
|
|
101
|
+
phreeqcrm.BMIPhreeqcRM.__init__(self)
|
|
102
|
+
self.initialize(yaml)
|
|
103
|
+
|
|
104
|
+
def get_grid_to_map(self):
|
|
105
|
+
'''Function to get grid to map
|
|
106
|
+
'''
|
|
107
|
+
return self.GetGridToMap()
|
|
108
|
+
|
|
109
|
+
def _prepare_phreeqcrm_bmi(self):
|
|
110
|
+
'''Prepare phreeqc bmi for reaction calculations
|
|
111
|
+
'''
|
|
112
|
+
self.SetScreenOn(True)
|
|
113
|
+
self.set_scalar("NthSelectedOutput", 0)
|
|
114
|
+
sout_headers = self.GetSelectedOutputHeadings()
|
|
115
|
+
soutdf = pd.DataFrame(columns=sout_headers)
|
|
116
|
+
|
|
117
|
+
# set attributes
|
|
118
|
+
self.components = self.get_value_ptr("Components")
|
|
119
|
+
self.ncomps = len(self.components)
|
|
120
|
+
self.soutdf = soutdf
|
|
121
|
+
self.sout_headers = sout_headers
|
|
122
|
+
|
|
123
|
+
def _set_ctime(self, ctime):
|
|
124
|
+
'''Set the current time in phreeqc bmi
|
|
125
|
+
'''
|
|
126
|
+
# self.ctime = self.SetTime(ctime*86400)
|
|
127
|
+
self.ctime = ctime
|
|
128
|
+
|
|
129
|
+
def set_scalar(self, var_name, value):
|
|
130
|
+
itemsize = self.get_var_itemsize(var_name)
|
|
131
|
+
nbytes = self.get_var_nbytes(var_name)
|
|
132
|
+
dim = nbytes // itemsize
|
|
133
|
+
|
|
134
|
+
if dim != 1:
|
|
135
|
+
raise ValueError(f"{var_name} is not a scalar")
|
|
136
|
+
|
|
137
|
+
vtype = self.get_var_type(var_name)
|
|
138
|
+
dest = np.empty(1, dtype=vtype)
|
|
139
|
+
dest[0] = value
|
|
140
|
+
x = self.set_value(var_name, dest)
|
|
141
|
+
|
|
142
|
+
def _solve_phreeqcrm(self, dt, diffmask):
|
|
143
|
+
'''Function to solve phreeqc bmi
|
|
144
|
+
'''
|
|
145
|
+
|
|
146
|
+
# status = phreeqc_rm.SetTemperature([self.init_temp[0]] * self.ncpl)
|
|
147
|
+
# status = phreeqc_rm.SetPressure([2.0] * nxyz)
|
|
148
|
+
self.SetTimeStep(dt*86400)
|
|
149
|
+
|
|
150
|
+
# update which cells to run depending on conc change between tsteps
|
|
151
|
+
sat = [1]*self.GetGridCellCount()
|
|
152
|
+
self.SetSaturation(sat)
|
|
153
|
+
if diffmask is not None:
|
|
154
|
+
# get idx where diffmask is 0
|
|
155
|
+
inact = get_indices(0, diffmask)
|
|
156
|
+
if len(inact) > 0:
|
|
157
|
+
for i in inact:
|
|
158
|
+
sat[i] = 0
|
|
159
|
+
print(f"{'Cells sent to reactions':<25} | {self.GetGridCellCount()-len(inact):<0}/{self.GetGridCellCount():<15}")
|
|
160
|
+
self.SetSaturation(sat)
|
|
161
|
+
|
|
162
|
+
# allow phreeqc to print some info in the terminal
|
|
163
|
+
print_selected_output_on = True
|
|
164
|
+
print_chemistry_on = True
|
|
165
|
+
status = self.SetSelectedOutputOn(print_selected_output_on)
|
|
166
|
+
status = self.SetPrintChemistryOn(print_chemistry_on, False, True)
|
|
167
|
+
# reactions loop
|
|
168
|
+
sol_start = datetime.now()
|
|
169
|
+
|
|
170
|
+
message = f"{'Reaction loop':<25} | {'Stress period:':<15} {self.kper:<5} | {'Time step:':<15} {self.kstp:<10} | {'Running ...':<10}"
|
|
171
|
+
self.LogMessage(message+'\n') # log message
|
|
172
|
+
print(message)
|
|
173
|
+
# self.ScreenMessage(message)
|
|
174
|
+
# status = self.RunCells()
|
|
175
|
+
# if status < 0:
|
|
176
|
+
# print('Error in RunCells: {0}'.format(status))
|
|
177
|
+
self.update()
|
|
178
|
+
td = (datetime.now() - sol_start).total_seconds() / 60.0
|
|
179
|
+
message = f"{'Reaction loop':<25} | {'Stress period:':<15} {self.kper:<5} | {'Time step:':<15} {self.kstp:<10} | {'Completed in :':<10} {td//60:.0f} min {td%60:.4f} sec\n\n"
|
|
180
|
+
self.LogMessage(message)
|
|
181
|
+
print(message)
|
|
182
|
+
# self.ScreenMessage(message)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _get_kper_kstp_from_mf6api(self, mf6api):
|
|
186
|
+
'''Function to get the kper and kstp from mf6api
|
|
187
|
+
'''
|
|
188
|
+
assert isinstance(mf6api, Mf6API), 'mf6api must be an instance of Mf6API'
|
|
189
|
+
self.kper = mf6api.kper
|
|
190
|
+
self.kstp = mf6api.kstp
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class Mf6API(modflowapi.ModflowApi):
|
|
195
|
+
def __init__(self, wd, dll):
|
|
196
|
+
modflowapi.ModflowApi.__init__(self, dll, working_directory=wd)
|
|
197
|
+
self.initialize()
|
|
198
|
+
self.sim = flopy.mf6.MFSimulation.load(sim_ws=wd, verbosity_level=0)
|
|
199
|
+
|
|
200
|
+
def _prepare_mf6(self):
|
|
201
|
+
'''Prepare mf6 bmi for transport calculations
|
|
202
|
+
'''
|
|
203
|
+
self.modelnmes = ['Flow'] + [nme.capitalize() for nme in self.sim.model_names if nme != 'gwf']
|
|
204
|
+
self.components = [nme.capitalize() for nme in self.sim.model_names[1:]]
|
|
205
|
+
self.nsln = self.get_subcomponent_count()
|
|
206
|
+
self.sim_start = datetime.now()
|
|
207
|
+
self.ctimes = [0.0]
|
|
208
|
+
self.num_fails = 0
|
|
209
|
+
|
|
210
|
+
def _solve_gwt(self):
|
|
211
|
+
'''Function to solve the transport loop
|
|
212
|
+
'''
|
|
213
|
+
# prep to solve
|
|
214
|
+
for sln in range(1, self.nsln+1):
|
|
215
|
+
self.prepare_solve(sln)
|
|
216
|
+
# the one-based stress period number
|
|
217
|
+
stress_period = self.get_value(self.get_var_address("KPER", "TDIS"))[0]
|
|
218
|
+
time_step = self.get_value(self.get_var_address("KSTP", "TDIS"))[0]
|
|
219
|
+
|
|
220
|
+
self.kper = stress_period
|
|
221
|
+
self.kstp = time_step
|
|
222
|
+
msg = f"{'Transport loop':<25} | {'Stress period:':<15} {stress_period:<5} | {'Time step:':<15} {time_step:<10} | {'Running ...':<10}"
|
|
223
|
+
print(msg)
|
|
224
|
+
# mf6 transport loop block
|
|
225
|
+
for sln in range(1, self.nsln+1):
|
|
226
|
+
# if self.fixed_components is not None and modelnmes[sln-1] in self.fixed_components:
|
|
227
|
+
# print(f'not transporting {modelnmes[sln-1]}')
|
|
228
|
+
# continue
|
|
229
|
+
|
|
230
|
+
# set iteration counter
|
|
231
|
+
kiter = 0
|
|
232
|
+
# max number of solution iterations
|
|
233
|
+
max_iter = self.get_value(self.get_var_address("MXITER", f"SLN_{sln}")) # FIXME: not sure to define this inside the loop
|
|
234
|
+
self.prepare_solve(sln)
|
|
235
|
+
|
|
236
|
+
sol_start = datetime.now()
|
|
237
|
+
while kiter < max_iter:
|
|
238
|
+
convg = self.solve(sln)
|
|
239
|
+
if convg:
|
|
240
|
+
td = (datetime.now() - sol_start).total_seconds() / 60.0
|
|
241
|
+
break
|
|
242
|
+
kiter += 1
|
|
243
|
+
if not convg:
|
|
244
|
+
td = (datetime.now() - sol_start).total_seconds() / 60.0
|
|
245
|
+
print("\nTransport stress period: {0} --- time step: {1} --- did not converge with {2} iters --- took {3:10.5G} mins".format(stress_period, time_step, kiter, td))
|
|
246
|
+
self.num_fails += 1
|
|
247
|
+
try:
|
|
248
|
+
self.finalize_solve(sln)
|
|
249
|
+
except:
|
|
250
|
+
pass
|
|
251
|
+
td = (datetime.now() - sol_start).total_seconds() / 60.0
|
|
252
|
+
print(f"{'Transport loop':<25} | {'Stress period:':<15} {stress_period:<5} | {'Time step:':<15} {time_step:<10} | {'Completed in :':<10} {td//60:.0f} min {td%60:.4f} sec")
|
|
253
|
+
|
|
254
|
+
def _check_num_fails(self):
|
|
255
|
+
if self.num_fails > 0:
|
|
256
|
+
print("\nTransport failed to converge {0} times \n".format(self.num_fails))
|
|
257
|
+
else:
|
|
258
|
+
print("\nTransport converged successfully without any fails")
|
|
259
|
+
|
|
260
|
+
class Mf6RTM(object):
|
|
261
|
+
def __init__(self, wd, mf6api, phreeqcbmi):
|
|
262
|
+
assert isinstance(mf6api, Mf6API), 'MF6API must be an instance of Mf6API'
|
|
263
|
+
assert isinstance(phreeqcbmi, PhreeqcBMI), 'PhreeqcBMI must be an instance of PhreeqcBMI'
|
|
264
|
+
self.mf6api = mf6api
|
|
265
|
+
self.phreeqcbmi = phreeqcbmi
|
|
266
|
+
self.charge_offset = 0.0
|
|
267
|
+
self.wd = wd
|
|
268
|
+
self.sout_fname = 'sout.csv'
|
|
269
|
+
self.reactive = True
|
|
270
|
+
self.epsaqu = 0.0
|
|
271
|
+
self.fixed_components = None
|
|
272
|
+
|
|
273
|
+
#set discretization
|
|
274
|
+
self._set_dis()
|
|
275
|
+
|
|
276
|
+
def _set_fixed_components(self, fixed_components):
|
|
277
|
+
...
|
|
278
|
+
def _set_reactive(self, reactive):
|
|
279
|
+
'''Set the model to run only transport or transport and reactions
|
|
280
|
+
'''
|
|
281
|
+
self.reactive = reactive
|
|
282
|
+
|
|
283
|
+
def _set_dis(self):
|
|
284
|
+
'''Set the model grid dimensions according to mf6 grid type
|
|
285
|
+
'''
|
|
286
|
+
if determine_grid_type(self.mf6api.sim) == 'DIS':
|
|
287
|
+
self.nlay, self.nrow, self.ncol = get_mf6_dis(self.mf6api.sim)
|
|
288
|
+
self.nxyz = calc_nxyz_from_dis(self.mf6api.sim)
|
|
289
|
+
elif determine_grid_type(self.mf6api.sim) == 'DISV':
|
|
290
|
+
self.nlay = self.mf6api.sim.nlay
|
|
291
|
+
self.ncpl = self.mf6api.sim.ncpl
|
|
292
|
+
self.nxyz = self.nlay*self.ncpl
|
|
293
|
+
|
|
294
|
+
def _prepare_to_solve(self):
|
|
295
|
+
'''Prepare the model to solve
|
|
296
|
+
'''
|
|
297
|
+
# check if sout fname exists
|
|
298
|
+
if self._check_sout_exist():
|
|
299
|
+
self._rm_sout_file()
|
|
300
|
+
|
|
301
|
+
self.mf6api._prepare_mf6()
|
|
302
|
+
self.phreeqcbmi._prepare_phreeqcrm_bmi()
|
|
303
|
+
|
|
304
|
+
# get and write sout headers
|
|
305
|
+
self._write_sout_headers()
|
|
306
|
+
|
|
307
|
+
def _set_ctime(self):
|
|
308
|
+
'''Set the current time of the simulation from mf6api
|
|
309
|
+
'''
|
|
310
|
+
self.ctime = self.mf6api.get_current_time()
|
|
311
|
+
self.phreeqcbmi._set_ctime(self.ctime)
|
|
312
|
+
return self.ctime
|
|
313
|
+
|
|
314
|
+
def _set_etime(self):
|
|
315
|
+
'''Set the end time of the simulation from mf6api
|
|
316
|
+
'''
|
|
317
|
+
self.etime = self.mf6api.get_end_time()
|
|
318
|
+
return self.etime
|
|
319
|
+
|
|
320
|
+
def _set_time_step(self):
|
|
321
|
+
self.time_step = self.mf6api.get_time_step()
|
|
322
|
+
return self.time_step
|
|
323
|
+
|
|
324
|
+
def _finalize(self):
|
|
325
|
+
'''Finalize the APIs
|
|
326
|
+
'''
|
|
327
|
+
self._finalize_mf6api()
|
|
328
|
+
self._finalize_phreeqcrm()
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
def _finalize_mf6api(self):
|
|
332
|
+
'''Finalize the mf6api
|
|
333
|
+
'''
|
|
334
|
+
self.mf6api.finalize()
|
|
335
|
+
|
|
336
|
+
def _finalize_phreeqcrm(self):
|
|
337
|
+
'''Finalize the phreeqcrm api
|
|
338
|
+
'''
|
|
339
|
+
self.phreeqcbmi.finalize()
|
|
340
|
+
|
|
341
|
+
def _get_cdlbl_vect(self):
|
|
342
|
+
'''Get the concentration array from phreeqc bmi reshape to (ncomps, nxyz)
|
|
343
|
+
'''
|
|
344
|
+
c_dbl_vect = self.phreeqcbmi.GetConcentrations()
|
|
345
|
+
|
|
346
|
+
conc = [c_dbl_vect[i:i + self.nxyz] for i in range(0, len(c_dbl_vect), self.nxyz)] # reshape array
|
|
347
|
+
return conc
|
|
348
|
+
|
|
349
|
+
def _set_conc_at_current_kstep(self, c_dbl_vect):
|
|
350
|
+
'''Saves the current concentration array to the object
|
|
351
|
+
'''
|
|
352
|
+
self.current_iteration_conc = np.reshape(c_dbl_vect, (self.phreeqcbmi.ncomps, self.nxyz))
|
|
353
|
+
|
|
354
|
+
def _set_conc_at_previous_kstep(self, c_dbl_vect):
|
|
355
|
+
'''Saves the current concentration array to the object
|
|
356
|
+
'''
|
|
357
|
+
self.previous_iteration_conc = np.reshape(c_dbl_vect, (self.phreeqcbmi.ncomps, self.nxyz))
|
|
358
|
+
|
|
359
|
+
def _transfer_array_to_mf6(self):
|
|
360
|
+
'''Transfer the concentration array to mf6
|
|
361
|
+
'''
|
|
362
|
+
c_dbl_vect = self._get_cdlbl_vect()
|
|
363
|
+
|
|
364
|
+
# check if reactive cells were skipped due to small changes from transport and replace with previous conc
|
|
365
|
+
if self._check_previous_conc_exists() and self._check_inactive_cells_exist(self.diffmask):
|
|
366
|
+
c_dbl_vect = self._replace_inactive_cells(c_dbl_vect, self.diffmask)
|
|
367
|
+
else:
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
conc_dic = {}
|
|
371
|
+
for e, c in enumerate(self.phreeqcbmi.components):
|
|
372
|
+
# conc_dic[c] = np.reshape(c_dbl_vect[e], (self.nlay, self.nrow, self.ncol))
|
|
373
|
+
conc_dic[c] = c_dbl_vect[e]
|
|
374
|
+
# Set concentrations in mf6
|
|
375
|
+
if c.lower() == 'charge':
|
|
376
|
+
self.mf6api.set_value(f'{c.upper()}/X', concentration_l_to_m3(conc_dic[c]) + self.charge_offset)
|
|
377
|
+
else:
|
|
378
|
+
self.mf6api.set_value(f'{c.upper()}/X', concentration_l_to_m3(conc_dic[c]))
|
|
379
|
+
return c_dbl_vect
|
|
380
|
+
|
|
381
|
+
def _check_previous_conc_exists(self):
|
|
382
|
+
'''Function to replace inactive cells in the concentration array
|
|
383
|
+
'''
|
|
384
|
+
# check if self.previous_iteration_conc is a property
|
|
385
|
+
if not hasattr(self, 'previous_iteration_conc'):
|
|
386
|
+
return False
|
|
387
|
+
else:
|
|
388
|
+
return True
|
|
389
|
+
|
|
390
|
+
def _check_inactive_cells_exist(self, diffmask):
|
|
391
|
+
'''Function to check if inactive cells exist in the concentration array
|
|
392
|
+
'''
|
|
393
|
+
inact = get_indices(0, diffmask)
|
|
394
|
+
if len(inact) > 0:
|
|
395
|
+
return True
|
|
396
|
+
else:
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
def _replace_inactive_cells(self, c_dbl_vect, diffmask):
|
|
400
|
+
'''Function to replace inactive cells in the concentration array
|
|
401
|
+
'''
|
|
402
|
+
c_dbl_vect = np.reshape(c_dbl_vect, (self.phreeqcbmi.ncomps, self.nxyz))
|
|
403
|
+
# get inactive cells
|
|
404
|
+
inactive_idx = [get_indices(0, diffmask) for k in range(self.phreeqcbmi.ncomps)]
|
|
405
|
+
c_dbl_vect[:, inactive_idx] = self.previous_iteration_conc[:, inactive_idx]
|
|
406
|
+
c_dbl_vect = c_dbl_vect.flatten()
|
|
407
|
+
conc = [c_dbl_vect[i:i + self.nxyz] for i in range(0, len(c_dbl_vect), self.nxyz)]
|
|
408
|
+
return conc
|
|
409
|
+
|
|
410
|
+
def _transfer_array_to_phreeqcrm(self):
|
|
411
|
+
'''Transfer the concentration array to phreeqc bmi
|
|
412
|
+
'''
|
|
413
|
+
mf6_conc_array = []
|
|
414
|
+
for c in self.phreeqcbmi.components:
|
|
415
|
+
if c.lower() == 'charge':
|
|
416
|
+
mf6_conc_array.append(concentration_m3_to_l(self.mf6api.get_value(self.mf6api.get_var_address("X", f'{c.upper()}')) - self.charge_offset))
|
|
417
|
+
|
|
418
|
+
else:
|
|
419
|
+
mf6_conc_array.append(concentration_m3_to_l(self.mf6api.get_value(self.mf6api.get_var_address("X", f'{c.upper()}'))))
|
|
420
|
+
|
|
421
|
+
c_dbl_vect = np.reshape(mf6_conc_array, self.nxyz*self.phreeqcbmi.ncomps)
|
|
422
|
+
self.phreeqcbmi.SetConcentrations(c_dbl_vect)
|
|
423
|
+
|
|
424
|
+
#set the kper and kstp
|
|
425
|
+
self.phreeqcbmi._get_kper_kstp_from_mf6api(self.mf6api) # FIXME: calling this func here is not ideal
|
|
426
|
+
|
|
427
|
+
return c_dbl_vect
|
|
428
|
+
|
|
429
|
+
def _update_selected_output(self):
|
|
430
|
+
'''Update the selected output dataframe and save to attribute
|
|
431
|
+
'''
|
|
432
|
+
self._get_selected_output()
|
|
433
|
+
updf = pd.concat([self.phreeqcbmi.soutdf.astype(self._current_soutdf.dtypes), self._current_soutdf])
|
|
434
|
+
self._update_soutdf(updf)
|
|
435
|
+
|
|
436
|
+
def __replace_inactive_cells_in_sout(self, sout, diffmask):
|
|
437
|
+
'''Function to replace inactive cells in the selected output dataframe
|
|
438
|
+
'''
|
|
439
|
+
components = self.mf6api.modelnmes[1:]
|
|
440
|
+
headers = self.phreeqcbmi.sout_headers
|
|
441
|
+
# match headers in components closest string
|
|
442
|
+
|
|
443
|
+
inactive_idx = get_indices(0, diffmask)
|
|
444
|
+
|
|
445
|
+
sout[:, inactive_idx] = self._sout_k[:, inactive_idx]
|
|
446
|
+
return sout
|
|
447
|
+
|
|
448
|
+
def _get_selected_output(self):
|
|
449
|
+
'''Get the selected output from phreeqc bmi and replace skipped reactive cells with previous conc
|
|
450
|
+
'''
|
|
451
|
+
# selected ouput
|
|
452
|
+
self.phreeqcbmi.set_scalar("NthSelectedOutput", 0)
|
|
453
|
+
sout = self.phreeqcbmi.GetSelectedOutput()
|
|
454
|
+
sout = [sout[i:i + self.nxyz] for i in range(0, len(sout), self.nxyz)]
|
|
455
|
+
sout = np.array(sout)
|
|
456
|
+
if self._check_inactive_cells_exist(self.diffmask) and hasattr(self, '_sout_k'):
|
|
457
|
+
|
|
458
|
+
sout = self.__replace_inactive_cells_in_sout(sout, self.diffmask)
|
|
459
|
+
self._sout_k = sout #save sout to a private attribute
|
|
460
|
+
# add time to selected ouput
|
|
461
|
+
sout[0] = np.ones_like(sout[0])*(self.ctime+self.time_step)
|
|
462
|
+
df = pd.DataFrame(columns=self.phreeqcbmi.soutdf.columns)
|
|
463
|
+
for col, arr in zip(df.columns, sout):
|
|
464
|
+
df[col] = arr
|
|
465
|
+
self._current_soutdf = df
|
|
466
|
+
|
|
467
|
+
def _update_soutdf(self, df):
|
|
468
|
+
'''Update the selected output dataframe to phreeqcrm object
|
|
469
|
+
'''
|
|
470
|
+
self.phreeqcbmi.soutdf = df
|
|
471
|
+
|
|
472
|
+
def _check_sout_exist(self):
|
|
473
|
+
'''Check if selected output file exists
|
|
474
|
+
'''
|
|
475
|
+
if os.path.exists(os.path.join(self.wd, self.sout_fname)):
|
|
476
|
+
return True
|
|
477
|
+
else :
|
|
478
|
+
return False
|
|
479
|
+
|
|
480
|
+
def _write_sout_headers(self):
|
|
481
|
+
'''Write selected output headers to a file
|
|
482
|
+
'''
|
|
483
|
+
with open(os.path.join(self.wd,self.sout_fname), 'w') as f:
|
|
484
|
+
f.write(','.join(self.phreeqcbmi.sout_headers))
|
|
485
|
+
f.write('\n')
|
|
486
|
+
|
|
487
|
+
def _rm_sout_file(self):
|
|
488
|
+
'''Remove the selected output file
|
|
489
|
+
'''
|
|
490
|
+
try:
|
|
491
|
+
os.remove(os.path.join(self.wd, self.sout_fname))
|
|
492
|
+
except:
|
|
493
|
+
pass
|
|
494
|
+
|
|
495
|
+
def _append_to_soutdf_file(self):
|
|
496
|
+
'''Append the current selected output to the selected output file
|
|
497
|
+
'''
|
|
498
|
+
assert not self._current_soutdf.empty, 'current sout is empty'
|
|
499
|
+
self._current_soutdf.to_csv(os.path.join(self.wd, self.sout_fname), mode='a', index=False, header=False)
|
|
500
|
+
|
|
501
|
+
def _export_soutdf(self):
|
|
502
|
+
'''Export the selected output dataframe to a csv file
|
|
503
|
+
'''
|
|
504
|
+
self.phreeqcbmi.soutdf.to_csv(os.path.join(self.wd, self.sout_fname), index=False)
|
|
505
|
+
|
|
506
|
+
def _solve(self):
|
|
507
|
+
'''Solve the model
|
|
508
|
+
'''
|
|
509
|
+
success = False # initialize success flag
|
|
510
|
+
sim_start = datetime.now()
|
|
511
|
+
self._prepare_to_solve()
|
|
512
|
+
|
|
513
|
+
# check sout was created
|
|
514
|
+
assert self._check_sout_exist(), f'{self.sout_fname} not found'
|
|
515
|
+
|
|
516
|
+
print("Starting transport solution at {0}".format(sim_start.strftime(DT_FMT)))
|
|
517
|
+
print(f"Solving the following components: {', '.join([nme for nme in self.mf6api.modelnmes])}")
|
|
518
|
+
ctime = self._set_ctime()
|
|
519
|
+
etime = self._set_etime()
|
|
520
|
+
while ctime < etime:
|
|
521
|
+
temp_time = datetime.now()
|
|
522
|
+
print(f"Starting solution at {temp_time.strftime(DT_FMT)}")
|
|
523
|
+
# length of the current solve time
|
|
524
|
+
dt = self._set_time_step()
|
|
525
|
+
self.mf6api.prepare_time_step(dt)
|
|
526
|
+
self.mf6api._solve_gwt()
|
|
527
|
+
|
|
528
|
+
if self.reactive:
|
|
529
|
+
# reaction block
|
|
530
|
+
c_dbl_vect = self._transfer_array_to_phreeqcrm()
|
|
531
|
+
self._set_conc_at_current_kstep(c_dbl_vect)
|
|
532
|
+
if ctime == 0.0:
|
|
533
|
+
self.diffmask = np.ones(self.nxyz)
|
|
534
|
+
else:
|
|
535
|
+
diffmask = get_conc_change_mask(self.current_iteration_conc,
|
|
536
|
+
self.previous_iteration_conc,
|
|
537
|
+
self.phreeqcbmi.ncomps, self.nxyz,
|
|
538
|
+
treshold=self.epsaqu)
|
|
539
|
+
self.diffmask = diffmask
|
|
540
|
+
# solve reactions
|
|
541
|
+
self.phreeqcbmi._solve_phreeqcrm(dt, diffmask = self.diffmask)
|
|
542
|
+
c_dbl_vect = self._transfer_array_to_mf6()
|
|
543
|
+
# get sout and update df
|
|
544
|
+
self._update_selected_output()
|
|
545
|
+
# append current sout rows to file
|
|
546
|
+
self._append_to_soutdf_file()
|
|
547
|
+
self._set_conc_at_previous_kstep(c_dbl_vect)
|
|
548
|
+
|
|
549
|
+
self.mf6api.finalize_time_step()
|
|
550
|
+
ctime = self._set_ctime() # update the current time tracking
|
|
551
|
+
|
|
552
|
+
sim_end = datetime.now()
|
|
553
|
+
td = (sim_end - sim_start).total_seconds() / 60.0
|
|
554
|
+
|
|
555
|
+
self.mf6api._check_num_fails()
|
|
556
|
+
|
|
557
|
+
print("\nReactive transport solution finished at {0} --- it took: {1:10.5G} mins".format(sim_end.strftime(DT_FMT), td))
|
|
558
|
+
|
|
559
|
+
# Clean up and close api objs
|
|
560
|
+
try:
|
|
561
|
+
self._finalize()
|
|
562
|
+
success = True
|
|
563
|
+
print(mrbeaker())
|
|
564
|
+
print('\nMR BEAKER IMPORTANT MESSAGE: MODEL RUN FINISHED BUT CHECK THE RESULTS .. THEY ARE PROLY RUBBISH\n')
|
|
565
|
+
except:
|
|
566
|
+
print('MR BEAKER IMPORTANT MESSAGE: SOMETHING WENT WRONG. BUMMER\n')
|
|
567
|
+
pass
|
|
568
|
+
return success
|
|
569
|
+
|
|
570
|
+
def get_indices(element, lst):
|
|
571
|
+
return [i for i, x in enumerate(lst) if x == element]
|
|
572
|
+
|
|
573
|
+
def get_less_than_zero_idx(arr):
|
|
574
|
+
'''Function to get the index of all occurrences of <0 in an array
|
|
575
|
+
'''
|
|
576
|
+
idx = np.where(arr < 0)
|
|
577
|
+
return idx
|
|
578
|
+
|
|
579
|
+
def get_inactive_idx(arr, val = 1e30):
|
|
580
|
+
'''Function to get the index of all occurrences of <0 in an array
|
|
581
|
+
'''
|
|
582
|
+
idx = list(np.where(arr >= val)[0])
|
|
583
|
+
return idx
|
|
584
|
+
|
|
585
|
+
def get_conc_change_mask(ci, ck, ncomp, nxyz, treshold=1e-10):
|
|
586
|
+
'''Function to get the active-inactive cell mask for concentration change to inform phreeqc which cells to update
|
|
587
|
+
'''
|
|
588
|
+
# reshape arrays to 2D (nxyz, ncomp)
|
|
589
|
+
ci = ci.reshape(nxyz, ncomp)
|
|
590
|
+
ck = ck.reshape(nxyz, ncomp)+1e-30
|
|
591
|
+
|
|
592
|
+
# get the difference between the two arrays and divide by ci
|
|
593
|
+
diff = np.abs((ci - ck.reshape(-1*nxyz, ncomp))/ci) < treshold
|
|
594
|
+
diff = np.where(diff, 0, 1)
|
|
595
|
+
diff = diff.sum(axis=1)
|
|
596
|
+
|
|
597
|
+
# where values <0 put -1 else 1
|
|
598
|
+
diff = np.where(diff == 0, 0, 1)
|
|
599
|
+
return diff
|
|
600
|
+
|
|
601
|
+
def mrbeaker():
|
|
602
|
+
'''ASCII art of Mr. Beaker
|
|
603
|
+
'''
|
|
604
|
+
# get the path of this file
|
|
605
|
+
whereismrbeaker = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mrbeaker.png')
|
|
606
|
+
mr_beaker_image = Image.open(whereismrbeaker)
|
|
607
|
+
|
|
608
|
+
# Resize the image to fit the terminal width
|
|
609
|
+
terminal_width = 80 # Adjust this based on your terminal width
|
|
610
|
+
aspect_ratio = mr_beaker_image.width / mr_beaker_image.height
|
|
611
|
+
terminal_height = int(terminal_width / aspect_ratio*0.5)
|
|
612
|
+
mr_beaker_image = mr_beaker_image.resize((terminal_width, terminal_height))
|
|
613
|
+
|
|
614
|
+
# Convert the image to grayscale
|
|
615
|
+
mr_beaker_image = mr_beaker_image.convert("L")
|
|
616
|
+
|
|
617
|
+
# Convert the grayscale image to ASCII art
|
|
618
|
+
ascii_chars = "%,.?>#*+=-:."
|
|
619
|
+
|
|
620
|
+
mrbeaker = ""
|
|
621
|
+
for y in range(int(mr_beaker_image.height)):
|
|
622
|
+
mrbeaker += "\n"
|
|
623
|
+
for x in range(int(mr_beaker_image.width)):
|
|
624
|
+
pixel_value = mr_beaker_image.getpixel((x, y))
|
|
625
|
+
mrbeaker += ascii_chars[pixel_value // 64]
|
|
626
|
+
mrbeaker += "\n"
|
|
627
|
+
return mrbeaker
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def flatten_list(xss):
|
|
631
|
+
'''Flatten a list of lists
|
|
632
|
+
'''
|
|
633
|
+
return [x for xs in xss for x in xs]
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def concentration_l_to_m3(x):
|
|
637
|
+
'''Convert M/L to M/m3
|
|
638
|
+
'''
|
|
639
|
+
c = x*1e3
|
|
640
|
+
return c
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def concentration_m3_to_l(x):
|
|
644
|
+
'''Convert M/L to M/m3
|
|
645
|
+
'''
|
|
646
|
+
c = x*1e-3
|
|
647
|
+
return c
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def concentration_to_massrate(q, conc):
|
|
651
|
+
'''Calculate mass rate from rate (L3/T) and concentration (M/L3)
|
|
652
|
+
'''
|
|
653
|
+
mrate = q*conc # M/T
|
|
654
|
+
return mrate
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def concentration_volbulk_to_volwater(conc_volbulk, porosity):
|
|
658
|
+
'''Calculate concentrations as volume of pore water from bulk volume and porosity
|
|
659
|
+
'''
|
|
660
|
+
conc_volwater = conc_volbulk*(1/porosity)
|
|
661
|
+
return conc_volwater
|