🔬 Precision models: double, multiple, adaptive¶
Bertini can track in three precision models, and ZeroDimSolver selects between them with mptype:
'double'– hardware double precision (≈16 digits). Fastest; fine when the paths are well-conditioned.'multiple'– a fixed multiprecision, the same number of digits throughout.'adaptive'– adaptive multiprecision (AMP, the default): the precision rises and falls along each path as the conditioning demands. The most robust, and what makes a near-singular path solvable.
This tutorial is about how the three models differ in the interface – the types you get back and how you set the precision. For why and when you reach for adaptive precision (the conditioning story), see 🎚️ When double precision is not enough.
x, y = bertini.Variable('x'), bertini.Variable('y')
system = bertini.System()
system.add_function(x*x + y*y - 1)
system.add_function(x + y)
system.add_variable_group(bertini.VariableGroup([x, y]))
names = {mptype: type(bertini.ZeroDimSolver(system, mptype=mptype)).__name__
for mptype in ('double', 'multiple', 'adaptive')}
assert names['double'] == 'ZeroDimSolverCauchyDoublePrecision'
assert names['multiple'] == 'ZeroDimSolverCauchyFixedMultiplePrecision'
assert names['adaptive'] == 'ZeroDimSolverCauchyAdaptivePrecision'
# The solver type encodes the endgame and the precision model, but NOT the start system -- that is
# a construction choice now (the algorithm holds the start system polymorphically), so it no longer
# appears as a suffix on the class name.
Reading solutions: convert with complex()¶
The model changes the type of the numbers you get back. A double solve returns NumPy
complex128; a multiprecision solve returns arrays of bertini.complex_mp (NumPy
arrays with dtype Complex). The portable habit – works in every model – is to convert each
coordinate with complex():
bertini.random.set_random_seed(2)
dbl = bertini.ZeroDimSolver(system, mptype='double'); dbl.solve()
assert dbl.all_solutions()[0].dtype == np.complex128
amp = bertini.ZeroDimSolver(system, mptype='adaptive'); amp.solve()
assert str(amp.all_solutions()[0].dtype) == 'complex_mp' # bertini.complex_mp
# the same code reads either one:
def to_python(solution):
return np.array([complex(c) for c in solution])
for solver in (dbl, amp):
pts = sorted(tuple(np.round(to_python(s).real, 4)) for s in solver.all_solutions())
assert pts == [(-0.7071, 0.7071), (0.7071, -0.7071)]
Setting the precision¶
A fixed multiple solve works at one precision everywhere: the system, the tracker, and the points must all agree. So set the default precision and lift the system to it before constructing the solver:
bertini.default_precision(40) # 40 digits for this solve
system.precision(40) # the system must match
m = bertini.ZeroDimSolver(system, mptype='multiple')
m.solve()
assert len(m.all_solutions()) == 2
bertini.default_precision(30) # restore a modest default
system.precision(30)
(Mismatch is reported, not silently tolerated: constructing a multiple-precision solve whose system sits at a different precision raises rather than tracking at the wrong precision.)
An adaptive solve manages precision itself; its knobs live in the AMP config – most usefully
maximum_precision, the ceiling above which a path is declared to have failed:
from bertini.tracking import AMPConfig
amp = bertini.ZeroDimSolver(system, mptype='adaptive')
assert amp.get_tracker().get_config(AMPConfig).maximum_precision == 300
amp.get_tracker().update(maximum_precision=200) # tighten the ceiling
assert amp.get_tracker().get_config(AMPConfig).maximum_precision == 200
The config surface is the same shape across models¶
Apart from the precision-specific tracker knobs (the amp config exists only for an adaptive
tracker), the configs are the same across precision models – tolerances, zero_dim, stepping,
and so on are not precision-stamped. That is exactly why a settings bundle carries between models (see
⚙️ Carrying settings across related solves):
shared = {'tolerances', 'zero_dim', 'post_processing', 'auto_retrack'}
for mptype in ('double', 'multiple', 'adaptive'):
names = set(bertini.ZeroDimSolver(system, mptype=mptype).config_names())
assert shared <= names
So the rule of thumb: reach for 'adaptive' by default; drop to 'double' for speed when the
problem is well-conditioned; use 'multiple' when you want a fixed, known precision throughout. And
read every solution through complex() so your code does not care which model produced it.
Complete example¶
The whole tutorial as one runnable script – assemble nothing, just run it:
"""Precision models: double, multiple, adaptive -- Bertini 2 tutorial.
The three precision models differ in the interface: the types you get back
and how you set the precision.
Run: python precision_models.py
"""
import numpy as np
import bertini
def build_system():
"""Build the polynomial system used throughout the tutorial."""
x, y = bertini.Variable('x'), bertini.Variable('y')
system = bertini.System()
system.add_function(x*x + y*y - 1)
system.add_function(x + y)
system.add_variable_group(bertini.VariableGroup([x, y]))
return system
def solver_types(system):
"""mptype selects the solver class (endgame + precision model)."""
names = {mptype: type(bertini.ZeroDimSolver(system, mptype=mptype)).__name__
for mptype in ('double', 'multiple', 'adaptive')}
assert names['double'] == 'ZeroDimSolverCauchyDoublePrecision'
assert names['multiple'] == 'ZeroDimSolverCauchyFixedMultiplePrecision'
assert names['adaptive'] == 'ZeroDimSolverCauchyAdaptivePrecision'
def reading_solutions(system):
"""The model changes the type of the numbers; convert with complex()."""
bertini.random.set_random_seed(2)
dbl = bertini.ZeroDimSolver(system, mptype='double'); dbl.solve()
assert dbl.all_solutions()[0].dtype == np.complex128
amp = bertini.ZeroDimSolver(system, mptype='adaptive'); amp.solve()
assert str(amp.all_solutions()[0].dtype) == 'complex_mp' # bertini.complex_mp
# the same code reads either one:
def to_python(solution):
return np.array([complex(c) for c in solution])
for solver in (dbl, amp):
pts = sorted(tuple(np.round(to_python(s).real, 4)) for s in solver.all_solutions())
assert pts == [(-0.7071, 0.7071), (0.7071, -0.7071)]
def setting_precision(system):
"""Fixed multiple works at one precision everywhere; adaptive manages its own."""
bertini.default_precision(40) # 40 digits for this solve
system.precision(40) # the system must match
m = bertini.ZeroDimSolver(system, mptype='multiple')
m.solve()
assert len(m.all_solutions()) == 2
bertini.default_precision(30) # restore a modest default
system.precision(30)
from bertini.tracking import AMPConfig
amp = bertini.ZeroDimSolver(system, mptype='adaptive')
assert amp.get_tracker().get_config(AMPConfig).maximum_precision == 300
amp.get_tracker().update(maximum_precision=200) # tighten the ceiling
assert amp.get_tracker().get_config(AMPConfig).maximum_precision == 200
def shared_config_surface(system):
"""Apart from precision-specific knobs, the configs are the same across models."""
shared = {'tolerances', 'zero_dim', 'post_processing', 'auto_retrack'}
for mptype in ('double', 'multiple', 'adaptive'):
names = set(bertini.ZeroDimSolver(system, mptype=mptype).config_names())
assert shared <= names
def main():
system = build_system()
solver_types(system)
reading_solutions(system)
setting_precision(system)
shared_config_surface(system)
if __name__ == '__main__':
main()