⚙️ Carrying settings across related solves¶
A real computation is rarely one solve. A numerical irreducible decomposition, a parameter sweep, a multi-stage pipeline – each runs many related solves, and you usually want the same tracking settings on every one of them. Re-typing tolerances and step controls for each solver is both tedious and a place for them to silently drift apart.
Bertini 2 gives every config owner – every tracker and every solver – the same small interface for this. (Owning configs is a capability, not a class: a tracker is not a solver, but both carry configs, so both get these methods.)
This page is about moving settings around. For which settings exist and what they default to, see ⚙️ Configuration reference.
Set fields by name¶
Every numeric config field accepts a string – the exact, noise-free way to write a number
(0.05 the Python float is not \(1/20\); "0.05" is). And you can set fields right on the
solver without naming which config struct they live in: update() routes each field to whichever
config owns it.
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]))
solver = bertini.ZeroDimSolver(system)
solver.update(final_tolerance="1e-11", # -> EndgameConfig
max_num_crossed_path_resolve_attempts=3) # -> ZeroDimConfig
from bertini.endgame import EndgameConfig
from bertini.nag_algorithm import ZeroDimConfig
assert solver.get_config(EndgameConfig).final_tolerance == 1e-11
assert solver.get_config(ZeroDimConfig).max_num_crossed_path_resolve_attempts == 3
The solver keeps some of its settings in its tracker and its endgame, and one call reaches those
too. max_step_size belongs to the tracker and num_sample_points to the endgame; neither
needs naming the object it lives on:
solver.update(max_step_size="0.05", num_sample_points=6)
from bertini.tracking import SteppingConfig
assert solver.get_config(SteppingConfig).max_step_size == bertini.multiprec.real_mp("0.05")
assert solver.get_config(EndgameConfig).num_sample_points == 6
A misspelled field raises immediately rather than silently doing nothing:
try:
solver.update(max_stepsize="0.05") # no such field anywhere on this solver
raise AssertionError("should have raised")
except AttributeError:
pass
Each field name belongs to exactly one config, so the flat form and the named form reach the same
field. final_tolerance is the endgame’s:
solver.update(final_tolerance="1e-12") # flat
assert solver.get_config(EndgameConfig).final_tolerance == 1e-12
solver.configure(endgame={'final_tolerance': "1e-8"}) # named
assert solver.get_config(EndgameConfig).final_tolerance == 1e-8
Carry a whole group of settings¶
get_settings() hands you the owner’s entire configuration as a plain dict {name: config} –
independent, picklable copies. set_settings() applies one back. So you configure once and
stamp every subsequent solver with the same settings.
reference = bertini.ZeroDimSolver(system)
reference.update(final_tolerance="1e-11", newton_before_endgame="1e-6")
settings = reference.get_settings()
assert set(settings) == set(reference.config_names()) # one entry per config
# ... later, for each related solve ...
next_solver = bertini.ZeroDimSolver(system)
next_solver.set_settings(settings)
assert next_solver.get_config(EndgameConfig).final_tolerance == 1e-11
The bundle is an ordinary picklable value, so it can be stored or sent to another process – the same settings then drive a worker’s solves as drive the manager’s.
import pickle
carried = pickle.loads(pickle.dumps(settings))
worker_solver = bertini.ZeroDimSolver(system)
worker_solver.set_settings(carried)
assert worker_solver.get_config(EndgameConfig).final_tolerance == 1e-11
The settings are precision-agnostic¶
A bundle built from one precision model applies unchanged to another. This is what lets a multi-stage workflow mix precision models – a quick double-precision pass, then an adaptive re-solve – while carrying one set of tracking tolerances through all of them.
tuned = bertini.ZeroDimSolver(system, mptype='double')
tuned.update(final_tolerance="1e-10")
bundle = tuned.get_settings()
for mptype in ('multiple', 'adaptive'):
solver = bertini.ZeroDimSolver(system, mptype=mptype)
solver.set_settings(bundle) # drops on cleanly, any precision model
assert solver.get_config(EndgameConfig).final_tolerance == 1e-10
By default set_settings() applies only the configs the target actually has and skips the rest,
so a bundle also moves between different kinds of owner (a tracker has no solver-level tolerances).
Pass strict=True to instead require every config in the bundle to be applicable.
from bertini import AMPTracker
tracker = AMPTracker(system)
tracker.set_settings(settings) # silently skips the solver-only configs
try:
tracker.set_settings(settings, strict=True)
raise AssertionError("should have raised")
except KeyError:
pass
Complete example¶
"""Carrying settings across related solves -- Bertini 2 tutorial.
Tune one config owner, get_settings(), and set_settings() on each of the rest.
Run: python carrying_settings.py
"""
import pickle
import numpy as np
import bertini
from bertini import AMPTracker
from bertini.nag_algorithm import ZeroDimConfig
from bertini.tracking import SteppingConfig
from bertini.endgame import EndgameConfig
def build_system():
"""The little system every section solves."""
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 set_fields_by_name(system):
"""Route fields by name onto whichever config owns them."""
solver = bertini.ZeroDimSolver(system)
solver.update(final_tolerance="1e-11", # -> EndgameConfig
max_num_crossed_path_resolve_attempts=3) # -> ZeroDimConfig
assert solver.get_config(EndgameConfig).final_tolerance == 1e-11
assert solver.get_config(ZeroDimConfig).max_num_crossed_path_resolve_attempts == 3
# Settings the solver keeps in its tracker and its endgame are reached by the same call.
solver.update(max_step_size="0.05", num_sample_points=6)
assert solver.get_config(SteppingConfig).max_step_size == bertini.multiprec.real_mp("0.05")
assert solver.get_config(EndgameConfig).num_sample_points == 6
# A misspelled field raises immediately rather than doing nothing.
try:
solver.update(max_stepsize="0.05") # no such field anywhere on this solver
raise AssertionError("should have raised")
except AttributeError:
pass
return solver
def carry_a_whole_bundle(system):
"""get_settings() -> plain picklable dict; set_settings() stamps another solver."""
reference = bertini.ZeroDimSolver(system)
reference.update(final_tolerance="1e-11", newton_before_endgame="1e-6")
settings = reference.get_settings()
assert set(settings) == set(reference.config_names()) # one entry per config
# ... later, for each related solve ...
next_solver = bertini.ZeroDimSolver(system)
next_solver.set_settings(settings)
assert next_solver.get_config(EndgameConfig).final_tolerance == 1e-11
# The bundle is an ordinary picklable value -- store it or ship it to a worker.
carried = pickle.loads(pickle.dumps(settings))
worker_solver = bertini.ZeroDimSolver(system)
worker_solver.set_settings(carried)
assert worker_solver.get_config(EndgameConfig).final_tolerance == 1e-11
return settings
def settings_are_precision_agnostic(system, settings):
"""One bundle drops cleanly onto any precision model, and across owner kinds."""
tuned = bertini.ZeroDimSolver(system, mptype='double')
tuned.update(final_tolerance="1e-10")
bundle = tuned.get_settings()
for mptype in ('multiple', 'adaptive'):
solver = bertini.ZeroDimSolver(system, mptype=mptype)
solver.set_settings(bundle) # drops on cleanly, any precision model
assert solver.get_config(EndgameConfig).final_tolerance == 1e-10
# By default set_settings() applies only the configs the target has and skips the rest.
tracker = AMPTracker(system)
tracker.set_settings(settings) # silently skips the solver-only configs
try:
tracker.set_settings(settings, strict=True)
raise AssertionError("should have raised")
except KeyError:
pass
def main():
system = build_system()
set_fields_by_name(system)
settings = carry_a_whole_bundle(system)
settings_are_precision_agnostic(system, settings)
if __name__ == '__main__':
main()