Getting Started
###############
Installation
============
Reno's package name is ``reno-sd`` (``reno`` was already taken) and is available
on both `PyPI `__ and `conda-forge
`__.
To install via ``pip``:
.. code-block:: bash
pip install reno-sd
To install from conda-forge:
.. code-block:: bash
conda install conda-forge::reno-sd
The module itself is called ``reno`` and is simply imported as:
.. code-block:: python
import reno
Defining a model
================
Create a model by instantiating the :py:class:`reno.Model ` class,
optionally providing a name, simulation sample settings (``steps`` - how many
timesteps to run each sample for, and ``n`` - the number of samples to run in
parallel), and an optional ``doc`` description of the model.
.. code-block:: python
import reno
tub = reno.Model()
# or alternatively with more detail:
tub = reno.Model("tub", steps=30, doc="Model water flowing in/out of a bathtub")
Note that ``steps`` and ``n`` can be modified when the model is run, this simply
sets the defaults.
Adding stocks, flows, and variables to the model can be done by directly setting attributes on
the model to instantiated :ref:`components `.
.. code-block:: python
t = reno.TimeRef() # TimeRefs are variables that always equal current timestep
# make a user-controllable variable for flow rate
tub.faucet_flow_rate = reno.Variable(6.0)
# make in and out flows
tub.faucet = reno.Flow(tub.faucet_flow_rate)
tub.drain = reno.Flow(reno.sin(t) * 2 + 4)
# make a stock to represent the accumulation of water
tub.water_level = reno.Stock()
# hook up the in and out flows to the stock
tub.water_level += tub.faucet
tub.water_level -= tub.drain
Note that since components may need to reference other components that haven't
been created yet, the equations for flows and variables can be defined
separately from instantiation by setting the ``.eq`` attribute:
.. code-block:: python
tub.faucet = reno.Flow()
tub.faucet_flow_rate = reno.Variable()
tub.faucet.eq = tub.faucet_flow_rate + 3
tub.faucet_flow_rate.eq = 5
For more info on how equations in Reno work and how to construct them, see TODO
(math page)
Model ``with`` blocks
---------------------
It can be annoying to add a lot of components to a model, especially if the
model has a long variable name. Models can be used as context managers, and so
can be used in ``with`` blocks (similar to how PyMC models are conventionally
defined.) Any components defined within a model's ``with`` block are automatically added to the
model using the components' variable names when the context manager exits.
.. code-block:: python
import reno
my_long_model_name = reno.Model()
with my_long_model_name:
faucet_rate = reno.Variable(6.0)
facuet = reno.Flow(faucet_rate + 3)
drain = reno.Flow(7.0)
water_level = reno.Stock()
drain.max = water_level
faucet >> water_level >> drain
# my_long_model_name now has component attributes like previous examples:
# my_long_model_name.drain
Note that the ``>>`` or ``<<`` syntax as shown in the example above can be used
to simplify hooking up stock inflows and outflows, see
:ref:`Defining stock equations` for more details.
Inspecting a model
==================
The methods discussed below will be based on this example (which can
also be found in the [LINK] notebook).
.. code-block:: python
import reno
t = reno.TimeRef()
tub = reno.Model("tub", doc="Model the amount of water in a bathtub based on a drain and faucet rate")
with tub:
faucet, drain = reno.Flow(), reno.Flow()
water_level = reno.Stock()
faucet_off_time = reno.Variable(5, doc="Timestep to turn the faucet off in the simulation.")
faucet >> water_level >> drain
# the faucet should be some waterflow amount until the faucet is turned
# off, so we use a piecewise operation to make a conditional based on time
faucet.eq = reno.Piecewise([5, 0], [t < faucet_off_time, t >= faucet_off_time])
drain.eq = reno.sin(t) + 2
# the drain can't move negative water, and can't drain more than exists
# in the tub
drain.min = 0
drain.max = water_level
Stock and flow diagrams
-----------------------
Once a model is created, there are a few different ways to see what it looks
like. A stock and flow diagram is the easiest way to see how everything is
connected in the model, and can be generated using the :py:func:`model.graph() `
function.
.. code-block:: python
tub.graph()
The stock and flow diagram of this model looks like:
.. figure:: ../_static/tub_sf_diagram.png
:align: center
In these diagrams, the rectangular boxes represent stocks, labels between arrows represent
flows, and the green rounded boxes represent variables. The heavy solid arrows
represent stock in/out flows, while dashed and dotted lines indicate which
references are used in which other references.
Latex equations
---------------
A latex version of the equations for all of the stocks, flows, and variables can
be viewed with the :py:func:`model.latex() ` function.
By default this outputs (when running in Jupyter) an interactive widget with the
latex equations, and when clicking on any line, the reference name for that equation
line is highlighted everywhere else in the other equations. (This makes it
easier to track down where variables are used in very large systems.)
To get just a string version of the latex, pass ``raw_str=True``.
.. figure:: ../_static/tub_latex_example.png
:align: center
Model docstring
---------------
Models and every reference you add to models can be provided a ``doc``
attribute, describing what the reference is/how to use it. All of this
information for a whole model can be compiled into a single Python-like
docstring using the :py:func:`model.get_docs() `
function. This docstring shows how to configure and run the model, discussed in
the following section.
.. code-block::
>>> print(tub.get_docs())
Model the amount of water in a bathtub based on a drain and faucet rate
Example:
tub(faucet_off_time=5, water_level_0=None)
Args:
faucet_off_time: Timestep to turn the faucet off in the simulation.
water_level_0
Running a model
===============
Once a model is defined, it's time to run some simulations!
A model can be called like a function, with parameters for any free variables in
the system (including any initial values for stocks) and optionally
run-specific parameters such as the number of timesteps (``steps``) and the
number of instances to run in parallel (``n``).
In the model we defined above, there's one free variable (``faucet_off_time``) and
a stock initial value, ``water_level_0`` that can be set. (These can also be
found by running :py:func:`tub.free_refs() `, which
returns a list of string names for the free variables/initial values.
Passing values for any of these are optional, the model will rely on values
provided during definition if none are provided in the call itself.
To run one instance of the tub model with all default values, use:
.. code-block:: python
results = tub()
To run five instances in parallel for a longer time and different configuration:
.. code-block:: python
results2 = tub(n=5, steps=100, faucet_off_time=40, water_level_0=10.0)
The return from a simulation run is an XArray dataset, containing the values of
every stock/flow/variable at each timestep.
.. code-block::
>>> print(results)
Size: 336B
Dimensions: (sample: 1, step: 10)
Coordinates:
* sample (sample) int64 8B 0
* step (step) int64 80B 0 1 2 3 4 5 6 7 8 9
Data variables:
water_level (sample, step) float64 80B 0.0 5.0 7.159 ... 10.45 7.457
faucet (sample, step) int64 80B 5 5 5 5 5 0 0 0 0 0
drain (sample, step) float64 80B 0.0 2.841 2.909 ... 2.989 2.412
faucet_off_time (sample) int64 8B 5
Attributes:
faucet_off_time: Scalar(5)
water_level_0: 0
As seen above, variables that are static values (e.g. ``faucet_off_time``) don't
include the step dimension, since they don't change over time. A copy of the values
of each free variable in that run's configuration are included in the
``Attributes`` section of the output.
Running with distributions
--------------------------
TODO: this possibly belongs on the math page instead (wanted to introduce early so
the viz made more sense)
Running a model thus far with an ``n``/samples more than 1 hasn't made much sense since
these are deterministic - each sample should run the exact same way. Samples come
into play when distributions are used in variables, which are randomly drawn from
for each sample (and optionally each timestep.) The simplest "distribution"
(which isn't techncially a distribution) is :py:class:`reno.ops.List`, which
simply iterates which item is selected for each sample, making it easier to
quickly test multiple variable values:
.. code-block::
>>> tub.final_water_level = reno.Metric(tub.water_level.timeseries[-1])
>>> results = tub(n=3, faucet_off_time=reno.List([2, 4, 6]))
>>> results.final_water_level.values
array([ 0. , 2.456909, 12.456909])
Looking at the water level in the final timestep is now different in each
sample, corresponding to the different faucet off time in each simulation.
Actually random distributions are currently available through:
* :py:class:`reno.ops.Normal`
* :py:class:`reno.ops.Uniform`
* :py:class:`reno.ops.DiscreteUniform`
* :py:class:`reno.ops.Bernoulli`
* :py:class:`reno.ops.Categorical`
TODO: example with normal
Much more can be done with this, discussed further on the TODO: link bayes page.
Visualizing results
===================
TODO: links to the rest of the user guide