โ๏ธ 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 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.)
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", # -> TolerancesConfig
max_num_crossed_path_resolve_attempts=3) # -> ZeroDimConfig
from bertini.nag_algorithm import TolerancesConfig, ZeroDimConfig
assert solver.get_config(TolerancesConfig).final_tolerance == 1e-11
assert solver.get_config(ZeroDimConfig).max_num_crossed_path_resolve_attempts == 3
A misspelled field, or one that lives on a different owner, raises immediately rather than silently
doing nothing โ max_step_size is a tracker setting, so the solver rejects it:
try:
solver.update(max_step_size="0.05") # that field is on the tracker, not the solver
raise AssertionError("should have raised")
except AttributeError:
pass
solver.get_tracker().update(max_step_size="0.05") # set it where it lives
from bertini.tracking import SteppingConfig
assert solver.get_tracker().get_config(SteppingConfig).max_step_size == bertini.multiprec.real_mp("0.05")
Carry a whole bundleยถ
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(TolerancesConfig).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(TolerancesConfig).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(TolerancesConfig).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
Put together, the pattern for any multi-solve workflow is: tune one owner, ``get_settings()``, and ``set_settings()`` on each of the rest.
Complete exampleยถ
The whole tutorial as one runnable script โ assemble nothing, just run it:
"""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 TolerancesConfig, ZeroDimConfig
from bertini.tracking import SteppingConfig
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", # -> TolerancesConfig
max_num_crossed_path_resolve_attempts=3) # -> ZeroDimConfig
assert solver.get_config(TolerancesConfig).final_tolerance == 1e-11
assert solver.get_config(ZeroDimConfig).max_num_crossed_path_resolve_attempts == 3
# A field on a different owner raises immediately rather than doing nothing.
try:
solver.update(max_step_size="0.05") # that field is on the tracker, not the solver
raise AssertionError("should have raised")
except AttributeError:
pass
solver.get_tracker().update(max_step_size="0.05") # set it where it lives
assert solver.get_tracker().get_config(SteppingConfig).max_step_size == bertini.multiprec.real_mp("0.05")
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(TolerancesConfig).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(TolerancesConfig).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(TolerancesConfig).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()