.. _tolerances: Computations with tolerances ============================ .. note:: If you're a developer who wants to find out how to write a nonexact function or class, see :ref:`The developer's guide`. In many cases, computations can not be carried out in an exact way and we have to work with tolerances. For example, for some computations, we might want to consider numbers that are small but not equal to 0 to actually be 0. In general, we follow the absolute/relative tolerance concept familiar from e.g. numpy, but we add some of our own mechanisms to handle tolerances in a unified way. Namely, we have a mechanism to *globally* set defaults for these tolerances. Tolerances generally appear in the following ways and follow these conventions: - Functions can have arguments ``atol`` and ``rtol`` that are used for computations inside the function. - Classes can have attributes ``atol`` and ``rtol`` that are used for all computations internal to the class. - If a function is passed one main object with tolerance attributes, it can use its attributes as tolerances instead of having tolerance parameters itself. - If a function gets *multiple* (equal priority) objects with tolerance attributes and doesn't have its own tolerance parameters, it generally uses the *maximum* of the tolerances of the objects. The following sections explain how the global defaults work in these contexts. Accessing the global tolerance defaults --------------------------------------- The global tolerance defaults can be accessed through the module :py:mod:`ddg.nonexact` using the function :py:func:`~ddg.nonexact.get_tol_defaults()`. They can be changed with a context manager or functions in the module. The safest way to change the defaults is the context manager :py:class:`ddg.nonexact.TolDefaults`. It can be used to temporarily set defaults so you don't have to worry about resetting them later. For example: .. doctest:: >>> import ddg >>> ddg.nonexact.get_tol_defaults() {'atol': 1e-07, 'rtol': 0.0} >>> with ddg.nonexact.TolDefaults(atol=1e-6): ... ddg.nonexact.get_tol_defaults() ... {'atol': 1e-06, 'rtol': 0.0} >>> ddg.nonexact.get_tol_defaults() # Context has been exited {'atol': 1e-07, 'rtol': 0.0} To permanently alter tolerance defaults, use the functions :py:func:`~ddg.nonexact.set_tol_defaults()` and :py:func:`~ddg.nonexact.reset_tol_defaults()`: .. doctest:: >>> ddg.nonexact.get_tol_defaults() {'atol': 1e-07, 'rtol': 0.0} >>> ddg.nonexact.set_tol_defaults(rtol=0.01) >>> ddg.nonexact.get_tol_defaults() {'atol': 1e-07, 'rtol': 0.01} >>> ddg.nonexact.reset_tol_defaults() >>> ddg.nonexact.get_tol_defaults() {'atol': 1e-07, 'rtol': 0.0} These functions essentially just set global variables. In the other sections, we explain where and how these global defaults are used. In classes ---------- Classes that use the global tolerances internally if their attributes ``atol`` and ``rtol`` are set to ``None`` will mention this in their documentation. Here is an example with the :py:class:`.Subspace` class: .. doctest:: >>> from ddg.geometry.subspaces import subspace_from_affine_points as sfap >>> p = sfap([0, 1]) >>> q = sfap([0, 1 + 1e-9]) >>> print(p.atol) None >>> print(p.rtol) None >>> p == q True >>> p.atol = 1e-13 >>> p == q False Note that some classes could provide their own default for ``atol`` / ``rtol`` upon initialization. If this is the case and you want your object to use the global defaults, you have to explicitly set ``atol`` or ``rtol`` to ``None``. In functions ------------ Functions that take keyword arguments ``atol`` and/or ``rtol`` behave in a similar way to classes, as explained in the last section: If ``None`` is given for one of these arguments, the function will use the global default internally. Functions that use the global defaults in this way will mention this in their docstring. For this example, we use the function :py:func:`ddg.math.projective.dehomogenize`: .. doctest:: >>> import numpy as np >>> import ddg >>> import ddg.math.projective as pmath >>> v = np.array([1.0, 2.0, 3.0, 1e-6]) >>> pmath.dehomogenize(v) array([1000000., 2000000., 3000000.]) >>> ddg.nonexact.set_tol_defaults(atol=1e-5) >>> pmath.dehomogenize(v) Traceback (most recent call last): ... ValueError: Given point is at infinity. >>> pmath.dehomogenize(v, atol=1e-7) array([1000000., 2000000., 3000000.]) Again, note that some functions provide their own tolerance defaults that are hand-picked for that particular function. If you want the function to use the global defaults, you have to explicitly pass ``atol=None`` / ``rtol=None`` in that case. .. Reset everything for later tests .. doctest:: :hide: >>> import ddg >>> ddg.nonexact.reset_tol_defaults() >>> ddg.nonexact.get_tol_defaults() {'atol': 1e-07, 'rtol': 0.0}