Example #1
0
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})
Example #2
0
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
Example #3
0
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
Example #4
0
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})
Example #5
0
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.)