Skip to content

Commit d8a4bbc

Browse files
committed
Efficiency improvements for dual solver. Added changes for matrix-weighted slam tests
1 parent e33ab89 commit d8a4bbc

File tree

3 files changed

+117
-82
lines changed

3 files changed

+117
-82
lines changed

_scripts/wafr_problems.py

+36-9
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ def load_problem(fname="_examples/mw_loc_3d_small.pkl", id=8):
2929
if id is not None:
3030
df = df.loc[id]
3131
# convert to Homogeneous QCQP
32-
problem = convert_to_qcqp(df)
32+
if "hom_qcqp" in df:
33+
problem = df["hom_qcqp"]
34+
elif "homQCQP" in df:
35+
problem = df["homQCQP"]
36+
else:
37+
problem = convert_to_qcqp(df)
3338

3439
return problem, df
3540

@@ -80,11 +85,21 @@ def process_problems(fname="_examples/mw_loc_3d_small.pkl"):
8085
def test_problem(
8186
fname="_examples/mw_loc_3d_small.pkl",
8287
form="primal",
88+
rm_dep_constr=False,
89+
merge_cliques=True,
90+
tol=1e-8,
8391
id=8,
8492
):
8593
problem, df = load_problem(fname=fname, id=id)
94+
if rm_dep_constr:
95+
problem.remove_dependent_constraints()
96+
if merge_cliques:
97+
merge_function = problem.merge_cosmo
98+
else:
99+
merge_function = None
100+
problem.clique_decomposition(merge_function=merge_function)
86101

87-
problem.plot_asg(remove_vars=["h"], html="asg.html")
102+
problem.plot_asg(remove_vars=[problem.h], html="asg.html")
88103
problem.plot_ctree(html="ctree.html")
89104

90105
# Solve standard SDP
@@ -94,23 +109,25 @@ def test_problem(
94109
# Solve decomposed SDP
95110
methods = dict(objective="split", constraint="split")
96111
clq_list, info = solve_dsdp(
97-
problem, form=form, tol=1e-8, decomp_methods=methods, verbose=True
112+
problem, form=form, tol=tol, decomp_methods=methods, verbose=True
98113
)
99114
Y, ranks, factor_dict = problem.get_mr_completion(clq_list)
100115

101116
# Check solution
102117
assert Y.shape[1] == 1, ValueError("Decomposed solution is not rank 1")
103118
x = []
104119
for varname in df.var_sizes.keys():
105-
x.append(factor_dict[varname].flatten())
120+
x.append(factor_dict[varname][:, 0].flatten())
106121
x = np.concatenate(x)
107122
if factor_dict["h"][0, 0] < 0:
108123
x = -x
109124

110125
cost_decomp = x @ df.cost @ x
111126
cost_local = df.x_gt_init @ df.cost @ df.x_gt_init
112-
tol = 1e-3
113-
assert cost_decomp <= cost_local + tol, ValueError("Decomposed SDP has higher cost")
127+
rtol = 1e-5
128+
assert cost_decomp <= cost_local * (1 + rtol), ValueError(
129+
"Decomposed SDP has higher cost"
130+
)
114131

115132
if cost_local - cost_decomp > tol:
116133
print(f"Cost difference: {cost_local-cost_decomp}")
@@ -127,7 +144,7 @@ def test_problem(
127144
else:
128145
print("Cost difference is within tolerance. checking solution")
129146
np.testing.assert_allclose(
130-
x, df.x_gt_init, atol=1e-6, err_msg="solution doesn't match"
147+
x, df.x_gt_init, atol=5e-5, err_msg="solution doesn't match"
131148
)
132149

133150

@@ -175,7 +192,17 @@ def plot_results(dataset="mw_loc_3d"):
175192
# test_problem(fname="_examples/mw_loc_3d_small.pkl")
176193
# test_problem(fname="_examples/mw_loc_3d.pkl", form="dual", id=4)
177194
# test_problem(fname="_examples/rangeonlyloc2d_no_const-vel_small.pkl")
178-
# test_problem(fname="_examples/rangeonlyloc2d_no_const-vel.pkl", id=27)
195+
test_problem(
196+
fname="_examples/rangeonlyloc2d_no_const-vel.pkl", form="dual", tol=1e-12, id=9
197+
)
198+
# test_problem(
199+
# fname="_examples/mw_slam_10pose_r1.pkl",
200+
# rm_dep_constr=True,
201+
# form="dual",
202+
# merge_cliques=False,
203+
# tol=1e-7,
204+
# id=0,
205+
# )
179206

180207
# Process Problems
181208
# process_problems(fname="_examples/mw_loc_3d_small.pkl")
@@ -185,7 +212,7 @@ def plot_results(dataset="mw_loc_3d"):
185212

186213
# Run Speed test
187214
# run_speed_tests(dataset="mw_loc_3d", tol=1e-6)
188-
run_speed_tests(dataset="rangeonlyloc2d_no_const-vel", tol=1e-8)
215+
# run_speed_tests(dataset="rangeonlyloc2d_no_const-vel", tol=1e-8)
189216

190217
# Plot Results
191218
# plot_results()

cert_tools/hom_qcqp.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -603,18 +603,23 @@ def get_mr_completion(self, clique_mats, var_list=None, rank_tol=1e5, debug=Fals
603603
Yval = np.vstack([factor_dict[var] for var in clique.separator])
604604
# Find the orthogonal transformation between cliques using the polar decomposition
605605
Q, H = polar(V.T @ Yval)
606-
U_Q = U @ Q
606+
607607
if debug:
608608
V_Q = V @ Q
609+
U_Q = U @ Q
609610
y = np.vstack([Yval, U_Q])
610611
np.testing.assert_allclose(
611612
y @ y.T, clique_mats[i], atol=1e-5, rtol=1e-4
612613
)
613-
614+
# Retrieve "rotated" residuals
614615
for key in clique.residual:
615-
# NOTE: Assumes that separator comes before residual in variable ordering
616-
inds = clique._get_indices(key) - (max(sep_inds) + 1)
617-
factor_dict[key] = U_Q[inds, :]
616+
inds = clique._get_indices(key)
617+
factor_dict[key] = factor[inds, :] @ Q
618+
619+
# Check for and fix inverted solution
620+
if np.any(factor_dict[self.h] < 0):
621+
for key, value in factor_dict.items():
622+
factor_dict[key] = -value
618623

619624
# Construct full factor
620625
if var_list is None:

cert_tools/sparse_solvers.py

+71-68
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ def solve_dsdp_dual(
223223
tol (float, optional): Tolerance for solver. Defaults to TOL.
224224
adjust (bool, optional): If true, adjust the cost matrix. Defaults to False.
225225
"""
226+
T0 = time()
226227
# List of constraints with homogenizing constraint.
227228
A_h = PolyMatrix()
228229
A_h[problem.h, problem.h] = 1
@@ -233,62 +234,65 @@ def solve_dsdp_dual(
233234

234235
# CLIQUE VARIABLES
235236
cliques = problem.cliques
236-
zvars = [M.variable(fu.Domain.inPSDCone(c.size)) for c in cliques]
237+
cvars = [M.variable(fu.Domain.inPSDCone(c.size)) for c in cliques]
237238

238239
# LAGRANGE VARIABLES
239-
y = [M.variable(fu.Domain.unbounded(1)) for i in range(len(As))]
240+
y = [M.variable(f"y{i}") for i in range(len(As))]
240241

241242
# OBJECTIVE
242243
if verbose:
243244
print("Adding Objective")
244245
M.objective(fu.ObjectiveSense.Minimize, y[-1])
245246

246-
# AFFINE CONSTRAINTS: C + sum(Ai*y_i) - sum(Z_k) = 0
247+
# CONSTRUCT CERTIFICATE
248+
if verbose:
249+
print("Building Certificate Matrix")
250+
cert_mat_list = []
251+
# get constant cost matrix
252+
C_mat = problem.C.get_matrix(problem.var_sizes)
253+
C_fusion = fu.Expr.constTerm(sparse_to_fusion(C_mat))
254+
cert_mat_list.append(C_fusion)
255+
# get constraint-multiplier products
256+
for i, A in enumerate(As):
257+
A_mat = A.get_matrix(problem.var_sizes)
258+
A_fusion = sparse_to_fusion(A_mat)
259+
cert_mat_list.append(fu.Expr.mul(A_fusion, y[i]))
260+
# sum into certificate
261+
H = fu.Expr.add(cert_mat_list)
262+
263+
# AFFINE CONSTRAINTS:
264+
# H_ij = C_ij + sum(Ai*y_i)_ij - sum(Z_k)_ij = 0
247265
# Get a list of edges in the aggregate sparsity pattern (including main diagonal)
248266
if verbose:
249267
print("Generating Affine Constraints")
250268
edges = [e.tuple for e in problem.asg.es]
251269
edges += [(v.index, v.index) for v in problem.asg.vs]
270+
252271
# Generate one matrix constraint per edge. This links the cliques to the
253272
for edge_id in edges:
254273
# Get variables in edge from graph
255274
var0 = problem.asg.vs["name"][edge_id[0]]
256275
var1 = problem.asg.vs["name"][edge_id[1]]
257-
mat_list = []
258-
# Get component of Cost matrix
259-
C_mat = problem.C[var0, var1]
260-
if not np.all(C_mat == 0):
261-
c_mat = -C_mat.reshape(-1)
262-
else:
263-
c_mat = 0.0
264-
265-
# Get component of Constraint matrices
266-
for i, A in enumerate(As):
267-
A_mat = A[var0, var1]
268-
if not np.all(A_mat == 0):
269-
if not sp.issparse(A_mat):
270-
A_mat = sp.coo_array(A_mat)
271-
a_mat = sparse_to_fusion(A_mat.reshape(-1, 1))
272-
mat_list.append(fu.Expr.mul(a_mat, y[i]))
273-
274-
# Component of clique variables
275-
for k, clique in enumerate(problem.cliques):
276+
# Get component of certificate matrix
277+
row_inds = problem._get_indices(var0)
278+
col_inds = problem._get_indices(var1)
279+
inds = get_block_inds(row_inds, col_inds, var0 == var1)
280+
sum_list = [H.pick(inds)]
281+
# Find the cliques that are involved with these variables
282+
clique_inds = problem.var_clique_map[var0] & problem.var_clique_map[var1]
283+
cliques = [problem.cliques[i] for i in clique_inds]
284+
# get components of clique variables
285+
for clique in cliques:
276286
if var0 in clique.var_list and var1 in clique.var_list:
277-
ind1 = clique._get_indices(var0)
278-
ind2 = clique._get_indices(var1)
279-
inds = []
280-
for i in ind1:
281-
for j in ind2:
282-
inds.append([i, j])
283-
mat_list.append(-zvars[k].pick(inds))
284-
287+
row_inds = clique._get_indices(var0)
288+
col_inds = clique._get_indices(var1)
289+
inds = get_block_inds(row_inds, col_inds, var0 == var1)
290+
sum_list.append(-cvars[clique.index].pick(inds))
285291
# Add the list together
286-
matsum = fu.Expr.add(mat_list)
287-
M.constraint(f"e_{var0}_{var1}", matsum, fu.Domain.equalsTo(c_mat))
292+
matsumvec = fu.Expr.add(sum_list)
293+
M.constraint(f"e_{var0}_{var1}", matsumvec, fu.Domain.equalsTo(0.0))
288294

289295
# SOLVE
290-
if verbose:
291-
print("Starting problem solve")
292296
M.setSolverParam("intpntSolveForm", "dual")
293297
# record problem
294298
if verbose:
@@ -306,50 +310,49 @@ def solve_dsdp_dual(
306310
M.setLogHandler(f)
307311

308312
M.acceptedSolutionStatus(fu.AccSolutionStatus.Anything)
309-
T0 = time()
310-
M.solve()
311313
T1 = time()
314+
M.solve()
315+
T2 = time()
316+
317+
# Store information
318+
info = {
319+
"success": False,
320+
"cost": -np.inf,
321+
"runtime": T2 - T1,
322+
"preprocess_time": T1 - T0,
323+
"msg": str(M.getProblemStatus()),
324+
}
312325

313326
# EXTRACT SOLN
314-
if M.getProblemStatus() in [
315-
fu.ProblemStatus.PrimalAndDualFeasible,
316-
fu.ProblemStatus.Unknown,
317-
]:
327+
status = M.getProblemStatus()
328+
if status == fu.ProblemStatus.PrimalAndDualFeasible:
318329
# Get MOSEK cost
319330
cost = M.primalObjValue()
320-
if cost < 0:
321-
print("cost is negative! sanity check:")
322-
clq_list = [zvar.dual().reshape(zvar.shape) for zvar in zvars]
323-
dual = [zvar.level().reshape(zvar.shape) for zvar in zvars]
331+
clq_list = [cvar.dual().reshape(cvar.shape) for cvar in cvars]
332+
dual = [cvar.level().reshape(cvar.shape) for cvar in cvars]
324333
mults = [y_i.level() for y_i in y]
325-
info = {
326-
"success": True,
327-
"cost": cost,
328-
"time": T1 - T0,
329-
"msg": M.getProblemStatus(),
330-
"dual": dual,
331-
"mults": mults,
332-
}
333-
elif M.getProblemStatus() is fu.ProblemStatus.DualInfeasible:
334-
clq_list = []
335-
info = {
336-
"success": False,
337-
"cost": -np.inf,
338-
"time": T1 - T0,
339-
"msg": "dual infeasible",
340-
}
334+
info["success"] = True
335+
info["dual"] = dual
336+
info["cost"] = cost
337+
info["mults"] = mults
341338
else:
342-
print("Unknown status:", M.getProblemStatus())
343-
clq_list = []
344-
info = {
345-
"success": False,
346-
"cost": -np.inf,
347-
"time": T1 - T0,
348-
"msg": M.getProblemStatus(),
349-
}
339+
print("Solve Failed - Mosek Status: " + str(status))
350340
return clq_list, info
351341

352342

343+
def get_block_inds(row_inds, col_inds, triu=False):
344+
"""Helper function for getting a grid of indices based on row and column indices. Only selects upper triangle if triu is set to true"""
345+
inds = []
346+
for row in range(len(row_inds)):
347+
if triu:
348+
colstart = row
349+
else:
350+
colstart = 0
351+
for col in range(colstart, len(col_inds)):
352+
inds.append([row_inds[row], col_inds[col]])
353+
return np.array(inds)
354+
355+
353356
def solve_dsdp_primal(
354357
problem: HomQCQP,
355358
reduce_constrs=None,

0 commit comments

Comments
 (0)