sysid blog

Select Points

Given multiple sets of data points. Select one point per set so that the distances between the points is minimal.1

Model

Non-convex MIQP Model

$$ \min \sum_{i,j| ok_{i,j}} dist_{i,j} \cdot x_i \cdot x_j \\ \sum_{i|group_{i,g}} x_i = 1 \forall g \\ x_i \in {0,1} $$

Linear MIP Model

To linearize the model a standard procedure can be applied:

Let’s introduce a new binary variable, $z_{i,j}$, that represents the product of the binary variables $\color{darkred}x_i$ and $\color{darkred}x_j$. This new variable will replace the nonlinear term in the objective function, making the model linear. We will also add constraints to link the new variable $z_{i,j}$ with the original variables $\color{darkred}x_i$ and $\color{darkred}x_j$.

\begin{align} \min&\sum_{i,j|ok_{i,j}} dist_{i,j}\cdot z_{i,j} \\ & \sum_{i|group_{i,g}} x_i = 1 && \forall g\\ & x_i \in {0,1} \\ & z_{i,j} \leq x_i && \forall i,j|ok_{i,j} \\ & z_{i,j} \leq x_j && \forall i,j|ok_{i,j} \\ & z_{i,j} \geq x_i + x_j - 1 && \forall i,j|ok_{i,j} \\ & z_{i,j} \in {0,1} && \forall i,j|ok_{i,j} \end{align}

Implementation

Calculating the sparse $ok_{i,j}$ set is key for a simple Pyomo implementation of the model. With this set and using Pyomo’s decorator syntax the model looks compact:

        m.I = pyo.RangeSet(config['N'])
        m.G = pyo.RangeSet(config['G'])
        m.Ok = pyo.Set(initialize=Ok(config).ok)

        @m.Param(m.Ok)
        def distance(model, i, j):
            p = Point(*self.points[i])
            q = Point(*self.points[j])
            return sqrt((p.x - q.x) ** 2 + (p.y - q.y) ** 2)

        m.groups = pyo.Param(m.G, initialize=group(config))

        m.x = pyo.Var(m.I, domain=pyo.Binary, initialize=0)
        m.pair = pyo.Var(m.Ok, domain=pyo.Binary, initialize=0)

        @m.Constraint(m.G)
        def one_point_per_group(m, g):
            return sum(m.x[i] for i in m.groups[g]) == 1

        @m.Constraint(m.Ok)
        def both_selected(m, i, j):
            return m.pair[i, j] >= m.x[i] + m.x[j] - 1

        @m.Objective(sense=pyo.minimize)
        def total_distance(m):
            return sum(
                m.pair[i, j] * m.distance[i, j] for (i, j) in m.Ok)

Result

The CBC solver has no problems with this model:

The edges of the 10 solution points are visualized in the graph. The complete code can be found here.

Interesting to see is the solution time between CPLEX 1 0.672s and CBC 22s. There is a huge gap between commercial solvers and OSS solvers.

Maximize

Maximizing the same model seems to be much harder for the solver. The same model size as above did not solve within 6000s.

Halve the size solved:

#python #optimization #work #pyomo