Skip to content
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Run Tests

on:
push:
branches: [ "master" ]
branches: [ "main", "master" ]
pull_request:
branches: [ "master" ]
branches: [ "main", "master" ]

jobs:
build:
Expand Down
40 changes: 40 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
Change Log
===================

Unreleased 4.x
--------------

Changes:

- Includes the `main` branch in continuous integration automation.
- [**BREAKING**] Previously when tallying the uncertainty for a `UFloat` object, the
contribution from other `UFloat` objects with `std_dev == 0` were excluded. Now this
special casing for `std_dev == 0` has been removed so that the contribution from all
contributing `UFloat` objects is included. This changes the behavior in certain
corner cases where `UFloat` `f` is derived from `UFloat` `x`, `x` has `x.s == 0`, but
the derivative of `f` with respect to `x` is `NaN`. For example, previously
`(-1)**ufloat(1, 0)` gave `-1.0+/-0`. The justification for this was that the second
`UFloat` with `std_dev` of `0` should be treated like a regular float. Now the same
calculation returns `-1.0+/-nan`. In this case the `UFloat` in the second argument
of the power operator is treated as a degenerate `UFloat`.

Removes:

- [**BREAKING**] Removes certain deprecated `umath` functions and
`AffineScalarFunc`/`UFloat` methods. The following `umath` functions are removed:
`ceil`, `copysign`, `fabs`, `factorial`, `floor`, `fmod`, `frexp`, `ldexp`, `modf`,
`trunc`. The following `AffineScalarFunc`/`UFloat` methods are removed:
`__floordiv__`, `__mod__`, `__abs__`, `__trunc__`, `__lt__`, `__le__`, `__gt__`,
`__ge__`, `__bool__`.
- [**BREAKING**] Removes the `uncertainties.unumpy.matrix` class and the corresponding
`umatrix` constructor function. The `unumpy_to_numpy_matrix` function is also
removed. Various `unumpy` functions have dropped support for matrix compatibility.
- [**BREAKING**] Previously it was possible for a `UFloat` object to compare equal to a
`float` object if the `UFloat` `standard_deviation` was zero and the `UFloat`
`nominal_value` was equal to the `float`. Now, when an equality comparison is made
between a `UFloat` object and another object, if the object is not a `UFloat` then
the equality comparison is deferred to this other object. For the specific case of
`float` this means that the equality comparison always returns `False`.
- [**BREAKING**] The `uncertainties` package is generally dropping formal support for
edge cases involving `UFloat` objects with `std_dev == 0`.
- [**BREAKING**] Previously if a negative `std_dev` was used to construct a `UFloat`
object a custom `NegativeStdDev` exception was raised. Now a standard `ValueError`
exception is raised.

Unreleased
----------

Expand Down
14 changes: 14 additions & 0 deletions doc/formatting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ the common exponent.
An uncertainty which is *exactly* **zero** is always formatted as an
integer:

.. doctest::
:hide:

>>> import warnings
>>> original_filters = warnings.filters[:]
>>> warnings.filterwarnings("ignore", message=".*std_dev==0")


>>> print(ufloat(3.1415, 0))
3.1415+/-0
>>> print(ufloat(3.1415e10, 0))
Expand All @@ -170,6 +178,12 @@ integer:
>>> print('{:.2f}'.format(ufloat(3.14, 0.00)))
3.14+/-0


.. doctest::
:hide:

>>> warnings.filters = original_filters

**All the digits** of a number with uncertainty are given in its
representation:

Expand Down
43 changes: 3 additions & 40 deletions doc/numpy_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,41 +76,6 @@ through NumPy, thanks to NumPy's support of arrays of arbitrary objects:

>>> arr = np.array([ufloat(1, 0.1), ufloat(2, 0.002)])

.. index::
single: matrices; creation and manipulation
single: creation; matrices

Matrices
^^^^^^^^

.. warning::
``unumpy.umatrix`` is deprecated and will be removed in Uncertainties 4.0.

Matrices of numbers with uncertainties are best created in one of
two ways. The first way is similar to using :func:`uarray`:

>>> mat = unumpy.umatrix([1, 2], [0.01, 0.002])

Matrices can also be built by converting arrays of numbers with
uncertainties into matrices through the :class:`unumpy.matrix` class:

>>> mat = unumpy.matrix(arr)

:class:`unumpy.matrix` objects behave like :class:`numpy.matrix`
objects of numbers with uncertainties, but with better support for
some operations (such as matrix inversion). For instance, regular
NumPy matrices cannot be inverted, if they contain numbers with
uncertainties (i.e., ``numpy.matrix([[ufloat(…), …]]).I`` does not
work). This is why the :class:`unumpy.matrix` class is provided: both
the inverse and the pseudo-inverse of a matrix can be calculated in
the usual way: if :data:`mat` is a :class:`unumpy.matrix`,

>>> print(mat.I)
[[0.19999999999999996+/-0.012004265908417718]
[0.3999999999999999+/-0.01600179989876138]]

does calculate the inverse or pseudo-inverse of :data:`mat` with
uncertainties.

.. index::
pair: nominal value; uniform access (array)
Expand All @@ -120,14 +85,11 @@ uncertainties.
Uncertainties and nominal values
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Nominal values and uncertainties in arrays (and matrices) can be
directly accessed (through functions that work on pure float arrays
too):
Nominal values and uncertainties in arrays can be directly accessed (through functions
that work on pure float arrays too):

>>> unumpy.nominal_values(arr)
array([1., 2.])
>>> unumpy.std_devs(mat)
matrix([[0.1 , 0.002]])


.. index:: mathematical operation; on an array of numbers
Expand Down Expand Up @@ -263,6 +225,7 @@ numbers with uncertainties, the **matrix inverse and pseudo-inverse**:

>>> print(unumpy.ulinalg.inv([[ufloat(2, 0.1)]]))
[[0.5+/-0.025]]
>>> mat = np.array([[ufloat(1, 0.1), ufloat(2, 0.002)]])
>>> print(unumpy.ulinalg.pinv(mat))
[[0.19999999999999996+/-0.012004265908417718]
[0.3999999999999999+/-0.01600179989876138]]
Expand Down
140 changes: 35 additions & 105 deletions doc/tech_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,107 +93,47 @@ are completely uncorrelated.
.. _comparison_operators:


Comparison operators
--------------------
Equality Comparison
-------------------

Numbers with uncertainty, :class:`UFloat` objects, model random variables.
There are a number of senses of equality for two random variables.
The stronger senses of equality define two random variables to be equal if the two
random variables always produce the same result given a random sample from the sample
space.
For :class:`UFloat`, this is the case if two :class:`UFloat` objects have equal
nominal values and standard deviations and are perfectly correlated.
We can test for these conditions by taking the difference of two :class:`UFloat` objects
and looking at the nominal value and standard deviation of the result.
If both the nominal value and standard deviation of the difference are zero, then the
two :class:`UFloat` objects have the same nominal value, standard deviation, and are
perfectly correlated.
In this case we say the two :class:`UFloat` are equal.

.. warning::
Support for comparing variables with uncertainties is deprecated and will be
removed in Uncertainties 4.0. The behavior of ``bool`` will also be changed
to always return ``True`` for ``UFloat`` objects.

Comparison operations (>, ==, etc.) on numbers with uncertainties have
a **pragmatic semantics**, in this package: numbers with uncertainties
can be used wherever Python numbers are used, most of the time with a
result identical to the one that would be obtained with their nominal
value only. This allows code that runs with pure numbers to also work
with numbers with uncertainties.

.. index:: boolean value

The **boolean value** (``bool(x)``, ``if x …``) of a number with
uncertainty :data:`x` is defined as the result of ``x != 0``, as usual.

However, since the objects defined in this module represent
probability distributions and not pure numbers, comparison operators
are interpreted in a specific way.

The result of a comparison operation is defined so as to be
essentially consistent with the requirement that uncertainties be
small: the **value of a comparison operation** is True only if the
operation yields True for all *infinitesimal* variations of its random
variables around their nominal values, *except*, possibly, for an
*infinitely small number* of cases.

Example:

>>> x = ufloat(3.14, 0.01)
>>> x == x
>>> x = ufloat(1, 0.1)
>>> a = 2 * x
>>> b = x + x
>>> print(a - b)
0.0+/-0
>>> print(a == b)
True

because a sample from the probability distribution of :data:`x` is always
equal to itself. However:
It might be the case that two random variables have the same marginal probability
distribution but are uncorrelated.
A weaker sense of equality between random variables may consider two such random
variables to be equal.
This is equivalent to two :class:`UFloat` objects have equal nominal values and
standard deviations, but, are uncorrelated.
The :mod:`uncertainties` package, however, keeps to the stronger sense of random
variable equality such that two such :class:`UFloat` objects do not compare as equal.

>>> y = ufloat(3.14, 0.01)
>>> x == y
>>> x = ufloat(1, 0.1)
>>> y = ufloat(1, 0.1)
>>> print(x - y)
0.00+/-0.14
>>> print(x == y)
False

since :data:`x` and :data:`y` are independent random variables that
*almost* always give a different value (put differently,
:data:`x`-:data:`y` is not equal to 0, as it can take many different
values). Note that this is different
from the result of ``z = 3.14; t = 3.14; print(z == t)``, because
:data:`x` and :data:`y` are *random variables*, not pure numbers.

Similarly,

>>> x = ufloat(3.14, 0.01)
>>> y = ufloat(3.00, 0.01)
>>> x > y
True

because :data:`x` is supposed to have a probability distribution largely
contained in the 3.14±~0.01 interval, while :data:`y` is supposed to be
well in the 3.00±~0.01 one: random samples of :data:`x` and :data:`y` will
most of the time be such that the sample from :data:`x` is larger than the
sample from :data:`y`. Therefore, it is natural to consider that for all
practical purposes, ``x > y``.

Since comparison operations are subject to the same constraints as
other operations, as required by the :ref:`linear approximation
<linear_method>` method, their result should be essentially *constant*
over the regions of highest probability of their variables (this is
the equivalent of the linearity of a real function, for boolean
values). Thus, it is not meaningful to compare the following two
independent variables, whose probability distributions overlap:

>>> x = ufloat(3, 0.01)
>>> y = ufloat(3.0001, 0.01)

In fact the function (x, y) → (x > y) is not even continuous over the
region where x and y are concentrated, which violates the assumption
of approximate linearity made in this package on operations involving
numbers with uncertainties. Comparing such numbers therefore returns
a boolean result whose meaning is undefined.

However, values with largely overlapping probability distributions can
sometimes be compared unambiguously:

>>> x = ufloat(3, 1)
>>> x
3.0+/-1.0
>>> y = x + 0.0002
>>> y
3.0002+/-1.0
>>> y > x
True

In fact, correlations guarantee that :data:`y` is always larger than
:data:`x`: ``y-x`` correctly satisfies the assumption of linearity,
since it is a constant "random" function (with value 0.0002, even
though :data:`y` and :data:`x` are random). Thus, it is indeed true
that :data:`y` > :data:`x`.


.. index:: linear propagation of uncertainties
.. _linear_method:

Expand Down Expand Up @@ -257,16 +197,6 @@ This indicates that **the derivative required by linear error
propagation theory is not defined** (a Monte-Carlo calculation of the
resulting random variable is more adapted to this specific case).

However, even in this case where the derivative at the nominal value
is infinite, the :mod:`uncertainties` package **correctly handles
perfectly precise numbers**:

>>> umath.sqrt(ufloat(0, 0))
0.0+/-0

is thus the correct result, despite the fact that the derivative of
the square root is not defined in zero.

.. _math_def_num_uncert:

Mathematical definition of numbers with uncertainties
Expand Down
34 changes: 0 additions & 34 deletions doc/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -216,40 +216,6 @@ uncertainty of 0.
>>> (x -y)
0.0+/-0.7071067811865476


Comparisons of magnitude
------------------------------------

The concept of comparing the magnitude of values with uncertainties is a bit
complicated. That is, a Variable with a value of 25 +/- 10 might be greater
than a Variable with a value of 24 +/- 8 most of the time, but *sometimes* it
might be less than it. The :mod:`uncertainties` package takes the simple
approach of comparing nominal values. That is

>>> a = ufloat(25, 10)
>>> b = ufloat(24, 8)
>>> a > b
True

Note that combining this comparison and the above discussion of `==` and `!=`
can lead to a result that maybe somewhat surprising:


>>> a = ufloat(25, 10)
>>> b = ufloat(25, 8)
>>> a >= b
False
>>> a > b
False
>>> a == b
False
>>> a.nominal_value >= b.nominal_value
True

That is, since `a` is neither greater than `b` (nominal value only) nor equal to
`b`, it cannot be greater than or equal to `b`.


.. index::
pair: testing (scalar); NaN

Expand Down
3 changes: 3 additions & 0 deletions tests/test_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def test_PDG_precision():
assert formatting.PDG_precision(std_dev) == result


@pytest.mark.filterwarnings("ignore:.*std_dev==0")
def test_small_float():
"""
Make sure that very small floats do not error, even though printing as str
Expand All @@ -39,6 +40,7 @@ def test_small_float():
str(ufloat(b, 0.0))


@pytest.mark.filterwarnings("ignore:.*std_dev==0")
def test_repr():
"""Test the representation of numbers with uncertainty."""

Expand Down Expand Up @@ -457,6 +459,7 @@ def test_repr():
]


@pytest.mark.filterwarnings("ignore:.*std_dev==0")
@pytest.mark.parametrize("val, std_dev, fmt_spec, expected_str", formatting_cases)
def test_format(val, std_dev, fmt_spec, expected_str):
"""Test the formatting of numbers with uncertainty."""
Expand Down
Loading
Loading