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:

pip install reno-sd

To install from conda-forge:

conda install conda-forge::reno-sd

The module itself is called reno and is simply imported as:

import reno

Defining a model#

Create a model by instantiating the 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.

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 components.

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:

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.

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 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).

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 model.graph() function.

tub.graph()

The stock and flow diagram of this model looks like:

../_images/tub_sf_diagram.png

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 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.

../_images/tub_latex_example.png

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 model.get_docs() function. This docstring shows how to configure and run the model, discussed in the following section.

>>> 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 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:

results = tub()

To run five instances in parallel for a longer time and different configuration:

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.

>>> print(results)
<xarray.Dataset> 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 reno.ops.List, which simply iterates which item is selected for each sample, making it easier to quickly test multiple variable values:

>>> 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:

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