Calculate stationary distribution of Markov chain in Python - 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))

Related

Python Pulp linear programming with dynamic constraint

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".

Randomize part of an array

I'm working on a project involving binary patterns (here np.arrays of 0 and 1).
I'd like to modify a random subset of these and return several altered versions of the pattern where a given fraction of the values have been changed (like map a function to a random subset of an array of fixed size)
ex : take the pattern [0 0 1 0 1] and rate 0.2, return [[0 1 1 0 1] [1 0 1 0 1]]
It seems possible by using auxiliary arrays and iterating with a condition, but is there a "clean" way to do that ?
Thanks in advance !
The map function works on boolean arrays too. You could add the subsample logic to your function, like so:
import numpy as np
rate = 0.2
f = lambda x: np.random.choice((True, x),1,p=[rate,1-rate])[0]
a = np.array([0,0,1,0,1], dtype='bool')
map(f, a)
# This will output array a with on average 20% of the elements changed to "1"
# it can be slightly more or less than 20%, by chance.
Or you could rewrite a map function, like so:
import numpy as np
def map_bitarray(f, b, rate):
'''
maps function f on a random subset of b
:param f: the function, should take a binary array of size <= len(b)
:param b: the binary array
:param rate: the fraction of elements that will be replaced
:return: the modified binary array
'''
c = np.copy(b)
num_elem = len(c)
idx = np.random.choice(range(num_elem), num_elem*rate, replace=False)
c[idx] = f(c[idx])
return c
f = lambda x: True
b = np.array([0,0,1,0,1], dtype='bool')
map_bitarray(f, b, 0.2)
# This will output array b with exactly 20% of the elements changed to "1"
rate=0.2
repeats=5
seed=[0,0,1,0,1]
realizations=np.tile(seed,[repeats,1]) ^ np.random.binomial(1,rate,[repeats,len(seed)])
Use np.tile() to generate a matrix from the seed row.
np.random.binomial() to generate a binomial mask matrix with your requested rate.
Apply the mask with the xor binary operator ^
EDIT:
Based on #Jared Goguen comments, if you want to change 20% of the bits, you can elaborate a mask by choosing elements to change randomly:
seed=[1,0,1,0,1]
rate=0.2
repeats=10
mask_list=[]
for _ in xrange(repeats):
y=np.zeros(len(seed),np.int32)
y[np.random.choice(len(seed),0.2*len(seed))]=1
mask_list.append(y)
mask = np.vstack(mask_list)
realizations=np.tile(seed,[repeats,1]) ^ mask
So, there's already an answer that provides sequences where each element has a random transition probability. However, it seems like you might want an exact fraction of the elements to change instead. For example, [1, 0, 0, 1, 0] can change to [1, 1, 0, 1, 0] or [0, 0, 0, 1, 0], but not [1, 1, 1, 1, 0].
The premise, based off of xvan's answer, uses the bit-wise xor operator ^. When a bit is xor'd with 0, it's value will not change. When a bit is xor'd with 1, it will flip. From your question, it seems like you want to change len(seq)*rate number of bits in the sequence. First create mask which contains len(seq)*rate number of 1's. To get an altered sequence, xor the original sequence with a shuffled version of mask.
Here's a simple, inefficient implementation:
import numpy as np
def edit_sequence(seq, rate, count):
length = len(seq)
change = int(length * rate)
mask = [0]*(length - change) + [1]*change
return [seq ^ np.random.permutation(mask) for _ in range(count)]
rate = 0.2
seq = np.array([0, 0, 1, 0, 1])
print edit_sequence(seq, rate, 5)
# [0, 0, 1, 0, 0]
# [0, 1, 1, 0, 1]
# [1, 0, 1, 0, 1]
# [0, 1, 1, 0, 1]
# [0, 0, 0, 0, 1]
I don't really know much about NumPy, so maybe someone with more experience can make this efficient, but the approach seems solid.
Edit: Here's a version that times about 30% faster:
def edit_sequence(seq, rate, count):
mask = np.zeros(len(seq), dtype=int)
mask[:len(seq)*rate] = 1
output = []
for _ in range(count):
np.random.shuffle(mask)
output.append(seq ^ mask)
return output
It appears that this updated version scales very well with the size of seq and the value of count. Using dtype=bool in seq and mask yields another 50% improvement in the timing.

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)

build matrix from blocks

I have an object which is described by two quantities, A and B (in real case they can be more than two). Objects are correlated depending on the value of A and B. In particular I know the correlation matrix for A and for B. Just as example:
a = np.array([[1, 1, 0, 0],
[1, 1, 0, 0],
[0, 0, 1, 1],
[0, 0, 1, 1]])
b = np.array([[1, 1, 0],
[1, 1, 1],
[0, 1, 1]])
na = a.shape[0]
nb = b.shape[0]
correlation for A:
so if an element has A == 0.5 and the other equal to A == 1.5 they are fully correlated (red). Otherwise if an element has A == 0.5 and the second item has A == 3.5 they are uncorrelated (blue).
Similarly for B:
Now I want multiply the two correlation matrixes, but I want to obtain as final matrix a matrix with two axis, where the new axes are a folded version of the original axes:
def get_folded_bin(ia, ib):
return ia * nb + ib
here what I am doing:
result = np.swapaxes(np.tensordot(a, b, axes=0), 1, 2).reshape(na* nb, na * nb)
visually:
and in particular this must hold:
for ia1 in xrange(na):
for ia2 in xrange(na):
for ib1 in xrange(nb):
for ib2 in xrange(nb):
assert(a[ia1, ia2] * b[ib1, ib2] == result[get_folded_bin(ia1, ib1), get_folded_bin(ia2, ib2)])
actually my problem is to do it with more quantities (A, B, C, ...) in a general way. Maybe there is also a simpler function within numpy to do that.
np.einsum lets you simplify the tensordot expression a bit:
result = np.einsum('ij,kl->ikjl',a,b).reshape(-1, na * nb)
I don't think there's a way of eliminating the reshape.
It may also be easier to generalize to more arrays, though I wouldn't get carried away with too many iteration variables in one einsum expression.
I think finally I have found a solution:
np.kron(a,b)
and then I can compose with
np.kron(np.kron(a,b), c)

Categories