import numpy as np # import diffcp import time from dataclasses import dataclass from typing import Any import cvxpy as cp import tracemalloc import os import linecache import torch from scipy.linalg import block_diag import pathlib n_threads = os.cpu_count() def _np(x): if isinstance(x, torch.Tensor): return x.detach().cpu().numpy() return np.array(x) def to_numpy(x): # convert torch tensor to numpy array if isinstance(x, torch.Tensor): if x.device.type == 'cuda': return x.cpu().detach().double().numpy() else: return x.detach().double().numpy() else: return np.array(x) def to_torch(x, dtype, device): # convert numpy array to torch tensor return torch.from_numpy(x).type(dtype).to(device) def slice_params_for_batch(params_req, batch_sizes, i): """Pick p[i] if that parameter was batched; else p.""" out = [] for p, bs in zip(params_req, batch_sizes): out.append(p[i] if bs > 0 else p) return out def make_mask_torch_for_i(i, inequality_dual, inequality_functions, dual_cutoff, ctx): mask_list = [] for j in range(len(inequality_functions)): lam_ji = inequality_dual[j][i] mask_np = (lam_ji > dual_cutoff).astype(np.float64) mask_list.append(to_torch(mask_np, ctx.dtype, ctx.device)) return mask_list def extract_nBatch(Q, p, G, h, A, b): dims = [3, 2, 3, 2, 3, 2] params = [Q, p, G, h, A, b] for param, dim in zip(params, dims): if param.ndim == dim: return param.size(0) return 1 def expandParam(X, nBatch, nDim): if X.ndim in (0, nDim) or X.nelement() == 0: return X, False elif X.ndim == nDim - 1: return X.unsqueeze(0).expand(*([nBatch] + list(X.size()))), True else: raise RuntimeError("Unexpected number of dimensions.") # def forward_numpy(params_numpy, context): # """Forward pass in numpy.""" # # info = {} # # if context.gp: # param_map = {} # # construct a list of params for the DCP problem # for param, value in zip(context.param_order, params_numpy): # if param in context.old_params_to_new_params: # new_id = context.old_params_to_new_params[param].id # param_map[new_id] = np.log(value) # else: # new_id = param.id # param_map[new_id] = value # params_numpy = [param_map[pid] for pid in context.param_ids] # # # canonicalize problem # start = time.time() # As, bs, cs, cone_dicts, shapes = [], [], [], [], [] # for i in range(context.batch_size): # params_numpy_i = [ # p if sz == 0 else p[i] # for p, sz in zip(params_numpy, context.batch_sizes)] # c, _, neg_A, b = context.compiler.apply_parameters( # dict(zip(context.param_ids, params_numpy_i)), # keep_zeros=True) # A = -neg_A # cvxpy canonicalizes -A # As.append(A) # bs.append(b) # cs.append(c) # cone_dicts.append(context.cone_dims) # shapes.append(A.shape) # info['canon_time'] = time.time() - start # info['shapes'] = shapes # # # compute solution and derivative function # start = time.time() # try: # if context.solve_and_derivative: # xs, _, _, _, DT_batch = diffcp.solve_and_derivative_batch( # As, bs, cs, cone_dicts, **context.solver_args) # info['DT_batch'] = DT_batch # else: # xs, _, _ = diffcp.solve_only_batch( # As, bs, cs, cone_dicts, **context.solver_args) # except diffcp.SolverError as e: # print( # "Please consider re-formulating your problem so that " # "it is always solvable or increasing the number of " # "solver iterations.") # raise e # info['solve_time'] = time.time() - start # # # extract solutions and append along batch dimension # start = time.time() # sol = [[] for i in range(len(context.variables))] # for i in range(context.batch_size): # sltn_dict = context.compiler.split_solution( # xs[i], active_vars=context.var_dict) # for j, v in enumerate(context.variables): # sol[j].append(np.expand_dims(sltn_dict[v.id], axis=0)) # sol = [np.concatenate(s, axis=0) for s in sol] # # if not context.batch: # sol = [np.squeeze(s, axis=0) for s in sol] # # if context.gp: # sol = [np.exp(s) for s in sol] # info['sol'] = sol # # return sol, info def forward_single_np(Q, p, G, h, A, b, solver='GUROBI', solver_opts={"verbose": False}): nz, neq, nineq = p.shape[0], A.shape[0] if A is not None else 0, G.shape[0] z_ = cp.Variable(nz) obj = cp.Minimize(0.5 * cp.quad_form(z_, Q) + p.T @ z_) eqCon = A @ z_ == b if neq > 0 else None if nineq > 0: slacks = cp.Variable(nineq) ineqCon = G @ z_ + slacks == h slacksCon = slacks >= 0 else: ineqCon = slacks = slacksCon = None cons = [x for x in [eqCon, ineqCon, slacksCon] if x is not None] prob = cp.Problem(obj, cons) prob.solve(solver=solver, **solver_opts) # max_iters=5000) # prob.solve() # prob.solve(adaptive_rho = False) # solver=cp.SCS, max_iters=5000, verbose=False) # prob.solve(solver=cp.SCS, max_iters=10000, verbose=True) assert('optimal' in prob.status) zhat = np.array(z_.value).ravel() nu = np.array(eqCon.dual_value).ravel() if eqCon is not None else None if ineqCon is not None: lam = np.array(ineqCon.dual_value).ravel() slacks = np.array(slacks.value).ravel() else: lam = slacks = None return prob.value, zhat, nu, lam, slacks def forward_single_np_eq_cst(Q, p, G, h, A, b): """ -> kamo min_z 1/2 * z.T Q z + p.T z s.t. A z = b ; G z <= h """ nz, neq, nineq = p.shape[0], A.shape[0] if A is not None else 0, G.shape[0] if G is not None else 0 z_ = cp.Variable(nz) obj = cp.Minimize(0.5 * cp.quad_form(z_, Q) + p.T @ z_) eqCon = A @ z_ == b if neq > 0 else None if nineq > 0: slacks = cp.Variable(nineq) ineqCon = G @ z_ + slacks == h slacksCon = slacks >= 0 else: ineqCon = slacks = slacksCon = None cons = [x for x in [eqCon, ineqCon, slacksCon] if x is not None] prob = cp.Problem(obj, cons) prob.solve(solver=cp.GUROBI, verbose=False, **{"Threads": n_threads, "OutputFlag": 0} ) # max_iters=5000) # prob.solve() # prob.solve(adaptive_rho = False) # solver=cp.SCS, max_iters=5000, verbose=False) # prob.solve(solver=cp.SCS, max_iters=10000, verbose=True) assert('optimal' in prob.status) zhat = np.array(z_.value).ravel() nu = np.array(eqCon.dual_value).ravel() if eqCon is not None else None if ineqCon is not None: lam = np.array(ineqCon.dual_value).ravel() slacks = np.array(slacks.value).ravel() else: lam = slacks = None return prob.value, zhat, nu, lam, slacks def forward_batch_np(Q, p, G, h, A, b, solver='GUROBI', solver_opts={"verbose": False}): """ -> kamo Q : (nb, nz, nz) p : (nb, nz) G : (nb, nineq, nz) h : (nb, nineq) A : (nb, neq, nz) b : (nb, neq) """ nb = p.shape[0] nz, neq, nineq = p.shape[1], A.shape[1] if A is not None else 0, G.shape[1] if G is not None else 0 z_ = cp.Variable(nz * nb) Q_ = block_diag(*Q) p_ = p.reshape(-1) obj = cp.Minimize(0.5 * cp.quad_form(z_, Q_) + p_.T @ z_) eqCon = None if neq > 0: A_ = block_diag(*A) b_ = b.reshape(-1) eqCon = A_ @ z_ == b_ if nineq > 0: slacks = cp.Variable(nineq * nb) G_ = block_diag(*G) h_ = h.reshape(-1) ineqCon = G_ @ z_ + slacks == h_ slacksCon = slacks >= 0 else: ineqCon = slacks = slacksCon = None cons = [x for x in [eqCon, ineqCon, slacksCon] if x is not None] prob = cp.Problem(obj, cons) # prob.solve(solver=cp.GUROBI, verbose=False, **{"Threads": n_threads, "OutputFlag": 0} ) prob.solve(solver=solver, **solver_opts) # prob.solve(solver='SCS', max_iters=100, eps=1e-7, **solver_opts) # max_iters=5000) # prob.solve() # prob.solve(adaptive_rho = False) # solver=cp.SCS, max_iters=5000, verbose=False) # prob.solve(solver=cp.SCS, max_iters=10000, verbose=True) assert('optimal' in prob.status) zhat = np.array(z_.value).reshape(nb, nz) nu = np.array(eqCon.dual_value).reshape(nb, neq) if eqCon is not None else None if ineqCon is not None: lam = np.array(ineqCon.dual_value).reshape(nb, nineq) slacks = np.array(slacks.value).reshape(nb, nineq) else: lam = slacks = None return prob.value, zhat, nu, lam, slacks def display_top(snapshot, key_type='lineno', limit=3): snapshot = snapshot.filter_traces(( tracemalloc.Filter(False, ""), tracemalloc.Filter(False, ""), )) top_stats = snapshot.statistics(key_type) print("Top %s lines" % limit) for index, stat in enumerate(top_stats[:limit], 1): frame = stat.traceback[0] # replace "/path/to/module/file.py" with "module/file.py" filename = os.sep.join(frame.filename.split(os.sep)[-2:]) print("#%s: %s:%s: %.1f KiB" % (index, filename, frame.lineno, stat.size / 1024)) line = linecache.getline(frame.filename, frame.lineno).strip() if line: print(' %s' % line) other = top_stats[limit:] if other: size = sum(stat.size for stat in other) print("%s other: %.1f KiB" % (len(other), size / 1024)) total = sum(stat.size for stat in top_stats) print("Total allocated size: %.1f KiB" % (total / 1024)) def _dump_cvxpy( save_dir, file_name, batch_i, *, ctx, param_order, variables, alpha, dual_cutoff, solver_used, trigger, params_numpy, sol_numpy, equality_dual, inequality_dual, slack, new_sol_lagrangian, new_equality_dual, new_active_dual, active_mask_params, dvars_numpy, ): p = pathlib.Path(save_dir) p.mkdir(parents=True, exist_ok=True) meta = { "tag": f"{file_name}", "batch_i": int(batch_i), "dtype": str(ctx.dtype), "device": str(ctx.device), "alpha": float(alpha), "dual_cutoff": float(dual_cutoff), "solver_used": solver_used, "trigger": trigger, "param_count": len(param_order), "var_count": len(variables), "eq_count": len(equality_dual), "ineq_count": len(inequality_dual), } # (p / "meta.json").write_text(json.dumps(meta, indent=2)) arrs = {} for k in range(len(param_order)): arrs[f"param_{k}"] = _np(params_numpy[k][batch_i if ctx.batch else 0]) for j in range(len(variables)): arrs[f"y_old_{j}"] = _np(sol_numpy[j][batch_i]) for l in range(len(equality_dual)): arrs[f"dual_eq_old_{l}"] = _np(equality_dual[l][batch_i]) for m in range(len(inequality_dual)): lam = inequality_dual[m][batch_i] arrs[f"dual_ineq_old_{m}"] = _np(lam) arrs[f"slack_old_{m}"] = _np(slack[m][batch_i]) try: mask_val = active_mask_params[m].value except Exception: mask_val = (lam > dual_cutoff).astype(np.float64) arrs[f"active_mask_used_{m}"] = _np(mask_val) if dvars_numpy is not None: for j in range(len(variables)): dv = dvars_numpy[j] if dv is None: arrs[f"dvars_is_none_{j}"] = np.array([True]) else: arrs[f"dvars_{j}"] = _np(dv[batch_i]) np.savez_compressed(p / f"{file_name}.npz", **arrs)