In connection with the approaching Christmas, we are receiving many support questions about the use of mixed-integer conic optimization in the design of the traditional Danish "braided heart" Christmas tree decorations:
from mosek.fusion import * | |
import numpy as np | |
import matplotlib.pyplot as plt | |
import sys, itertools | |
# For a scalar variable x returns a new variable | |
# exactly representing abs(x) | |
def abs(M, x): | |
absx = M.variable() | |
# (x>=0 AND absx==x) OR (x<=0 AND absx==-x) | |
M.disjunction(DJC.AND(DJC.term(x, Domain.greaterThan(0)), | |
DJC.term(Expr.sub(absx, x), Domain.equalsTo(0))), | |
DJC.AND(DJC.term(x, Domain.lessThan(0)), | |
DJC.term(Expr.add(absx, x), Domain.equalsTo(0)))) | |
return absx | |
# Point belongs to a heart with parameters (p,r) | |
def inHeart(M, x, p, r): | |
x1, x2 = x.index(0), x.index(1) | |
# r^2 >= x1^2 + (x2-p*absx1)^2 | |
M.constraint(Expr.vstack(Expr.constTerm(r), | |
x1, | |
Expr.sub(x2, Expr.mul(p, abs(M, x1)))), | |
Domain.inQCone()) | |
# An integer variable or expression not equals a | |
def different(M, x, a): | |
# x>=a+1 OR x<=a-1 | |
M.disjunction(DJC.term(x, Domain.greaterThan(a+1)), | |
DJC.term(x, Domain.lessThan(a-1))) | |
################################## | |
# Model construction starts here # | |
################################## | |
# Set some parameters | |
p = 1.1 | |
r = 8.1 | |
seed = 77 | |
mindist = 7 | |
# Initialize the model | |
M = Model("Christmas Heart") | |
M.setSolverParam("mioSeed", seed) | |
M.setLogHandler(sys.stdout) | |
lines = [] | |
# Coordinates of the two points | |
x = M.variable(2, Domain.integral(Domain.unbounded())) | |
y = M.variable(2, Domain.integral(Domain.unbounded())) | |
inHeart(M, x, p, r) | |
inHeart(M, y, p, r) | |
# Make each line sufficiently long | |
dist = abs(M, Expr.sub(x.index(0), y.index(0))) | |
M.constraint(dist, Domain.greaterThan(mindist)) | |
# Each x - y line has slope 45deg in either direction | |
M.disjunction(DJC.term(Expr.dot(Var.vstack(x, y),[1,-1,-1,1]), Domain.equalsTo(0)), | |
DJC.term(Expr.dot(Var.vstack(x, y),[1,1,-1,-1]), Domain.equalsTo(0))) | |
# Add new lines as long as possible | |
M.solve() | |
while M.getProblemStatus() != ProblemStatus.PrimalInfeasible: | |
lines.append((x.level(),y.level())) | |
# Forbid endpoints on any 45deg sloped line passing through an existing point | |
for (v1, v2, v3) in itertools.product([x,y],[[1,-1],[1,1]],[x.level(),y.level()]): | |
different(M, Expr.dot(v1, v2), np.dot(v2, v3)) | |
# Resolve with the new constraints | |
M.solve() | |
# Final plot | |
u = np.linspace(-1.5*r, 1.5*r, 101) | |
v = np.linspace(-1.5*r, 1.5*r, 101) | |
z = np.zeros(shape=(len(u), len(v))) | |
for (i,j) in itertools.product(range(len(u)),range(len(v))): | |
z[i,j] = u[i]**2 + (v[j]-p*np.abs(u[i]))**2 - r**2 | |
plt.contour(u, v, z.T, [0]) | |
for ((a,b),(c,d)) in lines: | |
plt.scatter([a], [b]) | |
plt.scatter([c], [d]) | |
plt.plot((a,c),(b,d)) | |
plt.grid() | |
plt.show() |