I am trying to solve an optimization problem applied to bridges, in the context of a parametric design. The objective function is to try and split precast barriers into a minimum amount of unique barriers. There are two kind of barriers, normal and barriers with light poles. I have shared an image with a bridge of 3 spans.
The parameters are as follows:
The light pole barrier width is equal to w.
The light pole barriers are evenly spaced by the parameter s.
The light pole starting point is defined by the variable a.
The spans of the bridge, [l1,l2,...ln]
x is the width that all of the equations will have to be divisible by.
The idea is to as many barriers with the width of x and some remaining lengths to fill the spans kept at a minimum. I have to get all the distances [a,b,c,...f] in an array of equations (see picture), where I am also appending the light between the special barriers (s-w), and have all of them divisible by x. I also added in the elements arr array to account for constants in case the remaining length is not divisible by x so we will compensate with a different barrier (basically, we use that constant to subtract).
Here is a sketch of what I am talking about:
from scipy.optimize import minimize
import numpy as np
s = 12500
spans = np.array([25450, 25100, 25450])
ejs = np.zeros(len(spans)-1) # expansion or separation joints . ignore for now
min_bw, max_bw = 0, 1200 #light pole barrier width
consts = abs((len(spans) * 2 - 1)) # Number of distance elements [b,c,d...]
init = np.array([1500, 1500, 6250]) #x, w, a + w/2
n = (spans - s / 2) // s # consecutive spacings of barriers for a given span. At times we have more
m = np.ones(len(spans) * 2) # the length of the equation array that has to be divisible
def objective(arr, n, s, spans, ejs):
x = arr[0]
w = arr[1]
a = arr[2] # (the offset is until the mid axis of the light pole)
m = np.ones(len(spans) * 2 + 1) # We make room for the last constraint h=(s-w)
m[0] = a - w / 2
for i in range(1, len(spans) * 2):
j = (i - 1) // 2
if i % 2 == 0:
m[i] = (s - w) - (m[i - 1] + ejs[j] + arr[2 + i])
else:
m[i] = spans[j] - (m[i - 1] + n[j] * s + w + arr[2 + i])
m[-1] = s - w # We also need the light distance between two poles to be divisible by x
#print(m)
return np.sum(m%x)
#Bounds
bounds = np.array([(1500, 2100), (1500, 2600), (3000, s)])
extras = np.repeat([(min_bw, max_bw)], consts, axis=0) # Add the bounds for each additional element
all_bounds = np.concatenate((bounds, extras), axis=0)
initial = np.pad(init, (0, consts), 'constant', constant_values=min_bw)
sol = minimize(fun=objective, x0=initial, bounds=all_bounds, method='Nelder-Mead', args=(n, s, spans, ejs))
print(np.round(sol.x))
#m = [ 5500. , 5950., 5050. ,6050. ,4950. , 6500. ,11000.] #the print m statment
The problem that I can't seem to figure out is that the np.sum(m%x)
does not return 0! I get the right distances in the m array compared to the drawing. The divisibility equation constraints are not satisfied yet the function says it terminates successfully. I'd appreciate any insights! Thank you so much!
The main problems that Nick calls out are true (though not comprehensive): there are many local minima, and the objective is non-differentiable.
But just as importantly: your objective isn't well-suited as an objective at all - it should be a constraint; and this entire system (when re-constructed) is linear. That means it's time for integer linear programming, which has better guarantees of convergence and accuracy than a generic
minimize
(no matter which method you select). More specifically, you're doing constraint programming, where there is no objective at all. (Or maybe you have some idea of what a better objective should be, such as minimizing the coefficients of x).In addition to the variables for x, w, a, your "distance elements" and m, you also need a binary selection matrix of which multiple of x is equal to m. This gets around the problem that multiplication of x by some multiple to get m is non-linear, by using so-called big-M constraints.
I've tried to make this easy-to-follow, and at any point you can call
some_constraint.A.toarray()
and view the result in e.g. PyCharm's matrix visualiser to see what's happening; but you may also need to dig into some introductory material on linear programming to get a better handle on it. Anyway, the results seem sane.The problem is of fairly small size and high sparsity, so solves quickly. You can visualise the constraint sparsity pattern via