Python Pulp linear programming with dynamic constraint - python

I currently use Solver in excel to find somewhat optimal solution for manufacturing. Here is the current setup:
It regards manufacturing shoes on a rotary machine, that is, production is done in batches of repetitions. For example one batch would be '10x A1' (see A1 in table) which would yield 10x size 36, 20x size 37... 10x size 41.
There are some prefixed setups; A1, A2; R7... as you see in the table above.
Then there is the requested variable (or rather a list of variables) that basically says what the customer requested, quantities per size.
The objective function is to find a set of repetitions such that it matches requested quantities as closely as possible. Hence in the solver (sorry for non-English screenshot) you can see the goal is N21 (that is the sum of absolute differences per size). The variables are N2:N9 - that's the repetitions per setup and the only constraint is that N2:N9 is an integer.
How can I model this behaviour with python? My start:
from collections import namedtuple
from pulp import *
class Setup(namedtuple('IAmReallyLazy', 'name ' + ' '.join(f's{s}' for s in range(36, 47)))):
# inits with name and sizes 's36', 's37'... 's46'
repetitions = 0
setups = [
Setup('A1', 1, 2, 3, 3, 2, 1, 0, 0, 0, 0, 0),
Setup('A2', 0, 1, 2, 3, 3, 2, 1, 0, 0, 0, 0),
Setup('R7', 0, 0, 1, 1, 1, 1, 2, 0, 0, 0, 0),
Setup('D1', 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
# and others
]
setup_names = [s.name for s in setups]
requested = {
's36': 100,
's37': 250,
's38': 300,
's39': 450,
's40': 450,
's41': 250,
's42': 200,
}
def get_quantity_per_size(size: str) -> int:
return sum([getattr(setup, size) * setup.repetitions for setup in setups])
def get_abs_diff(size: str) -> int:
requested_size = requested.get(size, 0)
return abs(get_quantity_per_size(size) - requested_size)
problem = LpProblem('Optimize Batches', LpMinimize)
# goal is to minimise the sum(get_abs_diff(f's{size}') for size in range(36, 47))
# variables are [setup.repetitions for setup in setups]
# constraints are all([isinstance(setup.repetitions, int) for setup in setups])
In an ideal world if there is more than one optimal solution, the one with most spread abs diff should be selected (ie. the one with the smallest highest difference). That is, if one solution has abs diff of 10 per size and 10 sizes (total 100) and other has 20 + 80 = 100, the first one is more optimal for customer.
Another constraint should be min(setup.repetitions for setup in setups if setup.repetitions > 0) > 9 basically the repetitions constraint should be:
Is integer
Is either 0 or greater than 9 - This is not possible in linear programming from what I've understood so far though.

A few things here. First, if you use abs() then the problem will be nonlinear. Instead, you should introduce new variables called, say, over_mfg and under_mfg, that represent the number of units produced above of the target and the number below the target, respectively. You can declare these like this:
over_mfg = LpVariable.dicts("over_mfg", sizes, 0, None, LpInteger)
under_mfg = LpVariable.dicts("under_mfg", sizes, 0, None, LpInteger)
I declared a list called sizes, which is used in the definitions above:
min_size = 36
max_size = 46
sizes = ['s' + str(s) for s in range(min_size, max_size+1)]
You also need variables indicating the repetitions of each setup:
repetitions = LpVariable.dicts("repetitions", setup_names, 0, None, LpInteger)
Your objective function is then declared as:
problem += lpSum([over_mfg[size] + under_mfg[size] for size in sizes])
(Note that in pulp you use lpSum rather than sum.) Now, you need constraints that say that over_mfg is the excess and under_mfg is the shortfall:
for size in sizes:
problem += over_mfg[size] >= lpSum([repetitions[setup.name] * getattr(setup, size) for setup in setups]) - requested[size], "DefineOverMfg" + size
problem += under_mfg[size] >= requested[size] - lpSum([repetitions[setup.name] * getattr(setup, size) for setup in setups]), "DefineUnderMfg" + size
Note also that I didn't use your get_quantity_per_size() and get_abs_diff() functions. These will also confuse pulp since it won't realize that these are simple linear functions.
Here is my complete code:
from collections import namedtuple
from pulp import *
class Setup(namedtuple('IAmReallyLazy', 'name ' + ' '.join(f's{s}' for s in range(36, 47)))):
# inits with name and sizes 's36', 's37'... 's46'
repetitions = 0
setups = [
Setup('A1', 1, 2, 3, 3, 2, 1, 0, 0, 0, 0, 0),
Setup('A2', 0, 1, 2, 3, 3, 2, 1, 0, 0, 0, 0),
Setup('R7', 0, 0, 1, 1, 1, 1, 2, 0, 0, 0, 0),
Setup('D1', 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
# and others
]
setup_names = [s.name for s in setups]
min_size = 36
max_size = 46
sizes = ['s' + str(s) for s in range(min_size, max_size+1)]
requested = {
's36': 100,
's37': 250,
's38': 300,
's39': 450,
's40': 450,
's41': 250,
's42': 200,
's43': 0, # I added these for completeness
's44': 0,
's45': 0,
's46': 0
}
problem = LpProblem('Optimize Batches', LpMinimize)
# goal is to minimise the sum(get_abs_diff(f's{size}') for size in range(36, 47))
# variables are [setup.repetitions for setup in setups]
# constraints are all([isinstance(setup.repetitions, int) for setup in setups])
repetitions = LpVariable.dicts("repetitions", setup_names, 0, None, LpInteger)
over_mfg = LpVariable.dicts("over_mfg", sizes, 0, None, LpInteger)
under_mfg = LpVariable.dicts("under_mfg", sizes, 0, None, LpInteger)
problem += lpSum([over_mfg[size] + under_mfg[size] for size in sizes])
for size in sizes:
problem += over_mfg[size] >= lpSum([repetitions[setup.name] * getattr(setup, size) for setup in setups]) - requested[size], "DefineOverMfg" + size
problem += under_mfg[size] >= requested[size] - lpSum([repetitions[setup.name] * getattr(setup, size) for setup in setups]), "DefineUnderMfg" + size
# Solve problem
problem.solve()
# Print status
print("Status:", LpStatus[problem.status])
# Print optimal values of decision variables
for v in problem.variables():
if v.varValue is not None and v.varValue > 0:
print(v.name, "=", v.varValue)
Here is the output:
Status: Optimal
over_mfg_s38 = 62.0
over_mfg_s41 = 62.0
repetitions_A1 = 25.0
repetitions_A2 = 88.0
repetitions_D1 = 110.0
repetitions_R7 = 1.0
under_mfg_s36 = 75.0
under_mfg_s37 = 2.0
under_mfg_s40 = 25.0
So, we manufacture 25 repetitions of A1, 88 of A2, and 110 of D1, and 1 of R7. This gives 25 units of s36 (hence 75 units under the target of 100); 248 units of s37 (2 under target); 362 units of s38 (62 units over the target of 300); and so on.
Now, for your constraint that says you either have to produce 0 of a setup or >9, you can introduce new binary variables indicating whether each setup is produced:
is_produced = LpVariable.dicts("is_produced", setup_names, 0, 1, LpInteger)
Then add these constraints:
M = 1000
min_reps = 9
for s in setup_names:
problem += M * is_produced[s] >= repetitions[s] # if it's produced at all, must set is_produced = 1
problem += min_reps * (1 - is_produced[s]) + repetitions[s] >= min_reps
M is a big number; it should be bigger than the largest possible number of repetitions, but no bigger. And I defined min_reps to avoid "hard-coding" the 9 in the constraints. So, these constraints say that (1) if repetitions[s] > 0, then is_produced[s] must equal 1, and (2) either is_produced[s] = 1 or repetitions[s] > 9.
The output:
Status: Optimal
is_produced_A1 = 1.0
is_produced_A2 = 1.0
is_produced_D1 = 1.0
over_mfg_s38 = 63.0
over_mfg_s39 = 1.0
over_mfg_s41 = 63.0
repetitions_A1 = 25.0
repetitions_A2 = 88.0
repetitions_D1 = 112.0
under_mfg_s36 = 75.0
under_mfg_s40 = 24.0
Note that now we don't have any setups with >0 but <9 repetitions.
In an ideal world if there is more than one optimal solution, the one with most spread abs diff should be selected (ie. the one with the smallest highest difference).
This one is trickier, and (at least for now), I'll leave this to an ideal world, or another person's answer.
By the way: There is an effort to launch a Stack Exchange site for Operations Research, where we'll be all over questions like this one. If you're interested, I encourage you to click the link and "commit".

Related

Simple Mutation with a Probability

As per the section of code below, I am trying to implement an automatic and random mutation process.
data = [0,1,0,0,0,0,0,1,0,0,1,1,0,0,1]
data[random.randint(0,len(data)-1)]=random.randrange(0,1)
print(data)
The code is an adaptation of some other posts I have found, although it is randomly mutating a value every time with either a 0 or 1. I require this to occur with only a certain probability (such as a 0.05 chance of mutation) rather than always being guaranteed.
Additionally, often a 0 is being replaced with a 0 and therefore there is no change to the output, so I would like to limit it in a way that a 0 will only mutate to a 1 and a 1 mutates to a 0.
I would really appreciate the assistance in resolving these two issues.
Resume
mutate any value with a choosen probability
randomly choose the position
when the position is choosen, switch between 0 and 1
def mutate(data, proba=0.05):
if random.random() < proba:
data[random.randrange(len(data))] ^= 1
if __name__ == '__main__':
data = [0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1]
for i in range(10):
mutate(data)
print(data)
import random
def changeData(data):
seed = random.randint(0,1000)
# probability of 0.05 (50 / 1000)
if seed <= 50:
indexToChange = random.randint(0,len(data)-1)
# change 0 with 1 and viceversa
data[indexToChange] = 1 if data[indexToChange] == 0 else 0
if __name__== '__main__':
data = [0,1,0,0,0,0,0,1,0,0,1,1,0,0,1]
for i in range(0,100):
changeData(data)
print(data)
You can do as following:
For each element in data, mutate it (1 - val) only if a random value generated by random() function is less than the defined mutation probability.
For example:
import random
mutation_prob = 0.05
data = [0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1]
mutated_data = [1 - x if random.random() < mutation_prob else x for x in data]
If the mutation should be decided regarding the data as a whole, you can do:
mutation_prob = 0.05
data = [0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1]
do_mutation = random.random() < mutation_prob
mutated_data = [1 - x if do_mutation else x for x in data]

compare two arrays to make an accuracy of KNN prediction

I have two arrays from which I have to find the accuracy of my prediction.
predictions = [1, 0, 0, 1, 1, 1, 0, 1, 1, 0]
y_test = [1, 0, 0, 1, 0, 1, 0, 1, 1, 1]
so in this case, the accuracy is = (8/10)*100 = 80%
I have written a method to do this task. Here is my code, but I dont get the accuracy of 80% in this case.
def getAccuracy(y_test, predictions):
correct = 0
for x in range(len(y_test)):
if y_test[x] is predictions[x]:
correct += 1
return (correct/len(y_test)) * 100.0
Thanks for helping me.
You're code should work, if the numbers in the arrays are in a specific range that are not recreated by the python interpreter. This is because you used is which is an identity check and not an equality check. So, you are checking memory addresses, which are only equal for a specific range of numbers. So, use == instead and it will always work.
For a more Pythonic solution you can also take a look at list comprehensions:
assert len(predictions) == len(y_test), "Unequal arrays"
identity = sum([p == y for p, y in zip(predictions, y_test)]) / len(predictions) * 100
if you want to take 80.0 as result for your example, It's doing that.
Your code gives 80.0 as you wanted, however you should use == instead of is, see the reason.
def getAccuracy(y_test, predictions):
n = len(y_test)
correct = 0
for x in range(n):
if y_test[x] == predictions[x]:
correct += 1
return (correct/n) * 100.0
predictions = [1, 0, 0, 1, 1, 1, 0, 1, 1, 0]
y_test = [1, 0, 0, 1, 0, 1, 0, 1, 1, 1]
print(getAccuracy(y_test, predictions))
80.0
Here's an implementation using Numpy:
import numpy as np
n = len(y_test)
100*np.sum(np.isclose(predictions, y_test))/n
or if you convert your lists to numpy arrays, then
100*np.sum(predictions == y_test)/n

Calculate stationary distribution of Markov chain in Python

I've been working on a Google foobar problem for a couple of days and have all but one test passing, and I'm pretty stuck at this point. Let me know if you have any ideas! I'm using a method described here, and I have a working example up on repl.it here. Here's the problem spec:
Doomsday Fuel
Making fuel for the LAMBCHOP's reactor core is a tricky process because of the exotic matter involved. It starts as raw ore, then during processing, begins randomly changing between forms, eventually reaching a stable form. There may be multiple stable forms that a sample could ultimately reach, not all of which are useful as fuel.
Commander Lambda has tasked you to help the scientists increase fuel creation efficiency by predicting the end state of a given ore sample. You have carefully studied the different structures that the ore can take and which transitions it undergoes. It appears that, while random, the probability of each structure transforming is fixed. That is, each time the ore is in 1 state, it has the same probabilities of entering the next state (which might be the same state). You have recorded the observed transitions in a matrix. The others in the lab have hypothesized more exotic forms that the ore can become, but you haven't seen all of them.
Write a function answer(m) that takes an array of array of nonnegative ints representing how many times that state has gone to the next state and return an array of ints for each terminal state giving the exact probabilities of each terminal state, represented as the numerator for each state, then the denominator for all of them at the end and in simplest form. The matrix is at most 10 by 10. It is guaranteed that no matter which state the ore is in, there is a path from that state to a terminal state. That is, the processing will always eventually end in a stable state. The ore starts in state 0. The denominator will fit within a signed 32-bit integer during the calculation, as long as the fraction is simplified regularly.
*For example, consider the matrix m:
[
[0,1,0,0,0,1], # s0, the initial state, goes to s1 and s5 with equal probability
[4,0,0,3,2,0], # s1 can become s0, s3, or s4, but with different probabilities
[0,0,0,0,0,0], # s2 is terminal, and unreachable (never observed in practice)
[0,0,0,0,0,0], # s3 is terminal
[0,0,0,0,0,0], # s4 is terminal
[0,0,0,0,0,0], # s5 is terminal
]
So, we can consider different paths to terminal states, such as:
s0 -> s1 -> s3
s0 -> s1 -> s0 -> s1 -> s0 -> s1 -> s4
s0 -> s1 -> s0 -> s5
Tracing the probabilities of each, we find that
s2 has probability 0
s3 has probability 3/14
s4 has probability 1/7
s5 has probability 9/14
So, putting that together, and making a common denominator, gives an answer in the form of
[s2.numerator, s3.numerator, s4.numerator, s5.numerator, denominator] which is
[0, 3, 2, 9, 14].*
Test cases
Inputs:
(int) m = [[0, 2, 1, 0, 0], [0, 0, 0, 3, 4], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
Output:
(int list) [7, 6, 8, 21]
Inputs:
(int) m = [[0, 1, 0, 0, 0, 1], [4, 0, 0, 3, 2, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]]
Output:
(int list) [0, 3, 2, 9, 14]
Here's my code so far.
from __future__ import division
from itertools import compress
from itertools import starmap
from operator import mul
import fractions
def convertMatrix(transMatrix):
probMatrix = []
for i in range(len(transMatrix)):
row = transMatrix[i]
newRow = []
rowSum = sum(transMatrix[i])
if all([v == 0 for v in transMatrix[i]]):
for j in transMatrix[i]:
newRow.append(0)
newRow[i] = 1
probMatrix.append(newRow)
else:
for j in transMatrix[i]:
if j == 0:
newRow.append(0)
else:
newRow.append(j/rowSum)
probMatrix.append(newRow)
return probMatrix
def answer(m):
# convert matrix numbers into probabilities
probMatrix = convertMatrix(m)
# find terminal states
terminalStateFilter = []
for row in range(len(m)):
if all(x == 0 for x in m[row]):
terminalStateFilter.append(True)
else:
terminalStateFilter.append(False)
# multiply matrix by probability vector
oldFirstRow = probMatrix[0]
probVector = None
for i in range(3000):
probVector = [sum(starmap(mul, zip(oldFirstRow, col))) for col in zip(*probMatrix)]
oldFirstRow = probVector
# generate numerators
numerators = []
for i in probVector:
numerator = fractions.Fraction(i).limit_denominator().numerator
numerators.append(numerator)
# generate denominators
denominators = []
for i in probVector:
denominator = fractions.Fraction(i).limit_denominator().denominator
denominators.append(denominator)
# calculate factors to multiply numerators by
factors = [max(denominators)/x for x in denominators]
# multiply numerators by factors
numeratorsTimesFactors = [a*b for a,b in zip(numerators, factors)]
# filter numerators by terminal state booleans
terminalStateNumerators = list(compress(numeratorsTimesFactors, terminalStateFilter))
# append numerators and denominator to answer
answerlist = []
for i in terminalStateNumerators:
answerlist.append(i)
answerlist.append(max(denominators))
return list(map(int, answerlist))

Using the FFT to multiply two binary numbers

I'm completeing an assignment but a little stuck on part 5 and 6. It essentially does: Using the FFT to multiply two binary numbers. I was wondering if someone could help out.
# The binary numbers and their product
a_bin = 0b100100100100
b_bin = 0b111000111000
c_bin = a_bin * b_bin
print('The product of a and b is', c_bin)
# (i) The coefficients of the polynomials A and B
Acoeff = [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]
Bcoeff = [0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1]
# (ii) the value representations of A and B
Aval = np.fft(Acoeff, 32)
Bval = np.fft(Bcoeff, 32)
# (iii) The value representation of C
Cval = []
for i in range(len(Aval)):
Cval.append(Aval[i] * Bval[i])
print(Cval)
# (iv) The coefficients of the polynomial C
Ccoeff = np.ifft(Cval)
# we'll get rid of the imaginary parts, which are just numerical errors
for i, c in enumerate(Ccoeff):
Ccoeff[i] = int(round(c.real))
# (v) calculate the product by evaluating the polynomial at 2, i.e., C(2)
# (You may need to take the real part at the end if there is a small imaginary component)
prod = 0
print('Using the FFT the product of a and b is', int(round(prod.real)))
# (vi) write code to calculate the binary digits of c directly from the coefficients of C, Ccoeff.
# hint: You can use (q,r) = divmod(x, 2) to find the quotient and remainder of x when divided by 2

Vectorizing complex assignment logic in numpy

I have some complex assignment logic in a simulation that I would like to optimize for performance. The current logic is implemented as a set of nested for loops over a variety of numpy arrays. I would like to vectorize this assignment logic but haven't been able to figure out if this is possible
import numpy as np
from itertools import izip
def reverse_enumerate(l):
return izip(xrange(len(l)-1, -1, -1), reversed(l))
materials = np.array([[1, 0, 1, 1],
[1, 1, 0, 0],
[0, 1, 1, 1],
[1, 0, 0, 1]])
vectors = np.array([[1, 1, 0, 0],
[0, 0, 1, 1]])
prices = np.array([10, 20, 30, 40])
demands = np.array([1, 1, 1, 1])
supply_by_vector = np.zeros(len(vectors)).astype(int)
#go through each material and assign it to the first vector that the material covers
for m_indx, material in enumerate(materials):
#find the first vector where the material covers the SKU
for v_indx, vector in enumerate(vectors):
if (vector <= material).all():
supply_by_vector[v_indx] = supply_by_vector[v_indx] + 1
break
original_supply_by_vector = np.copy(supply_by_vector)
profit_by_vector = np.zeros(len(vectors))
remaining_ask_by_sku = np.copy(demands)
#calculate profit by assigning material from vectors to SKUs to satisfy demand
#go through vectors in reverse order (so lowest priority vectors are used up first)
profit = 0.0
for v_indx, vector in reverse_enumerate(vectors):
for sku_indx, price in enumerate(prices):
available = supply_by_vector[v_indx]
if available == 0:
continue
ask = remaining_ask_by_sku[sku_indx]
if ask <= 0:
continue
if vector[sku_indx]:
assign = ask if available > ask else available
remaining_ask_by_sku[sku_indx] = remaining_ask_by_sku[sku_indx] - assign
supply_by_vector[v_indx] = supply_by_vector[v_indx] - assign
profit_by_vector[v_indx] = profit_by_vector[v_indx] + assign*price
profit = profit + assign * price
print 'total profit:', profit
print 'unfulfilled demand:', remaining_ask_by_sku
print 'original supply:', original_supply_by_vector
result:
total profit: 80.0
unfulfilled demand: [0 1 0 0]
original supply: [1 2]
It seems there is a dependency between iterations within the innermost nested loop in the second part/group of the nested loops and that to me seemed like difficult if not impossible to vectorize. So, this post is basically a partial solution trying to vectorize instead the first group of two nested loops, which were -
supply_by_vector = np.zeros(len(vectors)).astype(int)
for m_indx, material in enumerate(materials):
#find the first vector where the material covers the SKU
for v_indx, vector in enumerate(vectors):
if (vector <= material).all():
supply_by_vector[v_indx] = supply_by_vector[v_indx] + 1
break
That entire section could be replaced by one line of vectorized code, like so -
supply_by_vector = ((vectors[:,None] <= materials).all(2)).sum(1)

Categories