| import sys |
| import os |
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
|
| import torch |
| import torch.nn as nn |
| import cvxpy as cp |
| from constants import * |
| from torch.nn.parameter import Parameter |
|
|
| from src.ffolayer.ffocp_eq import FFOLayer |
| from ffolayer.ffoqp_eq import FFOQPLayer |
|
|
| from dqp import dQP |
| from baselines.cvxpylayers_local.cvxpylayer import CvxpyLayer |
| from baselines.qpthlocal.qp import QPFunction |
| from baselines.cvxpylayers_local.cvxpylayer import CvxpyLayer as LPGDLayer |
| from baselines.BPQP import BPQPLayer |
| from baselines.BPQP_socp import BPQPLayer_socp |
| from baselines.AltDiff import AltDiffLayer |
| from baselines.AltDiff_socp import AltDiffLayer as AltDiffLayer_socp |
|
|
|
|
| class MLP(nn.Module): |
| def __init__(self, input_dim=64, output_dim=10): |
| super(MLP, self).__init__() |
| self.input_dim = input_dim |
| self.output_dim = output_dim |
| self.fc1 = nn.Linear(input_dim, 128) |
| self.batch_norm1 = nn.BatchNorm1d(128) |
| self.fc2 = nn.Linear(128, 128) |
| self.batch_norm2 = nn.BatchNorm1d(128) |
| self.fc3 = nn.Linear(128, output_dim) |
| self.activation = nn.ReLU() |
| self.bound = 10 |
|
|
| def forward(self, x): |
| x = x.view(-1, self.input_dim) |
| batch_size = x.shape[0] |
| if batch_size > 1: |
| x = self.activation(self.batch_norm1(self.fc1(x))) |
| x = self.activation(self.batch_norm2(self.fc2(x))) |
| else: |
| x = self.activation(self.fc1(x)) |
| x = self.activation(self.fc2(x)) |
| x = torch.clamp(self.fc3(x), min=-self.bound, max=self.bound) |
| return x |
|
|
| def setup_cvxpy_synthetic_problem(n, n_ineq_constraints, unconstrained=False): |
| Q_cp = cp.Parameter((n, n), PSD=True) |
| q_cp = cp.Parameter(n) |
| G_cp = cp.Parameter((n_ineq_constraints, n)) |
| h_cp = cp.Parameter(n_ineq_constraints) |
| z_cp = cp.Variable(n) |
|
|
| objective_fn = 0.5 * cp.sum_squares(Q_cp @ z_cp) + q_cp.T @ z_cp |
| variables = [z_cp] |
| if not unconstrained: |
| |
| constraints = [G_cp @ z_cp <= h_cp] |
|
|
| problem = cp.Problem(cp.Minimize(objective_fn), constraints) |
| assert problem.is_dpp() |
| |
| parameters = [Q_cp, q_cp, G_cp, h_cp] |
| |
| else: |
| parameters = [Q_cp, q_cp] |
| constraints = [] |
| problem = cp.Problem(cp.Minimize(objective_fn), constraints) |
|
|
| return problem, objective_fn, constraints, parameters, variables |
|
|
| def setup_cvxpy_synthetic_problem_with_cones(n, n_ineq_constraints, cone_dim, num_cones=30, unconstrained=False): |
| Q_cp = cp.Parameter((n, n), PSD=True) |
| q_cp = cp.Parameter(n) |
| if not unconstrained: |
| G_cp = cp.Parameter((n_ineq_constraints, n)) |
| h_cp = cp.Parameter(n_ineq_constraints) |
| z_cp = cp.Variable(n) |
|
|
| objective_fn = 0.5 * cp.sum_squares(Q_cp @ z_cp) + q_cp.T @ z_cp |
| variables = [z_cp] |
| constraints = [] |
| parameters = [] |
|
|
| |
|
|
| |
| |
|
|
| |
| |
| |
| |
| |
|
|
| A_cp = cp.Parameter((num_cones, n)) |
| b_cp = cp.Parameter(num_cones) |
| constraints = [] |
| for i in range(num_cones): |
| start_idx = (i * cone_dim) % n |
| end_idx = start_idx + cone_dim if i != num_cones - 1 else n |
| constraints.append(cp.SOC(b_cp[i] - A_cp[i, :] @ z_cp, z_cp[start_idx:end_idx])) |
|
|
| if not unconstrained: |
| constraints.append(G_cp @ z_cp <= h_cp) |
| parameters = [Q_cp, q_cp, G_cp, h_cp, A_cp, b_cp] |
| else: |
| parameters = [Q_cp, q_cp, A_cp, b_cp] |
|
|
| problem = cp.Problem(cp.Minimize(objective_fn), constraints) |
| assert problem.is_dpp() |
|
|
| return problem, objective_fn, constraints, parameters, variables |
|
|
|
|
| def get_feasible_h(G, z0, s0): |
| ''' |
| get a vector h such that the inequality constraint Gy<=h can be satisfied |
| |
| i.e. return h = G(z0) + s0 where s0 are all positive |
| |
| Args: |
| - G: (num_ineq, y_dim) |
| - s0: (num_ineq, ) |
| - z0: (y_dim,) |
| ''' |
| assert(not torch.any(s0<0)) |
| return torch.matmul(G, z0) + s0 |
|
|
|
|
| class OptModel(nn.Module): |
| def __init__(self, input_dim, opt_dim, layer_type, constraint_learnable, device, batch_size, alpha=100, dual_cutoff=1e-3, slack_tol=1e-6, backward_eps=1e-8, is_QP=False): |
| ''' |
| The architecture is {parameter - optLayer}. |
| |
| Args: |
| - delta = 1/alpha, which is the perturbation constant for finite difference |
| ''' |
| super().__init__() |
| self.layer_type = layer_type |
| assert(layer_type in [FFOCP_EQ, CVXPY_LAYER, LPGD, QPTH, LPGD_QP, FFOQP_EQ, FFOQP_EQ_SCHUR, FFOQP_EQ_PARALLELIZE, FFOQP_EQ_PDIPM, BPQP, ALTDIFF, DQP]) |
| |
| self.constraint_learnable = constraint_learnable |
| self.is_QP = is_QP |
| self.y_dim = opt_dim |
| self.input_dim = input_dim |
| self.num_ineq = 2*opt_dim + 1 |
| self.num_eq = 0 |
| |
| |
| self.predictor = MLP(input_dim, self.y_dim) |
|
|
| if self.is_QP: |
| |
| self.Q = torch.eye(opt_dim).to(device) |
| G = torch.cat([torch.eye(opt_dim), -torch.eye(opt_dim), torch.ones(1,opt_dim)], dim=0).to(device) |
| h = torch.cat([torch.zeros(opt_dim), torch.ones(opt_dim), torch.Tensor([3])], dim=0).to(device) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| self.A = torch.Tensor().to(device) |
| self.b = torch.Tensor().to(device) |
| |
| |
| if constraint_learnable: |
| self.G = Parameter(torch.rand((self.num_ineq, self.y_dim))) |
| self.z0_g = Parameter(torch.zeros((self.y_dim,))) |
| self.log_s0 = Parameter(torch.rand((self.num_ineq,))) |
| |
| else: |
| self.G = G.to(device) |
| self.h = h.to(device) |
| |
| if self.layer_type not in [QPTH, LPGD_QP, BPQP, ALTDIFF, DQP]: |
| problem, objective_fn, constraints, params, variables = setup_cvxpy_synthetic_problem(opt_dim, self.num_ineq) |
| |
| if layer_type==FFOCP_EQ: |
| self.optlayer = FFOLayer(problem, parameters=params, variables=variables, alpha=alpha, dual_cutoff=dual_cutoff, slack_tol=slack_tol, eps=1e-12, backward_eps=backward_eps) |
| |
| elif layer_type==CVXPY_LAYER: |
| self.optlayer = CvxpyLayer(problem, parameters=params, variables=variables) |
| elif layer_type==LPGD: |
| self.optlayer = LPGDLayer(problem, parameters=params, variables=variables, lpgd=True) |
| |
| elif layer_type == FFOQP_EQ_SCHUR: |
| problem, objective_fn, constraints, params, variables = setup_cvxpy_synthetic_problem(opt_dim, self.num_ineq) |
| eq_funcs, ineq_funcs = [], [] |
| for c in problem.constraints: |
| |
| if isinstance(c, cp.constraints.zero.Equality): |
| eq_funcs.append(c.expr) |
|
|
| |
| elif isinstance(c, cp.constraints.nonpos.Inequality): |
| ineq_funcs.append(c.expr) |
|
|
| else: |
| |
| raise NotImplementedError( |
| f"Constraint type {type(c)} not supported in FFOLayer wrapper." |
| ) |
| cvxpy_instance = {"variables":variables, "params":params, "problem":problem, "eq_constraints":[], "ineq_constraints":constraints,\ |
| "eq_functions":eq_funcs, "ineq_functions":ineq_funcs} |
| |
| |
| self.optlayer = FFOQPLayer(alpha=alpha, chunk_size=1, cvxpy_instance=cvxpy_instance, solver='qpsolvers') |
| |
| else: |
| if self.layer_type==QPTH: |
| self.optlayer = QPFunction(verbose=-1) |
| elif self.layer_type==BPQP: |
| self.optlayer = BPQPLayer(forward_eps=1e-12, backward_eps=1e-10) |
| elif self.layer_type==ALTDIFF: |
| self.optlayer = AltDiffLayer() |
| elif self.layer_type==DQP: |
| dQP_settings = dQP.build_settings( |
| solve_type="dense", |
| qp_solver="gurobi", |
| |
| ) |
| self.optlayer = dQP.dQP_layer(settings=dQP_settings) |
| else: |
| raise NotImplementedError("Not implemented for layer type: {}".format(layer_type)) |
| else: |
| self.Q = torch.eye(opt_dim).to(device) |
| |
| |
|
|
| G = torch.cat([torch.eye(opt_dim), -torch.eye(opt_dim), torch.ones(1,opt_dim)], dim=0).to(device) |
| h = torch.cat([torch.zeros(opt_dim), torch.ones(opt_dim), torch.Tensor([3])], dim=0).to(device) |
|
|
| self.A = torch.Tensor().to(device) |
| self.b = torch.Tensor().to(device) |
| self.t = torch.tensor(10.0, device=device) |
|
|
| cone_dim = 2 |
| num_cones = 100 |
|
|
| A0 = torch.randn(num_cones, opt_dim, device=device) |
| b0 = torch.ones(num_cones, device=device) |
|
|
| self.register_buffer("A_soc", A0) |
| self.register_buffer("b_soc", b0) |
| |
| |
| if constraint_learnable: |
| self.G = Parameter(torch.rand((self.num_ineq, self.y_dim))) |
| self.z0_g = Parameter(torch.zeros((self.y_dim,))) |
| self.log_s0 = Parameter(torch.rand((self.num_ineq,))) |
| else: |
| self.G = G.to(device) |
| self.h = h.to(device) |
| |
| problem, objective_fn, constraints, params, variables = setup_cvxpy_synthetic_problem_with_cones(opt_dim, self.num_ineq, cone_dim, num_cones, unconstrained=True) |
| if layer_type==FFOCP_EQ: |
| self.optlayer = FFOLayer(problem, parameters=params, variables=variables, alpha=alpha, dual_cutoff=dual_cutoff, slack_tol=slack_tol, eps=1e-12, backward_eps=backward_eps, verbose=False) |
| elif layer_type==CVXPY_LAYER: |
| self.optlayer = CvxpyLayer(problem, parameters=params, variables=variables) |
| elif layer_type==LPGD: |
| self.optlayer = LPGDLayer(problem, parameters=params, variables=variables, lpgd=True) |
| elif layer_type==BPQP: |
| self.optlayer = BPQPLayer_socp(forward_eps=1e-12, backward_eps=1e-10) |
| elif layer_type==ALTDIFF: |
| self.optlayer = AltDiffLayer_socp() |
| else: |
| raise NotImplementedError("Not implemented for layer type: {}".format(layer_type)) |
|
|
| def forward(self, x): |
| nBatch = x.size(0) |
| x = x.view(nBatch, -1) |
| |
| out = self.predictor(x) |
| q_pred = out[..., :self.y_dim] |
| |
| if self.constraint_learnable: |
| h = get_feasible_h(self.G, self.z0_g, torch.exp(self.log_s0)) |
| else: |
| h = self.h |
| |
| if self.is_QP: |
| if self.layer_type in [QPTH, FFOQP_EQ, FFOQP_EQ_PARALLELIZE, FFOQP_EQ_PDIPM, FFOQP_EQ_SCHUR]: |
| sol = self.optlayer( |
| self.Q, q_pred, self.G, h, self.A, self.b |
| ) |
| elif self.layer_type==BPQP or self.layer_type==ALTDIFF: |
| Q_batched = self.Q.unsqueeze(0).expand(nBatch, -1, -1) |
| G_batched = self.G.unsqueeze(0).expand(nBatch, -1, -1) |
| h_batched = h.unsqueeze(0).expand(nBatch, -1) |
| sol = self.optlayer(Q_batched, q_pred, G_batched, h_batched, self.A, self.b) |
| elif self.layer_type==DQP: |
| |
| Q_batched = self.Q.unsqueeze(0).expand(nBatch, -1, -1) |
|
|
| |
| G_batched = self.G.unsqueeze(0).expand(nBatch, -1, -1) |
|
|
| |
| h_batched = h.unsqueeze(0).expand(nBatch, -1) |
|
|
| |
| q_pred_batched = q_pred |
|
|
| if self.A.numel() == 0: |
| A_batched = None |
| b_batched = None |
| else: |
| |
| A_batched = self.A.unsqueeze(0).expand(nBatch, -1, -1) |
| |
| b_batched = self.b.unsqueeze(0).expand(nBatch, -1) |
|
|
| sol, lambda_star, mu_star, _, _ = self.optlayer( |
| Q_batched, q_pred_batched, G_batched, h_batched, A_batched, b_batched |
| ) |
| else: |
| |
| Q_batched = self.Q.unsqueeze(0).expand(nBatch, -1, -1) |
| G_batched = self.G.unsqueeze(0).expand(nBatch, -1, -1) |
| h_batched = h.unsqueeze(0).expand(nBatch, -1) |
| |
| params_batched = [Q_batched, q_pred, G_batched, h_batched] |
| |
| if self.layer_type==LPGD: |
| sol, = self.optlayer(*params_batched, solver_args={"eps": 1e-3}) |
| elif self.layer_type==CVXPY_LAYER: |
| sol, = self.optlayer(*params_batched) |
| else: |
| sol, = self.optlayer(*params_batched) |
| else: |
| if self.layer_type in [FFOCP_EQ, CVXPY_LAYER, LPGD]: |
| Q_batched = self.Q.unsqueeze(0).expand(nBatch, -1, -1) |
| |
| |
| A_batched = self.A_soc.unsqueeze(0).expand(nBatch, -1, -1) |
| b_batched = self.b_soc.unsqueeze(0).expand(nBatch, -1) |
| |
| params_batched = [Q_batched, q_pred, A_batched, b_batched] |
|
|
| sol = self.optlayer(*params_batched) |
| if isinstance(sol, tuple): |
| sol = sol[0] |
| elif self.layer_type==BPQP: |
| Q_batched = self.Q.unsqueeze(0).expand(nBatch, -1, -1) |
| G_batched = self.G.unsqueeze(0).expand(nBatch, -1, -1) |
| h_batched = h.unsqueeze(0).expand(nBatch, -1) |
|
|
| A_batched = torch.zeros((nBatch, 0, self.y_dim), device=self.Q.device, dtype=self.Q.dtype) |
| b_batched = torch.zeros((nBatch, 0), device=self.Q.device, dtype=self.Q.dtype) |
|
|
| |
| soc_a_batched = torch.zeros((nBatch, 1, self.y_dim), device=self.Q.device, dtype=self.Q.dtype) |
| soc_b_batched = torch.ones((nBatch, 1), device=self.Q.device, dtype=self.Q.dtype) |
|
|
| params_batched = [Q_batched, q_pred, G_batched, h_batched, A_batched, b_batched, soc_a_batched, soc_b_batched] |
| sol = self.optlayer(*params_batched) |
| if isinstance(sol, tuple): |
| sol = sol[0] |
| elif self.layer_type==ALTDIFF: |
| Q_batched = self.Q.unsqueeze(0).expand(nBatch, -1, -1) |
| G_batched = self.G.unsqueeze(0).expand(nBatch, -1, -1) |
| h_batched = h.unsqueeze(0).expand(nBatch, -1) |
|
|
| A_batched = torch.zeros((nBatch, 0, self.y_dim), device=self.Q.device, dtype=self.Q.dtype) |
| b_batched = torch.zeros((nBatch, 0), device=self.Q.device, dtype=self.Q.dtype) |
| |
| params_batched = [Q_batched, q_pred, G_batched, h_batched, A_batched, b_batched] |
| sol = self.optlayer(*params_batched) |
| if isinstance(sol, tuple): |
| sol = sol[0] |
| else: |
| raise NotImplementedError("Only FFOCP_EQ is supported for non-QP problems") |
| |
| return sol, q_pred |