"""Optimization result."""
import warnings
from collections import Counter
from copy import deepcopy
from typing import Sequence, Union
import numpy as np
import pandas as pd
from ..objective import History
from ..problem import Problem
from ..util import assign_clusters, delete_nan_inf
OptimizationResult = Union['OptimizerResult', 'OptimizeResult']
[docs]class OptimizerResult(dict):
"""
The result of an optimizer run.
Used as a standardized return value to map from the individual result
objects returned by the employed optimizers to the format understood by
pypesto.
Can be used like a dict.
Attributes
----------
id:
Id of the optimizer run. Usually the start index.
x:
The best found parameters.
fval:
The best found function value, `fun(x)`.
grad:
The gradient at `x`.
hess:
The Hessian at `x`.
res:
The residuals at `x`.
sres:
The residual sensitivities at `x`.
n_fval
Number of function evaluations.
n_grad:
Number of gradient evaluations.
n_hess:
Number of Hessian evaluations.
n_res:
Number of residuals evaluations.
n_sres:
Number of residual sensitivity evaluations.
x0:
The starting parameters.
fval0:
The starting function value, `fun(x0)`.
history:
Objective history.
exitflag:
The exitflag of the optimizer.
time:
Execution time.
message: str
Textual comment on the optimization result.
optimizer: str
The optimizer used for optimization.
Notes
-----
Any field not supported by the optimizer is filled with None.
"""
[docs] def __init__(
self,
id: str = None,
x: np.ndarray = None,
fval: float = None,
grad: np.ndarray = None,
hess: np.ndarray = None,
res: np.ndarray = None,
sres: np.ndarray = None,
n_fval: int = None,
n_grad: int = None,
n_hess: int = None,
n_res: int = None,
n_sres: int = None,
x0: np.ndarray = None,
fval0: float = None,
history: History = None,
exitflag: int = None,
time: float = None,
message: str = None,
optimizer: str = None,
):
super().__init__()
self.id = id
self.x: np.ndarray = np.array(x) if x is not None else None
self.fval: float = fval
self.grad: np.ndarray = np.array(grad) if grad is not None else None
self.hess: np.ndarray = np.array(hess) if hess is not None else None
self.res: np.ndarray = np.array(res) if res is not None else None
self.sres: np.ndarray = np.array(sres) if sres is not None else None
self.n_fval: int = n_fval
self.n_grad: int = n_grad
self.n_hess: int = n_hess
self.n_res: int = n_res
self.n_sres: int = n_sres
self.x0: np.ndarray = np.array(x0) if x0 is not None else None
self.fval0: float = fval0
self.history: History = history
self.exitflag: int = exitflag
self.time: float = time
self.message: str = message
self.optimizer = optimizer
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(key)
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
[docs] def summary(self):
"""Get summary of the object."""
message = (
"### Optimizer Result \n\n"
f"* optimizer used: {self.optimizer} \n"
f"* message: {self.message} \n"
f"* number of evaluations: {self.n_fval} \n"
f"* time taken to optimize: {self.time} \n"
f"* startpoint: {self.x0} \n"
f"* endpoint: {self.x} \n"
)
# add fval, gradient, hessian, res, sres if available
if self.fval is not None:
message += f"* final objective value: {self.fval} \n"
if self.grad is not None:
message += f"* final gradient value: {self.grad} \n"
if self.hess is not None:
message += f"* final hessian value: {self.hess} \n"
if self.res is not None:
message += f"* final residual value: {self.res} \n"
if self.sres is not None:
message += f"* final residual sensitivity: {self.sres} \n"
return message
[docs] def update_to_full(self, problem: Problem) -> None:
"""
Update values to full vectors/matrices.
Parameters
----------
problem:
problem which contains info about how to convert to full vectors
or matrices
"""
self.x = problem.get_full_vector(self.x, problem.x_fixed_vals)
self.grad = problem.get_full_vector(self.grad)
self.hess = problem.get_full_matrix(self.hess)
self.x0 = problem.get_full_vector(self.x0, problem.x_fixed_vals)
[docs]class OptimizeResult:
"""Result of the :py:func:`pypesto.optimize.minimize` function."""
[docs] def __init__(self):
self.list = []
def __deepcopy__(self, memo):
other = OptimizeResult()
other.list = deepcopy(self.list)
return other
def __getattr__(self, key):
"""Define `optimize_result.key`."""
try:
return [res[key] for res in self.list]
except KeyError:
raise AttributeError(key)
def __getitem__(self, index):
"""Define `optimize_result[i]` to access the i-th result."""
try:
return self.list[index]
except IndexError:
raise IndexError(
f"{index} out of range for optimize result of "
f"length {len(self.list)}."
)
def __len__(self):
return len(self.list)
[docs] def summary(self, disp_best: bool = True, disp_worst: bool = False):
"""
Get summary of the object.
Parameters
----------
disp_best:
Whether to display a detailed summary of the best run.
disp_worst:
Whether to display a detailed summary of the worst run.
"""
# perform clustering for better information
clust, clustsize = assign_clusters(delete_nan_inf(self.fval)[1])
counter_message = '\n'.join(
["\tCount\tMessage"]
+ [
f"\t{count}\t{message}"
for message, count in Counter(self.message).most_common()
]
)
times_message = (
f'\n\tMean execution time: {np.mean(self.time)}s\n'
f'\tMaximum execution time: {np.max(self.time)}s,'
f'\tid={self[np.argmax(self.time)].id}\n'
f'\tMinimum execution time: {np.min(self.time)}s,\t'
f'id={self[np.argmin(self.time)].id}'
)
summary = (
"## Optimization Result \n\n"
f"* number of starts: {len(self)} \n"
f"* best value: {self[0]['fval']}, id={self[0]['id']}\n"
f"* worst value: {self[-1]['fval']}, id={self[-1]['id']}\n"
f"* number of non-finite values: {np.logical_not(np.isfinite(self.fval)).sum()}\n\n"
f"* execution time summary: {times_message}\n"
f"* summary of optimizer messages:\n{counter_message}\n"
f"* best value found (approximately) {clustsize[0]} time(s) \n"
f"* number of plateaus found: "
f"{1 + max(clust) - sum(clustsize == 1)}"
)
if disp_best:
summary += f"\nA summary of the best run:\n\n{self[0].summary()}"
if disp_worst:
summary += f"\nA summary of the worst run:\n\n{self[-1].summary()}"
return summary
[docs] def append(
self,
optimize_result: OptimizationResult,
sort: bool = True,
prefix: str = '',
):
"""
Append an OptimizerResult or an OptimizeResult to the result object.
Parameters
----------
optimize_result:
The result of one or more (local) optimizer run.
sort:
Boolean used so we only sort once when appending an
optimize_result.
prefix:
The IDs for all appended results will be prefixed with this.
"""
current_ids = set(self.id)
if isinstance(optimize_result, OptimizeResult):
new_ids = [
prefix + identifier
for identifier in optimize_result.id
if identifier is not None
]
if current_ids.isdisjoint(new_ids) and new_ids:
raise ValueError(
"Some id's you want to merge coincide with "
"the existing id's. Please use an "
"appropriate prefix such as 'run_2_'."
)
for optimizer_result in optimize_result.list:
self.append(optimizer_result, sort=False, prefix=prefix)
elif isinstance(optimize_result, OptimizerResult):
# if id is None, append without checking for duplicate ids
if optimize_result.id is None:
self.list.append(optimize_result)
else:
new_id = prefix + optimize_result.id
if new_id in current_ids:
raise ValueError(
"The id you want to merge coincides with "
"the existing id's. Please use an "
"appropriate prefix such as 'run_2_'."
)
optimize_result.id = new_id
self.list.append(optimize_result)
if sort:
self.sort()
[docs] def sort(self):
"""Sort the optimizer results by function value fval (ascending)."""
def get_fval(res):
return res.fval if not np.isnan(res.fval) else np.inf
self.list = sorted(self.list, key=get_fval)
[docs] def as_dataframe(self, keys=None) -> pd.DataFrame:
"""
Get as pandas DataFrame.
If keys is a list, return only the specified values, otherwise all.
"""
lst = self.as_list(keys)
df = pd.DataFrame(lst)
return df
[docs] def as_list(self, keys=None) -> Sequence:
"""
Get as list.
If keys is a list, return only the specified values.
Parameters
----------
keys: list(str), optional
Labels of the field to extract.
"""
lst = self.list
if keys is not None:
lst = [{key: res[key] for key in keys} for res in lst]
return lst
[docs] def get_for_key(self, key) -> list:
"""Extract the list of values for the specified key as a list."""
warnings.warn(
"get_for_key() is deprecated in favour of "
"optimize_result['key'] and will be removed in future "
"releases."
)
return [res[key] for res in self.list]