ProCreations's picture
Publish six-claim native-scale FFOLayer reproduction
9860743 verified
Raw
History Blame Contribute Delete
17.9 kB
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 = []
# t_cp = cp.Parameter(nonneg=True)
# === SOC constraints ===
# constraints.append(cp.SOC(1, z_cp))
# soc_rhs = 10.0
# constraints = [
# cp.SOC(soc_rhs, z_cp[i * cone_dim : (i + 1) * cone_dim])
# for i in range(num_cones)
# ]
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:
### default optimization parameters
self.Q = torch.eye(opt_dim).to(device)#.double()
G = torch.cat([torch.eye(opt_dim), -torch.eye(opt_dim), torch.ones(1,opt_dim)], dim=0).to(device)#.double()
h = torch.cat([torch.zeros(opt_dim), torch.ones(opt_dim), torch.Tensor([3])], dim=0).to(device)#.double()
# self.Q = torch.eye(opt_dim).to(device)#.double()
# G = torch.cat([torch.eye(opt_dim)], dim=0).to(device)#.double()
# h = torch.cat([torch.zeros(opt_dim)], dim=0).to(device)#.double()
### simple
# G = torch.ones(1,opt_dim).to(device)
# G[:,1:] = 0.0
# h = torch.Tensor([0]).to(device)
### dense
# self.Q = torch.ones(opt_dim, opt_dim).to(device) + torch.eye(opt_dim).to(device)
# x_star = torch.zeros(opt_dim).to(device)
# G = torch.ones(self.num_ineq, opt_dim).to(device)
# eps = 1.0
# h = G @ x_star + eps
self.A = torch.Tensor().to(device)
self.b = torch.Tensor().to(device)
##### learnable constraints
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: ## use this ffoqp_cst
problem, objective_fn, constraints, params, variables = setup_cvxpy_synthetic_problem(opt_dim, self.num_ineq)
eq_funcs, ineq_funcs = [], []
for c in problem.constraints:
# Equality: g(x,θ) == 0 -> store g(x,θ)
if isinstance(c, cp.constraints.zero.Equality):
eq_funcs.append(c.expr)
# Inequality: g(x,θ) <= 0 -> store g(x,θ)
elif isinstance(c, cp.constraints.nonpos.Inequality):
ineq_funcs.append(c.expr)
else:
# save for PSD or SOC constraints
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 = ffoqp_eq_cst_schur.FFOQPLayer(alpha=alpha, chunk_size=1, cvxpy_instance=cvxpy_instance)
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",
# lin_solver="scipy LU",
)
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)#.double()
# d = torch.logspace(0, 6, steps=opt_dim, device=device) # cond ~ 1e6
# self.Q = torch.diag(d).to(device)
G = torch.cat([torch.eye(opt_dim), -torch.eye(opt_dim), torch.ones(1,opt_dim)], dim=0).to(device)#.double()
h = torch.cat([torch.zeros(opt_dim), torch.ones(opt_dim), torch.Tensor([3])], dim=0).to(device)#.double()
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)
##### learnable constraints
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) #(B, input_dim)
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) # (batch, y_dim, y_dim)
G_batched = self.G.unsqueeze(0).expand(nBatch, -1, -1) # (batch, num_ineq, y_dim)
h_batched = h.unsqueeze(0).expand(nBatch, -1) # (batch, num_ineq)
sol = self.optlayer(Q_batched, q_pred, G_batched, h_batched, self.A, self.b)
elif self.layer_type==DQP:
# Q: (y_dim, y_dim)
Q_batched = self.Q.unsqueeze(0).expand(nBatch, -1, -1) # (batch, y_dim, y_dim)
# G: (num_ineq, y_dim)
G_batched = self.G.unsqueeze(0).expand(nBatch, -1, -1) # (batch, num_ineq, y_dim)
# h: (num_ineq,) -> (batch, num_ineq)
h_batched = h.unsqueeze(0).expand(nBatch, -1) # (batch, num_ineq)
# q_pred: (batch, y_dim)
q_pred_batched = q_pred # (batch, y_dim)
if self.A.numel() == 0:
A_batched = None
b_batched = None
else:
# A: (num_eq, y_dim)
A_batched = self.A.unsqueeze(0).expand(nBatch, -1, -1) # (batch, num_eq, y_dim)
# b: (num_eq,) -> (batch, num_eq)
b_batched = self.b.unsqueeze(0).expand(nBatch, -1) # (batch, num_eq)
sol, lambda_star, mu_star, _, _ = self.optlayer(
Q_batched, q_pred_batched, G_batched, h_batched, A_batched, b_batched
)
else:
# Expand constant params along batch dimension
Q_batched = self.Q.unsqueeze(0).expand(nBatch, -1, -1) # (batch, y_dim, y_dim)
G_batched = self.G.unsqueeze(0).expand(nBatch, -1, -1) # (batch, num_ineq, y_dim)
h_batched = h.unsqueeze(0).expand(nBatch, -1) # (batch, num_ineq)
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}) #default eps for lpgd
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) # (batch, y_dim, y_dim)
# G_batched = self.G.unsqueeze(0).expand(nBatch, -1, -1) # (batch, num_ineq, y_dim)
# h_batched = h.unsqueeze(0).expand(nBatch, -1) # (batch, num_ineq)
A_batched = self.A_soc.unsqueeze(0).expand(nBatch, -1, -1) # (batch, num_cones, y_dim)
b_batched = self.b_soc.unsqueeze(0).expand(nBatch, -1) # (batch, num_cones)
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) # (batch, y_dim, y_dim)
G_batched = self.G.unsqueeze(0).expand(nBatch, -1, -1) # (batch, num_ineq, y_dim)
h_batched = h.unsqueeze(0).expand(nBatch, -1) # (batch, num_ineq)
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)
# Single SOC: ||z|| <= 1 -> soc_a = 0, soc_b = 1
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