"""Functions related to temporal discretization"""importcalendarimportcopyimportdatetimeasdtimportosimportshutilfrompathlibimportPathimportnumpyasnpimportpandasaspdimportmfsetupfrommfsetup.checksimportis_valid_perioddatafrommfsetup.utilsimportget_input_arguments,print_itemmonths={v.lower():kfork,vinenumerate(calendar.month_name)ifk>0}
[docs]defconvert_freq_to_period_start(freq):"""convert pandas frequency to period start"""ifisinstance(freq,str):forprefixin['M','Q','A','Y']:ifprefixinfreq.upper()and"S"notinfreq.upper():freq=freq.replace(prefix,"{}S".format(prefix)).upper()returnfreq
[docs]defget_parent_stress_periods(parent_model,nper=None,parent_stress_periods='all'):parent_sp=copy.copy(parent_stress_periods)parent_model_nper=parent_model.modeltime.nper# use all stress periods from parent modelifisinstance(parent_sp,str)andparent_sp.lower()=='all':ifnperisNone:# or nper < parent_model.nper:nper=parent_model_nperparent_sp=list(range(nper))elifnper>parent_model_nper:parent_sp=list(range(parent_model_nper))foriinrange(nper-parent_model_nper):parent_sp.append(parent_sp[-1])else:parent_sp=list(range(nper))# use only specified stress periods from parent modelelifisinstance(parent_sp,list):# limit parent stress periods to include# to those in parent model and nper specified for pfl_nwtifnperisNone:nper=len(parent_sp)perlen=[parent_model.modeltime.perlen[0]]fori,pinenumerate(parent_sp):ifi==nper:breakifp==parent_model_nper:breakifp>0andp>=parent_sp[-1]andlen(parent_sp)<nper:parent_sp.append(p)perlen.append(parent_model.modeltime.perlen[p])ifnper<len(parent_sp):nper=len(parent_sp)else:n_parent_per=len(parent_sp)foriinrange(nper-n_parent_per):parent_sp.append(parent_sp[-1])# no parent stress periods specified,# default to just using first stress period# (repeating if necessary;# for example if creating transient inset model with steady bc from parent)else:ifnperisNone:nper=1parent_sp=[0]foriinrange(nper-1):parent_sp.append(parent_sp[-1])assertlen(parent_sp)==nperreturnparent_sp
[docs]defparse_perioddata_groups(perioddata_dict,**kwargs):"""Reorganize input in perioddata dict into a list of groups (dicts). """perioddata_groups=[]defaults={'start_date_time':'1970-01-01'}defaults.update(kwargs)group0=defaults.copy()valid_txt="if transient: perlen specified or 3 of start_date_time, " \
"end_date_time, nper or freq;\n" \
"if steady: nper or perlen specified. Default perlen " \
"for steady-state periods is 1."fork,vinperioddata_dict.items():if'group'ink.lower():data=defaults.copy()data.update(v)ifis_valid_perioddata(data):data=get_input_arguments(data,setup_perioddata_group,errors='raise')perioddata_groups.append(data)else:print_item(k,data)prefix="perioddata input for {} must have".format(k)raiseException(prefix+valid_txt)elif'perioddata'ink.lower():perioddata_groups+=parse_perioddata_groups(perioddata_dict[k],**defaults)else:group0[k]=viflen(perioddata_groups)==0:ifnotis_valid_perioddata(group0):print_item('perioddata:',group0)prefix="perioddata input must have"raiseException(prefix+valid_txt)data=get_input_arguments(group0,setup_perioddata_group)perioddata_groups=[data]forgroupinperioddata_groups:if'steady'ingroup:ifnp.isscalar(group['steady'])orgroup['steady']isNone:group['steady']={0:group['steady']}elifnotisinstance(group['steady'],dict):group['steady']={i:sfori,sinenumerate(group['steady'])}returnperioddata_groups
[docs]defsetup_perioddata_group(start_date_time,end_date_time=None,nper=None,perlen=None,model_time_units='days',freq=None,steady={0:True,1:False},nstp=10,tsmult=1.5,oc_saverecord={0:['save head last','save budget last']},):"""Sets up time discretization for a model; outputs a DataFrame with stress period dates/times and properties. Stress periods can be established by explicitly specifying perlen as a list of period lengths in model units. Or, stress periods can be generated via :func:`pandas.date_range`, using three of the start_date_time, end_date_time, nper, and freq arguments. Parameters ---------- start_date_time : str or datetime-like Left bound for generating stress period dates. See :func:`pandas.date_range`. end_date_time : str or datetime-like, optional Right bound for generating stress period dates. See :func:`pandas.date_range`. nper : int, optional Number of stress periods. Only used if perlen is None, or in combination with freq if an end_date_time isn't specified. perlen : sequence or None, optional A list of stress period lengths in model time units. Or specify as None and specify 3 of start_date_time, end_date_time, nper and/or freq. model_time_units : str, optional 'days' or 'seconds'. By default, 'days'. freq : str or DateOffset, default None For setting up uniform stress periods between a start and end date, or of length nper. Same as argument to pandas.date_range. Frequency strings can have multiples, e.g. ‘6MS’ for a 6 month interval on the start of each month. See the pandas documentation for a list of frequency aliases. Note: Only "start" frequences (e.g. MS vs M for "month end") are supported. steady : dict Dictionary with zero-based stress periods as keys and boolean values. Similar to MODFLOW-6 input, the information specified for a period will continue to apply until information for another period is specified. nstp : int or sequence Number of timesteps in a stress period. Must be an integer if perlen=None. nstp : int or sequence Timestep multiplier for a stress period. Must be an integer if perlen=None. oc_saverecord : dict Dictionary with zero-based stress periods as keys and output control options as values. Similar to MODFLOW-6 input, the information specified for a period will continue to apply until information for another period is specified. Returns ------- perioddata : pandas.DataFrame DataFrame summarizing stress period information. Data columns: ================== ================ ============================================== **start_datetime** pandas datetimes start date/time of each stress period **end_datetime** pandas datetimes end date/time of each stress period **time** float cumulative MODFLOW time at end of period **per** int zero-based stress period **perlen** float stress period length in model time units **nstp** int number of timesteps in the stress period **tsmult** int timestep multiplier for stress period **steady** bool True=steady-state, False=Transient **oc** dict MODFLOW-6 output control options ================== ================ ============================================== Notes ----- *Initial steady-state period* If the first stress period is specified as steady-state (``steady[0] == True``), the period length (perlen) in MODFLOW time is automatically set to 1. If subsequent stress periods are specified, or if no end-date is specified, the end date for the initial steady-state stress period is set equal to the start date. In the latter case, the assumption is that the specified start date represents the start of the transient simulation, and the initial steady-state (which is time-invarient anyways) is intended to produce a valid starting condition. If only a single steady-state stress period is specified with an end date, then that end date is retained. *MODFLOW time vs real time* The ``time`` column of the output DataFrame represents time in the MODFLOW simulation, which cannot have zero-lengths for any period. Therefore, initial steady-state periods are automatically assigned lengths of one (as described above), and MODFLOW time is incremented accordingly. If the model has an initial steady-state period, this means that subsequent MODFLOW times will be 1 time unit greater than the acutal date-times. *End-dates* Specified ``end_date_time`` represents the right bound of the time discretization, or in other words, the time increment *after* the last time increment to be simulated. For example, ``end_date_time='2019-01-01'`` would mean that ``'2018-12-31'`` is the last date simulated by the model (which ends at ``2019-01-01 00:00:00``). """specified_start_datetime=Noneifstart_date_timeisnotNone:specified_start_datetime=pd.Timestamp(start_date_time)elifend_date_timeisNone:raiseValueError('If no start_datetime, must specify end_datetime')specified_end_datetime=Noneifend_date_timeisnotNone:specified_end_datetime=pd.Timestamp(end_date_time)# if times are specified by start & end dates and freq,# period is determined by pd.date_rangeifall({specified_start_datetime,specified_end_datetime,freq}):nper=Nonefreq=convert_freq_to_period_start(freq)oc=oc_saverecordifnotisinstance(steady,dict):steady={i:vfori,vinenumerate(steady)}# nstp and tsmult need to be listsifnotnp.isscalar(nstp):nstp=list(nstp)ifnotnp.isscalar(tsmult):tsmult=list(tsmult)txt="Specify perlen as a list of lengths in model units, or\nspecify 3 " \
"of start_date_time, end_date_time, nper and/or freq."# Explicitly specified stress period lengthsstart_datetime=[]# datetimes at period startsend_datetime=[]# datetimes at period endsifperlenisnotNone:ifnp.isscalar(perlen):perlen=[perlen]start_datetime=[specified_start_datetime]iflen(perlen)>1:fori,lengthinenumerate(perlen):# initial steady-state period# set perlen to 0# and start/end dates to be equalifi==0andsteady[0]:next_start=start_datetime[i]perlen[0]==1else:next_start=start_datetime[i]+ \
pd.Timedelta(length,unit=model_time_units)start_datetime.append(next_start)end_datetime=pd.to_datetime(start_datetime[1:])start_datetime=pd.to_datetime(start_datetime[:-1])# single specified stress period lengthelse:end_datetime=[specified_start_datetime+pd.Timedelta(perlen[0],unit=model_time_units)]time=np.cumsum(perlen)# time at end of period, in MODFLOW units# single steady-state periodelifnper==1andsteady[0]:perlen=[1]time=[1]start_datetime=pd.to_datetime([specified_start_datetime])ifspecified_end_datetimeisnotNone:end_datetime=pd.to_datetime([specified_end_datetime])else:end_datetime=pd.to_datetime([specified_start_datetime])# Set up datetimes based on 3 of start_date_time, specified_end_datetime, nper and/or freq (scalar perlen)else:assertnp.isscalar(nstp),"nstp: {}; nstp must be a scalar if perlen " \
"is not specified explicitly as a list.\n{}".format(nstp,txt)assertnp.isscalar(tsmult),"tsmult: {}; tsmult must be a scalar if perlen " \
"is not specified explicitly as a list.\n{}".format(tsmult,txt)periods=Noneifspecified_end_datetimeisNone:# start_date_time, periods and freq# (i.e. nper periods of length perlen starting on stat_date)iffreqisnotNone:periods=nperelse:raiseValueError("Unrecognized input for perlen: {}.\n{}".format(perlen,txt))else:# specified_end_datetime and freq and periodsifspecified_start_datetimeisNone:periods=nper+1# start_date_time, specified_end_datetime and uniform periods# (i.e. nper periods of uniform length between start_date_time and specified_end_datetime)eliffreqisNone:periods=nper#-1 if steady[0] else nper# start_date_time, specified_end_datetime and frequencyeliffreqisnotNone:passdatetimes=pd.date_range(specified_start_datetime,specified_end_datetime,periods=periods,freq=freq)# if end_datetime, periods and freq were specifiedifspecified_start_datetimeisNone:specified_start_datetime=datetimes[0]start_datetime=datetimes[:-1]end_datetime=datetimes[1:]time_edges=getattr((datetimes-start_datetime[0]),model_time_units).tolist()perlen=np.diff(time_edges)# time is elapsed time at the end of each periodtime=time_edges[1:]else:start_datetime=datetimesend_datetime=pd.to_datetime(datetimes[1:].tolist()+[specified_end_datetime])# Edge case of end date falling on the start date freq# (zero-length sp at end)ifend_datetime[-1]==start_datetime[-1]:start_datetime=start_datetime[:-1]end_datetime=end_datetime[:-1]time_edges=getattr((end_datetime-start_datetime[0]),model_time_units).tolist()time_edges=[0]+time_edgesperlen=np.diff(time_edges)# time is elapsed time at the end of each periodtime=time_edges[1:]#if len(datetimes) == 1:# perlen = [(specified_end_datetime - specified_start_datetime).days]# time = np.array(perlen)# if first period is steady-state,# insert it at the beginning of the generated range# (only do for pd.date_range -based discretization)ifsteady[0]:start_datetime=[start_datetime[0]]+start_datetime.tolist()end_datetime=[start_datetime[0]]+end_datetime.tolist()perlen=[1]+list(perlen)time=[1]+(np.array(time)+1).tolist()ifisinstance(nstp,list):nstp=[1]+nstpifisinstance(tsmult,list):tsmult=[1]+tsmultperioddata=pd.DataFrame({'start_datetime':start_datetime,'end_datetime':end_datetime,'time':time,'per':range(len(time)),'perlen':np.array(perlen).astype(float),'nstp':nstp,'tsmult':tsmult,})# specify steady-state or transient for each period, filling empty# periods with previous state (same logic as MF6 input)issteady=[steady[0]]foriinrange(len(perioddata)):issteady.append(steady.get(i,issteady[i]))perioddata['steady']=issteady[1:]perioddata['steady']=perioddata['steady'].astype(bool)# set up output control, using previous value to fill empty periods# (same as MF6)oclist=[None]foriinrange(len(perioddata)):oclist.append(oc.get(i,oclist[i]))perioddata['oc']=oclist[1:]# correct nstp and tsmult to be 1 for steady-state periodsperioddata.loc[perioddata.steady.values,'nstp']=1perioddata.loc[perioddata.steady.values,'tsmult']=1returnperioddata
[docs]defconcat_periodata_groups(perioddata_groups,time_units='days'):"""Concatenate multiple perioddata DataFrames, but sort result on (absolute) datetimes and increment model time and stress period numbers accordingly."""# update any missing variables in the groups with global variablesgroup_dfs=[]fori,groupinenumerate(perioddata_groups):group.update({'model_time_units':time_units,})df=setup_perioddata_group(**group)group_dfs.append(df)df=pd.concat(group_dfs).sort_values(by=['end_datetime'])perlen=np.ones(len(df))perlen[~df.steady.values]=df.loc[~df.steady.values,'perlen']df['time']=np.cumsum(perlen)df['per']=range(len(df))df.index=range(len(df))returndf
[docs]defsetup_perioddata(model,tdis_perioddata_config,default_start_datetime=None,nper=None,steady=None,time_units='days',oc_saverecord=None,parent_model=None,parent_stress_periods=None,):"""Sets up the perioddata DataFrame that is used to reference model stress period start and end times to real date time. Parameters ---------- model : _type_ _description_ tdis_perioddata_config : dict ``perioddata:``, ``tdis:`` (MODFLOW 6 models) or ``dis:`` (MODFLOW-2005 models) block from the Modflow-setup configuration file. default_start_datetime : str, optional Start date for model from the tdis: options: block in the configuration file, or ``model.modeltime.start_datetime`` Flopy attribute. Only used where start_datetime information is missing, for example if a group for an initial steady-state period in ``tdis_perioddata_config`` doesn't have a start_datetime: entry. By default, None, in which case the default start_datetime of 1970-01-01 may be applied by py:func:`setup_perioddata_group`. nper : int, optional Number of stress periods. Only used if nper is specified in the tdis: dimensions: block of the configuration file and not in a perioddata group. steady : bool, sequence or dict Whether each period is steady-state or transient. Only used if steady is specified in the tdis: or sto: configuration file blocks (MODFLOW 6 models) or the dis: block (MODFLOW-2005 models), and not in perioddata groups. time_units : str, optional Model time units, by default 'days'. oc_saverecord : dict, optional Output control settings, keyed by stress period. Only used to record this information in the stress period data table. parent_model : flopy model instance, optional Parent model, if model is an inset. parent_stress_periods : list of ints, optional Parent model stress periods to apply to the inset model (read from the parent: copy_stress_periods: item in the configuration file). Returns ------- perioddata : DataFrame Table of stress period information with columns: ============== ========================================= start_datetime Start date of each model stress period end_datetime End date of each model stress period time MODFLOW elapsed time, in days [#f1]_ per Model stress period number perlen Stress period length (days) nstp Number of timesteps in stress period tsmult Timestep multiplier steady Steady-state or transient oc Output control setting for MODFLOW parent_sp Corresponding parent model stress period ============== ========================================= Notes ----- perioddata is also saved to stress_period_data.csv in the tables folder (usually `/tables`). .. rubric:: Footnotes .. [#f1] Modflow elapsed time includes the time lengths specified for any steady-state periods (at least 1 day). Therefore if the model has an initial steady-state period with a ``perlen`` of one day, the elapsed time at the model start date will already be 1 day. """# get start_date_time from parent if available and start_date_time wasn't specified# only apply to tdis_perioddata_config if it wasn't specified thereiftdis_perioddata_config.get('start_datetime','1970-01-01')=='1970-01-01'and \
default_start_datetime!='1970-01-01':tdis_perioddata_config['start_date_time']=default_start_datetime# option to define stress periods in table prior to model buildif'csvfile'intdis_perioddata_config:csvfile=Path(model._config_path)/tdis_perioddata_config['csvfile']['filename']perioddata=pd.read_csv(csvfile)defaults={'start_datetime_column':'start_datetime','end_datetime_column':'end_datetime','steady_column':'steady','nstp_column':'nstp','tsmult_column':'tsmult'}csv_config=tdis_perioddata_config['csvfile']renames={csv_config.get(k):vfork,vindefaults.items()ifkincsv_config}perioddata.rename(columns=renames,inplace=True)required_cols=defaults.values()forcolinrequired_cols:ifcolnotinperioddata.columns:raiseKeyError(f"{col} column missing in supplied stress "f"period table {csvfile}.")perioddata['start_datetime']=pd.to_datetime(perioddata['start_datetime'])perioddata['end_datetime']=pd.to_datetime(perioddata['end_datetime'])perioddata['per']=np.arange(len(perioddata))perlen=getattr((perioddata['end_datetime']-perioddata['start_datetime']).dt,model.time_units).tolist()# set initial steady-state stress period to at least length 1ifperioddata['steady'][0]andperlen[0]<1:perlen[0]=1perioddata['perlen']=perlenperioddata['time']=np.cumsum(perlen)cols=['start_datetime','end_datetime','time','per','perlen','nstp','tsmult','steady']# option to supply Output Contorl INstructions as wellif'oc'inperioddata.columns:cols.append('oc')perioddata=perioddata[cols]# some validationassertnp.all(perioddata['perlen']>0)assertnp.all(np.diff(perioddata['time'])>0)# define stress periods from perioddata group blocks in configuration fileelse:perioddata_groups=parse_perioddata_groups(tdis_perioddata_config,nper=nper,steady=steady,start_date_time=default_start_datetime)# set up the perioddata table from the groupsperioddata=concat_periodata_groups(perioddata_groups,time_units)# assign parent model stress periods to each inset model stress periodparent_sp=Noneifparent_modelisnotNone:ifparent_stress_periodsisnotNone:# parent_sp has parent model stress period corresponding# to each inset model stress period (len=nper)# the same parent stress period can be specified for multiple inset model periodsparent_sp=get_parent_stress_periods(parent_model,nper=len(perioddata),parent_stress_periods=parent_stress_periods)elifmodel._is_lgr:parent_sp=perioddata['per'].values# add corresponding stress periods in parent model if there are anyperioddata['parent_sp']=parent_spassertnp.array_equal(perioddata['per'].values,np.arange(len(perioddata)))returnperioddata
[docs]defaggregate_dataframe_to_stress_period(data,id_column,data_column,datetime_column='datetime',end_datetime_column=None,category_column=None,start_datetime=None,end_datetime=None,period_stat='mean',resolve_duplicates_with='raise error'):"""Aggregate time-series data in a DataFrame to a single value representing a period defined by a start and end date. Parameters ---------- data : DataFrame Must have an id_column, data_column, datetime_column, and optionally, an end_datetime_column. id_column : str Column in data with location identifier (e.g. node or well id). data_column : str or list Column(s) in data with values to aggregate. datetime_column : str Column in data with times for each value. For downsampling of multiple values in data to a longer period represented by start_datetime and end_datetime, this is all that is needed. Aggregated values will include values in datetime_column that are >= start_datetime and < end_datetime. In other words, datetime_column represents the start of each time interval in data. Values can be strings (e.g. YYYY-MM-DD) or pandas Timestamps. By default, None. end_datetime_column : str Column in data with end times for period represented by each value. This is only needed for upsampling, where the interval defined by start_datetime and end_datetime is smaller than the time intervals in data. The row(s) in data that have a datetime_column value < end_datetime, and an end_datetime_column value > start_datetime will be retained in aggregated. Values can be strings (e.g. YYYY-MM-DD) or pandas Timestamps. By default, None. start_datetime : str or pandas.Timestamp Start time of aggregation period. Only used if an aggregation start and end time are not given in period_stat. If None, and no start and end time are specified in period_stat, the first time in datetime_column is used. By default, None. end_datetime : str or pandas.Timestamp End time of aggregation period. Only used if an aggregation start and end time are not given in period_stat. If None, and no start and end time are specified in period_stat, the last time in datetime_column is used. By default, None. period_stat : str, list, or NoneType Method for aggregating data. By default, 'mean'. * Strings will be passed to DataFrame.groupby as the aggregation method. For example, ``'mean'`` would result in DataFrame.groupby().mean(). * If period_stat is None, ``'mean'`` is used. * Lists of length 2 can be used to specify a statistic for a month (e.g. ``['mean', 'august']``), or for a time period that can be represented as a single string in pandas. For example, ``['mean', '2014']`` would average all values in the year 2014; ``['mean', '2014-01']`` would average all values in January of 2014, etc. Basically, if the string can be used to slice a DataFrame or Series, it can be used here. * Lists of length 3 can be used to specify a statistic and a start and end date. For example, ``['mean', '2014-01-01', '2014-03-31']`` would average the values for the first three months of 2014. resolve_duplicates_with : {'sum', 'mean', 'first', 'raise error'} Method for reducing duplicates (of times, sites and measured or estimated category). By default, 'raise error' will result in a ValueError if duplicates are encountered. Otherwise any aggregate method in pandas can be used (e.g. DataFrame.groupby().<method>()) Returns ------- aggregated : DataFrame Aggregated values. Columns are the same as data, except the time column is named 'start_datetime'. In other words, aggregated periods are represented by their start dates (as opposed to midpoint dates or end dates). """data=data.copy()ifdata.index.name==datetime_column:data.sort_index(inplace=True)else:data.sort_values(by=datetime_column,inplace=True)ifisinstance(period_stat,str):period_stat=[period_stat]elifperiod_statisNone:period_stat=['mean']else:period_stat=period_stat.copy()ifisinstance(data_column,str):data_columns=[data_column]else:data_columns=data_columniflen(data_columns)>1:passstart,end=None,Noneifisinstance(period_stat,list):stat=period_stat.pop(0)# stat for specified periodiflen(period_stat)==2:start,end=period_statperiod_data=data.loc[start:end]# stat specified by single itemeliflen(period_stat)==1:period=period_stat.pop()# stat for a specified monthifperiodinmonths.keys()orperiodinmonths.values():period_data=data.loc[data.index.dt.month==months.get(period,period)]# stat for a period specified by single string (e.g. '2014', '2014-01', etc.)else:period_data=data.loc[period]# no time period in source data specified for statistic; use start/end of current model periodeliflen(period_stat)==0:assertdatetime_columnindata.columns, \
"datetime_column needed for " \
"resampling irregular data to model stress periods"ifdata[datetime_column].dtype==object:data[datetime_column]=pd.to_datetime(data[datetime_column])ifend_datetime_columnindata.columnsand \
data[end_datetime_column].dtype==object:data[end_datetime_column]=pd.to_datetime(data[end_datetime_column])ifstart_datetimeisNone:start_datetime=data[datetime_column].iloc[0]ifend_datetimeisNone:end_datetime=data[datetime_column].iloc[-1]# >= includes the start datetime# if there is no end_datetime column, select values that have start_datetimes within the period# this excludes values that start before the period but don't have an end dateifend_datetime_columnnotindata.columns:data_overlaps_period=(data[datetime_column]<end_datetime)& \
(data[datetime_column]>=start_datetime)# if some end_datetimes are missing, assume end_datetime is the period end# this assumes that missing end datetimes indicate pumping that continues to the end of the simulationelifdata[end_datetime_column].isna().any():data.loc[data[end_datetime_column].isna(),'end_datetime']=end_datetimedata_overlaps_period=(data[datetime_column]<end_datetime)& \
(data[end_datetime_column]>=start_datetime)# otherwise, select values with start datetimes that are before the period end# and end datetimes that are after the period start# in other words, include all values that overlap in time with the periodelse:ifdata[end_datetime_column].dtype==object:data[end_datetime_column]=pd.to_datetime(data[end_datetime_column])data_overlaps_period=(data[datetime_column]<end_datetime)& \
(data[end_datetime_column]>start_datetime)period_data=data.loc[data_overlaps_period]else:raiseException("")# create category column if there is none, to conform to logic belowcategories=Falseifcategory_columnisNone:category_column='category'period_data[category_column]='measured'elifcategory_columnnotinperiod_data.columns:raiseKeyError('category_column: {} not in data'.format(category_column))else:categories=True# compute statistic on data# ensure that ids are unique in each time period# by summing multiple id instances by period# (only sum the data column)# check for duplicates with same time, id, and category (measured vs estimated)duplicated=pd.Series(list(zip(period_data[datetime_column],period_data[id_column],period_data[category_column]))).duplicated()aggregated=period_data.groupby(id_column).first()fordata_columnindata_columns:ifany(duplicated):ifresolve_duplicates_with=='raise error':duplicate_info=period_data.loc[duplicated.values]msg='The following locations are duplicates which need to be resolved:\n'.format(duplicate_info.__str__())raiseValueError(msg)period_data.index.name=Noneby_period=period_data.groupby([id_column,datetime_column]).first().reset_index()agg_groupedby=getattr(period_data.groupby([id_column,datetime_column]),resolve_duplicates_with)(numeric_only=True)by_period[data_column]=agg_groupedby[data_column].valuesperiod_data=by_periodagg_groupedby=getattr(period_data.groupby(id_column),stat)(numeric_only=True)aggregated[data_column]=agg_groupedby[data_column].values# if category column was argued, get counts of measured vs estimated# for each measurement location, for current stress periodifcategories:counts=period_data.groupby([id_column,category_column]).size().unstack(fill_value=0)forcolin'measured','estimated':ifcolnotincounts.columns:counts[col]=0aggregated['n_{}'.format(col)]=counts[col]aggregated.reset_index(inplace=True)# add datetime back inaggregated['start_datetime']=startifstartisnotNoneelsestart_datetime# enforce consistent datetime dtypes# (otherwise pd.concat of multiple outputs from this function may fail)forcolin'start_datetime','end_datetime':ifcolinaggregated.columns:aggregated[col]=aggregated[col].astype('datetime64[ns]')# drop original datetime column, which doesn't reflect dates for period averagesdrop_cols=[datetime_column]ifnotcategories:# drop category column if it was createddrop_cols.append(category_column)aggregated.drop(drop_cols,axis=1,inplace=True)returnaggregated
[docs]defaggregate_xarray_to_stress_period(data,datetime_coords_name='time',start_datetime=None,end_datetime=None,period_stat='mean'):period_stat=copy.copy(period_stat)ifisinstance(start_datetime,pd.Timestamp):start_datetime=start_datetime.strftime('%Y-%m-%d')ifisinstance(end_datetime,pd.Timestamp):end_datetime=end_datetime.strftime('%Y-%m-%d')ifisinstance(period_stat,str):period_stat=[period_stat]elifperiod_statisNone:period_stat=['mean']ifisinstance(period_stat,list):stat=period_stat.pop(0)# stat for specified periodiflen(period_stat)==2:start,end=period_statarr=data.loc[start:end].values# stat specified by single itemeliflen(period_stat)==1:period=period_stat.pop()# stat for a specified monthifperiodinmonths.keys()orperiodinmonths.values():arr=data.loc[data[datetime_coords_name].dt.month==months.get(period,period)].values# stat for a period specified by single string (e.g. '2014', '2014-01', etc.)else:arr=data.loc[period].values# no period specified; use start/end of current periodeliflen(period_stat)==0:assertdatetime_coords_nameindata.coords, \
"datetime_column needed for " \
"resampling irregular data to model stress periods"# not sure if this is needed for xarrayifdata[datetime_coords_name].dtype==object:data[datetime_coords_name]=pd.to_datetime(data[datetime_coords_name])# default to aggregating whole dataset# if start_ and end_datetime not providedifstart_datetimeisNone:start_datetime=data[datetime_coords_name].values[0]ifend_datetimeisNone:end_datetime=data[datetime_coords_name].values[-1]# >= includes the start datetime# for now, in comparison to aggregate_dataframe_to_stress_period() fn# for tabular data (pandas)# assume that xarray data does not have an end_datetime column# (infer the end datetimes)arr=data.loc[start_datetime:end_datetime].valueselse:raiseException("")# compute statistic on dataaggregated=getattr(arr,stat)(axis=0)returnaggregated
[docs]defadd_date_comments_to_tdis(tdis_file,start_dates,end_dates=None):"""Add stress period start and end dates to a tdis file as comments; add modflow-setup version info to tdis file header. """tempfile=tdis_file+'.temp'shutil.copy(tdis_file,tempfile)withopen(tempfile)assrc:withopen(tdis_file,'w')asdest:header=''read_header=Trueforlineinsrc:ifread_headerandlen(line)>0and \
line.strip()[0]in{'#','!','//'}:header+=lineelif'begin options'in' '.join(line.lower().split()):if'modflow-setup'notinheader:if'flopy'inheader.lower():mfsetup_text='# via 'else:mfsetup_text='# File created by 'mfsetup_text+='modflow-setup version {}'.format(mfsetup.__version__)mfsetup_text+=' at {:%Y-%m-%d %H:%M:%S}'.format(dt.datetime.now())header+=mfsetup_text+'\n'dest.write(header)read_header=Falsedest.write(line)elif'begin perioddata'in' '.join(line.lower().split()):dest.write(line)dest.write(2*' '+'# perlen nstp tsmult\n')fori,lineinenumerate(src):if'end perioddata'in' '.join(line.lower().split()):dest.write(line)breakelse:line=2*' '+line.strip()+f' # period {i+1}: {start_dates[i]:%Y-%m-%d}'ifend_datesisnotNone:line+=f' to {end_dates[i]:%Y-%m-%d}'line+='\n'dest.write(line)else:dest.write(line)os.remove(tempfile)