Basic use of mibitrans¶
This example present the features implemented in the mibitrans package, and explains how to use them. The parameters used here are example field data in reasonable orders of magnitude, but do not represent any field site in paticular.
import matplotlib.pyplot as plt
import numpy as np
import mibitrans as mbt
Input by dataclasses¶
mibitrans use dataclasses to handle data input. Input is grouped into four categories, each having a separate input dataclass:
- hydrological parameters
- attenuation parameters
- source parameters
- model parameters
Those are required for a regular transport model. An instantaneous reaction model additionally needs electron acceptor concentrations, this is discussed in a later section.
Units of parameters have default values, consistent between data input categories. Input values need to be expressed accordingly to avoid mishaps. It is possible to provide input in other units, but this requires the user to ensure that they are consistent throughout the entire modelling process.
Hydrological parameters¶
The data class HydrologicalParameters contains parameters that are inherent to the aquifer properties and relevant for the conservative transport processes of advection and dispersion: flow velocity, porosity, dispersivity and diffusion.
hydro = mbt.HydrologicalParameters(
velocity=0.08, # Groundwater flow velocity, in [m/day]
porosity=0.25, # Effective soil porosity [-]
alpha_x=5, # Longitudinal dispersivity, in [m]
alpha_y=0.2, # Transverse horizontal dispersivity, in [m]
alpha_z=0.005, # Transverse vertical dispersivity, in [m]
diffusion=1e-5 # Molecular diffusion, in [m2/day]
)
Alternatively, hydraulic conductivity and hydraulic gradient can be provided. Flow velocity is then calculated from those.
hydro = mbt.HydrologicalParameters(
h_gradient=0.004, # Hydraulic gradient [-]
h_conductivity=6, # Hydraulic conductivity [m/day]
porosity=0.3, # Effective soil porosity [-]
alpha_x=5, # Longitudinal dispersivity, in [m]
alpha_y=0.2, # Transverse horizontal dispersivity, in [m]
alpha_z=0.005 # Transverse vertical dispersivity, in [m]
)
Check the value of an input parameter by calling the dataclass with the corresponding attribute name.
print("The calculated groundwater flow velocity is: ", hydro.velocity, r"m/d")
print(f"The dispersivity values are: {hydro.alpha_x}m, {hydro.alpha_y}m and {hydro.alpha_z}m,"
" for the x, y and z directions respectively.")
The calculated groundwater flow velocity is: 0.08 m/d The dispersivity values are: 5m, 0.2m and 0.005m, for the x, y and z directions respectively.
If transverse vertical dispersivity, $\alpha_z$ and/or diffusion are not given, they default to $0$.
Note that only the Mibitrans model includes diffusion in its calculations. Anatrans and Bioscreen do not, due to assumptions taken in their derivation.
att = mbt.AttenuationParameters(
retardation=1.3 # Retardation factor for transported contaminants [-]
)
Alternatively, the retardation coefficient $R$ can be calculated from soil bulk density, partition coefficient and the soil organic carbon fraction - when these parameters are provided. Note that the calculation of the retardation factor also requires porosity, which has to be provided in the HydrologicalParameters class. The calculation of retardation is automatically performed through the function calculate_retardation() when running the model. But this function can also be manually called.
att = mbt.AttenuationParameters(
bulk_density=1.7, # Soil bulk density in [g/m^3]
partition_coefficient=53, # Partition coefficient of contaminant to soil organic matter, in [m^3/g]
fraction_organic_carbon=1e-3 # Fraction of organic material in the soil [-]
)
# Calculate the retardation factor beforehand to see its value by specifying the porosity
att.calculate_retardation(porosity=hydro.porosity)
# And then check the value:
print("The calculated retardation value is: ", att.retardation)
The calculated retardation value is: 1.3003333333333333
Attenuation: degradation parameters¶
The basic concept of modelling degradation (e.g. as result of biological activity) is by a linear decay model, independent of any geochemical and biological settings. Input for the linear decay model is either the contaminant decay rate $\mu$ or half life $t_{1/2}$.
Both options to provide these parameters are implemented. The other corresponding parameter is automatically calculated.
att = mbt.AttenuationParameters(
half_life=3650, # Contaminant half life, in [days]
)
print("The internally decay rate (1/days) is: ", att.decay_rate)
att = mbt.AttenuationParameters(
decay_rate=1.899e-4 # Contaminant first order decay rate in [1/days]
)
print("The internally calculated contaminant half life (days) is: ", att.half_life)
The internally decay rate (1/days) is: 0.00018990333713971103 The internally calculated contaminant half life (days) is: 3650.064141969169
A complete set of attenuation parameters (for the examples below) would look like:
att = mbt.AttenuationParameters(
bulk_density=1.7, # Soil bulk density in [g/m^3]
partition_coefficient=53, # Partition coefficient of contaminant to soil organic matter, in [m^3/g]
fraction_organic_carbon=1e-3, # Fraction of organic material in the soil [-]
half_life=3650 # Contaminant half life, in [days]
)
Source parameters¶
The data class SourceParameters handles all parameters related to the contaminant source. Conceptually, the source is treated as a seperate phase, which dissolves into the groundwater over time. mibitrans also makes use of source superposition that reflects the typical field observation that the source expands over a certain width with decreasing concentrations towards the margins. Input parameters are thus:
- source depth
depth: extension of contaminant source in depth ($z$-direction); is assumed constant over the entire width of the source - total mass
total_mass: total amound of contaminant source in NAPL phase; can be a specific value or infinite. - width of the different horizontal source zones
source_zone_boundary: defined as array of boundary locations relative to source center - the contaminant concentrations within each source zone
source_zone_concentration: array of similar shape assource_zone_boundaryas it specifies the contaminant concentration released to the groundwater within each of the source zones.
First an example for a simple source zone with no separate zones having a width of $20$m, depth of $3m$, a continuous input (infinite source mass) and a release concentration of $5$ g/m^3:
source = mbt.SourceParameters(
source_zone_boundary=np.array([20]), # Source zone extent, in [m]
source_zone_concentration=np.array([5]), # Source zone concentrations, in [g/m^3]
depth=3, # Source depth extent, in [m]
total_mass="infinite" # Source mass, in [g] or as np.inf / "infinite"
)
Source zonation¶
The source is assumed to be symmetrical in $y$-direction with the center located at $y=0$. The width of the source is divided into $n$ zones, where $n$ is a free choice of the user. The array source_zone_boundary defines the boundaries between zones. The release concentration within each zone is defined in the array source_zone_concentration. Release concentrations decrease from the center to the fringes. The source depth is assumed to be constant over the entire width.
Second example: Consider a source of total width of $40$m. It has a very high concentration in the source center at $y \in [-2m,2m]$, a lower concentration up to the distance of $\pm 11$ m (specifically for $y \in [-11m,-2m]$ and $y \in [2m,11m]$) and the lowest concentration from $\pm 11m$ up to $\pm 20m$. This would be entered as array of $[2,11,20]$ for source_zone_boundary and an array of the same shape for the corresponding source zone concentrations.
The shape of the source can be easily visualized with source.visualize()
source = mbt.SourceParameters(
source_zone_boundary=np.array([2, 11, 20]), #[m]
source_zone_concentration=np.array([15, 4, 1]), #[g/m3]
depth=3, #[m]
total_mass=np.inf #[g]
)
# Visualize what the source zone looks like to check your input:
source.visualize()
plt.show()
By giving the total solid-phase contaminant source mass (total_mass) a finite amount, the source will not stay constant in strength but deplete over time. Consequently during simulation, the source mass and the source zone concentrations will diminish over time. This aspect is implemented as source depletion at the rate $\gamma_s$ in $1/day$. The rate $\gamma_s$ depends on the flow velocity, size of the source zone and source zone concentrations. When the source mass is considered infinite, contaminant source concentrations will not diminish over time (i.e. $\gamma_s = 0$). For more information about source depletion, see the theory documentation.
source = mbt.SourceParameters(
source_zone_boundary=np.array([2, 5, 10, 21]), #[m]
source_zone_concentration=np.array([13, 6, 3, 1]), #[g/m3]
depth=3, #[m]
total_mass=5e6 #[g]
)
Model parameters¶
The data class ModelParameters handles all parameters related to the spatial and temporal settings of the domain in which the contaminant plume should be calculated, including model dimensions and discretization parameters:
model_lengthis the domain extent in the (x) direction parallel to the groundwater flow direction.model_widthis the domain extent of the model perpendicular (y) to the groundwater flow direction.model_timeis the model duration, i.e. the final time of plume calculation.dxanddyhandle step size of the grid at which values are calculated.dthandles time step sizes, i.e. time steps at which values are calculated.
The user should ensure that the source zone fits inside of the given model width. If step sizes are undefined, default values are chosen as ratios to model dimensions with model_length$/100$, model_width$/50$ and model_time$/10$.
model = mbt.ModelParameters(
model_length=600, # Model extent in the longitudinal (x) direction in [m].
model_width=80, # Model extent in the transverse horizontal (y) direction in [m].
model_time=20 * 365, # Model duration in [days].
dx=4, # Model grid discretization step size in the longitudinal (x) direction, in [m].
dy=0.5, # Model grid discretization step size in the transverse horizontal (y) direction, in [m].
dt=365 # Model time discretization step size, in [days]
)
Input checking¶
All data classes for parameter input are checked. It evaluates if the required input parameters are present, if they are of the correct data type and if they are in the expected domain. We provide here some examples of data input that fails the checks.
Uncomment the cells below to see results of the input check - Cells are intended to raise an error!
# bad_hydro = mbt.HydrologicalParameters(velocity = 1)
Not all required parameters are specified; HydrologicalParameters needs the porosity, longitudinal dispersivity and transverse horizontal dispersivity as well.
# bad_source = mbt.SourceParameters(
# source_zone_boundary = np.array([1,2,3]),
# source_zone_concentration = "this is a string, not an array",
# depth = 10,
# total_mass = "inf",
# )
The given input datatype is not what is expected; source_zone_concentration expects a numpy array of the same length as the array given in source_zone_boundary.
# bad_att = mbt.AttenuationParameters(
# retardation = 0.1
# )
The parameter has a value outside its valid domain; retardation should have a value $R\geq1$.
Transport simulation¶
Choice of transport solution¶
The package mibitrans offers three analytical solutions to the contaminant transport equation ADE (with advection, dispersion, diffusion, retardation and decay) under uniform groundwater flow in 3D and constant source supply. All three solutions implemented different equations:
Mibitranssolution is the full exact solution to the ADE under the specified initial and boundary conditions (Wexler, 1992; Karanovic 2007). TheMibitranssolution requires numerical integration which causes a higher computational effort and duration of calculation.Anatranssolution is derived based on a small simplification for transport in transverse directions. It provides a fully analytical (i.e. does not require numerical integration), but approximate solution to the ADE. It is interesting for screening given speed up of calculation, particularly when concentrations in transverse direction are of minor interest.Bioscreensolution makes use of the approximate analytical solution introduced in BIOSCREEN (Newell et al., 1996,1997) building up on the solution of Domenico (1987) in various adaptions. It is a further simplification of the equation used inAnatrans, introducing a mathematical truncation affecting correctness of calculated contaminant concentration in flow direction, particularly for Peclet numbers lower that 100.
Each solution is implemented as separate model class, located in mibitrans.transport.models. A model is first initialized through making the model object and passing on the model parameters defined in the input data classes$^1$. During model initialization, the model grid is generated and some prior parameter are calculated. After initialization, no transport results have been calculated yet.
Contaminant concentrations as result of transport are calculated when calling the model instance with its class method run. This generates a results class object, which contains the concentration distribution as a 3D numpy array (indexed as $C[t,y,x]$) and the parameters used for this specific run.
$^1$Note that the input argument names for the dataclass follow snake_case instead of PascalCase (e.g. hydrological_paramaters takes in the data class HydrologicalParameters) following Python conventions.
Mibitrans model¶
As the namesake model of this package, it is the recommended model to use. It is the fully exact solution to the contaminant transport equation under uniform flow and a constant contaminant source while the other two models introduce a margin of error as consequence of taken assumptions. The exact nature of these differences as well as the underlying equation can be found in the package documentation.
However, since this model requires evaluation of an integral, computation time might be longer, depending on model discretization.
# Initializing the model object
mbt_object = mbt.Mibitrans(
hydrological_parameters=hydro,
attenuation_parameters=att,
source_parameters=source,
model_parameters=model,
verbose=False #Set to True to follow model calculation steps
)
# Can use model object to request input and various attributes
print("Retarded flow velocity: ", mbt_object.rv, "r$m/day$")
print(len(mbt_object.x), "x-steps,", len(mbt_object.y), "y-steps,", len(mbt_object.t), "t-steps")
print("Input source mass: ", mbt_object.source_parameters.total_mass, "g")
Retarded flow velocity: 0.06152268649064343 r$m/day$ 151 x-steps, 161 y-steps, 20 t-steps Input source mass: 5000000.0 g
# Run the model once initialized and obtain the results object
mbt_results = mbt_object.run()
Model results¶
Contaminant concentrations and the input parameters used for this specific run are returned as result object (here mbt_results) after running the model. This ensures that it is always clear which conditions correspond with which result, even when re-running the model.
The model results object does not only contain all data, but also offers features for visualization and analysis, such as plotting and generating a mass balance. This is further elaborated upon in the Visualization and Mass Balance sections below.
# Concentration distribution is contained in the cxyt attribute
model_cxyt = mbt_results.cxyt
# The Results object also contains all information of parameters used to generate the output contained within
print("The flow velocity of the model was:", mbt_results.hydrological_parameters.velocity, "m/d")
# cxyt is indexed as [time, y-position, x-position]
# Thus to get the concentration at the last time step, in the center of the plume for all x:
plume_center = model_cxyt[-1, len(mbt_results.y)//2, :]
print("The first 3 concentrations at the plume center are: ", plume_center[:3], "g/m3")
# The Results object has build-in visualization methods (see visualization section for more details).
mbt_results.centerline()
plt.show()
The flow velocity of the model was: 0.08 m/d The first 3 concentrations at the plume center are: [12.81008252 12.02839838 11.14122013] g/m3
Anatrans model¶
The Anatrans model implements a fully analytical equation speeding calculation up as no numerical integration is needed as in the Mibitrans model. Anatrans is fully exact for transport in horizontal ($x$) direction of the groundwater flow and takes assumption for transport in vertical directions ($y$ and $z$). The underlying equation can be found in the package documentation. Differences in concentrations in vertical directions introduced through the approximation are discussed in the notebook example_mibitrans_anatrans.ipynb.
ana_object = mbt.Anatrans(
hydrological_parameters=hydro,
attenuation_parameters=att,
source_parameters=source,
model_parameters=model
)
ana_results = ana_object.run()
ana_results.centerline()
plt.show()
Bioscreen model¶
This model is an exact implementation of the transport equations implemented in the BIOSCREEN screening model of Newell et al. (1997), which is based on the Domenico (1987) analytical model. Using a truncated version of the equation used in the Anatrans model. This model is implemented as a method of comparison with the original BIOSCREEN software. And is included for legacy reasons, since it is the first model implemented in the mibitrans package, serving as a basis for the other models. However, caution should be taken when using this model, since a varying error is introduced by using the truncated analytical solution. The error is most prominent for shorter times and distances from the source, and depends on the ratio of flow velocity and longitudinal dispersivity. For modelling, the Anatrans (untruncated approximate solution) and Mibitrans (exact analytical solution) models are recommended instead.
bio_object = mbt.Bioscreen(
hydrological_parameters=hydro,
attenuation_parameters=att,
source_parameters=source,
model_parameters=model
)
bio_results = bio_object.run()
bio_results.centerline()
plt.show()
Instant reaction model¶
The mibitrans package offers another option for modelling degredation of contaminants during transport (beside the linear decay), which is the instant reaction model. If geochemical field site data (e.g. from observations) is available, contaminant transport and biodegradation can be modeled by usage of the instantaneous reaction model coupled with transport. This model superimposes the effect of biodegradation reactions that are limited by electron acceptor availability to the conservative analytical transport model. Details on the theoretical background of the method are provided in the documentation, as are the details on this type of reaction model.
The data class ElectronAcceptors handles the concentrations of electron acceptors available for degradation (oxygen, nitrate, sulfate) and metabolic by-products of the degradation reactions (ferrous iron and methane). The dataclass UtilizationFactors takes the utilization factor for each respective electron acceptor.
These dataclasses are input for the instant_reaction class method of the transport class (available for Mibitrans, Anatrans and Bioscreen). Electron acceptor concentrations are required as input. For the utilization factor, default values for a BTEX mixture are used, unless different input is provided.
# For streamlined input, use the ElectronAcceptor dataclass;
ea = mbt.ElectronAcceptors(
# Difference between background oxygen and current oxygen concentration in groundwater, in [g/m^3]
delta_oxygen=9,
# Difference between background nitrate and current nitrate concentration in groundwater, in [g/m^3]
delta_nitrate=6,
# Current ferrous iron concentration in groundwater, in [g/m^3]
ferrous_iron=8,
# Difference between background sulfate and current sulfate concentration in groundwater, in [g/m^3]
delta_sulfate=5,
# Current methane concentration in groundwater, in [g/m^3]
methane=4
)
mbt_object.instant_reaction(electron_acceptors=ea)
# Can adapt utilization factors if needed
uf = mbt.UtilizationFactor(
# utilization factor of oxygen, as mass of oxygen consumed per mass of biodegraded contaminant [g/g].
util_oxygen=2,
# utilization factor of nitrate, as mass of nitrate consumed per mass of biodegraded contaminant [g/g].
util_nitrate=1,
# utilization factor of ferrous iron, as mass of ferrous iron generated per mass of biodegraded contaminant [g/g].
util_ferrous_iron=4,
# utilization factor of sulfate, as mass of sulfate consumed per mass of biodegraded contaminant [g/g].
util_sulfate=3,
# utilization factor of methane, as mass of methane generated per mass of biodegraded contaminant [g/g].
util_methane=5,
)
mbt_object.instant_reaction(electron_acceptors=ea, utilization_factor=uf)
# Alternatively, electron acceptors and/or utilization factors can be entered as a dictionary
mbt_object.instant_reaction(
electron_acceptors={
# Difference between background oxygen and current oxygen concentration in groundwater, in [g/m^3]
"delta_oxygen":9,
# Difference between background nitrate and current nitrate concentration in groundwater, in [g/m^3]
"delta_nitrate":6,
# Current ferrous iron concentration in groundwater, in [g/m^3]
"ferrous_iron":8,
# Difference between background sulfate and current sulfate concentration in groundwater, in [g/m^3]
"delta_sulfate":5,
# Current methane concentration in groundwater, in [g/m^3]
"methane":4,
}
)
# Or as a list
mbt_object.instant_reaction(
electron_acceptors=[9, 6, 8, 5, 4],
utilization_factor=[3.14, 4.9, 21.8, 4.7, 0.78]
)
# Note that using instant_reaction also resets the utilization factor to default.
print("electron acceptor concentrations: ", mbt_object.electron_acceptors)
print("electron acceptor utilization factors: ", mbt_object.utilization_factor)
electron acceptor concentrations: {'delta_oxygen': 9, 'delta_nitrate': 6, 'ferrous_iron': 8, 'delta_sulfate': 5, 'methane': 4}
electron acceptor utilization factors: {'util_oxygen': 3.14, 'util_nitrate': 4.9, 'util_ferrous_iron': 21.8, 'util_sulfate': 4.7, 'util_methane': 0.78}
Running the model now would provide results using the instantaneous reaction model. You can switch between linear degradation and instant reaction using the model object.
# Switch between instant reaction and linear model
mbt_object.mode = "linear"
print("The current model mode is: ", mbt_object.mode)
# Running now will show the results of the linear model
# Switch back to instant reaction as follows:
mbt_object.mode = "instant_reaction"
print("The current model mode is: ", mbt_object.mode)
The current model mode is: linear The current model mode is: instant_reaction
Sample¶
Use the sample method to get the concentration at a specific location and time.
# Instead of distribution, sample a specific location at any point along the plume and any point in time
concentration = mbt_object.sample(
x_position=100,
y_position=0,
time=10*365)
print("The concentration at 150m downstream from the source after 10 years is:", concentration, r"g/m^3")
The concentration at 150m downstream from the source after 10 years is: 6.420055571477114 g/m^3
Visualization¶
The mibitrans package has various ways to visualize and output the model results, including 1D plotting and multidimensional plots. Other options to retrieve information from modelling results are output of sample data and calculations of the mass balance. Find examples for each feature below.
First, make some different model objects to use in the plotting, takes a couple of seconds to run.
#Mibitrans model
mbt_object = mbt.Mibitrans(hydro, att, source, model)
mbt_lineardecay = mbt_object.run()
mbt_object.attenuation_parameters.decay_rate = 0
mbt_nodecay = mbt_object.run()
mbt_object.instant_reaction(electron_acceptors=ea)
mbt_instant = mbt_object.run()
#Anatrans model
ana_object = mbt.Anatrans(hydro, att, source, model)
ana_lineardecay = ana_object.run()
ana_object.attenuation_parameters.decay_rate = 0
ana_nodecay = ana_object.run()
ana_object.instant_reaction(electron_acceptors=ea)
ana_instant = ana_object.run()
#Bioscreen model
bio_object = mbt.Bioscreen(hydro, att, source, model)
bio_lineardecay = bio_object.run()
bio_object.attenuation_parameters.decay_rate = 0
bio_nodecay = bio_object.run()
bio_object.instant_reaction(electron_acceptors=ea)
bio_instant = bio_object.run()
Centerline plot¶
It shows the concentration distribution $C(x, y_s, t_s)$ in the longitudinal ($x$) direction, at a specific vertical plume location $y_s$ and specific time $t_s$. By default, the fixed vertical location is the center of the plume ($y_s=0$) and the default time is the last time step model_time as defined in the data class ModelParameters. The $y_s$-position along the plume and point of time $t_s$ can be adapted with function arguments.
Plotting can be performed either as class method, i.e. model_object.centerline(), or by calling the centerline function with the model object as argument, i.e. centerline(model_object).
# Pass result object to plotting function, using default time and
mbt.centerline(mbt_nodecay)
plt.show()
Plume location¶
Define arguments time and y_position to plot somewhere of the plume center and at a specific time. If the time is not identical to a specific time step defined during time stepping (determined by dt in the data class ModelParameters), the closest point in time is identified and the concentration profile at that time is provided.
mbt.centerline(mbt_nodecay, time=15*365, y_position=7)
plt.show()
Plot modification¶
Plot settings can be changed to adapt appearance and combine plots.
The centerline function (transverse and breakthrough as well) accepts a list of multiple models and can plot them at the same time. It further accepts a list of names to use for the plot legend.
legend = ["no degradation", "linear decay", "instant reaction"]
mbt.centerline([mbt_nodecay, mbt_lineardecay, mbt_instant], time=12*365, legend_names=legend)
plt.show()
Can change (axis) title, figure size, plot extent, etc. like one normally would.
legend = ["mibitrans", "anatrans", "bioscreen"]
mbt.centerline([mbt_nodecay, ana_nodecay, bio_nodecay], time=3*365, legend_names=legend)
plt.title("Better title than the one generated automatically")
plt.xlabel("I have changed!")
plt.ylabel("And so have I")
plt.xlim((-10, 150))
plt.show()
Calling each centerline function separately per model gives more control over plot layout. Keyword arguments for plt.plot can be passed on through the function.
time = 20*365 #[days]
y0, y1, y2, y3 = 0, 6, 9, 20
mbt.centerline(mbt_nodecay, time = time, y_position = y0, linestyle="-", color="blue", label = f"y={y0}m")
mbt.centerline(mbt_nodecay, time = time, y_position = y1, linestyle="--", color="orange", label = f"y={y1}m")
mbt.centerline(mbt_nodecay, time = time, y_position = y2, linestyle="-.", color="green", label = f"y={y2}m")
mbt.centerline(mbt_nodecay, time = time, y_position = y3, linestyle=":", color="red", label = f"y={y3}m")
plt.legend()
plt.title("Mibitrans with no degradation at various plume locations $y$",fontsize =10)
plt.xlabel("Plume travel distance $x$ in m")
plt.ylabel(f"Concentration distribution $C(x,y,t={time/365}y) [g/m^3]$")
plt.show()
This also works with the use of the inherent plotting class method of the result objects. Loop to make combining plots even easier.
times = np.array([365, 3*365, 5*365, 10*365, 15*365, 20*365])
colors = ["darkblue", "mediumblue", "blue", "blueviolet", "mediumorchid", "violet"]
linestyles = ["-", "-", "--", "--", ":", ":"]
for i in range(len(times)):
ana_instant.centerline(time = times[i], color=colors[i], linestyle=linestyles[i], label=f"t={times[i]}days")
plt.title("Concentration distribution for anatrans instant reaction, at various times")
plt.xlim((-20, 400))
plt.legend()
plt.show()
Transverse distribution¶
The plotting routine transverse visualizes the concentration distribution $C(x_s, y, t_s)$ at a plume cross section, i.e. in horizontal transverse direction at a specified distance $x_s$ and time $t_s$. By default the time is the last time step model_time as defined in the data class ModelParameters.
Plotting can be performed either as class method, i.e. model_object.transverse(), or by calling the function with the model object as argument, i.e. transverse(model_object).
mbt.transverse(mbt_lineardecay, x_position=100)
plt.show()
Plot modification¶
Modifications to transverse plots can be done similarly to the centerline plots.
mbt.transverse(ana_nodecay, x_position=80, time=6*365, linestyle="--", color="green", label="no degradation")
mbt.transverse(ana_instant, x_position=80, time=6*365, linestyle="-.", color="red", label="instant reaction")
plt.title("No degradation and instant reaction Domenico model at x=80m, t=6years")
plt.legend()
plt.show()
mbt_instant.transverse(5, label="x = 5m", color="darkolivegreen")
mbt_instant.transverse(20, label="x = 20m", color="yellowgreen")
mbt_instant.transverse(100, label="x = 100m", color="greenyellow")
mbt_instant.transverse(300, label="x = 300m", color="palegreen")
plt.xlim([-20,20])
plt.legend()
plt.title("Cross section concentration at various $x$ for Mibitrans instant reaction model, $t=20$y")
plt.show()
Breakthrough curve¶
Breakthrough curve shows the concentration over time for a specified point in the contaminant plume. Specifically, breakthrough visualizes the concentration distribution $C(x_s, y_s, t)$ at the plume location ($x_s$,$y_s$) over time. By default, the fixed vertical location is the center of the plume ($y_s=0$). Total displayed time corresponds to the last time step model_time as defined in the data class ModelParameters and intermediate points reflect the defined time stepping dt.
Plotting can be performed either as class method, i.e. model_object.breakthrough(), or by calling the function with the model object as argument, i.e. breakthrough(model_object). In the latter form, it can accept a list of models to be displayed in a single plot. Calling each function separately per model gives more control over plot layout. Keyword arguments for plt.plot can be passed on through the function.
# Concentration distribution can also be plotted in the transverse direction
mbt.breakthrough([bio_nodecay, bio_lineardecay, bio_instant], x_position=150,
legend_names=["no degradation", "linear decay", "instant reaction"])
plt.title("Breakthrough curve of Bioscreen model for different degradation settings, at x=80m")
plt.show()
mbt_nodecay.breakthrough(x_position=200,marker = 'o',mec = 'k', color="red")
plt.show()
Two-dimensional plume¶
The function plume_2d visualizes the concentration distribution $C(x,y,t_s)$ at desired point in time $t_s$ as color-coded 2D image. By default, concentration distribution of the last time step model_time is shown, as defined in the data class ModelParameters. Total displayed dimensions corresponds to the domain parameters defined in the data class ModelParameters.
Plotting can be performed either as class method, i.e. model_object.plume_2d(), or by calling the function with the model object as argument, i.e. plume_2d(model_object). Keyword arguments for plt.pcolormesh (on which plume_2d is based) can be passed on through the function.
mbt.plume_2d(mbt_nodecay)
plt.show()
# Function passes plt.colormesh keyword arguments
mbt.plume_2d(mbt_lineardecay, time=20*365, cmap="plasma")
plt.show()
# Once again also can be accessed through the class method
ana_instant.plume_2d(time=15*365, cmap="Spectral", shading="gouraud")
plt.title("Contaminant concentration for anatrans instant reaction model, at t=15y")
plt.show()
3-dimensional plume¶
The concentration distribution can be visualized as plume in 3D view using plume_3d. Specifically, plume_3d shows the distribution $C(x,y,t_s)$ at desired point in time $t_s$ in a 3D plot, with the vertical axis representing the concentration. By default, $t_s$ is the last time step. Total displayed dimensions corresponds to the domain parameters defined in the data class ModelParameters.
Plotting can be performed either as class method, i.e. model_object.plume_3d(), or by calling the function with the model object as argument, i.e. plume_3d(model_object). Keyword arguments for plt.plot_surface (on which plume_3d is based) can be passed on through the function.
# Plot the x and y concentration distribution for no degradation decay model, uses plot_surface
ana_nodecay.plume_3d(time=13*365)
plt.title("Contaminant plume with no degradation Anatrans model, t = 6 years")
plt.show()
# Function passes plot_surface keyword arguments
mbt.plume_3d(ana_lineardecay, cmap="summer")
plt.title("Contaminant plume with linear degradation Anatrans model, t = 6 years")
plt.show()
Adopting view point of plot¶
Functionality of plume_3d allows modification to the view point of the plot similarly to plt.plot_surface. Call the function and store the returned ax object. Adaptions are performed on ax as outlined below. See the documentation of plt.plot_surface for more information.
ax = ana_instant.plume_3d(cmap="cividis")
plt.title("Contaminant plume with instant reaction Anatrans model, t = 20 years")
ax.view_init(elev=15, azim=340)
plt.show()
Animate plots¶
All plots have the option to be animated, producing an animation that can be saved as file. Multiple models can be combined in a single animation, but all models should have the same spatial and temporal discretization.
Animations unfortunately do not work well in Jupyter Notebooks, in combination with other plots. For examples how to make animations in mibitrans, see animate_plume_plots.py
Mass balance¶
The mibitrans package provides the feature to calculate mass balances, specifically mass within the source and mass within the plume.
It operates as method mass_balance on the results object of the model of choice (in our example we use the results of the Mibitrans model with the instantaneous reaction model, mbt_instant.) Each of the mass balance elements is a class property.
Examples of mass balance elements are:
- plume mass for given point in time
mb.plume_mass - source mass for given time
mb.source_mass - contaminant mass that has left the source zone
mb.delta_source - degraded mass over time
mb.degraded_mass - change in electron acceptor mass due to biodegradation in instantaneous reaction model
mb.electron_acceptor_change
# Get mass balance for a specified point in time
mb = mbt_lineardecay.mass_balance(time=20*365)
print(mb.plume_mass)
29503.69549555226
Call the mass balance object to change the time of the mass balance. If time is set to all (or call is left empty), mass balance will instead show all time steps.
mb(time="all")
print(mb.plume_mass)
[ 3076.12967052 5644.61636817 8022.43249021 10234.99779054 12292.42641138 14201.93383247 15970.41712074 17605.14628399 19113.79759038 20504.27534983 21784.50444156 22962.25733435 24045.02946442 25039.95846977 25953.77689881 26792.78141974 27562.77585663 28268.88862074 28915.13155769 29503.69549555]
These mass balance elements can be used for various plots
mb_nd = mbt_nodecay.mass_balance()
plt.plot(mb.t, mb.plume_mass, color="green", label=r"$m_{plume}$ decay")
plt.plot(mb.t, mb_nd.plume_mass, color="blue", label=r"$m_{plume}$ no decay")
plt.plot(mb.t, mb.degraded_mass, color="red", label=r"$m_{deg}$ decay")
plt.plot(mb.t, mb.plume_mass + mb.degraded_mass, linestyle="--", label=r"$m_{deg} + m_{plume}$ decay", color="orange")
plt.xlabel("Time [days]")
plt.ylabel("Contaminant mass [g]")
plt.title("Temporal change of degraded and plume mass for no decay and linear decay models")
plt.legend()
plt.show()
Here, the entire plume fits neatly in the model domain. However, the mass balance does not give representative results if the contaminant plume does not fit in the model domain (i.e. at the boundaries of the domain, concentrations are more than 1% of the maximum initial source concentration), since the mass balance is only calculated inside the model domain. In such case, creating the mass balance object will display a warning.
Sources¶
Domenico, P. A., An analytical model for multidimensional transport of a decaying contaminant species, Journal of Hydrology, 91 (1), 49–58, doi:10.1016/0022-1694(87)90127-2, 1987.
Karanovic, M., C. J. Neville, and C. B. Andrews, BIOSCREEN-AT: BIOSCREEN with an exact analytical solution, Groundwater, 45 (2), 242–245, doi:10.1111/j.1745-6584.2006.00296.x, 2007.
Newell, C. J., R. K. McLeod, and J. R. Gonzales, BIOSCREEN Natural Attenuation Decision Support System User’s Manual Version 1.3, Tech. Rep. EPA/600/R-96/087, US EPA, 1996.
Newell, C. J., R. K. McLeod, and J. R. Gonzales, BIOSCREEN natural attenuation decision support system version 1.4 revisions, Tech. rep., U.S. EPA, 1997.
Wexler, E. J., Analytical solutions for one-, two-, and three-dimensional solute transport in ground-water systems with uniform flow, Tech. Rep. 03-B7, U.S. G.P.O. ; doi:10.3133/twri03B7, 1992.