Tutorial 3: Formulas

In the last tutorial we created a calculation that used data and outputs as arguments in formulas that resulted in new output values. In this tutorial we’ll create the formulas that are used in calculations.

Formulas

Formulas are functions or equations that take input arguments and return values. Carousel currently supports formulas that are written in Python as function definitions or strings that can be evaluated by the Python numexpr package. For the PV system power example, we will use formulas written as Python functions. To add the formulas we need for this example create a Python package in our project package called formulas, don’t forget to add __init__.py to make it a package, and copy the following code into a Python module called utils.py inside the formulas folder, i.e.: PVPower/pvpower/formulas/utils.py.

# -*- coding: utf-8 -*-

"""
This module contains formulas for calculating PV power.
"""

import numpy as np
from scipy import constants as sc_const
import itertools
from dateutil import rrule


def f_energy(ac_power, times):
    """
    Calculate the total energy accumulated from AC power at the end of each
    timestep between the given times.

    :param ac_power: AC Power [W]
    :param times: times
    :type times: np.datetime64[s]
    :return: energy [W*h] and energy times
    """
    dt = np.diff(times)  # calculate timesteps
    # convert timedeltas to quantities
    dt = dt.astype('timedelta64[s]').astype('float') / sc_const.hour
    # energy accumulate during timestep
    energy = dt * (ac_power[:-1] + ac_power[1:]) / 2
    return energy, times[1:]


def groupby_freq(items, times, freq, wkst='SU'):
    """
    Group timeseries by frequency. The frequency must be a string in the
    following list: YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY or
    SECONDLY. The optional weekstart must be a string in the following list:
    MO, TU, WE, TH, FR, SA and SU.

    :param items: items in timeseries
    :param times: times corresponding to items
    :param freq: One of the ``dateutil.rrule`` frequency constants
    :type freq: str
    :param wkst: One of the ``dateutil.rrule`` weekday constants
    :type wkst: str
    :return: generator
    """
    timeseries = zip(times, items)  # timeseries map of items
    # create a key lambda to group timeseries by
    if freq.upper() == 'DAILY':
        def key(ts_): return ts_[0].day
    elif freq.upper() == 'WEEKLY':
        weekday = getattr(rrule, wkst.upper())  # weekday start
        # generator that searches times for weekday start
        days = (day for day in times if day.weekday() == weekday.weekday)
        day0 = days.next()  # first weekday start of all times

        def key(ts_): return (ts_[0] - day0).days // 7
    else:
        def key(ts_): return getattr(ts_[0], freq.lower()[:-2])
    for k, ts in itertools.groupby(timeseries, key):
        yield k, ts


def f_rollup(items, times, freq):
    """
    Use :func:`groupby_freq` to rollup items

    :param items: items in timeseries
    :param times: times corresponding to items
    :param freq: One of the ``dateutil.rrule`` frequency constants
    :type freq: str
    """
    rollup = [np.sum(item for __, item in ts)
              for _, ts in groupby_freq(items, times, freq)]
    return np.array(rollup)

Formulas can use any code or packages necessary. However here are a couple of conventions that may be helpful.

  • keep formulas short
  • name formulas after the main output preceeded by f_ - Carousel can be configured to search for functions with this prefix
  • use NumPy arrays for arguments so uncertainty and units are propagated
  • document functions verbosely
  • group related formulas together in the same module or file

Formula Class

We’ll use the same performance.py module again that we used in the previous tutorials to add these formulas to our model. We’ll need to import Formula and FormulaParameter. Then we’ll list the formulas as class attributes and their attributes, like args and units, as formula parameter arguments. Finally we put the module and package where we import the corresponding Python functions from in a nested Meta class. Note that the formulas have the same names as the Python functions.

from carousel.core.formulas import Formula, FormulaParameter


class UtilityFormulas(Formula):
    """
    Formulas for PV Power demo
    """
    f_daterange = FormulaParameter()
    f_energy = FormulaParameter(
        args=["ac_power", "times"],
        units=[["watt_hour", None], ["W", None]]
    )
    f_rollup = FormulaParameter(
        args=["items", "times", "freq"],
        units=["=A", ["=A", None, None]]
    )

    class Meta:
        module = ".utils"
        package = "pvpower.formulas"

Formula Attributes

All of the formulas and formula attributes are defined as class attributes using formula parameters. If formula attributes are provided as positional arguments, the order is given in the table below, but keyword arguments can be passed to FormulaParameter in any order.

Attribute Description
islinear flag to indicate linear vs nonlinear formulas [not used]
args list of names of input arguments
units list of return value and input argument units for the Pint method wraps
isconstant list of arguments that don’t have any covariance

Formula Importers

Formulas can be written as Python functions or as strings that are evaluated using the Python numexpr package. Carousel uses FormulaImporter to create callable objects from the formulas specified by the formula class. The formula importer can be specified as a Meta class option in the formula class using formula_importer, otherwise the default is PyModuleImporter.

Python Module Importer

If formulas are written in Python and use the default FormulaImporter for Python modules, PyModuleImporter, then we need to specify the path, package, and module that contains the function definitions. This information is specified for the entire formula class in it’s Meta class options. If the module is in a package, then the full namespace of the module can be specified or the relative module name and the package. If the module or its package are on the Python path, then that’s enough to import the formulas. Otherwise specify the path to the module or package as well.

from carousel.core.formulas import Formula, PyModuleImporter


class Utils(Formula):
    class Meta:
        formula_importer = PyModuleImporter
        module = '.utils'  # relative module name
        package = 'pvpower.formulas'  # module package
        path = 'examples/PVPower'  # path to package if not on PYTHONPATH


class Irradiance(Formula):
    class Meta:
        formula_importer = PyModuleImporter
        module = 'irradiance'  # module name
        package = None # no package
        path = 'examples/PVPower/pvpower/formulas'  # path to module


class Performance(formulas.Formula):
    class Meta:
        formula_importer = PyModuleImporter
        module = 'pvpower.formulas.performance'  # module name with package
        package = None
        path = 'examples/PVPower'  # path to package
Meta Class Option Description
formula_importer FormulaImporter subclass that can import functions
module name of the module containing formulas as Python functions
package package containing Python functions used as formulas
path path to folder containing formulas module or package

The formulas should be given as individual formula parameters. If there are no formula parameters in the formula class then any function preceded with f_ in the module specified in the Meta class options will be imported as a formula, and arguments will be inferred using inspect.getargspec() but no units or uncertainty will be propagated, and Carousel will log an AttributeError as a warning.

Numerical Expressions Importer

Formulas can be written as string expressions that are evaluated using the Python numexpr package. These formulas are specified by passing the string as the expression argument, a list of the arguments as args, and any other desired formula attributes like units or isconstant to FormulaParameter and setting the formula_importer in the Meta class options to NumericalExpressionImporter. For example, the following formula contains a numerical expression for the Pythagorean theorem with arguments a and b, output units that match whatever the input units are, and propagates uncertainty for all arguments, ie: nothing is constant

class PythagoreanFormula(Formula):
    """
    Formulas to calculate the hypotenuse of a right triangle.
    """
    class Meta:
        formula_importer = NumericalExpressionImporter

    f_hypotenuse = FormulaParameter(
        expression='sqrt(a * a + b * b)',
        args=['a', 'b'],
        units=[('=A', ), ('=A', '=A', None, None)],
        isconstant=[]
    )

Units and Uncertainty

Carousel uses Pint, a Python package that converts and validates units. Pint provides a wrapper that checks and converts specified units of function arguments going into a function and then applies the desired units to the return values. The units are stripped from the arguments passed to the original function so it doesn’t impose any additional constraints or increase computation time. Specify the arguments for the Pint wrapper in the units formula attribute. If units attribute is None or missing, then Carousel does not wrap the formula.

Warning

Carousel is incompatible with Pint-0.8, please downgrade to v0.7.2, see Caramel Corn CONSTANTS (v0.3.1) for more details.

Carousel uses UncertaintyWrapper to propagate uncertainty across formulas. Uncertainties are specified in the data which will be discussed in the next tutorial. In order to propagate uncertainty correctly, especially for multiple argument, multiple return value or vectorized calculations, the return value may need to be reshaped so that it is a 2-dimensional NumPy array with the number of return values on the first axis and the number of observations on the second axis.

For more detail about when and how formulas should be adjusted for units and uncertainty wrappers, take a look at the examples in Tutorial 3: More Detail on Units and Uncertainty

Arguments

The Formula class actually determines the order of positional arguments using the Python Standard Library inspect module, but you can explicitly state the arguments by passing the args attribute to the formula parameter. This can be useful if the function has *args or **kwargs, for example if the function is wrapped and the wrapped function has *args or **kwargs. If using the numerical expression importer, then you must provide the positional arguments in order.

Sensitivity

The uncertainty wrapper also calculates the sensitivity of each function to its inputs. Set the isconstant attribute to a list of the terms to exclude from the Jacobian. If isconstant is missing or None then the sensitivity will not be calculated and therefore the uncertainty will not be propagated. To include all inputs set isconstant=[].

Note

To include propagate uncertainty for all inputs, set isconstant=[].