2

I'm trying to design a constraint that is based on optimizing groups/buckets of elements rather than just individual elements. Here is a list of all the constraints I have:

  1. Total sum of all the elements must equal a certain value
  2. Total sum of each product (column sum of elements) must equal a certain value
  3. Bucket of size 'x', where every element in the bucket must be the same

Here is an example of what I'm after:

Constraints

Total Budget = 10000

Product-A Budget = 2500
Product-B Budget = 7500

Product-A Bucket Size = 3
Product-B Bucket Size = 2


Below is an illustration of the buckets (have placed them randomly, optimizer should decide where to best place the bucket within each column):

        Product-A   Product-B 
         _______                 
Week 1  |       |             
Week 2  |       |    ________ 
Week 3  |_______|   |        |
Week 4              |________|
Week 5                        

After passing the constraints to the optimizer, I want to the optimizer to allocate elements like this (every element inside the bucket should be equal):

Desired Optimizer Output

        Product-A   Product-B 
         _______                 
Week 1  |  500  |      150    
Week 2  |  500  |    __2350__ 
Week 3  |__500__|   |  1000  |
Week 4     700      |__1000__|
Week 5     300         3000    

Here is my attempt at trying to create the bucket constraint. I've used a simple, dummy objective function for demo purposes:

# Import Libraries
import pandas as pd
import numpy as np
import scipy.optimize as so
import random

# Define Objective function (Maximization)
def obj_func(matrix):
    return -np.sum(matrix)


# Create optimizer function
def optimizer_result(tot_budget, col_budget_list, bucket_size_list):

    # Create constraint 1) - total matrix sum range
    constraints_list = [{'type': 'eq', 'fun': lambda x: np.sum(x) - tot_budget},
                        {'type': 'eq', 'fun': lambda x: (sum(x[i] for i in range(0, 10, 5)) - col_budget_list[0])},
                        {'type': 'eq', 'fun': lambda x: (sum(x[i] for i in range(1, 10, 5)) - col_budget_list[1])},
                        {'type': 'eq', 'fun': lambda x, bucket_size_list[0]: [item for item in x for i in range(bucket_size_list[0])]},
                        {'type': 'eq', 'fun': lambda x, bucket_size_list[1]: [item for item in x for i in range(bucket_size_list[1])]}]

    # Create an inital matrix
    start_matrix = [random.randint(0, 3) for i in range(0, 10)]

    # Run optimizer
    optimizer_solution = so.minimize(obj_func, start_matrix, method='SLSQP', bounds=[(0, tot_budget)] * 10,
                                     tol=0.01,
                                     options={'disp': True, 'maxiter': 100}, constraints=constraints_list)
    return optimizer_solution


# Initalise constraints
tot_budget = 10000
col_budget_list = [2500, 7500]
bucket_size_list = [3, 2]


# Run Optimizer
y = optimizer_result(tot_budget, col_budget_list, bucket_size_list)
print(y)
star_it8293
  • 399
  • 3
  • 12
  • Do the buckets need to be contiguous? – Reinderien Apr 12 '23 at 15:09
  • @Reinderien yes buckets need to be contiguous and corrected typo (removed output_matrix) :) – star_it8293 Apr 12 '23 at 15:23
  • 2
    couple q's: is fractional production OK? (@joni solution). Is there an incentive to do things in a "bucket" (or batch)? What limits the weekly production? And **why isn't a uniform solution the best**, meaning 500 'A' /week and 1500 'B' /week which meets the quota and has uniform production throughout and satisfies all "bucket" requirements? – AirSquid Apr 12 '23 at 20:59
  • Hey @AirSquid, not too sure what you mean by factional production? Yep, there is an incentive to do bucket/batch optimisation as some of the budgets need to be allocated per program window - each program window can consist of any number of weeks (will be set by user). Regarding uniform solution - the actual model consists of multiple columns where each column has it's own function (piecewise non-linear function) thus the objective function is quite complicated which would mean that getting uniform solutions all the time would be unlikely. – star_it8293 Apr 13 '23 at 12:19
  • fractional production is ending up with some non-integer number per week, such as 120.3333 in each week of 3 weeks for a total of 161. sometimes this is a completely fine simplification as the total at the end will be integer. Not enforcing integrality can be a huge accelerant – AirSquid Apr 13 '23 at 14:15
  • @AirSquid, yep I'm after fractional production :) - for any large continuous values I will be able to round them – star_it8293 Apr 13 '23 at 15:15
  • @Reinderien Apologies for the delay, I have reworded the question in a new post for added clarity. Please if you can answer there, it would be greatly appreciated: https://stackoverflow.com/questions/76070398/batch-interval-optimisation – star_it8293 Apr 21 '23 at 06:23

1 Answers1

3

I'm not really sure if you really want to maximise something here or if you just want to find any feasible solution that satisfies all of your constraints. I'll assume the latter in this answer, but it's straightforward to include an objective function into the following model.

First of all, you'll need binary variables to model the decision which elements inside the same bucket must have the same values, so your problem becomes a mixed-integer linear optimization problem (MILP). Due to the fact that the buckets need to be contiguous, you can easily calculate all possible bucket intervals. This means that your optimization model only needs to decide which of these intervals is the best one.

It's also worth mentioning that the scipy.optimize.minimize doesn't support integer variables by default (you could try some penalty-approach, but I guess that's out of the scope for this answer). That's why you'll need a modelling library like python-mip, pyomo or PuLP to model your problem and pass it to an MILP solver.

Here's a solution by means of the PuLP package:

from pulp import LpProblem, LpVariable


# parameters
weeks = [1, 2, 3, 4, 5]
products = ["A", "B"]
total_budget = 10_000
col_budgets = {"A": 2500, "B": 7500}
bucket_sizes = {"A": 3, "B" : 2}
bucket_intervals= {
    "A": {0: [1, 2, 3], 1: [2, 3, 4], 2: [3, 4, 5]},
    "B": {0: [1, 2], 1: [2, 3], 2: [3, 4], 3: [4, 5]}
}

# create the model
mdl = LpProblem()

# x[i, j] is the (contiguous and positive) value of product i at week j
x = {(i, j) : LpVariable(name=f"x[{i},{j}]", cat="Continuous", lowBound=0.0) for i in products for j in weeks}

# b[i, k] = 1 if product i is placed inside the k-th possible bucket interval, 0 otherwise
b = {(i, k) : LpVariable(name=f"b[{i},{k}]", cat="Binary") for i in products for k in bucket_intervals[i].keys()}

# total sum of all elements must equal a certain value
mdl.addConstraint(sum(x[i, j] for i in products for j in weeks) == total_budget)

# total sum of each product must equal a certain value
for i in products:
    mdl.addConstraint(sum(x[i, j] for j in weeks) == col_budgets[i])

# we can only choose exactly one interval for the bucket of product i
for i in products:
    mdl.addConstraint(sum(b[i, k] for k in bucket_intervals[i].keys()) == 1)

# Every element in the bucket must be the same
# If b[i, k] == 1, then all x[i, t] (with t in bucket_intervals[i][k]) have the same value 
for i in products:
    for k in bucket_intervals[i].keys():
        for t in bucket_intervals[i][k][:-1]:
            # b[i, k] == 1 ---> x[i, t] == x[i, t + 1]
            bigM = total_budget 
            mdl.addConstraint(0 - bigM * (1 - b[i, k]) <= x[i, t] - x[i, t + 1])
            mdl.addConstraint(0 + bigM * (1 - b[i, k]) >= x[i, t] - x[i, t + 1])

mdl.solve()

# print solution
for i in products:
    for j in weeks:
        val = x[i, j].varValue
        print(f"x[{i},{j}] = {val}")

which gives me

x[A,1] = 833.33333
x[A,2] = 833.33333
x[A,3] = 833.33333
x[A,4] = 0.0
x[A,5] = 0.0
x[B,1] = 0.0
x[B,2] = 3750.0
x[B,3] = 3750.0
x[B,4] = 0.0
x[B,5] = 0.0
joni
  • 6,840
  • 2
  • 13
  • 20
  • Many thanks @joni, to clarify I'm trying to maximize here. Kindly requesting how you would go about optimising the intervals along with the values with in each interval? Also is there any way to implement this using Scipy optimise? – star_it8293 Apr 13 '23 at 13:25
  • Do you want to maximize the values of each x[A, 1], ..., x[B, 5]? Then simply maximize the sum of all x in the objective function. And scipy.optimize.minimize is the wrong tool for this kind of problems as it it's made for contiguous nonliear optimization problems, not integer optimization problems. – joni Apr 13 '23 at 13:46
  • 1
    @star_it8293 It really isn't clear what you mean by "optimize the intervals" or the values in the interval as there is no expressed incentive or constraint to do anything other than what is expressed in the answer above. You may need to further augment your question with some examples that illuminate either some constraint or incentive to do something different than above if that is desired. – AirSquid Apr 13 '23 at 14:39
  • @joni I'm after just maximising the objective function. Just to clarify, in reality each product (column) is made up of an inverse exponential curve (parameters are slightly varied from column to column). Thus the objective function is a piecewise non-linear function hence it will be a non-linear problem rather than an integer optimisation problem :) – star_it8293 Apr 13 '23 at 18:46
  • @AirSquid Apologies, will try to re-phrase. Basically what I'm trying to do is optimise for batches of weeks rather than individual weeks. – star_it8293 Apr 13 '23 at 18:48
  • 2
    @star_it8293 You again use the term "optimize for batches of weeks" without explanation or example. For all we know, "optimizing" could mean make the batches as big as possible, or make the batches non-overlap, or make the weekly production as smooth as possible, or make the batches as soon as feasible within some mystery constraint. Strongly suggest you include some examples, reasoning, and any other math (like obj function in an edit to your question if you want more assistance. – AirSquid Apr 13 '23 at 18:59
  • @AirSquid Apologies for the delay, I have reworded the question in a new post for added clarity. Please if you can answer there, it would be greatly appreciated: https://stackoverflow.com/questions/76070398/batch-interval-optimisation – star_it8293 Apr 21 '23 at 06:22
  • @joni Apologies for the delay, I have reworded the question in a new post for added clarity. Please if you can answer there, it would be greatly appreciated: https://stackoverflow.com/questions/76070398/batch-interval-optimisation – star_it8293 Apr 21 '23 at 06:23