Metadata-Version: 2.4
Name: auto_uncertainties
Version: 1.2.0
Summary: Linear Uncertainty Propagation with Auto-Differentiation
Author-email: Varchas Gopalaswamy <vgop@lle.rochester.edu>, Ethan Mentzer <ementzer@u.rochester.edu>
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Requires-Python: >=3.15
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=2.0
Requires-Dist: jax>=0.9
Requires-Dist: jaxlib>=0.9
Requires-Dist: pydantic-settings<3,>=2.0
Provides-Extra: docs
Requires-Dist: sphinx>=4.1.2; extra == "docs"
Requires-Dist: sphinx_rtd_theme>=1.0.0; extra == "docs"
Requires-Dist: sphinxcontrib-bibtex>=2.3.1; extra == "docs"
Requires-Dist: sphinxcontrib-napoleon>=0.7; extra == "docs"
Requires-Dist: sphinx-autodoc-typehints; extra == "docs"
Requires-Dist: sphinx-copybutton; extra == "docs"
Requires-Dist: sphinx-design; extra == "docs"
Requires-Dist: graphviz; extra == "docs"
Requires-Dist: sphinx-autoapi; extra == "docs"
Requires-Dist: furo; extra == "docs"
Dynamic: license-file


# AutoUncertainties

[![Static Badge](https://img.shields.io/badge/GitHub-AutoUncertainties-blue?logo=github&labelColor=black)](https://github.com/varchasgopalaswamy/AutoUncertainties)
[![GitHub Release](https://img.shields.io/github/v/release/varchasgopalaswamy/AutoUncertainties?label=Current%20Release&color)](https://github.com/varchasgopalaswamy/AutoUncertainties/releases)
[![python](https://img.shields.io/badge/Python-3.15%2B-ffed57?logo=python&logoColor=white)](https://www.python.org/downloads/)
[![JOSS Paper](https://joss.theoj.org/papers/d357e888e33e56df674e15c82b82dcac/status.svg)](https://joss.theoj.org/papers/d357e888e33e56df674e15c82b82dcac)

AutoUncertainties is a package that makes handling linear uncertainty propagation for scientific applications 
straightforward and automatic using auto-differentiation.

* View the [full documentation here](https://autouncertainties.readthedocs.io/en/latest/). 

* If you find this package useful, please consider citing it!
```
@article{gopalaswamy2025autouncertainties,
  title={AutoUncertainties: A Python Package for Uncertainty Propagation},
  author={Gopalaswamy, Varchas and Mentzer, Ethan},
  journal={Journal of Open Source Software},
  volume={10},
  number={111},
  pages={8037},
  year={2025}
}
```
  
## Statement of Need

AutoUncertainties is a Python package for uncertainty propagation of independent random variables. 
It provides a drop-in mechanism to add uncertainty information to Python scalar and NumPy array 
objects. It implements manual propagation rules for the Python dunder math methods, and uses automatic 
differentiation via JAX to propagate uncertainties for most NumPy methods applied to both scalar and 
NumPy array variables. In doing so, it eliminates the need for carrying around additional uncertainty 
variables or for implementing custom propagation rules for any NumPy operator with a gradient rule 
implemented by JAX. In most cases, it requires minimal modification to existing code—typically only 
when uncertainties are attached to central values.

One of the most important aspects of AutoUncertainties is its seamless support for NumPy:

```python
import numpy as np
from auto_uncertainties import Uncertainty

vals = np.array([0.5, 0.75])
errs = np.array([0.05, 0.3])
u = Uncertainty(vals, errs)
print(np.cos(u))  # [0.878 +/- 0.024, 0.73 +/- 0.20]
```

This is in contrast to the `uncertainties` package, which would have required the use of `unumpy`,
a module containing several hand-implemented analogs of the true NumPy functions. 


## Supported Features

- [x] Scalars
- [x] Arrays, with support for most NumPy ufuncs and functions


## Prerequisites

Python 3.15 or newer is required. Core numerical dependencies are:

* `jax >= 0.9`
* `jaxlib >= 0.9`
* `numpy >= 2.0`

The installer selects versions compatible with your Python version.


## Installation

To install, simply run:

```
pip install auto-uncertainties
```


## Build Documentation

To build the documentation locally, clone the repository, create a virtual Python environment 
(if desired), and run the following commands within the repository directory:

```bash
pip install -e .[docs]
sphinx-build docs/source docs/build
```

Once built, the docs can be found under the `docs/build` subdirectory.


## CI and Unit Testing

Development of AutoUncertainties relies on a series of unit tests located in the `tests` directory. These
are automatically run using Woodpecker when commits are pushed to Codeberg. To run the tests
manually, sync the development dependency group with `uv`:

```bash
uv sync --locked --group ci
uv run --locked --group ci pytest --cov --cov-report=term
```

Run the same lint, formatting, type, and generated-export checks used by CI:

```bash
uv run --locked --group ci ruff check .
uv run --locked --group ci ruff format --check .
uv run --locked --group ci pyrefly check
uv run --locked --group ci geninit --check
```

Package initializers are generated by `geninit` from module `__all__` declarations,
using Python 3.15 native lazy imports. Run `uv run --locked --group ci geninit`
after changing exports; generated initializers should not be edited manually.
The pre-commit hooks use these same tools and locked dependency versions.
Static checking is configured in [pyrefly.toml](pyrefly.toml).


## Basic Usage

* Creating a scalar `Uncertainty` variable is relatively simple:

  ```python
  from auto_uncertainties import Uncertainty

  value = 1.0
  error = 0.1
  u = Uncertainty(value, error)
  print(u)  # 1.00 +/- 0.10
  ```
  
  As is creating a NumPy array of Uncertainties:

  ```python
  from auto_uncertainties import Uncertainty
  import numpy as np

  value = np.linspace(start=0, stop=10, num=5)
  error = np.ones_like(value) * 0.1
  u = Uncertainty(value, error)
  print(u)  # [0.00 +/- 0.10, 2.50 +/- 0.10, 5.00 +/- 0.10, 7.50 +/- 0.10, 10.00 +/- 0.10]
  ```

  The `Uncertainty` class automatically determines which methods should be implemented based on 
  whether it represents a vector uncertainty, or a scalar uncertainty. When instantiated with
  sequences or NumPy arrays, vector-based operations are enabled; when instantiated with scalars,
  only scalar operations are permitted. 

* Scalar uncertainties implement all mathematical and logical 
  [dunder methods](https://docs.python.org/3/reference/datamodel.html#object.__repr__>) explicitly using linear
  uncertainty propagation.

  ```python
  from auto_uncertainties import Uncertainty

  u = Uncertainty(10.0, 3.0)
  v = Uncertainty(20.0, 4.0)
  print(u + v)  # 30 +/- 5
  ```

* Array uncertainties implement a large subset of the NumPy ufuncs and methods using
  JAX automatic differentiation.

  ```python
  from auto_uncertainties import Uncertainty
  import numpy as np

  value = np.linspace(start=0, stop=10, num=5)
  error = np.ones_like(value) * 0.1
  u = Uncertainty(value, error)
  print(np.exp(u))
  # [1.00 +/- 0.10, 12.2 +/- 1.2, 148 +/- 15, 1.81e+3 +/- 1.8e+2, 2.20e+4 +/- 2.2e+3]

  print(np.sum(u))  # 25.00 +/- 0.22
  print(u.sum())  # 25.00 +/- 0.22
  print(np.sqrt(np.sum(error**2)))  # 0.223606797749979
  ```

* The central value, uncertainty, and relative error are available as attributes:

  ```python
  from auto_uncertainties import Uncertainty

  u = Uncertainty(10.0, 3.0)
  print(u.value)  # 10.0
  print(u.error)  # 3.0
  print(u.relative)  # 0.3
  ```

* To strip central values and uncertainty from arbitrary variables, accessor functions `nominal_values`
  and `std_devs` are provided:

  ```python
  from auto_uncertainties import nominal_values, std_devs

  u = Uncertainty(10.0, 3.0)
  v = 5.0
  print(nominal_values(u))  # 10.0
  print(std_devs(u))  # 3.0

  print(nominal_values(v))  # 5.0
  print(std_devs(v))  # 0.0
  ```

* Displayed values follow the Particle Data Group (PDG) rounding rules by default.
  Use `configure(round_on_display=False)` to disable PDG rounding and display values
  with the `g` format specifier:

  ```python
  from auto_uncertainties import Uncertainty, configure
  import numpy as np

  value = np.linspace(start=0, stop=10, num=5)
  error = np.ones_like(value) * 0.1
  u = Uncertainty(value, error)
  print(np.sum(u))  # 25.00 +/- 0.22
  configure(round_on_display=False)  # disable PDG rounding
  print(np.sum(u))  # 25 +/- 0.223607
  configure(round_on_display=True)  # restore the default
  ```

  PDG rounding displays `Uncertainty` objects with:
    - Error to one or two significant digits, following the PDG thresholds.
    - Central value to the same decimal place as the rounded error.

  Rounding affects display only; `.value` and `.error` retain their stored precision.

* If `numpy.array` is called on an `Uncertainty` object, it will automatically get cast down to a 
  numpy array (losing all uncertainty information!), and emit a warning. To force an exception to be raised
  instead, use `configure`:

  ```python
  from auto_uncertainties import Uncertainty, configure
  import numpy as np

  configure(error_on_downcast=True)
  value = np.linspace(start=0, stop=10, num=5)
  error = np.ones_like(value) * 0.1
  u = Uncertainty(value, error)
  print(np.array(u))
  # Traceback (most recent call last):
  #     ...
  # auto_uncertainties.exceptions.DowncastError: The uncertainty is stripped when downcasting to ndarray.
  ```
  

## Propagating Through Custom Functions

Use `@propagate_uncertainties` (or `@propagate_uncertainties()`) to propagate errors
through a JAX-compatible function. The function receives nominal values and should
use `jax.numpy` for array operations. Uncertain inputs can be positional arguments,
keyword arguments, or leaves of nested lists, tuples, and dictionaries. Outputs can
have different shapes from the inputs and can use the same nested structures.

```python
import jax.numpy as jnp
from auto_uncertainties import Uncertainty, propagate_uncertainties


@propagate_uncertainties
def scale_and_sum(values, *, gain):
    scaled = values * gain
    return {"scaled": scaled, "total": jnp.sum(scaled)}


result = scale_and_sum(Uncertainty([1.0, 2.0], [0.1, 0.2]), gain=Uncertainty(2.0, 0.05))
print(result["scaled"])  # [2.00 +/- 0.21, 4.0 +/- 0.4]
print(result["total"])  # 6.0 +/- 0.5
```

The decorator uses the full Jacobian to combine independent input errors for each
output element. Reusing an input within the function is accounted for by
differentiation; correlations between separate returned values are not stored.
Keeping a complete calculation inside the decorated function preserves the shared
effect of inputs such as `gain` when computing its total.

Functions that mix array elements work too, and tuple outputs preserve their structure:

```python
@propagate_uncertainties()
def center_and_mean(values):
    mean = jnp.mean(values)
    return values - mean, mean


centered, mean = center_and_mean(Uncertainty([1.0, 2.0], [0.1, 0.2]))
print(centered)  # [-0.50 +/- 0.11, 0.50 +/- 0.11]
print(mean)  # 1.50 +/- 0.11
```

Each centered value depends on both inputs. Its error is
`sqrt(0.1**2 + 0.2**2) / 2`, approximately `0.1118`, as is the mean's error.
Correlations are not retained between separate calls or subsequent operations on
returned objects.

Dtypes follow JAX promotion. Float64 inputs enable JAX's 64-bit mode locally for
the call, restoring the caller's setting afterward. Results use NumPy-backed
containers, so the decorated wrapper itself cannot be used inside `jax.jit`.

## Current Limitations and Future Work

### Dependent Random Variables

To simplify operations on `Uncertainty` objects, `AutoUncertainties` assumes all variables are independent. 
This means that, in the case where the programmer assumes dependence between two or more `Uncertainty` 
objects, unexpected and counter-intuitive behavior may arise during uncertainty propagation. This is a common 
pitfall when working with `Uncertainty` objects, especially since the package will not prevent you from 
manipulating variables in a manner that implies dependence.

* **Subtracting Equivalent Uncertainties**

  Subtracting an `Uncertainty` from itself will not result in a standard deviation of zero:

  ```python
  x = Uncertainty(5.0, 0.5)
  print(x - x)  # 0.0 +/- 0.7
  ```

* **Mean Error Propagation**

  When multiplying a vector by a scalar `Uncertainty` object, each component of the resulting vector
  is assumed to be a multivariate normal distribution with no covariance,
  which may not be the desired behavior. For instance, taking the mean of such a
  vector will return an `Uncertainty` object with an unexpectedly small standard deviation.

  ```python
  u = Uncertainty(5.0, 0.5)
  arr = np.ones(10) * 10
  print(np.mean(u * arr))  # 50.0 +/- 1.6, rather than 50 +/- 5 as expected
  ```
  
  To obtain the uncertainty corresponding to the case where each element of the array is fully correlated,
  two workaround techniques can be used:

  1. Separate the central value from the relative error, multiply the vector by the central value, take the mean
     of the resulting vector, and then multiply by the previously stored relative error.

     ```python
     u = Uncertainty(5.0, 0.5)
     scale_error = Uncertainty(1, u.relative)  # collect relative error
     scale_value = u.value  # collect central value

     arr = np.ones(10) * 10
     print(np.mean(scale_value * arr) * scale_error)  # 50 +/- 5
     ```

  2. Take the mean of the vector, and then multiply by the `Uncertainty`:

     ```python
     u = Uncertainty(5.0, 0.5)
     arr = np.ones(10) * 10
     print(u * np.mean(arr))  # 50 +/- 5
     ```

These workarounds are nevertheless cumbersome, and cause `AutoUncertainties` to fall somewhat short of the original
goals of automated error propagation. In principle, this could be addressed by storing a full computational
graph of the result of chained operations, similar to what is done in `uncertainties`. However, the complexity
of such a system places it out of scope for `AutoUncertainties` at this time.

It should be noted that, in cases where random variables have covariance that lies somewhere between 
fully correlated and fully independent, calculations like those described above would be more complex.
To accurately propagate uncertainty, one would need to specify individual correlations between each 
variable, and adjust the computation as necessary. This is also currently out of scope for `AutoUncertainties`.


## Inspirations

The class structure of `Uncertainty` and the NumPy ufunc implementation is heavily inspired by the 
excellent package [Pint](https://github.com/hgrecco/pint).

## Configuration

Settings are initialized from environment variables with the `AUTO_UNCERTAINTIES_`
prefix when the configuration module is first imported. Environment variable names
are case-insensitive.

| Setting | Default | Environment variable |
| --- | --- | --- |
| `round_on_display` | `True` | `AUTO_UNCERTAINTIES_ROUND_ON_DISPLAY` |
| `error_on_downcast` | `False` | `AUTO_UNCERTAINTIES_ERROR_ON_DOWNCAST` |
| `error_on_equality` | `False` | `AUTO_UNCERTAINTIES_ERROR_ON_EQUALITY` |
| `compare_rtol` | `1e-9` | `AUTO_UNCERTAINTIES_COMPARE_RTOL` |
| `error_on_numpy_kwargs` | `True` | `AUTO_UNCERTAINTIES_ERROR_ON_NUMPY_KWARGS` |
| `warn_on_numpy_kwargs` | `True` | `AUTO_UNCERTAINTIES_WARN_ON_NUMPY_KWARGS` |
| `nonfinite_error_policy` | `"zero"` | `AUTO_UNCERTAINTIES_NONFINITE_ERROR_POLICY` |

`error_on_numpy_kwargs` and `warn_on_numpy_kwargs` are retained for configuration
compatibility and no longer affect propagation. Uncertain keyword arguments are supported.

For example, set these variables before starting Python:

```bash
export AUTO_UNCERTAINTIES_NONFINITE_ERROR_POLICY=propagate
export AUTO_UNCERTAINTIES_ROUND_ON_DISPLAY=true
```

Inspect or update configuration at runtime:

```python
from auto_uncertainties import Uncertainty, configure, get_settings

configure(nonfinite_error_policy="propagate", round_on_display=True)
print(get_settings().nonfinite_error_policy)  # propagate
u = Uncertainty(1.0, float("nan"))
print(u)  # nan +/- nan
```


The default `"zero"` policy replaces NaN and infinite errors with zero. Under `"propagate"`, NaN in either value or error sets
both to NaN, element by element for arrays. Positive infinite errors are preserved;
negative errors, including negative infinity, raise `NegativeStdDevError`.

## Releases

To publish a release, push an annotated version tag such as `v1.1.0` to Codeberg.
Woodpecker runs tests on Python 3.15, then builds and validates the
wheel and source distribution before uploading them to PyPI using the CI secret
`pypi_token`. Branch pushes and pull requests run tests without publishing.
The publishing workflow can also be triggered manually.
