# coding: utf-8
"""Classes for storing and reporting solutions of malloovia problems."""
from typing import Union, NamedTuple, Optional, List, Sequence, Tuple
from enum import IntEnum
from functools import singledispatch
import pulp # type: ignore
from .model import (
remove_namedtuple_defaultdoc,
PerformanceValues,
InstanceClass,
App,
Problem,
)
[docs]class Status(IntEnum):
"Possible status of malloovia's solution"
unsolved = 0
optimal = 1
infeasible = 2
integer_infeasible = 3
overfull = 4
trivial = 5
aborted = 6
cbc_error = 7
unknown = 8
def pulp_to_malloovia_status(status: int) -> Status:
"""Receives a PuLP status code and returns a Malloovia :class:`Status`."""
if status == pulp.LpStatusInfeasible:
r = Status.infeasible
elif status == pulp.LpStatusNotSolved:
r = Status.aborted
elif status == pulp.LpStatusOptimal:
r = Status.optimal
elif status == pulp.LpStatusUndefined:
r = Status.integer_infeasible
else:
r = Status.unknown
return r
class MallooviaHistogram(dict):
"""This class stores a multi-dimensional histogram, providing the same
interface than a standard dict whose keys are workload tuples and the
values are the count of the number of times that the tuple is observed
in the computed period."""
apps: Tuple[App, ...] = None
"""The apps attribute stores a tuple with references to the apps involved
in the workload. The order of this tuple must match the order of workloads for
of each tuple which acts as key in the histogram"""
def __missing__(self, key):
# Default value for missing keys is zero
return 0
def __repr__(self):
return "MallooviaHistogram with %d values" % len(self)
[docs]@remove_namedtuple_defaultdoc
class MallooviaStats(NamedTuple):
"""Stores data related to the Malloovia solver."""
gcd: bool
"bool: whether GCD technique was used or not."
status: Status
":class:`.Status`: status of the solution."
gcd_multiplier: float = 1.0
"""float: the multiplier used in GCD technique (defaults to 1.0)."""
frac_gap: Optional[float] = None
"""float: the fracGap passed to cbc solver (defaults to None)."""
max_seconds: Optional[float] = None
"""float: the maxSeconds passed to cbc solver (defaults to None)."""
lower_bound: Optional[float] = None
"""float: the lower bound of the solution as reported by cbc when the
optimal solution is not available (defaults to None)."""
[docs]@remove_namedtuple_defaultdoc
class SolvingStats(NamedTuple):
"""Stores the statistics that can be gathered from a solution
of Phase I, or one single timeslot in Phase II."""
algorithm: MallooviaStats
""":class:`.MallooviaStats`: additional info related to the particular
algorithm used to solve the problem."""
creation_time: float
"""float: time required to create the LP problem."""
solving_time: float
"""float: time required to solve the LP problem."""
optimal_cost: float
"""float: optimal cost as reported by the LP solver, or None if no solution
was found."""
[docs]@remove_namedtuple_defaultdoc
class GlobalSolvingStats(NamedTuple):
"""Stores the global statistics for Phase II, which are a sum of the
statistics of each timeslot."""
creation_time: float
"""float: sum of the time required to create the LP problem
for each timeslot."""
solving_time: float
"""float: sum of the time required to solve the LP problem
for each timeslot."""
optimal_cost: float
"""float: sum of the optimal costs as reported by the LP problem
for each timeslot."""
status: Status
""":class:`.Status`: global status computed from the status of
each timeslot."""
default_algorithm: Optional[str] = None
"""Currently unused"""
[docs]@remove_namedtuple_defaultdoc
class ReservedAllocation(NamedTuple):
"""Stores the number of reserved instances to allocate during the whole reservation
period."""
instance_classes: Tuple[InstanceClass, ...]
"""List[:class:`.InstanceClass`, ...]: list of reserved instance classes
in the allocation."""
vms_number: Tuple[float, ...]
"""List[float, ...]: list of numbers, representing the number of instance classes
to be reserved of each type. The corresponding instance class is obtained
from the ``instance_classes`` attribute using the same index."""
[docs]@remove_namedtuple_defaultdoc
class AllocationInfo(NamedTuple):
"""Stores the allocation for a series of timeslots. It can be a single
timeslot, or the sequence of allocations for the whole reservation period."""
values: Tuple[Tuple[Tuple[float, ...], ...], ...]
"""Tuple[Tuple[Tuple[float, ...], ...], ...]: contains a list with one element
per timeslot. Each element in this sequence is a list (with one element
per app), which is in turn a list (with one element per instance class).
These values are numbers which can represent the number of instance
classes of that type to be allocated for that app during that timeslot,
or the cost associated with these instance classes, or the performance
given by these instance classes, depending on the ``units`` field.
So, for example, if ``units`` is ``"vms"``, then ``values[2][1][3]``
represents the number of VMs of the instance class 3 to be allocated
for application 1 during the timseslot 2.
Note that, if the allocation contains a single timeslot, it is still
necessary to specify the index (0) in the first dimension,
e.g. ``vms_number[0][1][3]``.
To match the indexes in those arrays to actual instance classes and
apps, the attributes ``instance_classes`` and ``apps`` should be used.
So, for the above example, the application would be ``apps[1]`` and
the instance class would be ``instance_classes[3]``. If required,
the workload for that particular timeslot (2) can also be retrieved from
``workload_tuples[2]``."""
units: str
"""str: a string identifying the kind of information stored in the ``values``
field. It can be ``"vms"`` (number of VM instances), ``"cost"`` or any
currency (cost of these instances) or ``"rph"`` (performance of these
instances)."""
apps: Sequence[App]
"""Sequence[:class:`.App`]: is a list of apps to give meaning to the second
index in ``values``."""
instance_classes: Sequence[InstanceClass]
"""Sequence[:class:`.InstanceClass`]: is a list of instance classes to give
meaning to the third index in ``values``."""
workload_tuples: Sequence[Tuple[float, ...]]
"""Sequence[Tuple[float, ...]]: is a list of workload tuples to give meaning to the
first index in ``values``. Each element is a tuple with as many values
as apps, being each one the workload for each app."""
repeats: List[int] = []
"""List[int]: number of repetitions of each workload_tuple, for the case
in which the allocation is per load-level (histogram). It can be an empty
list (default value) for the case in which the allocation is per time-slot."""
def __repr__(self):
d0 = len(self.values)
if self.values:
d1 = len(self.values[0])
else:
d1 = 0
if d1:
d2 = len(self.values[0][0])
else:
d2 = 0
return "<{} {}x{}x{}>".format(self.__class__.__name__, d0, d1, d2)
[docs]@remove_namedtuple_defaultdoc
class SolutionI(NamedTuple):
"""Stores a solution for phase I."""
id: str
"str: arbitrary id for this object."
problem: Problem
""":class:`.Problem`: reference to the problem which originated
this solution."""
solving_stats: SolvingStats
""":class:`.SolvingStats`: statistics about this solution."""
allocation: AllocationInfo
""":class:`.AllocationInfo`: allocation provided in this solution."""
reserved_allocation: ReservedAllocation
""":class:`.ReservedAllocation`: allocation for reserved instances only."""
[docs]@remove_namedtuple_defaultdoc
class SolutionII(NamedTuple):
"""Stores a solution for phase II."""
id: str
"str: arbitrary id for this object."
problem: Problem
""":class:`.Problem`: reference to the problem which originated
this solution."""
solving_stats: Sequence[SolvingStats]
""":Sequence[class:`.SolvingStats`]: list of the SolvingStats for
each timeslot."""
global_solving_stats: GlobalSolvingStats
""":class:`.GlobalSolvingStats`: summary of the solving stats."""
previous_phase: SolutionI
""":class:`.SolutionI`: reference to the solution of the previous phase."""
allocation: AllocationInfo
""":class:`.AllocationInfo`: allocation for the whole period, built from the
allocations of the individual timeslots."""
[docs]@singledispatch
def compute_allocation_cost(alloc: AllocationInfo) -> AllocationInfo:
"""Computes the cost of each element of the allocation.
Args:
alloc: the allocation whose cost has to be computed
Returns:
Another allocation in which the ``values`` field contains
the cost of that element (it is the original ``values``
multiplied by the cost of the corresponding instance class)
"""
costs = []
for row in alloc.values:
costs_row = []
for app_alloc in row:
costs_app = []
for i, _ in enumerate(app_alloc):
costs_app.append(app_alloc[i] * alloc.instance_classes[i].price)
costs_row.append(tuple(costs_app))
costs.append(tuple(costs_row))
return alloc._replace(values=tuple(costs), units="cost")
@compute_allocation_cost.register(SolutionI)
@compute_allocation_cost.register(SolutionII)
def _(solution: Union[SolutionI, SolutionII]) -> AllocationInfo:
return compute_allocation_cost(solution.allocation)
@compute_allocation_performance.register(SolutionI)
@compute_allocation_performance.register(SolutionII)
def __(
solution: Union[SolutionI, SolutionII]
) -> AllocationInfo: # pylint:disable=function-redefined
return compute_allocation_performance(
solution.allocation, solution.problem.performances.values
)
__all__ = [
"Status",
"MallooviaStats",
"SolvingStats",
"GlobalSolvingStats",
"AllocationInfo",
"ReservedAllocation",
"SolutionI",
"SolutionII",
"compute_allocation_cost",
"compute_allocation_performance",
]