def calculate_uncertainty(terms, rho=0.):
    """
    Generically calculates the uncertainty of a quantity that depends on multiple *terms*. Each term
    is expected to be a 2-tuple containing the derivative and the uncertainty of the term.
    Correlations can be defined via *rho*. When *rho* is a numner, all correlations are set to this
    value. It can also be a mapping of a 2-tuple, the two indices of the terms to describe, to their
    correlation coefficient. In case the indices of two terms are not included in this mapping, they
    are assumed to be uncorrelated. Example:

    .. code-block:: python

        calculate_uncertainty([(3, 0.5), (4, 0.5)])
        # uncorrelated
        # -> 2.5

        calculate_uncertainty([(3, 0.5), (4, 0.5)], rho=1)
        # fully correlated
        # -> 3.5

        calculate_uncertainty([(3, 0.5), (4, 0.5)], rho={(0, 1): 1})
        # fully correlated
        # -> 3.5

        calculate_uncertainty([(3, 0.5), (4, 0.5)], rho={(1, 2): 1})
        # no rho value defined for pair (0, 1), assumes zero correlation
        # -> 2.5
    """
    # sum over squaresall single terms
    variance = sum((derivative * uncertainty)**2. for derivative, uncertainty in terms)

    # add second order terms of all pairs
    for i in range(len(terms) - 1):
        for j in range(i + 1, len(terms)):
            _rho = rho.get((i, j), 0.) if isinstance(rho, dict) else rho
            variance += 2. * terms[i][0] * terms[j][0] * _rho * terms[i][1] * terms[j][1]

    return variance**0.5