1046 lines
33 KiB
Python
1046 lines
33 KiB
Python
"""
|
|
This is a set of classes for reading budget information out of MODFLOW-style
|
|
listing files. Cumulative and incremental budgets are returned as numpy
|
|
recarrays, which can then be easily plotted.
|
|
|
|
"""
|
|
|
|
import collections
|
|
import os
|
|
import re
|
|
from datetime import timedelta
|
|
import numpy as np
|
|
import errno
|
|
|
|
from ..utils.utils_def import totim_to_datetime
|
|
|
|
|
|
class ListBudget(object):
|
|
"""
|
|
MODFLOW family list file handling
|
|
|
|
Parameters
|
|
----------
|
|
file_name : str
|
|
the list file name
|
|
budgetkey : str
|
|
the text string identifying the budget table. (default is None)
|
|
timeunit : str
|
|
the time unit to return in the recarray. (default is 'days')
|
|
|
|
Notes
|
|
-----
|
|
The ListBudget class should not be instantiated directly. Access is
|
|
through derived classes: MfListBudget (MODFLOW), SwtListBudget (SEAWAT)
|
|
and SwrListBudget (MODFLOW with the SWR process)
|
|
|
|
Examples
|
|
--------
|
|
>>> mf_list = MfListBudget("my_model.list")
|
|
>>> incremental, cumulative = mf_list.get_budget()
|
|
>>> df_in, df_out = mf_list.get_dataframes(start_datetime="10-21-2015")
|
|
|
|
"""
|
|
|
|
def __init__(self, file_name, budgetkey=None, timeunit="days"):
|
|
|
|
# Set up file reading
|
|
assert os.path.exists(file_name), "file_name {0} not found".format(
|
|
file_name
|
|
)
|
|
self.file_name = file_name
|
|
self.f = open(file_name, "r", encoding="ascii", errors="replace")
|
|
|
|
self.tssp_lines = 0
|
|
|
|
# Assign the budgetkey, which should have been overridden
|
|
if budgetkey is None:
|
|
self.set_budget_key()
|
|
else:
|
|
self.budgetkey = budgetkey
|
|
|
|
self.totim = []
|
|
self.timeunit = timeunit
|
|
self.idx_map = []
|
|
self.entries = []
|
|
self.null_entries = []
|
|
|
|
self.time_line_idx = 20
|
|
if timeunit.upper() == "SECONDS":
|
|
self.timeunit = "S"
|
|
self.time_idx = 0
|
|
elif timeunit.upper() == "MINUTES":
|
|
self.timeunit = "M"
|
|
self.time_idx = 1
|
|
elif timeunit.upper() == "HOURS":
|
|
self.timeunit = "H"
|
|
self.time_idx = 2
|
|
elif timeunit.upper() == "DAYS":
|
|
self.timeunit = "D"
|
|
self.time_idx = 3
|
|
elif timeunit.upper() == "YEARS":
|
|
self.timeunit = "Y"
|
|
self.time_idx = 4
|
|
else:
|
|
raise Exception(
|
|
"need to reset time_idxs attribute to "
|
|
"use units other than days and check usage of "
|
|
"timedelta"
|
|
)
|
|
|
|
# Fill budget recarrays
|
|
self._load()
|
|
self._isvalid = False
|
|
if len(self.idx_map) > 0:
|
|
self._isvalid = True
|
|
|
|
# Close the open file
|
|
self.f.close()
|
|
|
|
# return
|
|
return
|
|
|
|
def set_budget_key(self):
|
|
raise Exception("Must be overridden...")
|
|
|
|
def isvalid(self):
|
|
"""
|
|
Get a boolean indicating if budget data are available in the file.
|
|
|
|
Returns
|
|
-------
|
|
out : boolean
|
|
Boolean indicating if budget data are available in the file.
|
|
|
|
Examples
|
|
--------
|
|
>>> mf_list = MfListBudget('my_model.list')
|
|
>>> valid = mf_list.isvalid()
|
|
|
|
"""
|
|
return self._isvalid
|
|
|
|
def get_record_names(self):
|
|
"""
|
|
Get a list of water budget record names in the file.
|
|
|
|
Returns
|
|
-------
|
|
out : list of strings
|
|
List of unique text names in the binary file.
|
|
|
|
Examples
|
|
--------
|
|
>>> mf_list = MfListBudget('my_model.list')
|
|
>>> names = mf_list.get_record_names()
|
|
|
|
"""
|
|
if not self._isvalid:
|
|
return None
|
|
return self.inc.dtype.names
|
|
|
|
def get_times(self):
|
|
"""
|
|
Get a list of unique water budget times in the list file.
|
|
|
|
Returns
|
|
-------
|
|
out : list of floats
|
|
List contains unique water budget simulation times (totim) in list file.
|
|
|
|
Examples
|
|
--------
|
|
>>> mf_list = MfListBudget('my_model.list')
|
|
>>> times = mf_list.get_times()
|
|
|
|
"""
|
|
if not self._isvalid:
|
|
return None
|
|
return self.inc["totim"].tolist()
|
|
|
|
def get_kstpkper(self):
|
|
"""
|
|
Get a list of unique stress periods and time steps in the list file
|
|
water budgets.
|
|
|
|
Returns
|
|
----------
|
|
out : list of (kstp, kper) tuples
|
|
List of unique kstp, kper combinations in list file. kstp and
|
|
kper values are zero-based.
|
|
|
|
Examples
|
|
--------
|
|
>>> mf_list = MfListBudget("my_model.list")
|
|
>>> kstpkper = mf_list.get_kstpkper()
|
|
|
|
"""
|
|
if not self._isvalid:
|
|
return None
|
|
kstpkper = []
|
|
for kstp, kper in zip(
|
|
self.inc["time_step"], self.inc["stress_period"]
|
|
):
|
|
kstpkper.append((kstp, kper))
|
|
return kstpkper
|
|
|
|
def get_incremental(self, names=None):
|
|
"""
|
|
Get a recarray with the incremental water budget items in the list file.
|
|
|
|
Parameters
|
|
----------
|
|
names : str or list of strings
|
|
Selection of column names to return. If names is not None then
|
|
totim, time_step, stress_period, and selection(s) will be returned.
|
|
(default is None).
|
|
|
|
Returns
|
|
-------
|
|
out : recarray
|
|
Numpy recarray with the water budget items in list file. The
|
|
recarray also includes totim, time_step, and stress_period.
|
|
|
|
Examples
|
|
--------
|
|
>>> mf_list = MfListBudget("my_model.list")
|
|
>>> incremental = mf_list.get_incremental()
|
|
|
|
"""
|
|
if not self._isvalid:
|
|
return None
|
|
if names is None:
|
|
return self.inc
|
|
else:
|
|
if not isinstance(names, list):
|
|
names = [names]
|
|
names.insert(0, "stress_period")
|
|
names.insert(0, "time_step")
|
|
names.insert(0, "totim")
|
|
return self.inc[names].view(np.recarray)
|
|
|
|
def get_cumulative(self, names=None):
|
|
"""
|
|
Get a recarray with the cumulative water budget items in the list file.
|
|
|
|
Parameters
|
|
----------
|
|
names : str or list of strings
|
|
Selection of column names to return. If names is not None then
|
|
totim, time_step, stress_period, and selection(s) will be returned.
|
|
(default is None).
|
|
|
|
Returns
|
|
-------
|
|
out : recarray
|
|
Numpy recarray with the water budget items in list file. The
|
|
recarray also includes totim, time_step, and stress_period.
|
|
|
|
Examples
|
|
--------
|
|
>>> mf_list = MfListBudget("my_model.list")
|
|
>>> cumulative = mf_list.get_cumulative()
|
|
|
|
"""
|
|
if not self._isvalid:
|
|
return None
|
|
if names is None:
|
|
return self.cum
|
|
else:
|
|
if not isinstance(names, list):
|
|
names = [names]
|
|
names.insert(0, "stress_period")
|
|
names.insert(0, "time_step")
|
|
names.insert(0, "totim")
|
|
return np.array(self.cum)[names].view(np.recarray)
|
|
|
|
def get_model_runtime(self, units="seconds"):
|
|
"""
|
|
Get the elapsed runtime of the model from the list file.
|
|
|
|
Parameters
|
|
----------
|
|
units : str
|
|
Units in which to return the runtime. Acceptable values are 'seconds', 'minutes', 'hours'
|
|
(default is 'seconds')
|
|
|
|
Returns
|
|
-------
|
|
out : float
|
|
Floating point value with the runtime in requested units. Returns NaN if runtime not found in list file
|
|
|
|
Examples
|
|
--------
|
|
>>> mf_list = MfListBudget("my_model.list")
|
|
>>> budget = mf_list.get_model_runtime(units='hours')
|
|
"""
|
|
if not self._isvalid:
|
|
return None
|
|
|
|
# reopen the file
|
|
self.f = open(self.file_name, "r", encoding="ascii", errors="replace")
|
|
units = units.lower()
|
|
if (
|
|
not units == "seconds"
|
|
and not units == "minutes"
|
|
and not units == "hours"
|
|
):
|
|
raise (
|
|
'"units" input variable must be "minutes", "hours", or "seconds": {0} was specified'.format(
|
|
units
|
|
)
|
|
)
|
|
try:
|
|
seekpoint = self._seek_to_string("Elapsed run time:")
|
|
except:
|
|
print("Elapsed run time not included in list file. Returning NaN")
|
|
return np.nan
|
|
|
|
self.f.seek(seekpoint)
|
|
line = self.f.readline()
|
|
|
|
self.f.close()
|
|
# yank out the floating point values from the Elapsed run time string
|
|
times = list(map(float, re.findall(r"[+-]?[0-9.]+", line)))
|
|
# pad an array with zeros and times with [days, hours, minutes, seconds]
|
|
times = np.array([0 for i in range(4 - len(times))] + times)
|
|
# convert all to seconds
|
|
time2sec = np.array([24 * 60 * 60, 60 * 60, 60, 1])
|
|
times_sec = np.sum(times * time2sec)
|
|
# return in the requested units
|
|
if units == "seconds":
|
|
return times_sec
|
|
elif units == "minutes":
|
|
return times_sec / 60.0
|
|
elif units == "hours":
|
|
return times_sec / 60.0 / 60.0
|
|
|
|
def get_budget(self, names=None):
|
|
"""
|
|
Get the recarrays with the incremental and cumulative water budget items
|
|
in the list file.
|
|
|
|
Parameters
|
|
----------
|
|
names : str or list of strings
|
|
Selection of column names to return. If names is not None then
|
|
totim, time_step, stress_period, and selection(s) will be returned.
|
|
(default is None).
|
|
|
|
Returns
|
|
-------
|
|
out : recarrays
|
|
Numpy recarrays with the water budget items in list file. The
|
|
recarray also includes totim, time_step, and stress_period. A
|
|
separate recarray is returned for the incremental and cumulative
|
|
water budget entries.
|
|
|
|
Examples
|
|
--------
|
|
>>> mf_list = MfListBudget("my_model.list")
|
|
>>> budget = mf_list.get_budget()
|
|
|
|
"""
|
|
if not self._isvalid:
|
|
return None
|
|
if names is None:
|
|
return self.inc, self.cum
|
|
else:
|
|
if not isinstance(names, list):
|
|
names = [names]
|
|
names.insert(0, "stress_period")
|
|
names.insert(0, "time_step")
|
|
names.insert(0, "totim")
|
|
return (
|
|
self.inc[names].view(np.recarray),
|
|
self.cum[names].view(np.recarray),
|
|
)
|
|
|
|
def get_data(self, kstpkper=None, idx=None, totim=None, incremental=False):
|
|
"""
|
|
Get water budget data from the list file for the specified conditions.
|
|
|
|
Parameters
|
|
----------
|
|
idx : int
|
|
The zero-based record number. The first record is record 0.
|
|
(default is None).
|
|
kstpkper : tuple of ints
|
|
A tuple containing the time step and stress period (kstp, kper).
|
|
These are zero-based kstp and kper values. (default is None).
|
|
totim : float
|
|
The simulation time. (default is None).
|
|
incremental : bool
|
|
Boolean flag used to determine if incremental or cumulative water
|
|
budget data for the specified conditions will be returned. If
|
|
incremental=True, incremental water budget data will be returned.
|
|
If incremental=False, cumulative water budget data will be
|
|
returned. (default is False).
|
|
|
|
Returns
|
|
-------
|
|
data : numpy recarray
|
|
Array has size (number of budget items, 3). Recarray names are 'index',
|
|
'value', 'name'.
|
|
|
|
See Also
|
|
--------
|
|
|
|
Notes
|
|
-----
|
|
if both kstpkper and totim are None, will return the last entry
|
|
|
|
Examples
|
|
--------
|
|
>>> import matplotlib.pyplot as plt
|
|
>>> import flopy
|
|
>>> mf_list = flopy.utils.MfListBudget("my_model.list")
|
|
>>> data = mf_list.get_data(kstpkper=(0,0))
|
|
>>> plt.bar(data['index'], data['value'])
|
|
>>> plt.xticks(data['index'], data['name'], rotation=45, size=6)
|
|
>>> plt.show()
|
|
|
|
"""
|
|
if not self._isvalid:
|
|
return None
|
|
ipos = None
|
|
if kstpkper is not None:
|
|
try:
|
|
ipos = self.get_kstpkper().index(kstpkper)
|
|
except:
|
|
print(
|
|
" could not retrieve kstpkper "
|
|
+ "{} from the lst file".format(kstpkper)
|
|
)
|
|
elif totim is not None:
|
|
try:
|
|
ipos = self.get_times().index(totim)
|
|
except:
|
|
print(
|
|
" could not retrieve totime "
|
|
+ "{} from the lst file".format(totim)
|
|
)
|
|
elif idx is not None:
|
|
ipos = idx
|
|
else:
|
|
ipos = -1
|
|
|
|
if ipos is None:
|
|
print("Could not find specified condition.")
|
|
print(" kstpkper = {}".format(kstpkper))
|
|
print(" totim = {}".format(totim))
|
|
# TODO: return zero-length array, or update docstring return type
|
|
return None
|
|
|
|
if incremental:
|
|
t = self.inc[ipos]
|
|
else:
|
|
t = self.cum[ipos]
|
|
|
|
dtype = np.dtype(
|
|
[("index", np.int32), ("value", np.float32), ("name", "|S25")]
|
|
)
|
|
v = np.recarray(shape=(len(self.inc.dtype.names[3:])), dtype=dtype)
|
|
for i, name in enumerate(self.inc.dtype.names[3:]):
|
|
mult = 1.0
|
|
if "_OUT" in name:
|
|
mult = -1.0
|
|
v[i]["index"] = i
|
|
v[i]["value"] = mult * t[name]
|
|
v[i]["name"] = name
|
|
return v
|
|
|
|
def get_dataframes(self, start_datetime="1-1-1970", diff=False):
|
|
"""
|
|
Get pandas dataframes with the incremental and cumulative water budget
|
|
items in the list file.
|
|
|
|
Parameters
|
|
----------
|
|
start_datetime : str
|
|
If start_datetime is passed as None, the rows are indexed on totim.
|
|
Otherwise, a DatetimeIndex is set. (default is 1-1-1970).
|
|
|
|
Returns
|
|
-------
|
|
out : pandas dataframes
|
|
Pandas dataframes with the incremental and cumulative water budget
|
|
items in list file. A separate pandas dataframe is returned for the
|
|
incremental and cumulative water budget entries.
|
|
|
|
Examples
|
|
--------
|
|
>>> mf_list = MfListBudget("my_model.list")
|
|
>>> incrementaldf, cumulativedf = mf_list.get_dataframes()
|
|
|
|
"""
|
|
|
|
try:
|
|
import pandas as pd
|
|
except Exception as e:
|
|
msg = "ListBudget.get_dataframe(): requires pandas: " + str(e)
|
|
raise ImportError(msg)
|
|
|
|
if not self._isvalid:
|
|
return None
|
|
totim = self.get_times()
|
|
if start_datetime is not None:
|
|
totim = totim_to_datetime(
|
|
totim,
|
|
start=pd.to_datetime(start_datetime),
|
|
timeunit=self.timeunit,
|
|
)
|
|
|
|
df_flux = pd.DataFrame(self.inc, index=totim).loc[:, self.entries]
|
|
df_vol = pd.DataFrame(self.cum, index=totim).loc[:, self.entries]
|
|
|
|
if not diff:
|
|
return df_flux, df_vol
|
|
|
|
else:
|
|
in_names = [col for col in df_flux.columns if col.endswith("_IN")]
|
|
|
|
base_names = [name.replace("_IN", "") for name in in_names]
|
|
for name in base_names:
|
|
in_name = name + "_IN"
|
|
out_name = name + "_OUT"
|
|
df_flux.loc[:, name.lower()] = (
|
|
df_flux.loc[:, in_name] - df_flux.loc[:, out_name]
|
|
)
|
|
df_flux.pop(in_name)
|
|
df_flux.pop(out_name)
|
|
df_vol.loc[:, name.lower()] = (
|
|
df_vol.loc[:, in_name] - df_vol.loc[:, out_name]
|
|
)
|
|
df_vol.pop(in_name)
|
|
df_vol.pop(out_name)
|
|
cols = list(df_flux.columns)
|
|
cols = [col.lower() for col in cols]
|
|
df_flux.columns = cols
|
|
df_vol.columns = cols
|
|
df_flux.sort_index(axis=1, inplace=True)
|
|
df_vol.sort_index(axis=1, inplace=True)
|
|
return df_flux, df_vol
|
|
|
|
def get_reduced_pumping(self):
|
|
"""
|
|
Get numpy recarray of reduced pumping data from a list file.
|
|
Reduced pumping data most have been written to the list file
|
|
during the model run. Works with MfListBudget and MfusgListBudget.
|
|
|
|
Returns
|
|
-------
|
|
numpy recarray
|
|
A numpy recarray with the reduced pumping data from the list
|
|
file.
|
|
|
|
Example
|
|
--------
|
|
>>> objLST = MfListBudget("my_model.lst")
|
|
>>> raryReducedPpg = objLST.get_reduced_pumping()
|
|
>>> dfReducedPpg = pd.DataFrame.from_records(raryReducedPpg)
|
|
|
|
"""
|
|
|
|
# Ensure list file exists
|
|
if not os.path.isfile(self.f.name):
|
|
raise FileNotFoundError(
|
|
errno.ENOENT, os.strerror(errno.ENOENT), self.f.name
|
|
)
|
|
|
|
# Eval based on model list type
|
|
if isinstance(self, MfListBudget):
|
|
# Check if reduced pumping data was set to be written
|
|
# to list file
|
|
sCheck = (
|
|
"WELLS WITH REDUCED PUMPING WILL BE REPORTED "
|
|
+ "TO THE MAIN LISTING FILE"
|
|
)
|
|
assert open(self.f.name).read().find(sCheck) > 0, (
|
|
"Pumping reductions not written to list file. "
|
|
+ 'Try removing "noprint" keyword from well file.'
|
|
)
|
|
|
|
# Set dtypes for resulting data
|
|
dtype = np.dtype(
|
|
[
|
|
("SP", np.int32),
|
|
("TS", np.int32),
|
|
("LAY", np.int32),
|
|
("ROW", np.int32),
|
|
("COL", np.int32),
|
|
("APPL.Q", np.float64),
|
|
("ACT.Q", np.float64),
|
|
("GW-HEAD", np.float64),
|
|
("CELL-BOT", np.float64),
|
|
]
|
|
)
|
|
|
|
# Define string to id start of reduced ppg data
|
|
sKey = "WELLS WITH REDUCED PUMPING FOR STRESS PERIOD"
|
|
|
|
elif isinstance(self, MfusgListBudget):
|
|
# Check if reduced pumping data was written and if set to
|
|
# be written to list file
|
|
sCheck = "WELL REDUCTION INFO WILL BE WRITTEN TO UNIT:"
|
|
bLstUnit = False
|
|
bRdcdPpg = False
|
|
for l in open(self.f.name):
|
|
# Assumes LST unit always first
|
|
if "UNIT" in l and not bLstUnit:
|
|
iLstUnit = int(l.strip().split()[-1])
|
|
bLstUnit = True
|
|
if sCheck in l:
|
|
bRdcdPpg = True
|
|
assert int(l.strip().split()[-1]) == iLstUnit, (
|
|
"Pumping reductions not written to list file. "
|
|
+ "Try setting iunitafr to the list file unit number."
|
|
)
|
|
assert bRdcdPpg, "Auto pumping reductions not active."
|
|
|
|
# Set dtypes for resulting data
|
|
dtype = np.dtype(
|
|
[
|
|
("SP", np.int32),
|
|
("TS", np.int32),
|
|
("WELL.NO", np.int32),
|
|
("CLN NODE", np.int32),
|
|
("APPL.Q", np.float64),
|
|
("ACT.Q", np.float64),
|
|
("GW_HEAD", np.float64),
|
|
("CELL_BOT", np.float64),
|
|
]
|
|
)
|
|
|
|
# Define string to id start of reduced ppg data
|
|
sKey = "WELLS WITH REDUCED PUMPING FOR STRESS PERIOD"
|
|
|
|
# elif isinstance(self, other ListBudget class):
|
|
|
|
else:
|
|
msg = (
|
|
"get_reduced_pumping() is only implemented for the "
|
|
+ "MfListBudget or MfusgListBudget classes. Please "
|
|
+ "feel free to expand the functionality to other "
|
|
+ "ListBudget classes."
|
|
)
|
|
raise NotImplementedError(msg)
|
|
|
|
# Iterate through list file to read in reduced ppg info
|
|
f = open(self.f.name)
|
|
lsData = []
|
|
while True:
|
|
l = f.readline()
|
|
if l == "":
|
|
break
|
|
# If l is reduced ppg header row
|
|
if sKey in l:
|
|
# Extract sp and ts
|
|
ts, sp = self._get_ts_sp(l)
|
|
# Skip line of data column titles
|
|
f.readline()
|
|
# Iterate through lines of reduced ppg data
|
|
while True:
|
|
l = f.readline()
|
|
# Condition to exit loop
|
|
if len(l.strip().split()) < 6:
|
|
break
|
|
# Create list of hold line of data
|
|
ls = [sp, ts]
|
|
# Add other data to list
|
|
ls.extend([float(x) for x in l.split()])
|
|
# Add list to overall list of data
|
|
lsData.append(ls)
|
|
f.close()
|
|
|
|
return np.rec.fromrecords([tuple(x) for x in lsData], dtype=dtype)
|
|
|
|
def _build_index(self, maxentries):
|
|
self.idx_map = self._get_index(maxentries)
|
|
return
|
|
|
|
def _get_index(self, maxentries):
|
|
# --parse through the file looking for matches and parsing ts and sp
|
|
idxs = []
|
|
l_count = 1
|
|
while True:
|
|
seekpoint = self.f.tell()
|
|
line = self.f.readline()
|
|
if line == "":
|
|
break
|
|
if self.budgetkey in line:
|
|
for l in range(self.tssp_lines):
|
|
line = self.f.readline()
|
|
try:
|
|
ts, sp = self._get_ts_sp(line)
|
|
except:
|
|
print(
|
|
"unable to cast ts,sp on line number",
|
|
l_count,
|
|
" line: ",
|
|
line,
|
|
)
|
|
break
|
|
# print('info found for timestep stress period',ts,sp)
|
|
|
|
idxs.append([ts, sp, seekpoint])
|
|
|
|
if maxentries and len(idxs) >= maxentries:
|
|
break
|
|
|
|
return idxs
|
|
|
|
def _seek_to_string(self, s):
|
|
"""
|
|
Parameters
|
|
----------
|
|
s : str
|
|
Seek through the file to the next occurrence of s. Return the
|
|
seek location when found.
|
|
|
|
Returns
|
|
-------
|
|
seekpoint : int
|
|
Next location of the string
|
|
|
|
"""
|
|
while True:
|
|
seekpoint = self.f.tell()
|
|
line = self.f.readline()
|
|
if line == "":
|
|
break
|
|
if s in line:
|
|
break
|
|
return seekpoint
|
|
|
|
def _get_ts_sp(self, line):
|
|
"""
|
|
From the line string, extract the time step and stress period numbers.
|
|
|
|
"""
|
|
|
|
# Old method. Was not generic enough.
|
|
# ts = int(line[self.ts_idxs[0]:self.ts_idxs[1]])
|
|
# sp = int(line[self.sp_idxs[0]:self.sp_idxs[1]])
|
|
|
|
# Get rid of nasty things
|
|
line = line.replace(",", "").replace("*", "")
|
|
|
|
searchstring = "TIME STEP"
|
|
idx = line.index(searchstring) + len(searchstring)
|
|
ll = line[idx:].strip().split()
|
|
ts = int(ll[0])
|
|
|
|
searchstring = "STRESS PERIOD"
|
|
idx = line.index(searchstring) + len(searchstring)
|
|
ll = line[idx:].strip().split()
|
|
sp = int(ll[0])
|
|
|
|
return ts, sp
|
|
|
|
def _set_entries(self):
|
|
if len(self.idx_map) < 1:
|
|
return None, None
|
|
if len(self.entries) > 0:
|
|
raise Exception("entries already set:" + str(self.entries))
|
|
if not self.idx_map:
|
|
raise Exception("must call build_index before call set_entries")
|
|
try:
|
|
incdict, cumdict = self._get_sp(
|
|
self.idx_map[0][0], self.idx_map[0][1], self.idx_map[0][2]
|
|
)
|
|
except:
|
|
raise Exception(
|
|
"unable to read budget information from first "
|
|
"entry in list file"
|
|
)
|
|
self.entries = incdict.keys()
|
|
null_entries = collections.OrderedDict()
|
|
incdict = collections.OrderedDict()
|
|
cumdict = collections.OrderedDict()
|
|
for entry in self.entries:
|
|
incdict[entry] = []
|
|
cumdict[entry] = []
|
|
null_entries[entry] = np.NaN
|
|
self.null_entries = [null_entries, null_entries]
|
|
return incdict, cumdict
|
|
|
|
def _load(self, maxentries=None):
|
|
self._build_index(maxentries)
|
|
incdict, cumdict = self._set_entries()
|
|
if incdict is None and cumdict is None:
|
|
return
|
|
totim = []
|
|
for ts, sp, seekpoint in self.idx_map:
|
|
tinc, tcum = self._get_sp(ts, sp, seekpoint)
|
|
for entry in self.entries:
|
|
incdict[entry].append(tinc[entry])
|
|
cumdict[entry].append(tcum[entry])
|
|
|
|
# Get the time for this record
|
|
seekpoint = self._seek_to_string("TIME SUMMARY AT END")
|
|
tslen, sptim, tt = self._get_totim(ts, sp, seekpoint)
|
|
totim.append(tt)
|
|
|
|
# get kstp and kper
|
|
idx_array = np.array(self.idx_map)
|
|
|
|
# build dtype for recarray
|
|
dtype_tups = [
|
|
("totim", np.float32),
|
|
("time_step", np.int32),
|
|
("stress_period", np.int32),
|
|
]
|
|
for entry in self.entries:
|
|
dtype_tups.append((entry, np.float32))
|
|
dtype = np.dtype(dtype_tups)
|
|
|
|
# create recarray
|
|
nentries = len(incdict[entry])
|
|
self.inc = np.recarray(shape=(nentries,), dtype=dtype)
|
|
self.cum = np.recarray(shape=(nentries,), dtype=dtype)
|
|
|
|
# fill each column of the recarray
|
|
for entry in self.entries:
|
|
self.inc[entry] = incdict[entry]
|
|
self.cum[entry] = cumdict[entry]
|
|
|
|
# file the totim, time_step, and stress_period columns for the
|
|
# incremental and cumulative recarrays (zero-based kstp,kper)
|
|
self.inc["totim"] = np.array(totim)[:]
|
|
self.inc["time_step"] = idx_array[:, 0] - 1
|
|
self.inc["stress_period"] = idx_array[:, 1] - 1
|
|
|
|
self.cum["totim"] = np.array(totim)[:]
|
|
self.cum["time_step"] = idx_array[:, 0] - 1
|
|
self.cum["stress_period"] = idx_array[:, 1] - 1
|
|
|
|
return
|
|
|
|
def _get_sp(self, ts, sp, seekpoint):
|
|
self.f.seek(seekpoint)
|
|
# --read to the start of the "in" budget information
|
|
while True:
|
|
line = self.f.readline()
|
|
if line == "":
|
|
print(
|
|
"end of file found while seeking budget information for ts,sp",
|
|
ts,
|
|
sp,
|
|
)
|
|
return self.null_entries
|
|
|
|
# --if there are two '=' in this line, then it is a budget line
|
|
if len(re.findall("=", line)) == 2:
|
|
break
|
|
|
|
tag = "IN"
|
|
incdict = collections.OrderedDict()
|
|
cumdict = collections.OrderedDict()
|
|
entrydict = {}
|
|
while True:
|
|
|
|
if line == "":
|
|
# raise Exception('end of file found while seeking budget information')
|
|
print(
|
|
"end of file found while seeking budget information for ts,sp",
|
|
ts,
|
|
sp,
|
|
)
|
|
return self.null_entries
|
|
if len(re.findall("=", line)) == 2:
|
|
try:
|
|
entry, flux, cumu = self._parse_budget_line(line)
|
|
except Exception:
|
|
print("error parsing budget line in ts,sp", ts, sp)
|
|
return self.null_entries
|
|
if flux is None:
|
|
print(
|
|
"error casting in flux for",
|
|
entry,
|
|
" to float in ts,sp",
|
|
ts,
|
|
sp,
|
|
)
|
|
return self.null_entries
|
|
if cumu is None:
|
|
print(
|
|
"error casting in cumu for",
|
|
entry,
|
|
" to float in ts,sp",
|
|
ts,
|
|
sp,
|
|
)
|
|
return self.null_entries
|
|
if entry.endswith(tag.upper()):
|
|
if " - " in entry.upper():
|
|
key = entry.replace(" ", "")
|
|
else:
|
|
key = entry.replace(" ", "_")
|
|
elif "PERCENT DISCREPANCY" in entry.upper():
|
|
key = entry.replace(" ", "_")
|
|
else:
|
|
entry = entry.replace(" ", "_")
|
|
if entry in entrydict:
|
|
entrydict[entry] += 1
|
|
inum = entrydict[entry]
|
|
entry = "{}{}".format(entry, inum + 1)
|
|
else:
|
|
entrydict[entry] = 0
|
|
key = "{}_{}".format(entry, tag)
|
|
incdict[key] = flux
|
|
cumdict[key] = cumu
|
|
else:
|
|
if "OUT:" in line.upper():
|
|
tag = "OUT"
|
|
entrydict = {}
|
|
line = self.f.readline()
|
|
if entry.upper() == "PERCENT DISCREPANCY":
|
|
break
|
|
|
|
return incdict, cumdict
|
|
|
|
def _parse_budget_line(self, line):
|
|
|
|
# get the budget item name
|
|
entry = line.strip().split("=")[0].strip()
|
|
|
|
# get the cumulative string
|
|
idx = line.index("=") + 1
|
|
line2 = line[idx:]
|
|
ll = line2.strip().split()
|
|
cu_str = ll[0]
|
|
|
|
idx = line2.index("=") + 1
|
|
fx_str = line2[idx:].split()[0].strip()
|
|
|
|
#
|
|
# cu_str = line[self.cumu_idxs[0]:self.cumu_idxs[1]]
|
|
# fx_str = line[self.flux_idxs[0]:self.flux_idxs[1]]
|
|
|
|
flux, cumu = None, None
|
|
try:
|
|
cumu = float(cu_str)
|
|
except:
|
|
if "NAN" in cu_str.strip().upper():
|
|
cumu = np.NaN
|
|
try:
|
|
flux = float(fx_str)
|
|
except:
|
|
if "NAN" in fx_str.strip().upper():
|
|
flux = np.NaN
|
|
return entry, flux, cumu
|
|
|
|
def _get_totim(self, ts, sp, seekpoint):
|
|
self.f.seek(seekpoint)
|
|
# --read header lines
|
|
ihead = 0
|
|
while True:
|
|
line = self.f.readline()
|
|
ihead += 1
|
|
if line == "":
|
|
print(
|
|
"end of file found while seeking time information for ts,sp",
|
|
ts,
|
|
sp,
|
|
)
|
|
return np.NaN, np.NaN, np.NaN
|
|
elif (
|
|
ihead == 2
|
|
and "SECONDS MINUTES HOURS DAYS YEARS"
|
|
not in line
|
|
):
|
|
break
|
|
elif (
|
|
"-----------------------------------------------------------"
|
|
in line
|
|
):
|
|
line = self.f.readline()
|
|
break
|
|
|
|
if isinstance(self, SwtListBudget):
|
|
translen = self._parse_time_line(line)
|
|
line = self.f.readline()
|
|
if translen is None:
|
|
print("error parsing translen for ts,sp", ts, sp)
|
|
return np.NaN, np.NaN, np.NaN
|
|
|
|
tslen = self._parse_time_line(line)
|
|
if tslen is None:
|
|
print("error parsing tslen for ts,sp", ts, sp)
|
|
return np.NaN, np.NaN, np.NaN
|
|
|
|
sptim = self._parse_time_line(self.f.readline())
|
|
if sptim is None:
|
|
print("error parsing sptim for ts,sp", ts, sp)
|
|
return np.NaN, np.NaN, np.NaN
|
|
|
|
totim = self._parse_time_line(self.f.readline())
|
|
if totim is None:
|
|
print("error parsing totim for ts,sp", ts, sp)
|
|
return np.NaN, np.NaN, np.NaN
|
|
return tslen, sptim, totim
|
|
|
|
def _parse_time_line(self, line):
|
|
if line == "":
|
|
print("end of file found while parsing time information")
|
|
return None
|
|
try:
|
|
time_str = line[self.time_line_idx :]
|
|
raw = time_str.split()
|
|
idx = self.time_idx
|
|
# catch case where itmuni is undefined
|
|
# in this case, the table format is different
|
|
try:
|
|
v = float(raw[0])
|
|
except:
|
|
time_str = line[45:]
|
|
raw = time_str.split()
|
|
idx = 0
|
|
tval = float(raw[idx])
|
|
except:
|
|
print("error parsing tslen information", time_str)
|
|
return None
|
|
return tval
|
|
|
|
|
|
class SwtListBudget(ListBudget):
|
|
""""""
|
|
|
|
def set_budget_key(self):
|
|
self.budgetkey = "MASS BUDGET FOR ENTIRE MODEL"
|
|
return
|
|
|
|
|
|
class MfListBudget(ListBudget):
|
|
""""""
|
|
|
|
def set_budget_key(self):
|
|
self.budgetkey = "VOLUMETRIC BUDGET FOR ENTIRE MODEL"
|
|
return
|
|
|
|
|
|
class Mf6ListBudget(ListBudget):
|
|
""""""
|
|
|
|
def set_budget_key(self):
|
|
self.budgetkey = "VOLUME BUDGET FOR ENTIRE MODEL"
|
|
return
|
|
|
|
|
|
class MfusgListBudget(ListBudget):
|
|
""""""
|
|
|
|
def set_budget_key(self):
|
|
self.budgetkey = "VOLUMETRIC BUDGET FOR ENTIRE MODEL"
|
|
return
|
|
|
|
|
|
class SwrListBudget(ListBudget):
|
|
""""""
|
|
|
|
def set_budget_key(self):
|
|
self.budgetkey = "VOLUMETRIC SURFACE WATER BUDGET FOR ENTIRE MODEL"
|
|
self.tssp_lines = 1
|
|
return
|