(I posted a similar question a few days ago, but I have changed my approach given the answers in the last post and have a different approach)
I am trying to use scipy.optimize to solve my optimization problem, but I keep getting an incorrect answer, it just returns my initial guess (x0). Here, I am using the dual_annealing algorithm, but I have also tried different global optimization algorithms (differential_evolution, shgo) as well as local minimization (minimize with method SLSQP, but this caused problems as my function does not have a gradient) but to no avail.
For context, the program is trying to find the best way to allocate some product across multiple stores. Each stores has a forecast of what they are expected to sell in the following days (sales_data). This forecast does not necessarily have to be integers, or above 1 (it rarely is), it is an expectation in the statistical sense. So, if a store has sales_data = [0.33, 0.33, 0.33] the it is expected that after 3 days, they will sell 1 unit of product.
I want to minimize the total time it takes to sell the units i am allocating (i want to sell them the fastest) and my constraints are that I have to allocate the units that I have available, and I cannot allocate a negative number of product to a store. I am ok having non-integer allocations for now. For my initial allocations i am dividing the units I have available equally among all stores.
Thus, stated in a more mathematical way, I want to maximize the time_objective function, subject to the constraints that all allocations have to be of non-negative value (min(allocations) >= 0) and that I have to allocate all the units available (sum(allocations) == unitsAvailable). As dual_annealing does not support constraints, I deal with the first constraint by assigning 0 as a lower bound for every allocation (and unitsAvailable as the upper bound). For the second constraint, I wrap the objective function in the constrained_objective function, which returns numpy.inf if the constraint is violated. This constrained_objective function also takes all allocations but the last one, and sets the last allocation to the remainder units (as this is equivalent of constraining all of them, but does not require the sum of allocations to be exactly unitsAvailable).
Here is my code:
import numpy
import scipy.optimize as spo
unitsAvailable = 10
days = 50
class Store:
def __init__(self, num):
self.num = num
self.sales_data = []
# Mock Data
stores = []
for i in range(10):
# Identifier
stores.append(Store(i))
# Expected units to be sold that day (It's unlikey they will sell 1 every day)
stores[i].sales_data = [(i % 10) / 10 for x in range(days)]
def days_to_turn(alloc, store):
day = 0
inventory = alloc
while (inventory > 0 and day < days):
inventory -= store.sales_data[day]
day += 1
return day
def time_objective(allocations):
time = 0
for i in range(len(stores)):
time = max(time, days_to_turn(allocations[i], stores[i]))
return time
def constrained_objective(partial_allocs):
if numpy.sum(partial_allocs) > unitsAvailable:
# can't sell more than is available, so make the objective infeasible
return numpy.inf
# Partial_alloc contains allocations to all but one store.
# The final store gets allocated the remaining units.
allocs = numpy.append(partial_allocs, unitsAvailable - numpy.sum(partial_allocs))
return time_objective(allocs)
# Initial guess (x0)
guess_allocs = []
for i in range(len(stores)):
guess_allocs.append(unitsAvailable / len(stores))
guess_allocs = numpy.array(guess_allocs)
print('Optimizing...')
bounds = [(0, unitsAvailable)] * (len(stores))
time_solution = spo.dual_annealing(constrained_objective, bounds[:-1], x0=guess_allocs[:-1])
allocs = numpy.append(time_solution.x, unitsAvailable - numpy.sum(time_solution.x))
print("Allocations: " + str(allocs))
print("Days to turn: " + str(time_solution.fun))