boxsimu is a simple modelling framework based on the principle of a system of discrete boxes interacting with each other. boxsimu offers a simple interface to define a system, its components and properties. The system definition is done by instantiating several classes of the boxsimu package like Fluid
, Variable
, Flow
, Flux
, Process
, Reaction
, and Box
. The temporal evolution of a system defined in boxsimu can then easily be simulated and the result of the simulation can be visualized or further investigated.
After a short primer on what mathematical modelling is and how boxsimu fits in there, this tutorial will lead you through the installation of boxsimu. After the installion, it will briefly explain the basic structure of a system-definition in boxsimu. After that, you should have a basic understanding of the most important classes of boxsimu and you're ready to dive into a first example!
In order to understand the dynamics of a system, it is often useful to create a (mathematical) model thereof. A system can be almost anything, e.g. a mountain lake, the Atlantic ocean, the atmosphere, a machine, a company, or even the entire Earth System. A (mathematical) model represents such a system while neglecting a big amount of complexity. Thus a (mathematical) model tries to explain the fundamental system behaviour as simply as possible.
Schwarzenbach et al. (2003) uses the following defintion of a system:
''A model is an imitation of reality that stresses those aspects that are assumed to be important and omits all properties considered to be nonessential''.
Mathematical modelling of a system is a challenging and complex activity. Depending on the system an analytical solution can even be nonexistent. As a consequence one is forced to simulate/solve such systems numerically. This is where boxsimu comes into play:
boxsimu allows the user to easily define systems of simple to intermediate complexity and to simulate their temporal evolution. All that can be done on a high level of abstraction, meaning the user doesn't have to bother about the representation of the system with computer code.
At the current status of development of boxsimu, the numerical solver is not very efficient. As a consequence large and complexe systems with a lot of variables, boxes, and processes/reactions will be solved slowly.
Python: boxsimu was only tested with Python 3.6. Therefore boxsimu may not work as expected with older versions of Python.
The following python packages are required by boxsimu:
Scientifc packages:
Other packages:
All of the above listed python package are available in the official python repository and thus can be installed using pip. Thus, if a package is missing on your system just use:
pip install <package name>
Additionally an up-to-date C compiler like gcc is needed, since some computational critical parts of boxsimu are written in Cython, which has to be compiled into binary code on your computer.
boxsimu can easily be installed using pip. On your console type in
pip install boxsimu
this should automatically compile all Cython files and copy all source files into the right directory. If the installation fails check if all of the above mentioned dependencies are met.
Alternatively to the installation via pip, boxsimu can also be installed from source. For this, download the most recent source code from github {{URL}} or clone the repository. Afterwards open a system console and change into the directory where you downloaded boxsimu. Then execute the following commands:
\$ cd boxsimu <br> \$ python setup.py
A system is defined by instantiating the class BoxModelSystem and passing instances of the classes Box, Flow, and Flux to it. The instance of the class BoxModelSystem contains all information on the whole system while its components (Boxes, Flows, and Fluxes) store information on the distribution of Fluids and Variables in the system and how the different compartements (boxes) exchange these quantities. The basic structure of a BoxModelSystem instance is graphically shown below:
In this diagram an instance of BoxModelSystem is shown that contains two boxes: 'Box 1' and 'Box 2'. Both boxes contain the same fluid ('Fluid 1') and two instances of the class Variable ('A' and 'B'). Additionally both boxes can contain an arbitrary number of independet processes and reactions of these variables. Finally, the boxes exchange mass of the variables and the fluid via fluxes and flows (the difference between a flow and a flux will be explained further down).
A BoxModelSystem can contain an arbitrary number of boxes, however, the more boxes there are the slower the system's simulation will progress. Similarly the number of fluxes, flows, and variables, processes, reactions within boxes is only limited in regard of computational power. In contrast, every box can only contain zero or one instance of the class Fluid.
The most important class that a user of BoxSimu is interacting with are:
A system is most easily defined following these steps:
Given this first fundamental understanding of boxsimu classes and their sequence of appearence within a system-definition, we jump directly into a first example!
Our first system consists of a freshwater lake that only has one inflow and one outflow. We want to simulate how the concentration of phosphate in this lake evolves over time. In order to do that we assume the inflow to have a constant concentration of phosphate (PO4) while the outflow has the same concentration of PO4 as the (well-mixed) lake itself. The volume/mass of lake-water is constant over time.
In the following we have a simple depiction of the lake system and important parameters thereof:
Before we use boxsimu to simulate this system we can solve the govering equations of this system analytically in order to validate the output of boxsimu.
So lets start with the defintion of all needed variables and their physical units:
Variables (symbols are consistent with the figure above):
$t$ = Time [d]
$k_w$ = $\frac{Q}{V}$ = Specific flow rate [1/d]
Assumptions:
Based on the definition of our system given above we can set up the following differential equation:
$\frac{dm}{dt} = \sum sources - \sum sinks = Q \cdot C_{in} - Q \cdot C$.
The rate of change of the phosphate concentration in the lake is given by the sum of all source terms minus the sum of all sink terms. In this case the only source term is the inflow of phosphate from the river upstream, while the only sink term is the outflow of phosphate through the river downstream.
Where $Q \cdot C_{in}$ and $Q \cdot C$ represents the phosphate-gain and phosphate-loss of the lake per time, respectively.
Next, we devide both sides by the volume of the lake ($V$) and use the specific flow rate $k_w$ on the right hand side (r.h.s.):
$\frac{1}{V}\frac{dm}{dt} = k_w \cdot C_{in} - k_w \cdot C = k_w (C_{in} - C)$
Now, since the volume of the box is constant, we can incorporate the volume into the time-derivative and end up with:
$\frac{1}{V}\frac{dm}{dt} = \frac{d(m/V)}{dt} = \frac{dC}{dt} = k_w (C_{in} - C)$
The solution of this linear, inhomogene, ordinary differential equation is:
$C(t) = (C_0 - C_{in}) e^{-k_wt} + C_{in}$
where we also used the initial condition $C(t=0) = C_0$.
The solution is plotted below for a system with the following parameters:
We define a function that calculates and returns the concentration of phosphate at a time $t$. We also vectorize this function using numpy in order to be able to apply it on arrays. The resulting array is then plotted as a function of time:
In [5]:
import matplotlib.pyplot as plt
import numpy as np
@np.vectorize
def C(t):
V = 1e7
Q = 1e5
C0 = 1e-2
Cin = 3e-1
kw = Q/V
return (C0-Cin)*np.exp(-kw*t) + Cin
t = np.linspace(0, 8e2, 1000)
c_phosphate = C(t)
plt.plot(t, c_phosphate)
plt.xlabel('Time [days]')
plt.ylabel('PO4 concentration [kg/m^3]')
Out[5]:
We can see that the system reaches a steady-state after about $400$ days.
Now we want to use boxsimu to simulate this system...
Since boxsimu accepts some quantities only in certain units (dimensionalities) we first have to calculate the system parameters in the right dimensionalities/units:
Now we define these parameters as python variables. But before we can do that we have to import boxsimu (if not already happened) and the instance of pint.UnitRegistry that is used by boxsimu (boxsimu.ur).
If you use boxsimu you have to use the UnitRegistry from boxsimu, since different UnitRegistry instances are incompatible. Thus use
from boxsimu import urinstead of importing the UnitRegistry directly from the pint libary.
In [7]:
import boxsimu
from boxsimu import ur
Now you are able to specify the units of a python variable by simply multiplying by pint units:
In [8]:
length = 3 * ur.meter
Pint units can easily be transformed to other units of the same physical dimensionality:
In [9]:
length.to(ur.km) # transforms length to kilometer
Out[9]:
Now lets define the system parameters calculated above:
In [10]:
m_water = 1e10 * ur.kg
m_0 = 1e5 * ur.kg
flow_rate = 1e8 * ur.kg / ur.day
Next we define the boxsimu system! In the following you see the complete model defintion in boxsimu:
In [11]:
# FLUIDS
freshwater = boxsimu.Fluid('freshwater', rho=1000*ur.kg/ur.meter**3)
# VARIABLES
po4 = boxsimu.Variable('po4')
# PROCESSES
# No processes in this system
# REACTIONS
# No reactions in this system
# BOXES
lake = boxsimu.Box(
name='lake',
description='Little Lake',
fluid=freshwater.q(m_water),
variables=[po4.q(m_0)],
)
# FLOWS
inflow = boxsimu.Flow(
name='Inflow',
source_box=None,
target_box=lake,
rate=flow_rate,
tracer_transport=True,
concentrations={po4: 3e-1 * ur.gram / ur.kg},
)
outflow = boxsimu.Flow(
name='Outflow',
source_box=lake,
target_box=None,
rate=flow_rate,
tracer_transport=True,
)
# FLUXES
# No fluxes in this system
# BOXMODELSYSTEM
system = boxsimu.BoxModelSystem(
name='lake_system',
description='Simple Lake Box Model',
boxes=[lake,],
flows=[inflow, outflow,],
)
We will go through this code line by line. But before we dive into the details, have a quick look at the sequence of definitions of boxsimu classes:
In a first step, instances of the classes Fluid and Variable are instantiated. A fluid represents the solvent of a box in which variables can be dissolved (e.g. the fluid of a lake is water, in which a myrade of chemicals can be dissolved). A box can, but doesn't have to be filled with a fluid. This allows to also simulate quantites that are not typically dissolved (e.g. to simulate a population of animals). Variables are quantities of the system that are of interest to the user and that are simulated. A variable defined for one box, flow, flux, process, or reaction will automatically be created for all boxes.
At the time of instantiation (creation of the instance) instances of the classes Fluid and Variable are not quantified, that means the instances contain no information about the mass of fluid or variable they represent. However, before they are passed to an instance of the class Box they need to be quantified. This is done by calling the method ''q(mass)'' for a fluid or variable.
In our simple lake system the fluids and variables are:
Next, if needed, the classes Process and Reaction are instantiated (created). However, in this simple lake system there are no processes and no reactions.
The created instances of Fluid, Variable, Process, and Reaction are then assigned to instances of the class Box that represent a compartement of the system (see lines 14-19 in the code).
In a next step we have to define how the boxes in our system interact with each other and with the environement (outside the system). That means how the boxes are exchaning mass of fluids and variables. Therefore we instantiate the classes Flow and Flux (see lines 22-40 in the code). The difference between a flow and a flux is that flows represent a transport of fluid that again can (but doesn't have to) passively transport dissolved variables, whereas fluxes represent a transport of variables that is not associated with any fluid transport.
Finally we create an instance of the class BoxModelSystem that glues together all elements of the system (see lines 43-46 in the following code).
Lets go through this code line by line:
Line 2:
freshwater = boxsimu.Fluid('freshwater', rho=1000*ur.kg/ur.meter**3)
An instance of the class Fluid
, representing the freshwater of the lake, is created.
The class Fluid
has the following signature:
Fluid(name, rho, description=None)
(For further information on the class Fluid
confront the documentation: DOCUMENTATION_URL)
Line 5:
po4 = boxsimu.Variable('po4')
An instance of the class Variable
is instantiated (created). This variable represents the substance we are interested in: phosphate. Every system needs to define at least one variable since its the temporal evolution of these variables that is simulated with the boxsimu-solver.
The class Variable
has the following signature:
Variable(name, molar_mass=None, mobility=True, description=None)
Line 14-19:
lake = boxsimu.Box(
name='lake',
name_long='Little Lake',
fluid=freshwater.q(m_water),
variables=[po4.q(m_0)],
)
An instance of the class Box
is instantiated (created). A box represents a compartement (region/part of the system which is somehow separated or at least distinguishable from the rest of the system.
The class Box
has the following signature:
Box(name, description, fluid=None, condition=None,
variables=None, processes=None, reactions=None)
Fluid
or None. Specifies the fluid that is present within a box. If None is given, the box is assumed to not contain a fluid at all.boxsimu.Condition
. Specifies the condition within the box (e.g. the pH, the temperature, and more many more properties of a box that remain constant).Variable
. Specifies the variables that are present within the box. The given variables must be quantified - that means a variable's quantified (alias q) must have been called.Process
. Specifies the processes that are running within the box.Reaction
. Specifies the reactions that are running within the box.Line 22-37:
inflow = boxsimu.Flow(
name='Inflow',
source_box=None,
target_box=lake,
rate=flow_rate,
tracer_transport=True,
concentrations={po4: 3e-4},
)
outflow = boxsimu.Flow(
name='Outflow',
source_box=lake,
target_box=None,
rate=flow_rate,
tracer_transport=True,
)
Two instances of the class Flow
are instantiated. A flow represents a transport of a fluid from one box to another, or an exchange of fluid-mass with the environment of the system (the ''the outside''). The first flow (inflow
) represents a river that is flowing into the lake, and has an associated phosphate concentratin of $3\cdot10^{-4}\frac{kg}{kg}$. The second flow represent the outflow of the lake.
The class Flow
has the following signature:
Flow(name, source_box, target_box, rate,
tracer_transport=True, concentrations={}):
Box
or None. Specifies where the flow originates. If source_box is set to None, the flow is assumed to come from outside the system.Box
or None. Specifies where the flow ends. If target_box is set to None, the flow is assumed to go outside the system.Line 43-48:
system = boxsimu.BoxModelSystem(
name='lake_system',
description='Simple Lake Box Model',
boxes=[lake,],
flows=[inflow, outflow,],
)
An instance of the class BoxModelSystem
is instantiated. A BoxModelSystem represents the system that will be simulated with boxsimu.
The class BoxModelSystem
has the following signature:
BoxModelSystem
The system defined in boxsimu can now easily be solved using the solve method of the BoxModelSystem class:
In [13]:
sol = system.solve(800*ur.day, 1*ur.day)
The simulation output is accessible as a pandas dataframe: Solution.df
. In our example we can retrieve the model output as follows:
In [18]:
sol.df[:10]
Out[18]:
Example: The first 10 timesteps of the po4 mass in the box lake can be accessed by:
In [27]:
sol.df[('lake', 'po4')].head(10)
Out[27]:
The output can now be visualized using several methods of the class Solution
. In the following just a few examples are given.
In [29]:
sol.plot_variable_concentration(po4)
In [ ]:
In [ ]:
In [2]:
from IPython.core.display import HTML
#import urllib2
#HTML( urllib2.urlopen('http://bit.ly/1Bf5Hft').read() )
HTML(open('../costum_jupyter_look.css', 'r').read())
Out[2]:
In [ ]: