Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions inflation/lp/InflationLP.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from ..sdp.fast_npa import nb_is_knowable as is_knowable
from .monomial_classes import InternalAtomicMonomial, CompoundMoment
from ..sdp.quantum_tools import flatten_symbolic_powers
from .lp_utils import solveLP
from .lp_utils import solveLP, solve_Gurobi
from functools import reduce
from ..utils import clean_coefficients, eprint, partsextractor, \
expand_sparse_vec
Expand Down Expand Up @@ -490,6 +490,7 @@ def set_extra_inequalities(self,
for ineq in extra_inequalities]

def solve(self,
interpreter: str = "solveLP_sparse",
relax_known_vars: bool = False,
relax_inequalities: bool = False,
solve_dual: bool = True,
Expand All @@ -503,6 +504,8 @@ def solve(self,

Parameters
----------
interpreter : str, optional
The solver to be called. By default, ``"solveLP_sparse"``.
relax_known_vars : bool, optional
Do feasibility as optimization where each known value equality
becomes two relaxed inequality constraints. E.g., P(A) = 0.7
Expand Down Expand Up @@ -531,6 +534,10 @@ def solve(self,
"relax_known_vars=False, relax_inequalities=False, and "
"optimizing the objective...")
relax_known_vars = relax_inequalities = False
if interpreter == "solve_Gurobi" and solve_dual is True:
warn("Gurobi does not support solving the dual problem. Use "
"solveLP_sparse if you want to solve the dual problem. "
"Setting solve_dual=False...")
if verbose is None:
real_verbose = self.verbose
else:
Expand All @@ -539,8 +546,14 @@ def solve(self,
real_default_non_negative = self.default_non_negative
else:
real_default_non_negative = default_non_negative

args = self._prepare_solver_matrices()

qcp_interpreters = {"solve_Gurobi"}
sparse_interpreters = {"solveLP", "solveLP_sparse", "solve_Gurobi"}
is_qcp = (interpreter in qcp_interpreters)
if interpreter in sparse_interpreters:
args = self._prepare_solver_matrices(include_nonlinear=is_qcp)
else:
args = self._prepare_solver_arguments(include_nonlinear=is_qcp)

# Still allow for 'feas_as_optim' as an argument
if 'feas_as_optim' in solver_arguments:
Expand All @@ -559,7 +572,10 @@ def solve(self,
"solverparameters": solverparameters,
"solve_dual": solve_dual})

self.solution_object = solveLP(**args)
if is_qcp:
self.solution_object = solve_Gurobi(**args)
else:
self.solution_object = solveLP(**args)
self.success = self.solution_object["success"]
self.status = self.solution_object["status"]
if self.success:
Expand Down Expand Up @@ -1672,7 +1688,8 @@ def semiknown_by_name(self) -> Dict:
in self.semiknown_moments.items()}

def _prepare_solver_matrices(self,
separate_bounds: bool = True) -> dict:
separate_bounds: bool = True,
include_nonlinear: bool = False) -> dict:
"""Convert arguments from dictionaries to sparse coo_matrix form to
pass to the solver.

Expand All @@ -1682,6 +1699,9 @@ def _prepare_solver_matrices(self,
Whether to have variable bounds as a separate item in
``solverargs`` (True) or absorb them into the inequalities (False).
By default, ``True``.
include_nonlinear : bool, optional
Whether to include quadratic factorization conditions in the
solver arguments, by default, ``False``.

Returns
-------
Expand Down Expand Up @@ -1710,6 +1730,9 @@ def _prepare_solver_matrices(self,
"equalities": internal_equalities,
"inequalities": self.sparse_inequalities,
"variables": self.monomial_names}
if include_nonlinear:
solverargs["factorization_conditions"] = \
self.sparse_quadratic_factorization_conditions
if separate_bounds:
solverargs["lower_bounds"] = self.sparse_lowerbounds
solverargs["upper_bounds"] = self.sparse_upperbounds
Expand All @@ -1723,7 +1746,8 @@ def _prepare_solver_matrices(self,
return solverargs

def _prepare_solver_arguments(self,
separate_bounds: bool = True) -> dict:
separate_bounds: bool = True,
include_nonlinear: bool = False) -> dict:
"""Prepare arguments to pass to the solver.

The solver takes as input the following arguments, which are all
Expand All @@ -1744,6 +1768,9 @@ def _prepare_solver_arguments(self,
Whether to have variable bounds as a separate item in
``solverargs`` (True) or absorb them into the inequalities (False).
By default, ``True``.
include_nonlinear : bool, optional
Whether to include quadratic factorization conditions in the
solver arguments, by default, ``False``.

Returns
-------
Expand All @@ -1770,6 +1797,9 @@ def _prepare_solver_arguments(self,
"equalities": self.moment_equalities_by_name,
"inequalities": self.moment_inequalities_by_name
}
if include_nonlinear:
solverargs["factorization_conditions"] = \
self.quadratic_factorization_conditions_by_name
# Add the constant 1 in case of unnormalized problems removed it
solverargs["known_vars"][self.constant_term_name] = 1.
if separate_bounds:
Expand Down
140 changes: 140 additions & 0 deletions inflation/lp/lp_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
from time import perf_counter
from gc import collect
from inflation.utils import partsextractor, expand_sparse_vec, vstack
try:
import gurobipy as gp
except ModuleNotFoundError:
pass


def drop_zero_rows(coo_mat: coo_matrix):
Expand Down Expand Up @@ -532,6 +536,142 @@ def solveLP_sparse(objective: coo_matrix = blank_coo_matrix,
}


def solve_Gurobi(objective: coo_matrix = blank_coo_matrix,
known_vars: coo_matrix = blank_coo_matrix,
inequalities: coo_matrix = blank_coo_matrix,
equalities: coo_matrix = blank_coo_matrix,
lower_bounds: coo_matrix = blank_coo_matrix,
upper_bounds: coo_matrix = blank_coo_matrix,
factorization_conditions: dict = None,
default_non_negative: bool = True,
verbose: int = 0,
**kwargs
) -> Dict:
"""Internal function to solve an LP with the Gurobi Optimizer API using
sparse matrices. Columns of each matrix correspond to a fixed order of
variables in the LP.

Parameters
----------
objective : coo_matrix, optional
Objective function with coefficients as matrix entries.
known_vars : coo_matrix, optional
Known values of the monomials with values as matrix entries.
inequalities : coo_matrix, optional
Inequality constraints in matrix form.
equalities : coo_matrix, optional
Equality constraints in matrix form.
lower_bounds : coo_matrix, optional
Lower bounds of variables with bounds as matrix entries.
upper_bounds : coo_matrix, optional
Upper bounds of variables with bounds as matrix entries.
factorization_conditions : dict, optional
Allows one to specify that certain variables are equal to the product of other variables
default_non_negative : bool, optional
Whether to set default primal variables as non-negative. By default,
``True``.
verbose : int, optional
Verbosity. Higher means more messages. By default, 0.

Returns
-------
dict
Primal objective value, problem status, success status, x values
"""
if verbose > 1:
t0 = perf_counter()
t_total = perf_counter()
print("Starting pre-processing for the LP solver...")

env = gp.Env(empty=True)
if verbose == 0:
env.setParam('OutputFlag', 0) # Supress output from solver
env.start()

# Create new model
m = gp.Model(env=env)

(nof_primal_inequalities, nof_primal_variables) = inequalities.shape
nof_primal_equalities = equalities.shape[0]
kv_matrix = expand_sparse_vec(known_vars)

# Create variables and set variable bounds
x = m.addMVar(shape=nof_primal_variables)
m.update()
if not default_non_negative:
x.lb = -gp.GRB.INFINITY
if lower_bounds.nnz > 0:
new_lb = x.lb
new_lb[lower_bounds.col] = lower_bounds.data
x.lb = new_lb
if upper_bounds.nnz > 0:
new_ub = x.ub
new_ub[upper_bounds.col] = upper_bounds.data
x.ub = new_ub

# Set objective
objective_vector = objective.toarray().ravel()
m.setObjective(objective_vector @ x, gp.GRB.MAXIMIZE)

# Add inequality constraint matrix
if inequalities.shape[0]:
m.addConstr(inequalities @ x >= 0, name="ineq")

# Add equality constraint matrix
if equalities.shape[0]:
m.addConstr(equalities @ x == 0, name="eq")

# Add known value constraint matrix
rhs_kv = known_vars.data
if len(rhs_kv):
m.addConstr(kv_matrix @ x == rhs_kv, name="kv")

# Add quadratic constraints
if factorization_conditions:
m.setParam("NonConvex", 2)
m.addConstrs((x[mon] == x[factors[0]] * x[factors[1]]
for mon, factors in factorization_conditions.items()),
name="fac")

collect()
if verbose > 1:
print("Pre-processing took",
format(perf_counter() - t0, ".4f"), "seconds.\n")
t0 = perf_counter()

# Solve the problem
if verbose > 0:
print("\nSolving the problem...\n")
m.optimize()
if verbose > 1:
print("Solving took", format(perf_counter() - t0, ".4f"),
"seconds.")

sc = gp.StatusConstClass
solution_statuses = {sc.__dict__[k]: k for k in sc.__dict__.keys()
if 'A' <= k[0] <= 'Z'}
status_str = solution_statuses[m.Status]
if status_str == "OPTIMAL":
success = True
primal = m.ObjVal
x_values = x.X
else:
success = False
primal = None
x_values = None

if verbose > 1:
print("\nTotal execution time:",
format(perf_counter() - t_total, ".4f"), "seconds.")

return {
"primal_value": primal,
"status": status_str,
"success": success,
"x": x_values,
}


def streamprinter(text: str) -> None:
"""A stream printer to get output from Mosek.

Expand Down