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 ADDED
@@ -0,0 +1,10 @@
1
+ from mf6rtm import mf6rtm
2
+ import os
3
+
4
+ __author__ = "Pablo Ortega"
5
+
6
+ def run_cmd():
7
+ # get the current directory
8
+ cwd = os.getcwd()
9
+ # run the solve function
10
+ mf6rtm.solve(cwd)
mf6rtm/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from . import run_cmd
2
+
3
+ if __name__ == '__main__':
4
+ run_cmd()
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