Convex.jl Documentation

repository·master·Indexed 20 days ago

https://github.com/jump-dev/convex.jl

A Julia package for Disciplined Convex Programming (DCP) that allows users to solve linear programs, mixed-integer linear programs, and DCP-compliant convex programs (including SOCP, SDP, and exponential cone programs). It transforms problems into standard conic form and interfaces with various solvers via MathOptInterface. Convex.jl also provides an experimental JuMP solver for reformulating nonlinear JuMP models into conic programs.

Tokens
14.1K
Snippets
40
Records
58
Agent score
70%

What's inside Convex.jl

  1. Overview of Convex.jl

    master

    Convex.jl is a Julia package for Disciplined Convex Programming (DCP). It allows you to describe optimization problems using natural mathematical syntax and solve them using various commercial and open-source solvers.

    Convex.jl supports solving:

    • Linear programs
    • Mixed-integer linear programs (MILP)
    • Mixed-integer second-order cone programs (MISOCP)
    • DCP-compliant convex programs, including:
      • Second-order cone programs (SOCP)
      • Exponential cone programs
      • Semidefinite programs (SDP)
  2. Create and evaluate expressions

    master

    Expressions are formed by applying atoms (mathematical functions) to variables, constants, or other expressions. Convex.jl overloads standard Julia operators (like +, *, and array indexing) to return Convex.jl expressions.

    Important Rules:

    1. DCP Compliance: All expressions must be Disciplined Convex Programming (DCP) compliant.
    2. Avoid Broadcasting: Do not use Julia's broadcasting operator (.). For example, use 1.0 + A * x instead of 1.0 .+ A * x. Broadcasting will cause expressions to fail.
    3. Evaluation: To retrieve the numerical value of a variable or an expression after a problem has been solved, use evaluate().
    x = Variable(5)
    y = sum(x)
    z = 4 * x + y
    z_1 = z[1]
    
    # After solving a problem:
    val = evaluate(z_1)
  3. How Convex.jl uses extended formulations and the DCP ruleset

    master

    Convex.jl solves problems by transforming them into linear optimization problems subject to conic constraints. This process often involves creating an "extended formulation," which adds auxiliary variables to the original problem to handle nonlinear or nonsmooth constructions (like abs, log_det, or norm).

    To ensure these transformations are mathematically valid and preserve convexity, Convex.jl requires that the problem be modeled using its "atoms" (primitives) according to the Disciplined Convex Programming (DCP) ruleset. If atoms are combined in a way that violates DCP rules, the resulting extended formulation may be invalid, leading to incorrect or unbounded solutions. Convex.jl programmatically checks for DCP compliance and will throw a DCPViolationError if the rules are not satisfied.

    using Convex, SCS
    x = Variable();
    t = Variable();
    # An extended formulation adding auxiliary variable 't' to represent abs(x)
    model_min_extended = minimize(t, [x >= 1, x <= 2, t >= x, t >= -x]);
    solve!(model_min_extended, SCS.Optimizer; silent = true)
  4. Understand the difference between Convex.jl and JuMP

    master

    While both Convex.jl and JuMP are mathematical programming modeling languages in Julia that interface with solvers via MathOptInterface, they serve different purposes:

    • Convex.jl: Converts problems to a standard conic form. This approach requires and certifies that the problem is convex and DCP (Disciplined Convex Programming) compliant, guaranteeing global optimality (if the solver succeeds). It prioritizes linear algebraic and functional constructions (e.g., max(x, y) <= A * z). Note that solve! parses the problem again every time it is called.
    • JuMP: Allows nonlinear programming by learning about functions via derivatives. It is more flexible (allowing non-convex optimization) but cannot guarantee global optimality or warn you if a formulation is non-convex. It uses a scalar-based syntax (e.g., sum(x[i] for i in 1:n)) and is more efficient for solving sequences of problems where constraints or coefficients change.

    Choose Convex.jl when you need guaranteed global optimality and want to leverage linear algebra-style modeling. Choose JuMP for general nonlinear programming or when you need to solve sequences of related problems efficiently.

  5. Understand atom and constraint promotions

    master

    When an atom or a constraint is applied to a scalar and a higher-dimensional variable, the scalars are automatically promoted to match the shape of the variable.

    For example, applying max(x, 0) where x is a vector will result in an expression with the same shape as x, where each element is the maximum of the corresponding element of x and 0.

  6. Identify performance bottlenecks in Convex.jl

    master

    Convex.jl performance issues can occur in one of three distinct phases. To optimize your code, you must first isolate which phase is the bottleneck:

    1. Building the expression tree: Everything before solve! is called (creating variables, constraints, etc.). If this is slow, it may be a performance bug in Convex.jl.
    2. Formulating the problem in MathOptInterface: This happens automatically during solve!. If silent=false (the default), Convex.jl will log the time taken and total memory allocations for this step. If this is much slower than solving, consider avoiding scalar indexing.
    3. Solving the problem: This is the actual execution by the solver. If this is slow, consider dualization, different solvers, or inspecting the final conic formulation.

    Typically, the expected order of speed is: Step 1 (fastest) > Step 2 > Step 3 (slowest).

  7. Implement custom variable types

    master

    To create custom variable types that allow for specialized dispatch or function notation, you must create a mutable subtype of Convex.AbstractVariable.

    Requirements for AbstractVariable subtypes:

    • Must be mutable (to prevent variables with the same size/value from being treated as the same object).
    • Must have fields head and size.
    • Must implement one of the following for several properties:
      • Value: Field value OR implement Convex._value and Convex.set_value!.
      • Vexity: Field vexity OR implement Convex.vexity and Convex.vexity!.
      • Constraints: Field constraints OR implement Convex.get_constraints (optionally Convex.add_constraint!).
      • Sign: Field sign OR implement Convex.sign.
      • Vartype: Field vartype OR implement Convex.vartype (optionally Convex.vartype!).
    using Convex
    
    mutable struct ProbabilityVector <: Convex.AbstractVariable
        head::Symbol
        size::Tuple{Int,Int}
        value::Union{Convex.Value,Nothing}
        vexity::Convex.Vexity
        function ProbabilityVector(d)
            return new(:ProbabilityVector, (d, 1), nothing, Convex.AffineVexity())
        end
    end
    
    Convex.get_constraints(p::ProbabilityVector) = [ sum(p) == 1 ]
    Convex.sign(::ProbabilityVector) = Convex.Positive()
    Convex.vartype(::ProbabilityVector) = Convex.ContVar
    (p::ProbabilityVector)(x) = dot(p, x)
  8. Generate Convex.jl documentation and examples

    master

    To build the full documentation suite, including Jupyter notebooks for all examples, run the following command from the repository root. The notebooks will be placed in docs/notebooks and the documentation will be generated in doc/build. Note that this process can be time-consuming.

    To skip the example generation and only update the documentation, set the CONVEX_SKIP_EXAMPLES environment variable to true before running the command.

    njulia --project=docs -e 'using Pkg; Pkg.instantiate(); include("docs/make.jl")'
  9. Use warmstarting to speed up repeated solves

    master

    If you are solving a sequence of similar problems (e.g., changing a parameter value slightly), you can use warmstarting to initialize the solver with the previous solution. This can significantly reduce computation time.

    To enable this, pass warmstart = true to the solve! method.

    using Convex, SCS
    n = 1_000
    y = rand(n);
    x = Variable(n)
    lambda = Variable(Positive())
    fix!(lambda, 100)
    problem = minimize(sumsquares(y - x) + lambda * sumsquares(x - 10))
    
    # Initial solve
    @time solve!(problem, SCS.Optimizer)
    
    # Update parameter and warmstart the next solve
    fix!(lambda, 105)
    @time solve!(problem, SCS.Optimizer; warmstart = true)
  10. How to write a ProblemDepot problem

    master

    To add a new problem to the ProblemDepot, define a function annotated with the @add_problem macro. The function is registered in Convex.ProblemDepot.PROBLEMS.

    Function Signature

    Every problem function must follow this signature: function <group_name>_<problem_name>(handle_problem!, ::Val{test}, atol, rtol, ::Type{T}) where {T, test}

    • <group_name>: A prefix describing the type of atoms used (e.g., affine). This allows users to filter problems using the exclude keyword in run_tests.
    • handle_problem!: A callback function called at the end of the problem definition. This is where the solver is actually invoked (e.g., p -> solve!(p, solver)).
    • ::Val{test}: A boolean flag used to gate test logic. Tests should be wrapped in if test blocks so they are skipped during benchmarking.
    • atol, rtol: Absolute and relative tolerances.
    • ::Type{T}: The type parameter.

    Implementation Rules

    1. Call handle_problem!: Instead of calling solve!, call handle_problem!(p) at the end of the function.
    2. Gate Tests: Wrap all assertions and post-solve evaluations (like evaluate(x)) inside if test blocks. This prevents errors during benchmarking when a problem might not be solved.
    3. Safe Evaluation: Because evaluate(x) returns nothing if the problem hasn't been solved, avoid calling functions like real() on the result outside of a test block.

    Example of a correct implementation:

    @add_problem affine function affine_negate_atom(handle_problem!, ::Val{test}, atol, rtol, ::Type{T}) where {T, test}
        x = Variable()
        p = minimize(-x, [x <= 0])
        if test
            @test vexity(p) == AffineVexity()
        end
        handle_problem!(p)
        if test
            @test p.optval ≈ 0 atol=atol rtol=rtol
            @test evaluate(-x) ≈ 0 atol=atol rtol=rtol
        end
    end
  11. Inspect the final conic formulation

    master

    Convex.jl reformulates all problems into a conic programming form (affine objective functions and affine-function-in-cone constraints). If you suspect the automatic reformulation is inefficient, you can inspect the final problem by writing it to a file using Convex.write_to_file.

    Convex.write_to_file(problem, "problem_file.txt")
  12. How to add benchmark-only problems

    master

    If you want to add problems intended strictly for benchmarking (without associated tests), follow these rules:

    1. Location: Place the file in src/problem_depot/problems/benchmark.
    2. Naming: Include benchmark in the problem name.
    3. Behavior: These problems are automatically skipped by Convex.ProblemDepot.run_tests.
    4. Signature: Use args... in the function signature to ensure compatibility with the standard ProblemDepot calling convention.

    Example:

    @add_problem constraints_benchmark function sdp_constraint(handle_problem!, args...)
        p = satisfy()
        x = Variable(44, 44)
        push!(p.constraints, x ⪰ 0)
        handle_problem!(p)
        nothing
    end
    @add_problem constraints_benchmark function sdp_constraint(handle_problem!, args...)
        p = satisfy()
        x = Variable(44, 44) # 990 vectorized entries
        push!(p.constraints, x ⪰ 0)
        handle_problem!(p)
        nothing
    end