Refactoring of allocation solvers.
Implemented layering so it will allow to have multiple solver engines. Implements blueprint: dynamic-allocation Change-Id: I7ed1ec0216fb9778b4fa5be4fb4f6141a0e26fc9
This commit is contained in:
parent
5d6290ac1a
commit
b1cef8c4a0
@ -14,21 +14,13 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import itertools
|
|
||||||
import math
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from scipy.optimize import linprog
|
|
||||||
|
|
||||||
from bareon_allocator import errors
|
|
||||||
from bareon_allocator.parsers import DynamicSchemaParser
|
from bareon_allocator.parsers import DynamicSchemaParser
|
||||||
|
from bareon_allocator.solvers import LinearProgramCreator
|
||||||
|
from bareon_allocator.solvers import LinearProgrammingScipySolver
|
||||||
from bareon_allocator import utils
|
from bareon_allocator import utils
|
||||||
|
|
||||||
from bareon_allocator.sequences import CrossSumInequalitySequence
|
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -37,388 +29,39 @@ class DynamicAllocator(object):
|
|||||||
def __init__(self, hw_info, schema):
|
def __init__(self, hw_info, schema):
|
||||||
LOG.debug('Hardware information: %s', hw_info)
|
LOG.debug('Hardware information: %s', hw_info)
|
||||||
LOG.debug('Spaces schema: %s', schema)
|
LOG.debug('Spaces schema: %s', schema)
|
||||||
dynamic_schema = DynamicSchemaParser(hw_info, schema)
|
self.dynamic_schema = DynamicSchemaParser(hw_info, schema)
|
||||||
LOG.debug('Spaces objects: %s', dynamic_schema.spaces)
|
LOG.debug('Spaces objects: %s', self.dynamic_schema.spaces)
|
||||||
LOG.debug('Disks objects: \n%s', dynamic_schema.disks)
|
LOG.debug('Disks objects: %s', self.dynamic_schema.disks)
|
||||||
|
|
||||||
self.solver = DynamicAllocationLinearProgram(
|
linear_program = LinearProgramCreator(
|
||||||
dynamic_schema.disks,
|
self.dynamic_schema).linear_program()
|
||||||
dynamic_schema.spaces)
|
self.solver = LinearProgrammingScipySolver(linear_program)
|
||||||
|
|
||||||
def generate_static(self):
|
def generate_static(self):
|
||||||
sizes = self.solver.solve()
|
solution = self.solver.solve()
|
||||||
|
LOG.debug('Static allocation schema: \n%s', solution)
|
||||||
return sizes
|
return self._convert_solution(solution)
|
||||||
|
|
||||||
|
|
||||||
class DynamicAllocationLinearProgram(object):
|
|
||||||
"""Linear programming allocator.
|
|
||||||
|
|
||||||
Use Linear Programming method [0] (the method itself has nothing to do
|
|
||||||
with computer-programming) in order to formulate and solve the problem
|
|
||||||
of spaces allocation on disks, with the best outcome.
|
|
||||||
|
|
||||||
In this implementation scipy is being used since it already implements
|
|
||||||
simplex algorithm to find the best feasible solution.
|
|
||||||
|
|
||||||
[0] https://en.wikipedia.org/wiki/Linear_programming
|
|
||||||
[1] http://docs.scipy.org/doc/scipy-0.16.0/reference/generated
|
|
||||||
/scipy.optimize.linprog.html
|
|
||||||
[2] https://en.wikipedia.org/wiki/Simplex_algorithm
|
|
||||||
"""
|
|
||||||
|
|
||||||
weight_set_mapping = [
|
|
||||||
# Don't use minimal size, in this case
|
|
||||||
# we will get a weight for the space which
|
|
||||||
# in combination with space which has max_size
|
|
||||||
# so there will be unallocated space
|
|
||||||
# ['min_size', 'best_with_disks'],
|
|
||||||
# ['max_size', 'best_with_disks'],
|
|
||||||
['min_size', 'max_size', 'best_with_disks']]
|
|
||||||
|
|
||||||
def __init__(self, disks, spaces):
|
|
||||||
self.disks = disks
|
|
||||||
self.spaces = spaces
|
|
||||||
# Coefficients of the linear objective minimization function.
|
|
||||||
# During iteration over vertexes the function is used to identify
|
|
||||||
# if current solution (vertex) satisfies the equation more, than
|
|
||||||
# previous one.
|
|
||||||
# Example of equation: c[0]*x1 + c[1]*x2
|
|
||||||
self.objective_function_coefficients = []
|
|
||||||
|
|
||||||
# A matrix which, gives the values of the equality constraints at x,
|
|
||||||
# when multipled by x.
|
|
||||||
self.equality_constraint_matrix = []
|
|
||||||
|
|
||||||
# An array of values representing right side of equation,
|
|
||||||
# left side is represented by row of `equality_constraint_matrix`
|
|
||||||
# matrix
|
|
||||||
self.equality_constraint_vector = np.array([])
|
|
||||||
|
|
||||||
self.upper_bound_constraint_matrix = []
|
|
||||||
self.upper_bound_constraint_vector = []
|
|
||||||
self.lower_bound_constraint_matrix = []
|
|
||||||
self.lower_bound_constraint_vector = []
|
|
||||||
|
|
||||||
# Specify boundaries of each x in the next format (min, max). Use
|
|
||||||
# None for one of min or max when there is no bound.
|
|
||||||
self.bounds = np.array([])
|
|
||||||
|
|
||||||
# For each space, xn (size of the space) is represented
|
|
||||||
# for each disk as separate variable, so for each
|
|
||||||
# disk we have len(spaces) * len(disks) sizes
|
|
||||||
self.x_amount = len(self.disks) * len(self.spaces)
|
|
||||||
|
|
||||||
# TODO(eli): has to be refactored
|
|
||||||
# Here we store indexes for bounds and equation
|
|
||||||
# matrix, in order to be able to change it on
|
|
||||||
# refresh
|
|
||||||
self.weight_equation_indexes = []
|
|
||||||
|
|
||||||
self._set_spaces_sets_by(self.weight_set_mapping[0])
|
|
||||||
self._init_equation(self.disks, self.spaces)
|
|
||||||
self._init_objective_function_coefficient()
|
|
||||||
self._init_min_max()
|
|
||||||
self._refresh_weight()
|
|
||||||
|
|
||||||
def solve(self):
|
|
||||||
upper_bound_matrix = self._make_upper_bound_constraint_matrix() or None
|
|
||||||
upper_bound_vector = self._make_upper_bound_constraint_vector() or None
|
|
||||||
|
|
||||||
LOG.debug('Objective function coefficients human-readable:\n%s\n',
|
|
||||||
utils.format_x_vector(self.objective_function_coefficients,
|
|
||||||
len(self.spaces)))
|
|
||||||
|
|
||||||
LOG.debug('Equality equation:\n%s\n',
|
|
||||||
utils.format_equation(
|
|
||||||
self.equality_constraint_matrix,
|
|
||||||
self.equality_constraint_vector,
|
|
||||||
len(self.spaces)))
|
|
||||||
LOG.debug('Inequality equation:\n%s\n',
|
|
||||||
utils.format_equation(
|
|
||||||
upper_bound_matrix,
|
|
||||||
upper_bound_vector,
|
|
||||||
len(self.spaces)))
|
|
||||||
|
|
||||||
for weight_for_sets in self.weight_set_mapping:
|
|
||||||
LOG.debug('Parameters for spaces set formation: %s',
|
|
||||||
weight_for_sets)
|
|
||||||
self._set_spaces_sets_by(weight_for_sets)
|
|
||||||
solution = linprog(
|
|
||||||
self.objective_function_coefficients,
|
|
||||||
A_eq=self.equality_constraint_matrix,
|
|
||||||
b_eq=self.equality_constraint_vector,
|
|
||||||
A_ub=upper_bound_matrix,
|
|
||||||
b_ub=upper_bound_vector,
|
|
||||||
bounds=self.bounds,
|
|
||||||
options={"disp": False})
|
|
||||||
|
|
||||||
# If solution is found we can finish attempts to find
|
|
||||||
# the best solution
|
|
||||||
if not solution.success:
|
|
||||||
break
|
|
||||||
|
|
||||||
LOG.debug("Solution: %s", solution)
|
|
||||||
self._check_errors(solution)
|
|
||||||
# Naive implementation of getting integer result
|
|
||||||
# from a linear programming algorithm, MIP
|
|
||||||
# (mixed integer programming) should be considered
|
|
||||||
# instead, but it may have a lot of problems (solution
|
|
||||||
# of such equations is NP-hard in some cases),
|
|
||||||
# for our practical purposes it's enough to round
|
|
||||||
# the number down, in this case we may get `n` megabytes
|
|
||||||
# unallocated, where n is len(spaces) * len(disks)
|
|
||||||
solution_vector = self._round_down(solution.x)
|
|
||||||
|
|
||||||
return self._convert_solution(solution_vector)
|
|
||||||
|
|
||||||
def _check_errors(self, solution):
|
|
||||||
if not solution.success:
|
|
||||||
raise errors.NoSolutionFound(
|
|
||||||
'Allocation is not possible '
|
|
||||||
'with specified constraints: {0}'.format(solution.message))
|
|
||||||
|
|
||||||
def _round_down(self, vector):
|
|
||||||
return [int(math.floor(f)) for f in vector]
|
|
||||||
|
|
||||||
def _init_min_max(self):
|
|
||||||
"""Create min and max constraints for each space.
|
|
||||||
|
|
||||||
In case of 2 disks and 2 spaces
|
|
||||||
|
|
||||||
For first space min_size >= 10 and max_size <= 20
|
|
||||||
1 * x1 + 0 * x2 + 1 * x3 + 0 * x4 >= 10
|
|
||||||
1 * x1 + 0 * x2 + 1 * x3 + 0 * x4 <= 20
|
|
||||||
|
|
||||||
For second space min_size >= 15 and max_size <= 30
|
|
||||||
0 * x1 + 1 * x2 + 0 * x3 + 1 * x4 >= 15
|
|
||||||
0 * x1 + 1 * x2 + 0 * x3 + 1 * x4 <= 30
|
|
||||||
"""
|
|
||||||
for space_idx, space in enumerate(self.spaces):
|
|
||||||
row = self._make_matrix_row()
|
|
||||||
max_size = getattr(space, 'max_size', None)
|
|
||||||
min_size = getattr(space, 'min_size', None)
|
|
||||||
|
|
||||||
for disk_idx in range(len(self.disks)):
|
|
||||||
row[disk_idx * len(self.spaces) + space_idx] = 1
|
|
||||||
|
|
||||||
if min_size is not None:
|
|
||||||
self.lower_bound_constraint_matrix.append(row)
|
|
||||||
self.lower_bound_constraint_vector.append(min_size)
|
|
||||||
|
|
||||||
if max_size is not None:
|
|
||||||
self.upper_bound_constraint_matrix.append(row)
|
|
||||||
self.upper_bound_constraint_vector.append(max_size)
|
|
||||||
|
|
||||||
def _get_spaces_sets_by(self, criteria):
|
|
||||||
return [i[1] for i in self._get_sets_by(criteria)]
|
|
||||||
|
|
||||||
def _get_sets_by(self, criteria):
|
|
||||||
def get_values(space):
|
|
||||||
return [getattr(space, c, None) for c in criteria]
|
|
||||||
|
|
||||||
grouped_spaces = itertools.groupby(
|
|
||||||
sorted(self.spaces, key=get_values),
|
|
||||||
key=get_values)
|
|
||||||
|
|
||||||
return [(k, list(v)) for k, v in grouped_spaces]
|
|
||||||
|
|
||||||
def _set_spaces_sets_by(self, criteria):
|
|
||||||
self.weight_spaces_sets = self._get_spaces_sets_by(criteria)
|
|
||||||
|
|
||||||
def _refresh_weight(self):
|
|
||||||
"""Refresh weight.
|
|
||||||
|
|
||||||
Create weight constraints for spaces which have same
|
|
||||||
max constraint or for those which don't have it at all.
|
|
||||||
|
|
||||||
Lets say, second's space is equal to max of the third and fourth,
|
|
||||||
we will have next equation:
|
|
||||||
0 * x1 + (1 / weight) * x2 + (-1 / weight) * x3 +
|
|
||||||
0 * x4 + (1 / weight) * x5 + (-1 / weight) * x6 = 0
|
|
||||||
"""
|
|
||||||
DEFAULT_WEIGHT = 1
|
|
||||||
# Clean constraint matrix and vector from previous values
|
|
||||||
for idx in sorted(self.weight_equation_indexes, reverse=True):
|
|
||||||
del self.equality_constraint_matrix[idx]
|
|
||||||
del self.equality_constraint_vector[idx]
|
|
||||||
self.weight_equation_indexes = []
|
|
||||||
|
|
||||||
for spaces_set in self.weight_spaces_sets:
|
|
||||||
# Don't set weight if there is less than one space in the set
|
|
||||||
if len(spaces_set) < 2:
|
|
||||||
continue
|
|
||||||
|
|
||||||
first_weight = getattr(spaces_set[0], 'weight', DEFAULT_WEIGHT)
|
|
||||||
first_space_idx = self.spaces.index(spaces_set[0])
|
|
||||||
for space in spaces_set[1:]:
|
|
||||||
row = self._make_matrix_row()
|
|
||||||
weight = getattr(space, 'weight', DEFAULT_WEIGHT)
|
|
||||||
|
|
||||||
# If weight is 0, it doesn't make sense to set for such
|
|
||||||
# space a weight
|
|
||||||
if weight == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
space_idx = self.spaces.index(space)
|
|
||||||
|
|
||||||
for disk_idx in range(len(self.disks)):
|
|
||||||
row_i = disk_idx * len(self.spaces)
|
|
||||||
row[row_i + first_space_idx] = 1 / first_weight
|
|
||||||
row[row_i + space_idx] = -1 / weight
|
|
||||||
|
|
||||||
self.weight_equation_indexes.append(
|
|
||||||
len(self.equality_constraint_matrix) - 1)
|
|
||||||
|
|
||||||
self.equality_constraint_matrix.append(row)
|
|
||||||
self.equality_constraint_vector = np.append(
|
|
||||||
self.equality_constraint_vector,
|
|
||||||
0)
|
|
||||||
|
|
||||||
def _make_matrix_row(self):
|
|
||||||
return np.zeros(self.x_amount)
|
|
||||||
|
|
||||||
def _make_upper_bound_constraint_matrix(self):
|
|
||||||
"""Creates upper bound constraint matrix.
|
|
||||||
|
|
||||||
Upper bound constraint matrix consist of upper bound
|
|
||||||
matrix and lower bound matrix witch changed sign.
|
|
||||||
"""
|
|
||||||
return (self.upper_bound_constraint_matrix +
|
|
||||||
[[-i for i in row]
|
|
||||||
for row in self.lower_bound_constraint_matrix])
|
|
||||||
|
|
||||||
def _make_upper_bound_constraint_vector(self):
|
|
||||||
"""Create upper bound constraint vector.
|
|
||||||
|
|
||||||
Upper bound constraint vector consist of upper bound and
|
|
||||||
lower bound, with changed sign.
|
|
||||||
"""
|
|
||||||
return (self.upper_bound_constraint_vector +
|
|
||||||
[-i for i in self.lower_bound_constraint_vector])
|
|
||||||
|
|
||||||
def _convert_solution(self, solution_vector):
|
def _convert_solution(self, solution_vector):
|
||||||
|
# TODO(eli): convertation logic should be moved to solvers,
|
||||||
|
# as result Solver object should be returned and used
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
spaces_grouped_by_disk = list(utils.grouper(
|
spaces_grouped_by_disk = list(utils.grouper(
|
||||||
solution_vector,
|
solution_vector,
|
||||||
len(self.spaces)))
|
len(self.dynamic_schema.spaces)))
|
||||||
for disk_i in range(len(self.disks)):
|
for disk_i in range(len(self.dynamic_schema.disks)):
|
||||||
disk_id = self.disks[disk_i].id
|
disk_id = self.dynamic_schema.disks[disk_i].id
|
||||||
disk = {'disk_id': disk_id,
|
disk = {'disk_id': disk_id,
|
||||||
'size': self.disks[disk_i].size,
|
'size': self.dynamic_schema.disks[disk_i].size,
|
||||||
'spaces': []}
|
'spaces': []}
|
||||||
spaces_for_disk = spaces_grouped_by_disk[disk_i]
|
spaces_for_disk = spaces_grouped_by_disk[disk_i]
|
||||||
|
|
||||||
for space_i, space_size in enumerate(spaces_for_disk):
|
for space_i, space_size in enumerate(spaces_for_disk):
|
||||||
disk['spaces'].append({
|
disk['spaces'].append({
|
||||||
'space_id': self.spaces[space_i].id,
|
'space_id': self.dynamic_schema.spaces[space_i].id,
|
||||||
'size': space_size})
|
'size': space_size})
|
||||||
|
|
||||||
result.append(disk)
|
result.append(disk)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _init_equation(self, disks, spaces):
|
|
||||||
for d in disks:
|
|
||||||
# Initialize constraints, each row in the matrix should
|
|
||||||
# be equal to size of the disk
|
|
||||||
self.equality_constraint_vector = np.append(
|
|
||||||
self.equality_constraint_vector,
|
|
||||||
d.size)
|
|
||||||
|
|
||||||
# Initialize the matrix
|
|
||||||
# In case of 2 spaces and 3 disks the result should be:
|
|
||||||
# [[1, 1, 0, 0, 0, 0],
|
|
||||||
# [0, 0, 1, 1, 0, 0],
|
|
||||||
# [0, 0, 0, 0, 1, 1]]
|
|
||||||
#
|
|
||||||
# Explanation of the first row
|
|
||||||
# [1, - x1 multiplier, size of space 1 on the first disk
|
|
||||||
# 1, - x2 multiplier, size of space 2 on the first disk
|
|
||||||
# 0, - x3 multiplier, size of space 1 on 2nd disk, 0 for the first
|
|
||||||
# 0, - x4 multiplier, size of space 2 on 2nd disk, 0 for the first
|
|
||||||
# 0, - x5 multiplier, size of space 1 on 3rd disk, 0 for the first
|
|
||||||
# 0] - x6 multiplier, size of space 2 on 3rd disk, 0 for the first
|
|
||||||
equality_matrix_row = self._make_matrix_row()
|
|
||||||
|
|
||||||
# Set first len(spaces) elements to 1
|
|
||||||
equality_matrix_row = utils.shift(
|
|
||||||
equality_matrix_row,
|
|
||||||
len(spaces),
|
|
||||||
val=1)
|
|
||||||
|
|
||||||
for _ in range(len(disks)):
|
|
||||||
self.equality_constraint_matrix.append(equality_matrix_row)
|
|
||||||
equality_matrix_row = utils.shift(
|
|
||||||
equality_matrix_row,
|
|
||||||
len(spaces),
|
|
||||||
val=0)
|
|
||||||
|
|
||||||
# Size of each space should be more or equal to 0
|
|
||||||
for _ in range(self.x_amount):
|
|
||||||
self._add_bound(0, None)
|
|
||||||
|
|
||||||
def _init_objective_function_coefficient(self):
|
|
||||||
# Amount of coefficients is equal to amount of x
|
|
||||||
c_amount = self.x_amount
|
|
||||||
|
|
||||||
# We want spaces to be allocated on disks
|
|
||||||
# in order which user specified them in the schema.
|
|
||||||
# In order to do that, we set coefficients
|
|
||||||
# higher for those spaces which defined earlier
|
|
||||||
# in the list
|
|
||||||
|
|
||||||
# TODO(eli): describe why we should use special sequence
|
|
||||||
# as order coefficients
|
|
||||||
coefficients = [1.0 / i for i in CrossSumInequalitySequence(c_amount)]
|
|
||||||
|
|
||||||
NONE_ORDER_COEFF = 1
|
|
||||||
SET_COEFF = 2
|
|
||||||
|
|
||||||
space_sets = self._get_spaces_sets_by(['best_with_disks'])
|
|
||||||
|
|
||||||
# A list of disks ids which are not selected for specific spaces
|
|
||||||
all_disks_ids = [i for i in range(len(self.disks))]
|
|
||||||
used_disks_ids = []
|
|
||||||
|
|
||||||
for k, space in self._get_sets_by(['best_with_disks']):
|
|
||||||
if k[0]:
|
|
||||||
used_disks_ids.extend(list(k[0]))
|
|
||||||
|
|
||||||
not_best_disks = list(set(all_disks_ids) - set(used_disks_ids))
|
|
||||||
|
|
||||||
for i_set, space_set in enumerate(space_sets):
|
|
||||||
for space in space_set:
|
|
||||||
s_i = self.spaces.index(space)
|
|
||||||
|
|
||||||
for d_i in range(len(self.disks)):
|
|
||||||
c_i = len(self.spaces) * d_i + s_i
|
|
||||||
|
|
||||||
# Set constant for none_order spaces
|
|
||||||
if getattr(space, 'none_order', False):
|
|
||||||
coefficients[c_i] = NONE_ORDER_COEFF
|
|
||||||
continue
|
|
||||||
|
|
||||||
if space.best_with_disks:
|
|
||||||
if d_i in space.best_with_disks:
|
|
||||||
coefficients[c_i] += SET_COEFF
|
|
||||||
else:
|
|
||||||
# If current disk is not in the set, set it to 0
|
|
||||||
# TODO(eli): isn't it better to leave there order
|
|
||||||
# coefficient?
|
|
||||||
# coefficients[c_i] = 0
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# Don't allcoate coefficient for the spaces
|
|
||||||
# which have no best_with_disks, on best_with_disks
|
|
||||||
if d_i in not_best_disks:
|
|
||||||
coefficients[c_i] += SET_COEFF
|
|
||||||
|
|
||||||
# By default the algorithm tries to minimize the solution
|
|
||||||
# we should invert sign, in order to make it a maximization
|
|
||||||
# function, because we want disks to be maximally allocated.
|
|
||||||
self.objective_function_coefficients = [-c for c in coefficients]
|
|
||||||
|
|
||||||
def _add_bound(self, min_, max_):
|
|
||||||
np.append(self.bounds, (min_, max_))
|
|
||||||
|
@ -24,14 +24,16 @@ class Space(BaseObject):
|
|||||||
'min_size': 0,
|
'min_size': 0,
|
||||||
'max_size': None,
|
'max_size': None,
|
||||||
'best_with_disks': set([]),
|
'best_with_disks': set([]),
|
||||||
'weight': 1
|
'weight': 1,
|
||||||
|
'none_order': False,
|
||||||
|
'type': None
|
||||||
}
|
}
|
||||||
required = ['id', 'type']
|
required = ['id', 'type']
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(Space, self).__init__(**kwargs)
|
super(Space, self).__init__(**kwargs)
|
||||||
|
|
||||||
# Exact size should be repreneted as min_size and max_size
|
# Exact size should be represented as min_size and max_size
|
||||||
if kwargs.get('size'):
|
if kwargs.get('size'):
|
||||||
self.min_size = kwargs.get('size')
|
self.min_size = kwargs.get('size')
|
||||||
self.max_size = kwargs.get('size')
|
self.max_size = kwargs.get('size')
|
||||||
|
@ -70,13 +70,13 @@ class DynamicSchemaParser(object):
|
|||||||
for i, space in enumerate(spaces):
|
for i, space in enumerate(spaces):
|
||||||
|
|
||||||
if space.get('best_with_disks'):
|
if space.get('best_with_disks'):
|
||||||
disks_idx = set()
|
disks_ids = set()
|
||||||
for disk in space['best_with_disks']:
|
for disk in space['best_with_disks']:
|
||||||
try:
|
try:
|
||||||
disks_idx.add(self.raw_disks.index(disk))
|
disks_ids.add(disk['id'])
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
LOG.warn('Warning: %s', exc)
|
LOG.warn('Warning: %s', exc)
|
||||||
|
|
||||||
spaces[i]['best_with_disks'] = disks_idx
|
spaces[i]['best_with_disks'] = disks_ids
|
||||||
|
|
||||||
return spaces
|
return spaces
|
||||||
|
24
bareon_allocator/solvers/__init__.py
Normal file
24
bareon_allocator/solvers/__init__.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
# flake8: noqa
|
||||||
|
|
||||||
|
from bareon_allocator.solvers.base import BaseSolver
|
||||||
|
from bareon_allocator.solvers.linear_program import LinearProgram
|
||||||
|
from bareon_allocator.solvers.linear_programming_scipy_solver \
|
||||||
|
import LinearProgrammingScipySolver
|
||||||
|
from bareon_allocator.solvers.linear_program_creator \
|
||||||
|
import LinearProgramCreator
|
38
bareon_allocator/solvers/base.py
Normal file
38
bareon_allocator/solvers/base.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class BaseSolver(object):
|
||||||
|
"""Base class for Bareon Allocator Objects."""
|
||||||
|
|
||||||
|
def __init__(self, linear_program):
|
||||||
|
"""Initialize object.
|
||||||
|
|
||||||
|
:param linear_program: `class`:LinearProgram object
|
||||||
|
"""
|
||||||
|
self.linear_program = linear_program
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def solve(self):
|
||||||
|
"""Returns solution hash.
|
||||||
|
|
||||||
|
:raises: errors.NoSolutionFound
|
||||||
|
"""
|
97
bareon_allocator/solvers/linear_program.py
Normal file
97
bareon_allocator/solvers/linear_program.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
class LinearProgram(object):
|
||||||
|
"""LinearProgram object is abstract way to describe linear program."""
|
||||||
|
MAXIMIZE = 'maximize'
|
||||||
|
MINIMIZE = 'minimize'
|
||||||
|
|
||||||
|
# Linear Program
|
||||||
|
LP_TYPE_LP = 'lp'
|
||||||
|
# Mixed Integer Program
|
||||||
|
LP_TYPE_MIP = 'mip'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
x_amount=0,
|
||||||
|
optimization_type=MAXIMIZE,
|
||||||
|
lp_type=LP_TYPE_LP,
|
||||||
|
objective_function_coefficients=None,
|
||||||
|
|
||||||
|
equality_constraint_matrix=None,
|
||||||
|
lower_constraint_matrix=None,
|
||||||
|
upper_constraint_matrix=None,
|
||||||
|
|
||||||
|
equality_constraint_vector=None,
|
||||||
|
lower_constraint_vector=None,
|
||||||
|
upper_constraint_vector=None):
|
||||||
|
|
||||||
|
self.lp_type = lp_type
|
||||||
|
self.objective_function_optimization_type = optimization_type
|
||||||
|
|
||||||
|
# Coefficients of the linear objective minimization function.
|
||||||
|
# During iteration over vertexes the function is used to identify
|
||||||
|
# if current solution (vertex) satisfies the equation more, than
|
||||||
|
# previous one.
|
||||||
|
# Example of equation: c[0]*x1 + c[1]*x2
|
||||||
|
self.objective_function_coefficients = objective_function_coefficients
|
||||||
|
|
||||||
|
# Matrices which, gives values of the equality/inequality
|
||||||
|
# constraints, when multiplied by x.
|
||||||
|
self.equality_constraint_matrix = equality_constraint_matrix
|
||||||
|
self.lower_constraint_matrix = lower_constraint_matrix
|
||||||
|
self.upper_constraint_matrix = upper_constraint_matrix
|
||||||
|
|
||||||
|
# Vectors in combination with equality matrices give
|
||||||
|
# equality/inequality system of linear equations.
|
||||||
|
self.equality_constraint_vector = equality_constraint_vector
|
||||||
|
self.lower_constraint_vector = lower_constraint_vector
|
||||||
|
self.upper_constraint_vector = upper_constraint_vector
|
||||||
|
|
||||||
|
# Amount unknown of variables.
|
||||||
|
self.x_amount = x_amount
|
||||||
|
|
||||||
|
# A list of tuples which represents min and max possible values for
|
||||||
|
# each variable.
|
||||||
|
self.bounds = [(0, None) for _ in xrange(self.x_amount)]
|
||||||
|
|
||||||
|
def minimize_objective_function(self):
|
||||||
|
"""Minimize objective function."""
|
||||||
|
self.objective_function_optimization_type = self.MINIMIZE
|
||||||
|
|
||||||
|
def maximize_objective_function(self):
|
||||||
|
"""Maximize objective function."""
|
||||||
|
self.objective_function_optimization_type = self.MAXIMIZE
|
||||||
|
|
||||||
|
def set_type_lp(self):
|
||||||
|
"""Set type of linear program to Linear Program.
|
||||||
|
|
||||||
|
Is default, produces real number result, without any integer
|
||||||
|
constraints.
|
||||||
|
"""
|
||||||
|
self.lp_type = self.LP_TYPE_LP
|
||||||
|
|
||||||
|
def set_type_mip(self):
|
||||||
|
""""Set type of linear program to Mixed Integer Program.
|
||||||
|
|
||||||
|
This type may include integer constraints, as result wider range of
|
||||||
|
operations may be available.
|
||||||
|
|
||||||
|
Note: Not all linear programming solvers support this type.
|
||||||
|
See: https://en.wikipedia.org/wiki/Integer_programming
|
||||||
|
"""
|
||||||
|
self.lp_type = self.LP_TYPE_MIP
|
331
bareon_allocator/solvers/linear_program_creator.py
Normal file
331
bareon_allocator/solvers/linear_program_creator.py
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from bareon_allocator.sequences import CrossSumInequalitySequence
|
||||||
|
from bareon_allocator.solvers.linear_program import LinearProgram
|
||||||
|
|
||||||
|
|
||||||
|
class LinearProgramCreator(object):
|
||||||
|
"""Creates LinearProgram based on DynamicSchema object."""
|
||||||
|
|
||||||
|
NONE_ORDER_COEFFICIENT = 1
|
||||||
|
SET_COEFFICIENT = 2
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
dynamic_schema,
|
||||||
|
weight_sets_criteria=[
|
||||||
|
'min_size',
|
||||||
|
'max_size',
|
||||||
|
'best_with_disks']):
|
||||||
|
"""Initializes the object.
|
||||||
|
|
||||||
|
:param dynamic_schema: :class:`DynamicSchema` object
|
||||||
|
:param weight_sets_criteria: a list of strings, which represents
|
||||||
|
attributes of spaces based on which sets will be created to
|
||||||
|
make equations.
|
||||||
|
"""
|
||||||
|
self.weight_sets_criteria = weight_sets_criteria
|
||||||
|
self.disks = dynamic_schema.disks
|
||||||
|
self.spaces = dynamic_schema.spaces
|
||||||
|
|
||||||
|
self.spaces_len = len(self.spaces)
|
||||||
|
self.disks_len = len(self.disks)
|
||||||
|
|
||||||
|
# For each space, x (size of the space) is represented
|
||||||
|
# for each disk as separate variable, so for each
|
||||||
|
# disk we have len(spaces) * len(disks) sizes
|
||||||
|
self.x_amount = self.disks_len * self.spaces_len
|
||||||
|
|
||||||
|
def linear_program(self):
|
||||||
|
"""Returns linear program object
|
||||||
|
|
||||||
|
:return: :class:`LinearProgram` linear program object
|
||||||
|
"""
|
||||||
|
space_size_equation = self._make_space_size_constraints()
|
||||||
|
disk_size_equation = self._make_disk_size_constraints()
|
||||||
|
equality_weight_equation = self._make_weight_constraints()
|
||||||
|
|
||||||
|
# Merge both equality and constraint vectors into a single dictionary
|
||||||
|
equations = self._merge_equations(space_size_equation,
|
||||||
|
disk_size_equation)
|
||||||
|
equations = self._merge_equations(equations,
|
||||||
|
equality_weight_equation)
|
||||||
|
|
||||||
|
objective_coefficients = self._make_objective_function_coefficient()
|
||||||
|
return LinearProgram(
|
||||||
|
x_amount=self.x_amount,
|
||||||
|
optimization_type=LinearProgram.MAXIMIZE,
|
||||||
|
objective_function_coefficients=objective_coefficients,
|
||||||
|
**equations)
|
||||||
|
|
||||||
|
def _make_space_size_constraints(self):
|
||||||
|
"""Create min and max constraints for each space.
|
||||||
|
|
||||||
|
In case of 2 disks and 2 spaces
|
||||||
|
|
||||||
|
For first space min_size >= 10 and max_size <= 20
|
||||||
|
1 * x1 + 0 * x2 + 1 * x3 + 0 * x4 >= 10
|
||||||
|
1 * x1 + 0 * x2 + 1 * x3 + 0 * x4 <= 20
|
||||||
|
|
||||||
|
For second space min_size >= 15 and max_size <= 30
|
||||||
|
0 * x1 + 1 * x2 + 0 * x3 + 1 * x4 >= 15
|
||||||
|
0 * x1 + 1 * x2 + 0 * x3 + 1 * x4 <= 30
|
||||||
|
"""
|
||||||
|
constraint_equation = {
|
||||||
|
'lower_constraint_matrix': [],
|
||||||
|
'lower_constraint_vector': [],
|
||||||
|
'upper_constraint_matrix': [],
|
||||||
|
'upper_constraint_vector': []}
|
||||||
|
|
||||||
|
for space_idx, space in enumerate(self.spaces):
|
||||||
|
row = self._make_matrix_row()
|
||||||
|
|
||||||
|
for disk_idx in range(self.disks_len):
|
||||||
|
row[disk_idx * self.spaces_len + space_idx] = 1
|
||||||
|
|
||||||
|
if space.min_size is not None:
|
||||||
|
constraint_equation['lower_constraint_matrix'].append(
|
||||||
|
row)
|
||||||
|
constraint_equation['lower_constraint_vector'].append(
|
||||||
|
space.min_size)
|
||||||
|
|
||||||
|
if space.max_size is not None:
|
||||||
|
constraint_equation['upper_constraint_matrix'].append(
|
||||||
|
row)
|
||||||
|
constraint_equation['upper_constraint_vector'].append(
|
||||||
|
space.max_size)
|
||||||
|
|
||||||
|
return constraint_equation
|
||||||
|
|
||||||
|
def _merge_equations(self, eq1, eq2):
|
||||||
|
"""Merges two equations into a single dictionary of equations.
|
||||||
|
|
||||||
|
:param eq1: equation dictionary, where key is a name of equation and
|
||||||
|
value is a vector or matrix
|
||||||
|
:param eq2: same as eq1
|
||||||
|
:return: merged equation
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
all_keys = set(eq1.keys() + eq2.keys())
|
||||||
|
for key in all_keys:
|
||||||
|
if eq2.get(key) and eq1.get(key):
|
||||||
|
# Merge if both have values
|
||||||
|
result[key] = eq1[key] + eq2[key]
|
||||||
|
elif eq2.get(key):
|
||||||
|
result[key] = eq2[key]
|
||||||
|
elif eq1.get(key):
|
||||||
|
result[key] = eq1[key]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _make_disk_size_constraints(self):
|
||||||
|
"""Creates equations based on disk sizes.
|
||||||
|
|
||||||
|
So solver will not allocate more then "disk size" space for each disk.
|
||||||
|
|
||||||
|
In case of 2 spaces and 3 disks the result should be:
|
||||||
|
[[1, 1, 0, 0, 0, 0],
|
||||||
|
[0, 0, 1, 1, 0, 0],
|
||||||
|
[0, 0, 0, 0, 1, 1]]
|
||||||
|
|
||||||
|
Explanation of the first row
|
||||||
|
[1, - x1 multiplier, size of space 1 on the first disk
|
||||||
|
1, - x2 multiplier, size of space 2 on the first disk
|
||||||
|
0, - x3 multiplier, size of space 1 on 2nd disk, 0 for the first
|
||||||
|
0, - x4 multiplier, size of space 2 on 2nd disk, 0 for the first
|
||||||
|
0, - x5 multiplier, size of space 1 on 3rd disk, 0 for the first
|
||||||
|
0] - x6 multiplier, size of space 2 on 3rd disk, 0 for the first
|
||||||
|
|
||||||
|
:return: equations, where key is a name of equation, value is a list
|
||||||
|
or vector
|
||||||
|
"""
|
||||||
|
constraint_equation = {
|
||||||
|
'upper_constraint_matrix': [],
|
||||||
|
'upper_constraint_vector': []}
|
||||||
|
|
||||||
|
for disk_idx in range(self.disks_len):
|
||||||
|
row = self._make_matrix_row()
|
||||||
|
|
||||||
|
for space_idx, space in enumerate(self.spaces):
|
||||||
|
row[disk_idx * self.spaces_len + space_idx] = 1
|
||||||
|
|
||||||
|
constraint_equation['upper_constraint_matrix'].append(row)
|
||||||
|
constraint_equation['upper_constraint_vector'].append(
|
||||||
|
self.disks[disk_idx].size)
|
||||||
|
|
||||||
|
return constraint_equation
|
||||||
|
|
||||||
|
def _make_weight_constraints(self):
|
||||||
|
"""Refresh weight.
|
||||||
|
|
||||||
|
Create weight constraints for spaces which have same
|
||||||
|
max constraint or for those which don't have it at all.
|
||||||
|
|
||||||
|
Lets say, second space is equal to the third, as the result
|
||||||
|
we will have next equation:
|
||||||
|
0 * x1 + (1 / weight) * x2 + (-1 / weight) * x3 +
|
||||||
|
0 * x4 + (1 / weight) * x5 + (-1 / weight) * x6 = 0
|
||||||
|
|
||||||
|
See "Weight" section in the documentation for details:
|
||||||
|
http://bareon-allocator.readthedocs.org/en
|
||||||
|
/latest/architecture.html#weight
|
||||||
|
|
||||||
|
TODO(eli): it should be not equality, but inequality with some
|
||||||
|
range, so we will not get fails every time exact constraint cannot be
|
||||||
|
satisfied.
|
||||||
|
"""
|
||||||
|
weight_equations = {
|
||||||
|
'equality_constraint_matrix': [],
|
||||||
|
'equality_constraint_vector': []}
|
||||||
|
|
||||||
|
weight_spaces_sets = self._get_spaces_sets_by(
|
||||||
|
self.weight_sets_criteria)
|
||||||
|
|
||||||
|
for spaces_set in weight_spaces_sets:
|
||||||
|
# Don't set weight if there is less than one space in the set
|
||||||
|
if len(spaces_set) < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
first_weight = spaces_set[0].weight
|
||||||
|
first_space_idx = self.spaces.index(spaces_set[0])
|
||||||
|
for space in spaces_set[1:]:
|
||||||
|
row = self._make_matrix_row()
|
||||||
|
|
||||||
|
# If weight is 0, it doesn't make sense to set for such
|
||||||
|
# space a weight
|
||||||
|
if space.weight == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
space_idx = self.spaces.index(space)
|
||||||
|
|
||||||
|
for disk_idx in range(self.disks_len):
|
||||||
|
row_i = disk_idx * len(self.spaces)
|
||||||
|
row[row_i + first_space_idx] = 1.0 / first_weight
|
||||||
|
row[row_i + space_idx] = -1.0 / space.weight
|
||||||
|
|
||||||
|
weight_equations['equality_constraint_matrix'].append(row)
|
||||||
|
weight_equations['equality_constraint_vector'].append(0)
|
||||||
|
|
||||||
|
return weight_equations
|
||||||
|
|
||||||
|
def _make_objective_function_coefficient(self):
|
||||||
|
"""Creates objective function coefficients.
|
||||||
|
|
||||||
|
We want spaces to be allocated on disks in order which user
|
||||||
|
specified them in the schema. In order to do that, we set
|
||||||
|
coefficients higher for those spaces which defined earlier in the
|
||||||
|
list.
|
||||||
|
|
||||||
|
:return: a vector of coefficients
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Instead of just Integer sequence special type of sequence is being
|
||||||
|
# used, see documentation [1] for details.
|
||||||
|
# Every order coefficient should be between 0 and 1 (not included),
|
||||||
|
# in order to aviod having 1st element equal to 1, sequence should be
|
||||||
|
# started from 2nd element.
|
||||||
|
#
|
||||||
|
# [1] http://bareon-allocator.readthedocs.org/en
|
||||||
|
# /latest/architecture.html#ordering
|
||||||
|
seq = CrossSumInequalitySequence(self.x_amount + 1)
|
||||||
|
next(seq, None)
|
||||||
|
coefficients = [1.0 / i for i in seq]
|
||||||
|
|
||||||
|
space_sets = self._get_spaces_sets_by(['best_with_disks'])
|
||||||
|
no_best_disks = self._get_empty_sets_disks_ids(['best_with_disks'])
|
||||||
|
|
||||||
|
for i_set, space_set in enumerate(space_sets):
|
||||||
|
for space in space_set:
|
||||||
|
s_i = self.spaces.index(space)
|
||||||
|
|
||||||
|
for d_i, disk in enumerate(self.disks):
|
||||||
|
c_i = self.spaces_len * d_i + s_i
|
||||||
|
|
||||||
|
# Set constant for none_order spaces
|
||||||
|
if space.none_order:
|
||||||
|
coefficients[c_i] = self.NONE_ORDER_COEFFICIENT
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If space does not belong to any set, order coefficient
|
||||||
|
# will be left without any additional coefficients.
|
||||||
|
if (space.best_with_disks and
|
||||||
|
disk.id in space.best_with_disks):
|
||||||
|
# If the space has "best disks" and current disk is
|
||||||
|
# in best disks list, add coefficient.
|
||||||
|
coefficients[c_i] += self.SET_COEFFICIENT
|
||||||
|
elif (not space.best_with_disks and
|
||||||
|
disk.id in no_best_disks):
|
||||||
|
# If the space does *not* have "best disks" and
|
||||||
|
# current disk is not in the list of "best disks" of
|
||||||
|
# any space, add set coefficient.
|
||||||
|
coefficients[c_i] += self.SET_COEFFICIENT
|
||||||
|
|
||||||
|
# By default the algorithm tries to minimize the solution
|
||||||
|
# we should invert sign, in order to make it a maximization
|
||||||
|
# function, because we want disks to be maximally allocated.
|
||||||
|
return [-c for c in coefficients]
|
||||||
|
|
||||||
|
def _get_empty_sets_disks_ids(self, criteria):
|
||||||
|
"""Get disks indexes which do not belong to set of any spaces.
|
||||||
|
|
||||||
|
:param criteria: a list of strings, with criteria by which sets has
|
||||||
|
to be created
|
||||||
|
:return: a list of disks indexes
|
||||||
|
"""
|
||||||
|
all_disks_ids = [d.id for d in self.disks]
|
||||||
|
used_disks_ids = []
|
||||||
|
|
||||||
|
for k, space in self._get_sets_by(criteria):
|
||||||
|
if k[0]:
|
||||||
|
used_disks_ids.extend(list(k[0]))
|
||||||
|
|
||||||
|
return list(set(all_disks_ids) - set(used_disks_ids))
|
||||||
|
|
||||||
|
def _get_spaces_sets_by(self, criteria):
|
||||||
|
"""Get all spaces which are used for sets.
|
||||||
|
|
||||||
|
:param criteria: a list of strings with attributes by which sets has
|
||||||
|
to be created
|
||||||
|
:return: a list of spaces lists, where each list item is represents
|
||||||
|
a set
|
||||||
|
"""
|
||||||
|
return [i[1] for i in self._get_sets_by(criteria)]
|
||||||
|
|
||||||
|
def _get_sets_by(self, criteria):
|
||||||
|
"""Makes sets based on criteria from space attributes.
|
||||||
|
|
||||||
|
:param criteria: a list of strings with attributes by which sets has
|
||||||
|
to be created
|
||||||
|
:return: a list of tuples, where first item are criteria, second
|
||||||
|
item is a list of spaces
|
||||||
|
"""
|
||||||
|
def get_values(space):
|
||||||
|
return [getattr(space, c, None) for c in criteria]
|
||||||
|
|
||||||
|
grouped_spaces = itertools.groupby(
|
||||||
|
sorted(self.spaces, key=get_values),
|
||||||
|
key=get_values)
|
||||||
|
|
||||||
|
return [(k, list(v)) for k, v in grouped_spaces]
|
||||||
|
|
||||||
|
def _make_matrix_row(self):
|
||||||
|
"""Make a matrix row
|
||||||
|
|
||||||
|
:return: a vector where all the items are 0
|
||||||
|
"""
|
||||||
|
return [0] * self.x_amount
|
108
bareon_allocator/solvers/linear_programming_scipy_solver.py
Normal file
108
bareon_allocator/solvers/linear_programming_scipy_solver.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from scipy.optimize import linprog
|
||||||
|
|
||||||
|
from bareon_allocator import errors
|
||||||
|
from bareon_allocator.solvers import BaseSolver
|
||||||
|
from bareon_allocator.solvers import utils
|
||||||
|
|
||||||
|
|
||||||
|
class LinearProgrammingScipySolver(BaseSolver):
|
||||||
|
"""Linear programming allocator.
|
||||||
|
|
||||||
|
Use Linear Programming method [0] (the method itself has nothing to do
|
||||||
|
with computer-programming) in order to formulate and solve the problem
|
||||||
|
of spaces allocation on disks, with the best outcome.
|
||||||
|
|
||||||
|
In this implementation scipy is being used since it already implements
|
||||||
|
simplex algorithm to find the best feasible solution.
|
||||||
|
|
||||||
|
[0] https://en.wikipedia.org/wiki/Linear_programming
|
||||||
|
[1] http://docs.scipy.org/doc/scipy-0.16.0/reference/generated
|
||||||
|
/scipy.optimize.linprog.html
|
||||||
|
[2] https://en.wikipedia.org/wiki/Simplex_algorithm
|
||||||
|
"""
|
||||||
|
|
||||||
|
def solve(self):
|
||||||
|
"""Solves linear program.
|
||||||
|
|
||||||
|
:return: solution vector
|
||||||
|
"""
|
||||||
|
lp_solution = linprog(
|
||||||
|
self.linear_program.objective_function_coefficients,
|
||||||
|
A_eq=self.linear_program.equality_constraint_matrix or None,
|
||||||
|
b_eq=self.linear_program.equality_constraint_vector or None,
|
||||||
|
A_ub=self._make_upper_constraint_matrix() or None,
|
||||||
|
b_ub=self._make_upper_constraint_vector() or None,
|
||||||
|
bounds=self.linear_program.bounds,
|
||||||
|
options={"disp": False})
|
||||||
|
|
||||||
|
self._check_errors(lp_solution)
|
||||||
|
|
||||||
|
# Naive implementation of getting integer result
|
||||||
|
# from a linear programming algorithm, MIP
|
||||||
|
# (mixed integer programming) should be considered
|
||||||
|
# instead, but it may have a lot of problems (solution
|
||||||
|
# of such equations is NP-hard in some cases),
|
||||||
|
# for our practical purposes it's enough to round
|
||||||
|
# the number down, in this case we may get `n` megabytes
|
||||||
|
# unallocated, where n is len(spaces) * len(disks)
|
||||||
|
solution_vector = utils.round_vector_down(lp_solution.x)
|
||||||
|
|
||||||
|
return solution_vector
|
||||||
|
|
||||||
|
def _check_errors(self, solution):
|
||||||
|
"""Checks if solution is not found.
|
||||||
|
|
||||||
|
:param solution: solution object from scipy
|
||||||
|
:raises: errors.NoSolutionFound if solution is not found
|
||||||
|
"""
|
||||||
|
if not solution.success:
|
||||||
|
raise errors.NoSolutionFound(
|
||||||
|
'Allocation is not possible '
|
||||||
|
'with specified constraints: {0}'.format(solution.message))
|
||||||
|
|
||||||
|
def _make_upper_constraint_matrix(self):
|
||||||
|
"""Merges lower constraint matrix into upper."""
|
||||||
|
upper_constraint_matrix = []
|
||||||
|
if self.linear_program.upper_constraint_matrix:
|
||||||
|
upper_constraint_matrix.extend(
|
||||||
|
self.linear_program.upper_constraint_matrix)
|
||||||
|
|
||||||
|
if self.linear_program.lower_constraint_matrix:
|
||||||
|
# Swap sign for lower constraint matrix in order to make it
|
||||||
|
# upper bound instead of lower bound
|
||||||
|
upper_constraint_matrix.extend(
|
||||||
|
[-i for i in row] for row in
|
||||||
|
self.linear_program.lower_constraint_matrix)
|
||||||
|
|
||||||
|
return upper_constraint_matrix
|
||||||
|
|
||||||
|
def _make_upper_constraint_vector(self):
|
||||||
|
"""Merges lower constraint vector into upper."""
|
||||||
|
upper_constraint_vector = []
|
||||||
|
if self.linear_program.upper_constraint_vector:
|
||||||
|
upper_constraint_vector.extend(
|
||||||
|
self.linear_program.upper_constraint_vector)
|
||||||
|
|
||||||
|
if self.linear_program.lower_constraint_vector:
|
||||||
|
# Swap sign for items in the vector to make it upper bound
|
||||||
|
# instead of lower bound
|
||||||
|
upper_constraint_vector.extend(
|
||||||
|
[-i for i in self.linear_program.lower_constraint_vector])
|
||||||
|
|
||||||
|
return upper_constraint_vector
|
26
bareon_allocator/solvers/utils.py
Normal file
26
bareon_allocator/solvers/utils.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
def round_vector_down(vector):
|
||||||
|
"""Rounds items in the vector down.
|
||||||
|
|
||||||
|
:param vector: vector of float numbers
|
||||||
|
:return: a list of integers
|
||||||
|
"""
|
||||||
|
return [int(math.floor(f)) for f in vector]
|
@ -1,28 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
"""
|
|
||||||
test_bareon_allocator
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
Tests for `bareon_allocator` module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from bareon_allocator.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class TestBareon_dynamic_allocator(base.TestCase):
|
|
||||||
|
|
||||||
def test_something(self):
|
|
||||||
pass
|
|
26
bareon_allocator/tests/test_objects_disk.py
Normal file
26
bareon_allocator/tests/test_objects_disk.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from bareon_allocator.objects import Disk
|
||||||
|
from bareon_allocator.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestObjectsDisk(base.TestCase):
|
||||||
|
|
||||||
|
def test_object_creation(self):
|
||||||
|
disk = Disk(id=10, size=42)
|
||||||
|
self.assertEqual(disk.id, 10)
|
||||||
|
self.assertEqual(disk.size, 42)
|
44
bareon_allocator/tests/test_objects_space.py
Normal file
44
bareon_allocator/tests/test_objects_space.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from bareon_allocator import errors
|
||||||
|
from bareon_allocator.objects import Space
|
||||||
|
from bareon_allocator.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestObjectsSpace(base.TestCase):
|
||||||
|
|
||||||
|
def test_object_creation(self):
|
||||||
|
space = Space(id=10,
|
||||||
|
min_size=1,
|
||||||
|
max_size=2,
|
||||||
|
type='lv',
|
||||||
|
best_with_disks=[1, 2, 3])
|
||||||
|
self.assertEqual(space.id, 10)
|
||||||
|
self.assertEqual(space.min_size, 1)
|
||||||
|
self.assertEqual(space.max_size, 2)
|
||||||
|
self.assertEqual(space.type, 'lv')
|
||||||
|
self.assertEqual(space.best_with_disks, [1, 2, 3])
|
||||||
|
self.assertEqual(space.weight, 1)
|
||||||
|
self.assertEqual(space.none_order, False)
|
||||||
|
|
||||||
|
def test_size_sets_min_and_max(self):
|
||||||
|
space = Space(id=10, type='lv', size=15)
|
||||||
|
self.assertEqual(space.min_size, 15)
|
||||||
|
self.assertEqual(space.max_size, 15)
|
||||||
|
|
||||||
|
def test_fail_if_no_type(self):
|
||||||
|
self.assertRaises(errors.InvalidData, Space, id=11)
|
62
bareon_allocator/tests/test_parsers_dynamic_schema_parser.py
Normal file
62
bareon_allocator/tests/test_parsers_dynamic_schema_parser.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
from bareon_allocator.parsers import DynamicSchemaParser
|
||||||
|
from bareon_allocator.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestParsersDynamicSchemaParser(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestParsersDynamicSchemaParser, self).setUp()
|
||||||
|
hw_info = {
|
||||||
|
'disks': [
|
||||||
|
{'id': 'sda', 'size': 100},
|
||||||
|
{'id': 'sdb', 'size': 42},
|
||||||
|
{'id': 'sdc', 'size': 42}]}
|
||||||
|
schema = [
|
||||||
|
{'id': 'lv1',
|
||||||
|
'type': 'lv',
|
||||||
|
'max_size': 1},
|
||||||
|
{'id': 'lv2',
|
||||||
|
'type': 'lv',
|
||||||
|
'max_size': 1,
|
||||||
|
'best_with_disks': 'yaql=$.disks.where($.size=42)'},
|
||||||
|
{'id': 'vg1',
|
||||||
|
'type': 'vg'}]
|
||||||
|
self.dynamic_schema_parser = DynamicSchemaParser(hw_info, schema)
|
||||||
|
|
||||||
|
def test_unallocated_is_added(self):
|
||||||
|
unallocated = filter(lambda s: s.id == 'unallocated',
|
||||||
|
self.dynamic_schema_parser.spaces)
|
||||||
|
|
||||||
|
self.assertEqual(len(unallocated), 1)
|
||||||
|
self.assertEqual(unallocated[0].type, 'unallocated')
|
||||||
|
self.assertEqual(unallocated[0].none_order, True)
|
||||||
|
self.assertEqual(unallocated[0].weight, 0)
|
||||||
|
|
||||||
|
def test_aggregation_spaces_are_not_in_the_list(self):
|
||||||
|
spaces = filter(lambda d: d.type == 'vg',
|
||||||
|
self.dynamic_schema_parser.spaces)
|
||||||
|
self.assertEqual(len(spaces), 0)
|
||||||
|
|
||||||
|
def test_sets_best_with_disks_ids(self):
|
||||||
|
spaces = filter(lambda s: s.id == 'lv2',
|
||||||
|
self.dynamic_schema_parser.spaces)
|
||||||
|
|
||||||
|
self.assertEqual(len(spaces), 1)
|
||||||
|
self.assertEqual(spaces[0].best_with_disks, {'sdb', 'sdc'})
|
33
bareon_allocator/tests/test_parsers_expressions.py
Normal file
33
bareon_allocator/tests/test_parsers_expressions.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
from bareon_allocator.parsers import ExpressionsParser
|
||||||
|
from bareon_allocator.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestParsersExpressions(base.TestCase):
|
||||||
|
|
||||||
|
def test_substitutes_value_recursively(self):
|
||||||
|
parsed = ExpressionsParser(
|
||||||
|
[{'key1': 'key2'},
|
||||||
|
{'list': [{'list_key': 'yaql=$.some_key'}]}],
|
||||||
|
{'some_key': 'some_value'}).parse()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
parsed,
|
||||||
|
[{'key1': 'key2'},
|
||||||
|
{'list': [{'list_key': 'some_value'}]}])
|
67
bareon_allocator/tests/test_solvers_linear_program.py
Normal file
67
bareon_allocator/tests/test_solvers_linear_program.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from bareon_allocator.solvers import LinearProgram
|
||||||
|
from bareon_allocator.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestSolversLinearProgram(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestSolversLinearProgram, self).setUp()
|
||||||
|
self.lp = LinearProgram(
|
||||||
|
x_amount=3,
|
||||||
|
objective_function_coefficients=[1, 0, 0],
|
||||||
|
|
||||||
|
equality_constraint_matrix=[[1, 2, 3], [4, 5, 6]],
|
||||||
|
lower_constraint_matrix=[[7, 8, 9], [10, 11, 12]],
|
||||||
|
upper_constraint_matrix=[[13, 14, 15], [16, 17, 18]],
|
||||||
|
|
||||||
|
equality_constraint_vector=[1, 2, 3],
|
||||||
|
lower_constraint_vector=[3, 4, 5],
|
||||||
|
upper_constraint_vector=[6, 7, 8])
|
||||||
|
|
||||||
|
def test_values_are_set(self):
|
||||||
|
self.assertEqual(self.lp.x_amount, 3)
|
||||||
|
self.assertEqual(
|
||||||
|
self.lp.objective_function_coefficients,
|
||||||
|
[1, 0, 0])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.lp.equality_constraint_matrix,
|
||||||
|
[[1, 2, 3], [4, 5, 6]])
|
||||||
|
self.assertEqual(
|
||||||
|
self.lp.lower_constraint_matrix,
|
||||||
|
[[7, 8, 9], [10, 11, 12]])
|
||||||
|
self.assertEqual(
|
||||||
|
self.lp.upper_constraint_matrix,
|
||||||
|
[[13, 14, 15], [16, 17, 18]])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.lp.equality_constraint_vector,
|
||||||
|
[1, 2, 3])
|
||||||
|
self.assertEqual(
|
||||||
|
self.lp.lower_constraint_vector,
|
||||||
|
[3, 4, 5])
|
||||||
|
self.assertEqual(
|
||||||
|
self.lp.upper_constraint_vector,
|
||||||
|
[6, 7, 8])
|
||||||
|
|
||||||
|
def test_default_values_are_set(self):
|
||||||
|
self.assertEqual(self.lp.lp_type, self.lp.LP_TYPE_LP)
|
||||||
|
self.assertEqual(self.lp.objective_function_optimization_type,
|
||||||
|
self.lp.MAXIMIZE)
|
||||||
|
self.assertEqual(self.lp.bounds, [(0, None), (0, None), (0, None)])
|
173
bareon_allocator/tests/test_solvers_linear_program_creator.py
Normal file
173
bareon_allocator/tests/test_solvers_linear_program_creator.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from bareon_allocator.solvers import LinearProgramCreator
|
||||||
|
from bareon_allocator.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestSolversLinearProgramCreator(base.TestCase):
|
||||||
|
|
||||||
|
def create_lp(self, spaces_info=[], disks_info=[]):
|
||||||
|
dynamic_schema_mock = mock.MagicMock(spaces=[], disks=[])
|
||||||
|
for s in spaces_info:
|
||||||
|
dynamic_schema_mock.spaces.append(mock.MagicMock(**s))
|
||||||
|
|
||||||
|
for d in disks_info:
|
||||||
|
dynamic_schema_mock.disks.append(mock.MagicMock(**d))
|
||||||
|
|
||||||
|
return LinearProgramCreator(dynamic_schema_mock).linear_program()
|
||||||
|
|
||||||
|
def assert_lower_eq_exists(self, lp, eq, value):
|
||||||
|
self.assert_eq_and_value(
|
||||||
|
lp.lower_constraint_matrix,
|
||||||
|
lp.lower_constraint_vector,
|
||||||
|
eq,
|
||||||
|
value)
|
||||||
|
|
||||||
|
def assert_upper_eq_exists(self, lp, eq, value):
|
||||||
|
self.assert_eq_and_value(
|
||||||
|
lp.upper_constraint_matrix,
|
||||||
|
lp.upper_constraint_vector,
|
||||||
|
eq,
|
||||||
|
value)
|
||||||
|
|
||||||
|
def assert_eq_exists(self, lp, eq, value):
|
||||||
|
self.assert_eq_and_value(
|
||||||
|
lp.equality_constraint_matrix,
|
||||||
|
lp.equality_constraint_vector,
|
||||||
|
eq,
|
||||||
|
value)
|
||||||
|
|
||||||
|
def assert_eq_and_value(self, eq_list, eq_vector, expected_eq, value):
|
||||||
|
eq = None
|
||||||
|
i_eq = None
|
||||||
|
for i, _eq in enumerate(eq_list):
|
||||||
|
if _eq == expected_eq:
|
||||||
|
eq = _eq
|
||||||
|
i_eq = i
|
||||||
|
break
|
||||||
|
|
||||||
|
self.assertIsNotNone(eq, 'Cannot find equation')
|
||||||
|
self.assertEqual(eq_vector[i_eq], value,
|
||||||
|
'Value of equation does not match to expected')
|
||||||
|
|
||||||
|
def test_min_size_equations(self):
|
||||||
|
lp = self.create_lp(
|
||||||
|
spaces_info=[
|
||||||
|
{'min_size': 10},
|
||||||
|
{'min_size': 20},
|
||||||
|
{'min_size': 0}],
|
||||||
|
disks_info=[
|
||||||
|
{'id': 'sda', 'size': 50},
|
||||||
|
{'id': 'sda', 'size': 60}])
|
||||||
|
self.assert_lower_eq_exists(lp, [1, 0, 0, 1, 0, 0], 10)
|
||||||
|
self.assert_lower_eq_exists(lp, [0, 1, 0, 0, 1, 0], 20)
|
||||||
|
self.assert_lower_eq_exists(lp, [0, 0, 1, 0, 0, 1], 0)
|
||||||
|
|
||||||
|
def test_max_size_equations(self):
|
||||||
|
lp = self.create_lp(
|
||||||
|
spaces_info=[
|
||||||
|
{'max_size': 10},
|
||||||
|
{'max_size': 20},
|
||||||
|
{'min_size': 0}],
|
||||||
|
disks_info=[
|
||||||
|
{'id': 'sda', 'size': 50},
|
||||||
|
{'id': 'sda', 'size': 60}])
|
||||||
|
self.assert_upper_eq_exists(lp, [1, 0, 0, 1, 0, 0], 10)
|
||||||
|
self.assert_upper_eq_exists(lp, [0, 1, 0, 0, 1, 0], 20)
|
||||||
|
|
||||||
|
def test_disk_size_equations(self):
|
||||||
|
lp = self.create_lp(
|
||||||
|
spaces_info=[
|
||||||
|
{'max_size': 10},
|
||||||
|
{'max_size': 20},
|
||||||
|
{'min_size': 0}],
|
||||||
|
disks_info=[
|
||||||
|
{'id': 'sda', 'size': 50},
|
||||||
|
{'id': 'sda', 'size': 60}])
|
||||||
|
self.assert_upper_eq_exists(lp, [1, 1, 1, 0, 0, 0], 50)
|
||||||
|
self.assert_upper_eq_exists(lp, [0, 0, 0, 1, 1, 1], 60)
|
||||||
|
|
||||||
|
def test_weight_eq(self):
|
||||||
|
lp = self.create_lp(
|
||||||
|
spaces_info=[
|
||||||
|
{'id': 'v1', 'min_size': 20, 'max_size': None,
|
||||||
|
'best_with_disks': [], 'weight': 10},
|
||||||
|
{'id': 'v2', 'min_size': 20, 'max_size': None,
|
||||||
|
'best_with_disks': [], 'weight': 5},
|
||||||
|
{'id': 'v3', 'min_size': 30, 'max_size': None,
|
||||||
|
'best_with_disks': ['sda'], 'weight': 1},
|
||||||
|
{'id': 'v4', 'min_size': 30, 'max_size': None,
|
||||||
|
'best_with_disks': ['sda'], 'weight': 1}],
|
||||||
|
disks_info=[
|
||||||
|
{'id': 'sda', 'size': 100},
|
||||||
|
{'id': 'sdb', 'size': 200},
|
||||||
|
{'id': 'sdc', 'size': 300}])
|
||||||
|
|
||||||
|
self.assert_eq_exists(
|
||||||
|
lp,
|
||||||
|
[0.1, -0.2, 0, 0,
|
||||||
|
0.1, -0.2, 0, 0,
|
||||||
|
0.1, -0.2, 0, 0],
|
||||||
|
0)
|
||||||
|
|
||||||
|
self.assert_eq_exists(
|
||||||
|
lp,
|
||||||
|
[0, 0, 1.0, -1.0,
|
||||||
|
0, 0, 1.0, -1.0,
|
||||||
|
0, 0, 1.0, -1.0],
|
||||||
|
0)
|
||||||
|
|
||||||
|
def test_objective_function_equation(self):
|
||||||
|
lp = self.create_lp(
|
||||||
|
spaces_info=[
|
||||||
|
{'id': 'v0', 'min_size': 20, 'max_size': None,
|
||||||
|
'best_with_disks': [],
|
||||||
|
'weight': 10, 'none_order': False},
|
||||||
|
{'id': 'v1', 'min_size': 20, 'max_size': None,
|
||||||
|
'best_with_disks': [],
|
||||||
|
'weight': 5, 'none_order': False},
|
||||||
|
{'id': 'v2', 'min_size': 30, 'max_size': None,
|
||||||
|
'best_with_disks': ['sda'],
|
||||||
|
'weight': 1, 'none_order': False},
|
||||||
|
{'id': 'v3', 'min_size': 30, 'max_size': None,
|
||||||
|
'best_with_disks': ['sda'],
|
||||||
|
'weight': 1, 'none_order': False}],
|
||||||
|
disks_info=[
|
||||||
|
{'id': 'sda', 'size': 100},
|
||||||
|
{'id': 'sdb', 'size': 200},
|
||||||
|
{'id': 'sdc', 'size': 300}])
|
||||||
|
|
||||||
|
seq = [2, 4, 6, 9, 12, 16, 20, 25, 30, 36, 42, 49]
|
||||||
|
reverse_seq = [-1.0 / s for s in seq]
|
||||||
|
|
||||||
|
weight_indexes = [
|
||||||
|
2, # v2, sda
|
||||||
|
3, # v3, sda
|
||||||
|
4, # v0, sdb
|
||||||
|
5, # v1, sdb
|
||||||
|
8, # v0, sdc
|
||||||
|
9] # v1, sdc
|
||||||
|
|
||||||
|
for idx in weight_indexes:
|
||||||
|
# Substitute "set" coefficient
|
||||||
|
reverse_seq[idx] -= 2
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
lp.objective_function_coefficients,
|
||||||
|
reverse_seq)
|
@ -0,0 +1,55 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from bareon_allocator import errors
|
||||||
|
from bareon_allocator.solvers.linear_program import LinearProgram
|
||||||
|
from bareon_allocator.solvers.linear_programming_scipy_solver \
|
||||||
|
import LinearProgrammingScipySolver
|
||||||
|
from bareon_allocator.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestSolversLinearProgrammingScipySolver(base.TestCase):
|
||||||
|
|
||||||
|
def test_solves_lp(self):
|
||||||
|
# x = 1
|
||||||
|
# y = 1
|
||||||
|
# z + j >= 2
|
||||||
|
# x + y + z + j <= 4
|
||||||
|
lp = LinearProgram(
|
||||||
|
equality_constraint_matrix=[[1, 0, 0, 0], [0, 1, 0, 0]],
|
||||||
|
equality_constraint_vector=[1, 1],
|
||||||
|
lower_constraint_matrix=[[0, 0, 1, 1]],
|
||||||
|
lower_constraint_vector=[2],
|
||||||
|
upper_constraint_matrix=[[1, 1, 1, 1]],
|
||||||
|
upper_constraint_vector=[4],
|
||||||
|
objective_function_coefficients=[0, 0, 0, 0],
|
||||||
|
x_amount=4)
|
||||||
|
|
||||||
|
solver = LinearProgrammingScipySolver(lp)
|
||||||
|
self.assertEqual(
|
||||||
|
solver.solve(),
|
||||||
|
[1, 1, 2, 0])
|
||||||
|
|
||||||
|
def test_raises_error(self):
|
||||||
|
# 0 + 0 = 2
|
||||||
|
lp = LinearProgram(
|
||||||
|
equality_constraint_matrix=[[0, 0]],
|
||||||
|
equality_constraint_vector=[1],
|
||||||
|
objective_function_coefficients=[0, 0],
|
||||||
|
x_amount=2)
|
||||||
|
|
||||||
|
solver = LinearProgrammingScipySolver(lp)
|
||||||
|
self.assertRaises(errors.NoSolutionFound, solver.solve)
|
27
bareon_allocator/tests/test_solvers_utils.py
Normal file
27
bareon_allocator/tests/test_solvers_utils.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
from bareon_allocator.solvers.utils import round_vector_down
|
||||||
|
from bareon_allocator.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestSolversUtils(base.TestCase):
|
||||||
|
|
||||||
|
def test_round_down(self):
|
||||||
|
self.assertEqual(
|
||||||
|
round_vector_down([1.9, 2.0, 3.1]),
|
||||||
|
[1, 2, 3])
|
@ -673,7 +673,7 @@ or
|
|||||||
|
|
||||||
#. Build sets according to selected disks, in our case we have two sets, **hdd** and **ssd** disks.
|
#. Build sets according to selected disks, in our case we have two sets, **hdd** and **ssd** disks.
|
||||||
#. For spaces which belong to specific set of disks add **1** to a coefficient which represents this space on a disk from the set.
|
#. For spaces which belong to specific set of disks add **1** to a coefficient which represents this space on a disk from the set.
|
||||||
#. If space does not belong to the set of disks, add **0**.
|
#. Spaces which do not belong to any disks sets are assigned to set of disks which is left, in our case it is **hdd** disks set.
|
||||||
|
|
||||||
To make sure that spaces are always (unless size constraints are not violated) allocated on the disks which they best suited with,
|
To make sure that spaces are always (unless size constraints are not violated) allocated on the disks which they best suited with,
|
||||||
we automatically add a special artificial volume **unallocated**, whose coefficient is always **1**, and in this case we should change
|
we automatically add a special artificial volume **unallocated**, whose coefficient is always **1**, and in this case we should change
|
||||||
|
Loading…
x
Reference in New Issue
Block a user