8.17. Solvers and random numbers¶
When the function under study is defined by Python code
rather than a buffer of samples, a different family of
tools is the right call: where is the function’s root,
its minimum, its integral over a given interval? The
scipy.integrate and scipy.optimize
submodules cover that work. Each algorithm calls back
into the user-supplied Python function, so the
per-iteration cost is higher than a buffer reduction;
the convenience is in not having to write the solver.
The scipy.special submodule covers the
statistical special functions (error function, gamma)
that come up when computing a probability distribution’s
cumulative distribution function (CDF, the probability
that a sample is at most a given value) or probability
density function (PDF, the relative likelihood at a
given value). numpy.random covers the
pseudo-random generator for dithering, simulation, and
synthetic test data.
8.17.1. Numerical integration of a callable¶
When the integrand is a Python function rather than a
buffer of samples, scipy.integrate exposes four
quadrature algorithms:
quad()– adaptive Gauss-Kronrod. The right default for smooth integrands. Returns(value, error).romberg()– classical Romberg / Newton-Cotes. Returns a single float. Deprecated upstream; included for compatibility.simpson()– adaptive Simpson’s rule. Returns a single float.tanhsinh()– double-exponential quadrature. Use when the integrand has endpoint singularities or an infinite limit. Returns(value, error).
The Gaussian integral evaluated with the
double-exponential rule (tanhsinh):
from math import exp
from math import pi
from math import sqrt
from ulab import numpy as np
from ulab import scipy as sp
f = lambda x: exp(-x * x)
value, err = sp.integrate.tanhsinh(f, -np.inf, np.inf)
print("approx:", value, " exact:", sqrt(pi))
Output:
approx: 1.7724538... exact: 1.7724538...
8.17.2. Root finding and minimisation¶
scipy.optimize covers three classic single-variable
solvers. Each iteration calls back into the user-supplied
Python function, so the speedup over a pure-Python solver
is modest (roughly 2x); the convenience is in not having
to write the solver.
bisect()– find a root offon[a, b]by halving the interval.f(a)andf(b)must have opposite signs:def f(x): return x * x - 1 sp.optimize.bisect(f, 0, 4) # ~1.0
newton()– find a root using secant / Newton-Raphson iteration:def f(x): return x * x * x - 2.0 sp.optimize.newton(f, 3., tol=0.001, rtol=0.01) # ~1.260
fmin()– find a local minimum using the downhill-simplex (Nelder-Mead) method:def f(x): return (x - 1) ** 2 - 1 sp.optimize.fmin(f, 3.0) # ~1.0
The single-variable scope is enough for most camera-side optimisations – a sensor’s calibration constant, the gain that maximises a contrast measurement, the threshold where a histogram bimodality is sharpest. For multi-variable problems, the right answer is usually to re-formulate the problem as a small linear-algebra solve rather than reach for a general nonlinear optimiser.
8.17.3. Special functions¶
scipy.special exposes a handful of statistical and
probability functions that behave like universal
functions – they accept a scalar, an iterable, or an
ndarray and return a float
ndarray:
x = np.linspace(0, 4, num=8)
sp.special.erf(x) # error function
sp.special.erfc(x) # complementary error function
sp.special.gamma(x + 1) # gamma function
sp.special.gammaln(x + 1) # log-gamma function
The error function and its complement appear in the CDF
of a Gaussian – the application of choice for converting
between a measured z-score and a probability or for
computing the tail integral of a normal distribution. The
gamma and log-gamma functions show up in beta /
chi-squared / student-t calculations; gammaln is the
numerically stable form for large arguments where
gamma itself would overflow.
8.17.4. Random numbers¶
numpy.random provides a
Generator class that draws
samples from common distributions. The generator is
stateful: each call advances its internal state, so
consecutive calls return independent samples:
from ulab import numpy as np
rng = np.random.Generator(seed=42)
rng.random(size=5) # 5 uniform [0.0, 1.0) samples
rng.uniform(low=-1.0, high=1.0, size=10)
rng.normal(loc=0.0, scale=1.0, size=(2, 4))
The output dtype is always float. size= accepts an
integer (1-D output) or a tuple (n-D output); when
omitted, a single Python float is returned.
The generator is suitable for simulation, dithering,
synthetic test data, and any other application where
cryptographic strength is not required. It is not
suitable for keys or tokens; use the system random source
through os for those.
8.17.5. Build-time availability¶
Whether each submodule is actually present depends on
how the cam was built. scipy.optimize and
scipy.special are not enabled on every cam;
calling a function the cam does not include raises
AttributeError. dir(sp), dir(sp.optimize),
dir(np.random) and friends report what is available
on the cam being targeted.