"""
Branch-and-bound solver results object.
Copyright by Gabriel A. Hackebeil (gabe.hackebeil@gmail.com).
"""
# recognized by the pytest-doctestplus plugin,
# not the standard doctest
__doctest_requires__ = {"SolverResults.write": ["yaml"]}
from typing import Union, IO, Optional
import sys
import base64
from pybnb.common import SolutionStatus, TerminationCondition
from pybnb.misc import time_format, as_stream
from pybnb.node import dumps, Node
import six
[docs]class SolverResults(object):
"""Stores the results of a branch-and-bound solve.
Attributes
----------
solution_status : string
The solution status will be set to one of the strings
documented by the :class:`SolutionStatus
<pybnb.common.SolutionStatus>` enum.
termination_condition : string
The solve termination condition, as determined by
the dispatcher, will be set to one of the strings
documented by the :class:`TerminationCondition
<pybnb.common.TerminationCondition>` enum.
objective : float
The best objective found.
bound : float
The global optimality bound.
absolute_gap : float or None
The absolute gap between the objective and
bound. This will only be set when the solution
status sf "optimal" or "feasible"; otherwise, it
will be None.
relative_gap : float or None
The relative gap between the objective and
bound. This will only be set when the solution
status sf "optimal" or "feasible"; otherwise, it
will be None.
nodes : int
The total number of nodes processes by all workers.
wall_time : float
The process-local wall time (seconds). This is the
only value on the results object that varies between
processes.
best_node : :class:`Node <pybnb.node.Node>`
The node with the best objective obtained during the
solve. Note that if the best_objective solver option
was used, the best_node on the results object may
have an objective that is worse than the objective
stored on the results (or may be None).
"""
def __init__(self):
# type: () -> None
self.solution_status = None # type: Optional[SolutionStatus]
self.termination_condition = None # type: Optional[TerminationCondition]
self.objective = None # type: Optional[Union[int, float]]
self.bound = None # type: Optional[Union[int, float]]
self.absolute_gap = None # type: Optional[Union[int, float]]
self.relative_gap = None # type: Optional[Union[int, float]]
self.nodes = None # type: Optional[int]
self.wall_time = None # type: Optional[float]
self.best_node = None # type: Optional[Node]
[docs] def pprint(self, stream=sys.stdout):
# type: (Union[IO, str]) -> None
"""Prints a nicely formatted representation of the
results.
Parameters
----------
stream : file-like object or string, optional
A file-like object or a filename where results
should be written to. (default: ``sys.stdout``)
"""
with as_stream(stream) as out:
out.write("solver results:\n")
self.write(out, prefix=" - ", pretty=True)
[docs] def write(self, stream, prefix="", pretty=False):
# type: (Union[IO, str], str, bool) -> None
"""Writes results in YAML format to a stream or
file. Changing the parameter values from their
defaults may result in the output becoming
non-compatible with the YAML format.
Parameters
----------
stream : file-like object or string
A file-like object or a filename where results
should be written to.
prefix : string, optional
A string to use as a prefix for each line that
is written. (default: '')
pretty : bool, optional
Indicates whether or not certain recognized
attributes should be formatted for more
human-readable output. (default: False)
Example
-------
>>> import six
>>> import pybnb
>>> results = pybnb.SolverResults()
>>> results.best_node = pybnb.Node()
>>> results.best_node.objective = 123
>>> out = six.StringIO()
>>> # the best_node is serialized
>>> results.write(out)
>>> del results
>>> import yaml
>>> results_dict = yaml.safe_load(out.getvalue())
>>> # de-serialize the best_node
>>> best_node = pybnb.node.loads(results_dict['best_node'])
>>> assert best_node.objective == 123
"""
with as_stream(stream) as out:
attrs = vars(self)
names = sorted(list(attrs.keys()))
first = (
"solution_status",
"termination_condition",
"objective",
"bound",
"absolute_gap",
"relative_gap",
"nodes",
"wall_time",
"best_node",
)
for cnt, name in enumerate(first):
if not hasattr(self, name):
continue
names.remove(name)
val = getattr(self, name)
if val is not None:
if name in ("solution_status", "termination_condition"):
if type(val) in (SolutionStatus, TerminationCondition):
val = val.value
elif pretty:
if name == "wall_time":
val = time_format(val, digits=2)
elif name in (
"objective",
"bound",
"absolute_gap",
"relative_gap",
):
val = "%.7g" % (val)
elif name == "best_node":
assert isinstance(val, Node)
if val.objective is not None:
val = "Node(objective=%.7g)" % (val.objective)
else:
val = "Node(objective=None)"
else:
if name == "best_node":
val = dumps(val)
if not six.PY2:
val = base64.encodebytes(val).decode("ascii")
else:
val = base64.encodestring(val).decode("ascii")
val = "\n ".join(val.splitlines())
val = "!!binary |\n %s" % (val)
else:
val_ = "%r" % (val)
if type(val) is float:
if val_ == "inf":
val_ = ".inf"
elif val_ == "-inf":
val_ = "-.inf"
elif val_ == "nan":
val_ = ".nan"
val = val_
del val_
if pretty or (val is not None):
out.write(prefix + "%s: %s\n" % (name, val))
else:
assert val is None
out.write(prefix + "%s: null\n" % (name))
for name in names:
val = getattr(self, name)
if pretty:
out.write(prefix + "%s: %r\n" % (name, val))
else:
if val is None:
out.write(prefix + "%s: null\n" % (name))
else:
val_ = "%r" % (val)
if type(val) is float:
if val_ == "inf":
val_ = ".inf"
elif val_ == "-inf":
val_ = "-.inf"
elif val_ == "nan":
val_ = ".nan"
val = val_
del val_
out.write(prefix + "%s: %s\n" % (name, val))
def __str__(self):
# type: () -> str
"""Represents the results as a string."""
tmp = six.StringIO()
self.pprint(stream=tmp)
return tmp.getvalue()