def plot_budget_term(df, term, title_prefix='', title_suffix='', plotted=set(), model_length_units=None, model_time_units=None, secondary_axis_units=None, xtick_stride=None): if term not in {'IN-OUT', 'PERCENT_DISCREPANCY'}: # get the absolute quantity for the term if isinstance(term, list): series = df[term].sum(axis=1) out_term = [s.replace('_IN', '_OUT') for s in term] out_series = df[out_term].sum(axis=1) else: term = term.replace('_IN', '').replace('_OUT', '') in_term = f'{term}_IN' out_term = f'{term}_OUT' series = df[in_term] out_series = df[out_term] # get the net net_series = series - out_series # get the percent relative to the total budget pct_series = series/df['TOTAL_IN'] pct_out_series = out_series/df['TOTAL_OUT'] pct_net_series = pct_series - pct_out_series else: series = df[term] out_term = None out_series = None if model_length_units is not None and model_time_units is not None: units_text = get_figure_label_unit_text(model_length_units, model_time_units, length_unit_exp=3) else: units_text = '$L^3/T$' if out_series is not None: fig, axes = plt.subplots(2, 1, sharex=True, figsize=(11, 8.5)) axes = axes.flat ax = axes[0] series.plot(ax=ax, c='C0') ax.axhline(0, zorder=-1, lw=0.5, c='k') (-out_series).plot(ax=ax, c='C1') net_series.plot(ax=ax, c='0.5', zorder=-1) h, l = ax.get_legend_handles_labels() ax.legend(h, ['In', 'Out', 'Net']) ax.set_ylabel(f'Flow rate, in model units of {units_text}') # add commas to y axis values formatter = mpl.ticker.FuncFormatter(lambda x, p: format(int(x), ',')) ax.get_yaxis().set_major_formatter(formatter) # optional 2nd y-axis with other units if secondary_axis_units is not None: length_units2, time_units2 = parse_flux_units(secondary_axis_units) vol_conversion = convert_volume_units(model_length_units, length_units2) time_conversion = convert_time_units(model_time_units, time_units2) def fconvert(x): return x * vol_conversion * time_conversion def rconvert(x): return x / (vol_conversion * time_conversion) secondary_axis_unit_text = get_figure_label_unit_text(length_units2, time_units2, length_unit_exp=3) secax = ax.secondary_yaxis('right', functions=(fconvert, rconvert)) secax.set_ylabel(f'Flow rate, in {secondary_axis_unit_text}') secax.get_yaxis().set_major_formatter(formatter) # plot the percentage of total budget on second axis ax2 = axes[1] pct_series.plot(ax=axes[1], c='C0') ax2.axhline(0, zorder=-1, lw=0.5, c='k') (-pct_out_series).plot(ax=ax2, c='C1') pct_net_series.plot(ax=ax2, c='0.5', zorder=-1) ax2.set_ylabel('Fraction of model budget') # add stress period info ymin, ymax = ax.get_ylim() yloc = np.ones(len(df)) * (ymin - 0.02 * (ymax - ymin)) if xtick_stride is None: xtick_stride = int(np.round(len(df) / 10, 0)) xtick_stride = 1 if xtick_stride < 1 else xtick_stride kpers = set() for x, y in zip(df.index.values[::xtick_stride], yloc[::xtick_stride]): kper = df.loc[x, 'kper'] # only make one line for each stress period if kper not in kpers: ax.axvline(x, lw=0.5, c='k', zorder=-2) ax2.axvline(x, lw=0.5, c='k', zorder=-2) ax2.text(x, y, kper, transform=ax.transData, ha='center', va='top') kpers.add(kper) ax2.text(0.5, -0.07, 'Model Stress Period', ha='center', va='top', transform=ax.transAxes) else: fig, ax = plt.subplots(1, 1, sharex=True, figsize=(11, 8.5)) series.plot(ax=ax, c='C0') ax.axhline(0, zorder=-1, lw=0.5, c='k') ax2 = ax title_text = ' '.join((title_prefix, term.split('_')[0], title_suffix)).strip() ax.set_title(title_text) xmin, xmax = series.index.min(), series.index.max() ax.set_xlim(xmin, xmax) if not isinstance(df.index, pd.DatetimeIndex): ax2.set_xlabel('Time since the start of the simulation, in model units') plotted.update({term, out_term})
def plot_budget_summary(df, title_prefix='', title_suffix='', date_index_fmt='%Y-%m', term_nets=False, model_length_units=None, model_time_units=None, secondary_axis_units=None, xtick_stride=6, plot_start_date=None, plot_end_date=None): fig, ax = plt.subplots(figsize=(11, 8.5)) df = df.copy() if not term_nets: in_cols = [c for c in df.columns if '_IN' in c and 'TOTAL' not in c] out_cols = [c for c in df.columns if '_OUT' in c and 'TOTAL' not in c] ax = df[in_cols].plot.bar(stacked=True, ax=ax, width=20) ax = (-df[out_cols]).plot.bar(stacked=True, ax=ax, width=20) if isinstance(df.index, pd.DatetimeIndex): ax.set_xticklabels(df.index.strftime(date_index_fmt)) else: ax.set_xlabel('Time since the start of the simulation, in model units') ax.axhline(0, zorder=-1, lw=0.5, c='k') # create ylabel with model units, if provided if model_length_units is not None and model_time_units is not None: units_text = get_figure_label_unit_text(model_length_units, model_time_units, length_unit_exp=3) else: units_text = '$L^3/T$' ax.set_ylabel(f'Flow rate, in model units of {units_text}') # add commas to y axis values formatter = mpl.ticker.FuncFormatter(lambda x, p: format(int(x), ',')) ax.get_yaxis().set_major_formatter(formatter) # optional 2nd y-axis with other units if secondary_axis_units is not None: length_units2, time_units2 = parse_flux_units(secondary_axis_units) vol_conversion = convert_volume_units(model_length_units, length_units2) time_conversion = convert_time_units(model_time_units, time_units2) def fconvert(x): return x * vol_conversion * time_conversion def rconvert(x): return x / (vol_conversion * time_conversion) secondary_axis_unit_text = get_figure_label_unit_text(length_units2, time_units2, length_unit_exp=3) secax = ax.secondary_yaxis('right', functions=(fconvert, rconvert)) secax.set_ylabel(f'Flow rate, in {secondary_axis_unit_text}') secax.get_yaxis().set_major_formatter(formatter) # add stress period info ymin, ymax = ax.get_ylim() xlocs = np.arange(len(df)) yloc = np.ones(len(df)) * (ymin + 0.03 * (ymax - ymin)) if xtick_stride is None: xtick_stride = int(np.round(len(df) / 10, 0)) xtick_stride = 1 if xtick_stride < 1 else xtick_stride kpers = set() for x, y in zip(xlocs[::xtick_stride], yloc[::xtick_stride]): kper = int(df.iloc[x]['kper']) # only make one line for each stress period if kper not in kpers: ax.axvline(x, lw=0.5, c='k', zorder=-2) ax.text(x, y, f" {kper}", transform=ax.transData, ha='left', va='top') kpers.add(kper) ax.text(min(kpers), y + abs(0.06*y), ' model stress period:', transform=ax.transData, ha='left', va='top') title_text = ' '.join((title_prefix, 'budget summary', title_suffix)).strip() ax.set_title(title_text) ax.legend(ncol=2) # reduce x-tick density ticks = ax.xaxis.get_ticklocs() ticklabels = [l.get_text() for l in ax.xaxis.get_ticklabels()] ax.xaxis.set_ticks(ticks[::xtick_stride]) ax.xaxis.set_ticklabels(ticklabels[::xtick_stride]) # set x axis limits xmin, xmax = ax.get_xlim() if plot_start_date is not None: xmin = pd.Timestamp(plot_start_date) if plot_end_date is not None: xmax = pd.Timestamp(plot_end_date) ax.set_xlim(xmin, xmax) return ax
def plot_budget_summary(df, title_prefix='', title_suffix='', date_index_fmt='%Y-%m', term_nets=False, model_length_units=None, model_time_units=None, secondary_axis_units=None, xtick_stride=6, plot_start_date=None, plot_end_date=None): """Plot a stacked bar chart summary of a MODFLOW listing file budget dataframe. Parameters ---------- df : DataFrame Table of listing file budget results produced by flopy; typically the flux (not volume) terms (see example below). title_prefix : str, optional Prefix to insert at the begining of the title, for example the model name. by default '' title_suffix : str, optional Suffix to insert at the end of the title, by default '' date_index_fmt : str, optional Date format for the plot x-axis, by default '%Y-%m' term_nets : bool, optional Option to only plot net quantities for each stress period. For example if the inflows and outflows for the WEL package were +10 and -10, a bar of zero height would be plotted. by default False model_length_units : str, optional Length units of the model, for labeling and conversion to secondary_axis_units, by default None model_time_units : str, optional Time units of the model, for labeling and conversion to secondary_axis_units, by default None secondary_axis_units : str, optional Option to include a secondary y-axis on the right with another unit, for example 'mgal/day' for million gallons per day. Requires `model_length_units` and `model_time_units`. by default None xtick_stride : int, optional Spacing between x-ticks. May be useful for models with many stress periods. by default 6 plot_start_date : str, optional Minimum date to plot on the x-axis, in a string format recognizable by pandas (if `df` has a datetime index) or a numeric value (if `df` has a numeric index). by default None (plot all dates) plot_end_date : str, optional Maximum date to plot on the x-axis, in a string format recognizable by pandas (if `df` has a datetime index) or a numeric value (if `df` has a numeric index). by default None (plot all dates) Returns ------- ax : matplotlib axes subplot instance Examples -------- .. code-block:: python from mfexport.listfile import get_listfile_data, plot_budget_summary df = get_listfile_data(listfile='model.list', model_start_datetime='2000-01-01') plot_budget_summary(df) """ # slice the dataframe to the specified time range (if any) df = df.copy() df = df.loc[slice(plot_start_date, plot_end_date)] fig, ax = plt.subplots(figsize=(11, 8.5)) in_cols = [c for c in df.columns if '_IN' in c and 'TOTAL' not in c] out_cols = [c for c in df.columns if '_OUT' in c and 'TOTAL' not in c] if not term_nets: ax = df[in_cols].plot.bar( stacked=True, ax=ax, # width=20 ) ax = (-df[out_cols]).plot.bar( stacked=True, ax=ax, # width=20 ) else: pairs = list(zip(in_cols, out_cols)) net_cols = [] for in_col, out_col in pairs: net_col = f"{in_col.split('_')[0]} (net)" df[net_col] = df[in_col] - df[out_col] net_cols.append(net_col) ax = df[net_cols].plot.bar( stacked=True, ax=ax, # width=20 ) if isinstance(df.index, pd.DatetimeIndex): ax.set_xticklabels(df.index.strftime(date_index_fmt)) else: ax.set_xlabel('Time since the start of the simulation, in model units') ax.axhline(0, zorder=-1, lw=0.5, c='k') # create ylabel with model units, if provided if model_length_units is not None and model_time_units is not None: units_text = get_figure_label_unit_text(model_length_units, model_time_units, length_unit_exp=3) else: units_text = '$L^3/T$' ax.set_ylabel(f'Flow rate, in model units of {units_text}') # add commas to y axis values formatter = mpl.ticker.FuncFormatter(lambda x, p: format(int(x), ',')) ax.get_yaxis().set_major_formatter(formatter) # optional 2nd y-axis with other units if secondary_axis_units is not None: length_units2, time_units2 = parse_flux_units(secondary_axis_units) vol_conversion = convert_volume_units(model_length_units, length_units2) time_conversion = convert_time_units(model_time_units, time_units2) def fconvert(x): return x * vol_conversion * time_conversion def rconvert(x): return x / (vol_conversion * time_conversion) secondary_axis_unit_text = get_figure_label_unit_text( length_units2, time_units2, length_unit_exp=3) secax = ax.secondary_yaxis('right', functions=(fconvert, rconvert)) secax.set_ylabel(f'Flow rate, in {secondary_axis_unit_text}') secax.get_yaxis().set_major_formatter(formatter) # add stress period info ymin, ymax = ax.get_ylim() xlocs = np.arange(len(df)) yloc = np.ones(len(df)) * (ymin + 0.03 * (ymax - ymin)) if xtick_stride is None: xtick_stride = int(np.round(len(df) / 10, 0)) xtick_stride = 1 if xtick_stride < 1 else xtick_stride kpers = set() for x, y in zip(xlocs[::xtick_stride], yloc[::xtick_stride]): kper = int(df.iloc[x]['kper']) # only make one line for each stress period if kper not in kpers: ax.axvline(x, lw=0.5, c='k', zorder=-2) ax.text(x, y, f" {kper}", transform=ax.transData, ha='left', va='top') kpers.add(kper) ax.text(min(kpers), y + abs(0.06 * y), ' model stress period:', transform=ax.transData, ha='left', va='top') title_text = 'budget summary' if term_nets: title_text += ' (net fluxes)' title_text = ' '.join((title_prefix, title_text, title_suffix)).strip() ax.set_title(title_text) ax.legend(ncol=2) # reduce x-tick density ticks = ax.xaxis.get_ticklocs() ticklabels = [l.get_text() for l in ax.xaxis.get_ticklabels()] ax.xaxis.set_ticks(ticks[::xtick_stride]) ax.xaxis.set_ticklabels(ticklabels[::xtick_stride]) return ax
def plot_budget_term(df, term, title_prefix='', title_suffix='', model_length_units=None, model_time_units=None, secondary_axis_units=None, xtick_stride=None, plot_start_date=None, plot_end_date=None, datetime_xaxis=True): """Make a timeseries plot of an individual MODFLOW listing file budget term. Parameters ---------- df : DataFrame Table of listing file budget results produced by flopy; typically the flux (not volume) terms (see example below). title_prefix : str, optional Prefix to insert at the begining of the title, for example the model name. by default '' title_suffix : str, optional Suffix to insert at the end of the title, by default '' model_length_units : str, optional Length units of the model, for labeling and conversion to secondary_axis_units, by default None model_time_units : str, optional Time units of the model, for labeling and conversion to secondary_axis_units, by default None secondary_axis_units : str, optional Option to include a secondary y-axis on the right with another unit, for example 'mgal/day' for million gallons per day. Requires `model_length_units` and `model_time_units`. by default None xtick_stride : int, optional Spacing between x-ticks. May be useful for models with many stress periods. by default 6 plot_start_date : str, optional Minimum date to plot on the x-axis, in a string format recognizable by pandas (if `df` has a datetime index) or a numeric value (if `df` has a numeric index). May be useful if the model has long spinup period(s) that would obscure later periods of interest when datetime_xaxis=True. by default None (plot all dates) plot_end_date : str, optional Maximum date to plot on the x-axis, in a string format recognizable by pandas (if `df` has a datetime index) or a numeric value (if `df` has a numeric index). by default None (plot all dates) datetime_xaxis : bool Plot budget values as a function of time. If False, plot as a function of stress period. by default, True Returns ------- ax : matplotlib axes subplot instance Examples -------- .. code-block:: python from mfexport.listfile import get_listfile_data, plot_budget_summary df = get_listfile_data(listfile='model.list', model_start_datetime='2000-01-01') plot_budget_term(df, 'WELLS') """ # slice the dataframe to the specified time range (if any) df = df.copy() df = df.loc[slice(plot_start_date, plot_end_date)] if not datetime_xaxis and 'datetime' in df.index.dtype.name: df['datetime'] = df.index df.index = df['kper'] if term not in {'IN-OUT', 'PERCENT_DISCREPANCY'}: # get the absolute quantity for the term if isinstance(term, list): series = df[term].sum(axis=1) out_term = [s.replace('_IN', '_OUT') for s in term] out_series = df[out_term].sum(axis=1) else: term = term.replace('_IN', '').replace('_OUT', '') in_term = f'{term}_IN' out_term = f'{term}_OUT' series = df[in_term] out_series = df[out_term] # get the net net_series = series - out_series # get the percent relative to the total budget pct_series = series / df['TOTAL_IN'] pct_out_series = out_series / df['TOTAL_OUT'] pct_net_series = pct_series - pct_out_series else: series = df[term] out_term = None out_series = None if model_length_units is not None and model_time_units is not None: units_text = get_figure_label_unit_text(model_length_units, model_time_units, length_unit_exp=3) else: units_text = '$L^3/T$' if out_series is not None: fig, axes = plt.subplots(2, 1, sharex=True, figsize=(11, 8.5)) axes = axes.flat ax = axes[0] series.plot(ax=ax, c='C0') ax.axhline(0, zorder=-1, lw=0.5, c='k') (-out_series).plot(ax=ax, c='C1') net_series.plot(ax=ax, c='0.5', zorder=-1) h, l = ax.get_legend_handles_labels() ax.legend(h, ['In', 'Out', 'Net']) ax.set_ylabel(f'Flow rate, in model units of {units_text}') # add commas to y axis values formatter = mpl.ticker.FuncFormatter(lambda x, p: format(int(x), ',')) ax.get_yaxis().set_major_formatter(formatter) # optional 2nd y-axis with other units if secondary_axis_units is not None: length_units2, time_units2 = parse_flux_units(secondary_axis_units) vol_conversion = convert_volume_units(model_length_units, length_units2) time_conversion = convert_time_units(model_time_units, time_units2) def fconvert(x): return x * vol_conversion * time_conversion def rconvert(x): return x / (vol_conversion * time_conversion) secondary_axis_unit_text = get_figure_label_unit_text( length_units2, time_units2, length_unit_exp=3) secax = ax.secondary_yaxis('right', functions=(fconvert, rconvert)) secax.set_ylabel(f'Flow rate, in {secondary_axis_unit_text}') secax.get_yaxis().set_major_formatter(formatter) # plot the percentage of total budget on second axis ax2 = axes[1] pct_series.plot(ax=axes[1], c='C0') ax2.axhline(0, zorder=-1, lw=0.5, c='k') (-pct_out_series).plot(ax=ax2, c='C1') pct_net_series.plot(ax=ax2, c='0.5', zorder=-1) ax2.set_ylabel('Fraction of budget') single_subplot = False # add stress period info #ymin, ymax = ax.get_ylim() #yloc = np.ones(len(df)) * (ymin - 0.02 * (ymax - ymin)) #if xtick_stride is None: # xtick_stride = int(np.round(len(df) / 10, 0)) # xtick_stride = 1 if xtick_stride < 1 else xtick_stride #kpers = set() #for x, y in zip(df.index.values[::xtick_stride], yloc[::xtick_stride]): # kper = int(df.loc[x, 'kper']) # # only make one line for each stress period # if kper not in kpers: # ax.axvline(x, lw=0.5, c='k', zorder=-2) # ax2.axvline(x, lw=0.5, c='k', zorder=-2) # ax2.text(x, y, kper, transform=ax.transData, ha='center', va='top') # kpers.add(kper) #ax2.text(0.5, -0.07, 'Model Stress Period', ha='center', va='top', transform=ax.transAxes) else: fig, ax = plt.subplots(1, 1, sharex=True, figsize=(11, 8.5)) series.plot(ax=ax, c='C0') ax.axhline(0, zorder=-1, lw=0.5, c='k') ax2 = ax single_subplot = True # add stress period info ymin, ymax = ax.get_ylim() pad = 0.02 if single_subplot: pad = 0.1 yloc = np.ones(len(df)) * (ymin - pad * (ymax - ymin)) if xtick_stride is None: xtick_stride = int(np.round(len(df) / 10, 0)) xtick_stride = 1 if xtick_stride < 1 else xtick_stride kpers = set() for x, y in zip(df.index.values[::xtick_stride], yloc[::xtick_stride]): kper = int(df.loc[x, 'kper']) # only make one line for each stress period if kper not in kpers: ax.axvline(x, lw=0.5, c='k', zorder=-2) ax2.axvline(x, lw=0.5, c='k', zorder=-2) ax2.text(x, y, kper, transform=ax.transData, ha='center', va='top') kpers.add(kper) ax2.text(0.5, -0.07, 'Model Stress Period', ha='center', va='top', transform=ax.transAxes) title_text = ' '.join( (title_prefix, term.split('_')[0], title_suffix)).strip() ax.set_title(title_text) # set x axis limits xmin, xmax = series.index.min(), series.index.max() if plot_start_date is not None: if not datetime_xaxis: loc = df.datetime >= plot_start_date xmin = df.datetime.loc[loc].index[0] else: xmin = pd.Timestamp(plot_start_date) if plot_end_date is not None: if not datetime_xaxis: loc = df.datetime <= plot_end_date xmax = df.datetime.loc[loc].index[-1] else: xmax = pd.Timestamp(plot_end_date) ax.set_xlim(xmin, xmax) if not datetime_xaxis: if 'datetime' in df.columns: xticks = ax2.get_xticks() datetime_labels = [ df['datetime'].loc[int(i)].strftime('%Y-%m-%d') for i in xticks if i <= df.index.max() ] ax2.set_xticklabels(datetime_labels, rotation=90) if not isinstance(df.index, pd.DatetimeIndex): ax2.set_xlabel( 'Time since the start of the simulation, in model units') plotted.update({term, out_term})
def test_convert_volume_units(): assert np.allclose(convert_volume_units('cubic meters', 'mgal'), 264.172 / 1e6) assert np.allclose(convert_volume_units('$m^3$', '$ft^3$'), 35.3147) assert np.allclose(convert_volume_units('cubic meters', 'cubic feet'), 35.3147) assert np.allclose(convert_volume_units('cubic feet', 'cubic meters'), 0.0283168) assert np.allclose(convert_volume_units('meters', 'feet'), 35.3147) assert np.allclose(convert_volume_units('feet', 'meters'), 0.0283168) assert np.allclose(convert_volume_units('feet3', 'm3'), 0.0283168) assert np.allclose(convert_volume_units('feet3', 'meters3'), 0.0283168) assert np.allclose(convert_volume_units('gallons', 'ft3'), 1 / 7.48052) assert np.allclose(convert_volume_units('gallons', 'm3'), (.3048**3) / 7.48052) assert np.allclose(convert_volume_units('gallons', 'acre foot'), 1 / 7.48052 / 43560) assert np.allclose(convert_volume_units('gallons', 'af'), 1 / 7.48052 / 43560) assert np.allclose(convert_volume_units('gallons', 'acre-ft'), 1 / 7.48052 / 43560) assert np.allclose(convert_volume_units('mgal', 'acre-ft'), 1e6 / 7.48052 / 43560) assert np.allclose(convert_volume_units('m3', 'million gallons'), 0.0002641720707034298) assert np.allclose(convert_volume_units('mgal', 'm3'), 1 / 0.0002641720707034298) assert np.allclose(convert_volume_units('liters', 'gallon'), 1 / 3.78541) assert np.allclose(convert_volume_units(None, 'cubic feet'), 1.) assert np.allclose(convert_volume_units('cubic feet', None), 1.) assert np.allclose(convert_volume_units('junk', 'junk'), 1.)