`max` constraints in JuMP

Similar to this question, how can I formulate a constraint involving max of the form \sum_{i=1}^m [z_i + t]_+ \leq c where [x]_+ = \max(x,0), both t,c \in \mathbb{R}, and z = My \in \mathbb{R}^m for variable y \in \mathbb{R}^n with data matrix M \in \mathbb{R}^{m \times n}. My attempt is

m, n = size(M)
@variable(model, y[1:n])
@variable(model, t)
zz = [M[i,:]'*y + t for i=1:m]
z = [max(zz[i], 0.0) for i=1:m]
@constraint(model, sum(z) <= c)

but I got the following error:

MethodError: no method matching isless(::Float64, ::JuMP.GenericAffExpr{Float64,JuMP.Variable})

I tried adding a non-negative variable s and doing the following

m, n = size(M)
@variable(model, y[1:n])
@variable(model, t)
@variable(model, s >= 0.0)
z = [M[i,:]'*y + t for i=1:m]
@constraint(model, z >= s)
@constraint(model, sum(z) <= c)

but wasn’t sure if this is the standard way of adding this type of constraint. For example, is this what Convex.jl does, or where could I check?

This makes no sense to me. yy is a bunch of expressions, and the “constraint” is not really constraining anything. Also data[i] should probably be data[:,i]. In the question you referred to, the OP was trying to minimize the sum of terms each of which is of the form max(expr, 0), so the standard way is to define z and constrain z >= expr and z >= 0, then minimize z instead, which due to the minimization behavior of the optimization algorithm, z will eventually land on either expr or 0, and in the process expr will be pushed as low as possible until it gets to or below 0, then z will not push it down anymore.

1 Like

Thanks, my original question was off… Edited to reflect my issue better

@variable(model, s[1:m] >= 0.0)

@constraint(model, [i=1:m], s[i] >= z[i])

@constraint(model, sum(s[i] for i=1:m) <= c)

Note that under this transformation, there is no guarantee that s[i] will be exactly equal to max(z[i], 0) (may or may not be the case depending on your objective). s[i] can take any value greater than or equal to max(z[i], 0) as long as sum(s) is less than or equal to c. But satisfying the transformed constraints clearly guarantees satisfying your original constraints. That is for every feasible solution (y_f,t_f,s_f) to the transformed constraints, the solution (y_f, t_f) is guaranteed to satisfy your original constraints. And conversely, for every feasible solution (y_f,t_f) to the original constraints, there exists at least 1 feasible solution (y_f,t_f,s_f) to the transformed constraints by setting s_f[i] = max(z[i], 0) for example, so you are not “missing” on any feasible solution to the original constraints by replacing them with the transformed set of constraints.

2 Likes

Thanks – the feasibility argument makes sense, but why did you constrain the sum of s[i] and not z?. Is there a way to doe the max(z[i] + t, 0) constraint precisely? In Convex.jl, I was able to solve the toy problem:

using Convex
using Mosek
m = 10
n = 3
t = Variable(1)
y = Variable(n)
M = randn(m,n)
c = 100
r = randn(n)
problem = minimize(r'*y)
problem.constraints += [t >= 0]
z = max.([M[i,:]'*y + t for i=1:m], 0.0)
problem.constraints += [sum(z) <= c]
solve!(problem, MosekSolver(),verbose=true) 
y.value

but don’t entirely know how Convex is treating that max constraint.

The above transformation will do precisely what you want, just ignore s in the final solution, it doesn’t add any semantics to your model. As for Convex.jl, I haven’t read the src but one possibility is that Convex.jl is doing the same transformation under the hood and is hiding away the s details from you, so you only see what you want to see. Or another possibility is that the max expression is processed as a non-smooth function using sub-gradients, I have no idea which one it is using.

1 Like