Registers a new math function *function* with *name* and returns an :py:class:`Operation`
        instance. A math function expects a :py:class:`Number` as its first argument, followed by
        optional (keyword) arguments. When *name* is *None*, the name of the *function* is used. The
        returned object can be used to set the derivative (similar to *property*). Example:

        .. code-block:: python

            @ops.register
            def my_op(x):
                return x * 2 + 1

            @my_op.derive
            def my_op(x):
                return 2

            num = Number(5, 2)
            print(num) # -> 5.00 (+2.00, -2.00)

            num = ops.my_op(num)
            print(num) # -> 11.00 (+4.00, -4.00)

        Please note that there is no need to register *simple* functions as in the particular
        example above as most of them are just composite operations whose derivatives are already
        known.

def register(cls, function=None, name=None):
        """
        Registers a new math function *function* with *name* and returns an :py:class:`Operation`
        instance. A math function expects a :py:class:`Number` as its first argument, followed by
        optional (keyword) arguments. When *name* is *None*, the name of the *function* is used. The
        returned object can be used to set the derivative (similar to *property*). Example:

        .. code-block:: python

            @ops.register
            def my_op(x):
                return x * 2 + 1

            @my_op.derive
            def my_op(x):
                return 2

            num = Number(5, 2)
            print(num) # -> 5.00 (+2.00, -2.00)

            num = ops.my_op(num)
            print(num) # -> 11.00 (+4.00, -4.00)

        Please note that there is no need to register *simple* functions as in the particular
        example above as most of them are just composite operations whose derivatives are already
        known.
        """
        def register(function):
            op = Operation(function, name=name)

            @functools.wraps(function)
            def wrapper(num, *args, **kwargs):
                if op.derivative is None:
                    raise Exception("cannot run operation '{}', no derivative registered".format(
                        op.name))

                # ensure we deal with a number instance
                num = ensure_number(num)

                # apply to the nominal value
                nominal = op.function(num.nominal, *args, **kwargs)

                # apply to all uncertainties via
                # unc_f = derivative_f(x) * unc_x
                x = abs(op.derivative(num.nominal, *args, **kwargs))
                uncertainties = {}
                for name in num.uncertainties.keys():
                    up, down = num.get_uncertainty(name)
                    uncertainties[name] = (x * up, x * down)

                # create and return the new number
                return num.__class__(nominal, uncertainties)

            # actual registration
            cls._instances[op.name] = op
            setattr(cls, op.name, staticmethod(wrapper))

            return op

        if function is None:
            return register
        else:
            return register(function)