diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index b43f90e..b758da6 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -29,3 +29,8 @@ jobs: - name: Sphinx build run: | sphinx-build docs/source docs/build + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/build/ diff --git a/.gitignore b/.gitignore index 47530eb..26a192e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ mosek_output.tmp *.ptf bin/act docs/build/doctest +docs/build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 152d311..30491e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,36 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] -- YYYY-MM-DD +(00-changelog)= ### Added +(01-changelog)= ### Changed +(02-changelog)= ### Fixed +## [0.0.2] - 2025-06-04 + +(6-changelog)= +### Added + +- RangeOnlyLifter base class for 2D/3D range-only localization (base_lifters/range_only_lifter.py) +- RangeOnlyNsqLifter: specialized lifter for range-only localization without squared distances (examples/ro_nsq_lifter.py) +- RangeOnlySqLifter: specialized lifter for squared-distance-based range-only localization +- GitHub Pages deployment step to documentation.yml using peaceiris/actions-gh-pages for automatic documentation publishing + +(5-changelog)= +### Removed + +- docs/build/ files + +(4-changelog)= +### Fixed + +- Corrected return type annotations in get_grad and get_hess methods in state_lifter.py from float to np.ndarray +- Documentation of MonoLifter and WahbaLifter + ## [0.0.1] - 2025-05-25 -This is the initial release of the toolbox. It is based on and now included there as a submodule. \ No newline at end of file +This is the initial release of the toolbox. It is based on and now included there as a submodule. diff --git a/docs/build/.buildinfo b/docs/build/.buildinfo deleted file mode 100644 index 5f839cd..0000000 --- a/docs/build/.buildinfo +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. -config: f1a2f84a0c1211555f287d37db8023c9 -tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/build/.buildinfo.bak b/docs/build/.buildinfo.bak deleted file mode 100644 index 4b8e038..0000000 --- a/docs/build/.buildinfo.bak +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. -config: c20782f7044a50dd9aea291fbf376105 -tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/build/.doctrees/api.doctree b/docs/build/.doctrees/api.doctree deleted file mode 100644 index a80678f..0000000 Binary files a/docs/build/.doctrees/api.doctree and /dev/null differ diff --git a/docs/build/.doctrees/api/algorithms.doctree b/docs/build/.doctrees/api/algorithms.doctree deleted file mode 100644 index fc3673f..0000000 Binary files a/docs/build/.doctrees/api/algorithms.doctree and /dev/null differ diff --git a/docs/build/.doctrees/api/auto_tight.doctree b/docs/build/.doctrees/api/auto_tight.doctree deleted file mode 100644 index e555c12..0000000 Binary files a/docs/build/.doctrees/api/auto_tight.doctree and /dev/null differ diff --git a/docs/build/.doctrees/api/lifters.doctree b/docs/build/.doctrees/api/lifters.doctree deleted file mode 100644 index 7b68f48..0000000 Binary files a/docs/build/.doctrees/api/lifters.doctree and /dev/null differ diff --git a/docs/build/.doctrees/api/utils.doctree b/docs/build/.doctrees/api/utils.doctree deleted file mode 100644 index c8d6c79..0000000 Binary files a/docs/build/.doctrees/api/utils.doctree and /dev/null differ diff --git a/docs/build/.doctrees/contributing.doctree b/docs/build/.doctrees/contributing.doctree deleted file mode 100644 index cd51827..0000000 Binary files a/docs/build/.doctrees/contributing.doctree and /dev/null differ diff --git a/docs/build/.doctrees/environment.pickle b/docs/build/.doctrees/environment.pickle deleted file mode 100644 index 0df1afe..0000000 Binary files a/docs/build/.doctrees/environment.pickle and /dev/null differ diff --git a/docs/build/.doctrees/examples.doctree b/docs/build/.doctrees/examples.doctree deleted file mode 100644 index dede53d..0000000 Binary files a/docs/build/.doctrees/examples.doctree and /dev/null differ diff --git a/docs/build/.doctrees/examples/a.doctree b/docs/build/.doctrees/examples/a.doctree deleted file mode 100644 index 9d60d2d..0000000 Binary files a/docs/build/.doctrees/examples/a.doctree and /dev/null differ diff --git a/docs/build/.doctrees/examples/b.doctree b/docs/build/.doctrees/examples/b.doctree deleted file mode 100644 index b560c14..0000000 Binary files a/docs/build/.doctrees/examples/b.doctree and /dev/null differ diff --git a/docs/build/.doctrees/examples/c.doctree b/docs/build/.doctrees/examples/c.doctree deleted file mode 100644 index a55f2f9..0000000 Binary files a/docs/build/.doctrees/examples/c.doctree and /dev/null differ diff --git a/docs/build/.doctrees/examples/d.doctree b/docs/build/.doctrees/examples/d.doctree deleted file mode 100644 index 9d80038..0000000 Binary files a/docs/build/.doctrees/examples/d.doctree and /dev/null differ diff --git a/docs/build/.doctrees/examples/robust.doctree b/docs/build/.doctrees/examples/robust.doctree deleted file mode 100644 index 65141b7..0000000 Binary files a/docs/build/.doctrees/examples/robust.doctree and /dev/null differ diff --git a/docs/build/.doctrees/examples/standard.doctree b/docs/build/.doctrees/examples/standard.doctree deleted file mode 100644 index 3aef5c2..0000000 Binary files a/docs/build/.doctrees/examples/standard.doctree and /dev/null differ diff --git a/docs/build/.doctrees/examples/templates.doctree b/docs/build/.doctrees/examples/templates.doctree deleted file mode 100644 index c0e4180..0000000 Binary files a/docs/build/.doctrees/examples/templates.doctree and /dev/null differ diff --git a/docs/build/.doctrees/examples/toy.doctree b/docs/build/.doctrees/examples/toy.doctree deleted file mode 100644 index 3da8b28..0000000 Binary files a/docs/build/.doctrees/examples/toy.doctree and /dev/null differ diff --git a/docs/build/.doctrees/index.doctree b/docs/build/.doctrees/index.doctree deleted file mode 100644 index b5ce655..0000000 Binary files a/docs/build/.doctrees/index.doctree and /dev/null differ diff --git a/docs/build/.doctrees/quickstart.doctree b/docs/build/.doctrees/quickstart.doctree deleted file mode 100644 index d36497b..0000000 Binary files a/docs/build/.doctrees/quickstart.doctree and /dev/null differ diff --git a/docs/build/.doctrees/whatsnew.doctree b/docs/build/.doctrees/whatsnew.doctree deleted file mode 100644 index 7b75de4..0000000 Binary files a/docs/build/.doctrees/whatsnew.doctree and /dev/null differ diff --git a/docs/build/.doctrees/whatsnew/0.0.1.doctree b/docs/build/.doctrees/whatsnew/0.0.1.doctree deleted file mode 100644 index e3c3f82..0000000 Binary files a/docs/build/.doctrees/whatsnew/0.0.1.doctree and /dev/null differ diff --git a/docs/build/.doctrees/whatsnew/CHANGELOG.doctree b/docs/build/.doctrees/whatsnew/CHANGELOG.doctree deleted file mode 100644 index 54ad474..0000000 Binary files a/docs/build/.doctrees/whatsnew/CHANGELOG.doctree and /dev/null differ diff --git a/docs/build/_images/overview.png b/docs/build/_images/overview.png deleted file mode 100644 index 8bf338e..0000000 Binary files a/docs/build/_images/overview.png and /dev/null differ diff --git a/docs/build/_images/overview.webp b/docs/build/_images/overview.webp deleted file mode 100644 index d4cc81b..0000000 Binary files a/docs/build/_images/overview.webp and /dev/null differ diff --git a/docs/build/_images/poly4_lifter_A.png b/docs/build/_images/poly4_lifter_A.png deleted file mode 100644 index 35ce631..0000000 Binary files a/docs/build/_images/poly4_lifter_A.png and /dev/null differ diff --git a/docs/build/_images/poly4_lifter_B.png b/docs/build/_images/poly4_lifter_B.png deleted file mode 100644 index 4c8ac96..0000000 Binary files a/docs/build/_images/poly4_lifter_B.png and /dev/null differ diff --git a/docs/build/_images/poly6_lifter_A.png b/docs/build/_images/poly6_lifter_A.png deleted file mode 100644 index b6f900c..0000000 Binary files a/docs/build/_images/poly6_lifter_A.png and /dev/null differ diff --git a/docs/build/_images/poly6_lifter_B.png b/docs/build/_images/poly6_lifter_B.png deleted file mode 100644 index 3fb47cc..0000000 Binary files a/docs/build/_images/poly6_lifter_B.png and /dev/null differ diff --git a/docs/build/_modules/index.html b/docs/build/_modules/index.html deleted file mode 100644 index 94616f9..0000000 --- a/docs/build/_modules/index.html +++ /dev/null @@ -1,285 +0,0 @@ - - - - - - - - Overview: module code — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - -
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/auto_template.html b/docs/build/_modules/popcor/auto_template.html deleted file mode 100644 index 2884553..0000000 --- a/docs/build/_modules/popcor/auto_template.html +++ /dev/null @@ -1,1508 +0,0 @@ - - - - - - - - popcor.auto_template — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.auto_template

-import time
-from copy import deepcopy
-
-import matplotlib
-import matplotlib.patches
-import numpy as np
-import pandas as pd
-import scipy.sparse as sp
-from cert_tools.linalg_tools import find_dependent_columns, rank_project
-from cert_tools.sdp_solvers import solve_feasibility_sdp
-from cert_tools.sdp_solvers import solve_lambda_cvxpy as solve_lambda
-from cert_tools.sdp_solvers import solve_sdp_cvxpy
-from poly_matrix import PolyMatrix
-
-from popcor.base_lifters import StateLifter
-from popcor.solvers.common import find_local_minimum
-from popcor.solvers.sparse import bisection, brute_force
-from popcor.utils.common import get_aggregate_sparsity, get_vec
-from popcor.utils.constraint import Constraint, generate_poly_matrix, plot_poly_matrix
-from popcor.utils.plotting_tools import (
-    add_colorbar,
-    add_rectangles,
-    import_plt,
-    initialize_discrete_cbar,
-    plot_basis,
-    plot_singular_values,
-    savefig,
-)
-
-plt = import_plt()
-
-# parameter of SDP solver
-TOL = 1e-10
-
-NOISE_SEED = 5
-
-ADJUST_Q = True  # rescale Q matrix
-PRIMAL = False  # use primal or dual formulation of SDP. Recommended is False, because of how MOSEK handles this parameter.
-
-FACTOR = 1.2  # oversampling factor.
-
-PLOT_MAX_MATRICES = 10  # set to np.inf to plot all individual matrices.
-
-USE_KNOWN = True
-USE_INCREMENTAL = False
-
-GLOBAL_THRESH = 1e-3  # consider dual problem optimal when eps<GLOBAL_THRESH
-
-METHOD_NULL = "qrp"  # use svd or qp for comparison only, otherwise leave it at qrp
-
-EPSILON = 1e-4  # fixed epsilon for sparsity-promoting SDP
-
-
-
-[docs] -class AutoTemplate(object): - """ - Class to incrementally learn and augment constraint templates until we reach tightness. - """ - - TOL_RANK_ONE = 1e7 - TOL_REL_GAP = 1e-3 - - # number of random inits to find the global solution - # we always try from ground truth, so for low-enough noise - # that should be good enough. - N_INITS = 1 - - APPLY_TEMPLATES_TO_OTHERS = True - - def __init__( - self, - lifter: StateLifter, - ): - self.lifter = lifter - - # templates contains the learned "templates" of the form: - # ((i, mat_vars), <i-th learned vector for these mat_vars variables, PolyRow form>) - self.templates_poly_ = None # for plotting only: all templats stacked in one - - # constraints after applying templates to the parameters. - self.constraints = [] - - # templates contains all the learned templates - self.templates = [] - - # contains all known constraints, computed only once. - self.templates_known = [] - - # contains the currently relevant known constraints - self.templates_known_sub = [] - self.constraint_index = 0 - - # keep track of which constraints have been tested any constraint twice. - self.index_tested = set() - - # solver results - self.solver_vars = None - self.df_tight = None - self.ranks = [] - self.dual_costs = [] - - # tightness dict makes sure we don't compute tightness twice. - self.reset_tightness_dict() - - # currently used variables - self.mat_vars = [] - - # so-far used variables - self.variable_list = [] - - # can be overwritten later - self.use_known = USE_KNOWN - self.use_incremental = USE_INCREMENTAL - - def reset_tightness_dict(self): - self.tightness_dict = {"rank": None, "cost": None} - - @property - def templates_poly(self): - if self.templates_poly_ is None: - self.templates_poly_ = self.generate_templates_poly( - factor_out_parameters=True - ) - return self.templates_poly_ - - @property - def A_matrices(self): - return [c.A_sparse_ for c in self.constraints] - - def check_violation(self, dual_cost): - assert self.solver_vars is not None - primal_cost = self.solver_vars["qcqp_cost"] - if primal_cost is None: - print("warning can't check violation, no primal cost.") - return False - return (dual_cost - primal_cost) / abs(dual_cost) > self.TOL_REL_GAP - - def duality_gap_is_zero(self, dual_cost, verbose=False, data_dict={}): - assert self.solver_vars is not None - primal_cost = self.solver_vars["qcqp_cost"] - RDG = (primal_cost - dual_cost) / abs(dual_cost) - if RDG < -1e-2: - print( - f"Warning: dual is significantly larger than primal: d={dual_cost:.3e} > p={primal_cost:.3e}, diff={dual_cost-primal_cost:.3e}" - ) - return False - res = RDG < self.TOL_REL_GAP - data_dict["RDG"] = RDG - if not verbose: - return res - - if res: - print("achieved cost tightness:") - else: - print("no cost tightness yet:") - print(f"qcqp cost={primal_cost:.4e}, dual cost={dual_cost:.4e}") - return res - - def is_rank_one(self, eigs, verbose=False, data_dict={}): - SVR = eigs[0] / eigs[1] - data_dict["SVR"] = SVR - res = SVR > self.TOL_RANK_ONE - if not verbose: - return res - if res: - print("achieved rank tightness:") - else: - print("no rank tightness yet:") - print( - f"first two eigenvalues: {eigs[0]:.2e}, {eigs[1]:.2e}, ratio:{eigs[0] / eigs[1]:.2e}" - ) - return res - - def is_tight(self, verbose=False, data_dict={}, tightness=None): - - if tightness is None: - tightness = self.lifter.TIGHTNESS - - if self.tightness_dict[tightness] is not None: - return self.tightness_dict[tightness] - - A_b_list_all = self.get_A_b_list() - A_list = [A for A, __ in A_b_list_all[1:]] # for debugging only - - B_list = self.lifter.get_B_known() - X, info = self._test_tightness(A_b_list_all, B_list, verbose=verbose) - - assert self.solver_vars is not None - self.solver_vars["X"] = X # type: ignore - - self.dual_costs.append(info["cost"]) - self.variable_list.append(self.mat_vars) - - data_dict["q"] = self.solver_vars["qcqp_cost"] - data_dict.update(self.solver_vars) - - if info["cost"] is None: - self.ranks.append(np.zeros(self.lifter.get_dim_x())) - print(f"Warning: solver failed with message: {info['msg']}") - max_error, bad_list = self.lifter.test_constraints(A_list, errors="print") - print("Maximum error:", max_error) - return False - elif self.check_violation(info["cost"]): - self.ranks.append(np.zeros(A_list[0].shape[0])) # type: ignore - print( - f"Warning: dual cost higher than QCQP, d={info['cost']:.2e}, q={self.solver_vars['qcqp_cost']:.2e}" - ) - print( - "Usually this means that MOSEK tolerances are too loose, or that there is a mistake in the constraints." - ) - print( - "It can also mean that we are not sampling enough of the space close to the true solution." - ) - max_error, bad_list = self.lifter.test_constraints(A_list, errors="print") - print("Maximum feasibility error at random x:", max_error) - - tol = 1e-10 - xhat = self.solver_vars["xhat"] - max_error = -np.inf - - assert xhat is not None - assert X is not None - for Ai in A_list: - assert isinstance(Ai, np.ndarray) or isinstance(Ai, sp.spmatrix) - assert isinstance(xhat, np.ndarray) - error = xhat.T @ Ai @ xhat - - errorX = np.trace(X @ Ai) - max_error = max(errorX, max_error) - if abs(error) > tol: - print( - f"Feasibility error too high! xAx:{error:.2e}, <X,A>:{errorX:.2e}" - ) - print(f"Maximum feasibility error at solution x: {max_error}") - - assert X is not None - - data_dict["d"] = info["cost"] - - # sanity check - final_cost = np.trace(self.solver_vars["Q"] @ X) - if abs(final_cost - info["cost"]) / info["cost"] >= 1e-1: - print( - f"Warning: cost is inconsistent: {final_cost:.3e}, {info['cost']:.3e}" - ) - - eigs = np.linalg.eigvalsh(X)[::-1] - self.ranks.append(eigs) - - if self.lifter.robust: - x_dim = self.lifter.d + self.lifter.d**2 + 1 - wi = X[0, x_dim::x_dim] - print("should be plus or minus ones:", wi.round(4)) - - if self.solver_vars["qcqp_cost"] is not None: - cost_tight = self.duality_gap_is_zero( - info["cost"], verbose=tightness == "cost", data_dict=data_dict - ) - else: - cost_tight = False - rank_tight = self.is_rank_one( - eigs, verbose=tightness == "rank", data_dict=data_dict - ) - self.tightness_dict["rank"] = rank_tight - self.tightness_dict["cost"] = cost_tight # type: ignore - if tightness == "rank": - return rank_tight - elif tightness == "cost": - return cost_tight - - def get_A_list(self, var_dict=None): - if var_dict is None: - A_known = [] - if self.use_known: - A_known += [constraint.A_sparse_ for constraint in self.templates_known] - return A_known + [constraint.A_sparse_ for constraint in self.constraints] - else: - A_known = [] - if self.use_known: - A_known += [constraint.A_poly_ for constraint in self.templates_known] - A_list_poly = A_known + [ - constraint.A_poly_ for constraint in self.constraints - ] - return [A.get_matrix(var_dict) for A in A_list_poly] - - def get_A_b_list(self): - A_list = self.get_A_list() - A_b_list_all = self.lifter.get_A_b_list(A_list) - return A_b_list_all - - def generate_minimal_subset( - self, - reorder=False, - tightness="rank", - use_last=None, - use_bisection=False, - tol=TOL, - ): - def function(A_b_list_here, df_data, verbose=False): - """Function for bisection or brute_force""" - if (len(A_b_list_here) in df_data.keys()) and not verbose: - new_data = df_data[len(A_b_list_here)] - else: - new_data = {"lifter": str(self.lifter), "reorder": reorder} - X, info = self._test_tightness( - A_b_list_here, B_list=B_list, verbose=False - ) - dual_cost = info["cost"] - new_data["dual cost"] = dual_cost - if dual_cost is None: - print(f"{len(A_b_list_here)}: solver error? msg: {info['msg']}") - new_data["eigs"] = np.full(self.lifter.get_dim_X(), np.nan) - new_data["cost_tight"] = False - new_data["rank_tight"] = False - df_data[len(A_b_list_here)] = deepcopy(new_data) - return False - - elif self.duality_gap_is_zero(dual_cost): - print(f"{len(A_b_list_here)}: cost-tight") - new_data["cost_tight"] = True - else: - print(f"{len(A_b_list_here)}: not cost-tight yet") - new_data["cost_tight"] = False - - assert X is not None - assert self.solver_vars is not None - - eigs = np.linalg.eigvalsh(X)[::-1] - new_data["eigs"] = eigs - if self.is_rank_one(eigs): - print(f"{len(A_b_list_here)}: rank-tight") - new_data["rank_tight"] = True - else: - new_data["rank_tight"] = False - print(f"{len(A_b_list_here)}: not rank-tight yet") - df_data[len(A_b_list_here)] = deepcopy(new_data) - - if verbose: - print( - f"dual cost: {dual_cost:4.4e}, primal cost: {self.solver_vars['qcqp_cost']:4.4f}" - ) - print(f"largest 10 eigenvalues: {eigs[:10]}") - - if tightness == "rank": - return new_data["rank_tight"] - else: - return new_data["cost_tight"] - - A_b_list_all = self.get_A_b_list() - B_list = self.lifter.get_B_known() - - force_first = 1 - if self.use_known: - force_first += len(self.templates_known) - - if reorder: - if self.solver_vars is None: - self.find_local_solution() - assert self.solver_vars is not None - - # find the importance of each constraint - _, lamdas = solve_lambda( - self.solver_vars["Q"], - A_b_list_all, - self.solver_vars["xhat"], - B_list=B_list, - force_first=force_first, - tol=tol, - adjust=True, - primal=False, - verbose=False, - fixed_epsilon=EPSILON, - ) - if lamdas is None: - print("Warning: problem doesn't have feasible solution!") - print("Sanity checks:") - B_list = self.lifter.get_B_known() - X, info = self._test_tightness(A_b_list_all, B_list, verbose=False) - xhat_from_X, _ = rank_project(X, p=1) - xhat = self.solver_vars["xhat"] - - assert xhat is not None - print("max xhat error:", np.min(xhat - xhat_from_X)) - print("max Hx", np.max(np.abs(info["H"] @ xhat))) - print("max Hx_from_X", np.max(np.abs(info["H"] @ xhat_from_X))) - eigs = np.linalg.eigvalsh(info["H"].toarray()) - print("min eig of H", np.min(eigs)) - return None - print("found valid lamdas") - - # order the redundant constraints by importance - redundant_idx = np.argsort(np.abs(lamdas[force_first:]))[::-1] - sorted_idx = np.r_[np.arange(force_first), force_first + redundant_idx] - else: - sorted_idx = range(len(A_b_list_all)) - - inputs = [A_b_list_all[idx] for idx in sorted_idx] - - B_list = self.lifter.get_B_known() - df_data = [] - - if use_last is None: - start_idx = force_first - else: - start_idx = max(len(inputs) - use_last, force_first) - - df_data = {} - if use_bisection: - bisection( - function, (inputs, df_data), left_num=start_idx, right_num=len(inputs) - ) - else: - brute_force( - function, (inputs, df_data), left_num=start_idx, right_num=len(inputs) - ) - - df_tight = pd.DataFrame(df_data.values(), index=list(df_data.keys())) - if self.df_tight is None: - self.df_tight = df_tight - else: - self.df_tight = pd.concat([self.df_tight, df_tight], axis=0) - - minimal_indices = [] - if tightness == "cost": - min_num = df_tight[df_tight.cost_tight == 1].index.min() - elif tightness == "rank": - min_num = df_tight[df_tight.rank_tight == 1].index.min() - if not np.isnan(min_num): - minimal_indices = list(sorted_idx[:min_num]) - return minimal_indices - - def find_local_solution(self, n_inits=None, verbose=False, plot=False): - if n_inits is None: - n_inits = self.N_INITS - np.random.seed(NOISE_SEED) - Q = self.lifter.get_Q() - y = self.lifter.y_ - qcqp_that, qcqp_cost, info = find_local_minimum( - self.lifter, y=y, verbose=verbose, n_inits=n_inits, plot=plot - ) - self.solver_vars = dict(Q=Q, y=y, qcqp_cost=qcqp_cost, xhat=None) - self.solver_vars.update(info) # type: ignore - if qcqp_cost is not None: - xhat = self.lifter.get_x(qcqp_that) - self.solver_vars["xhat"] = xhat # type: ignore - - # calculate error for global estimate - self.solver_vars.update(self.lifter.get_error(qcqp_that)) - # calculate errors for local estimates - for key, qcqp_that_local in info.items(): - if key.startswith("local solution"): - solution_idx = key.strip("local solution ") - error_dict = self.lifter.get_error(qcqp_that_local) - self.solver_vars.update( - { - f"local {solution_idx} {error_name}": err - for error_name, err in error_dict.items() - } - ) - - return True - - def find_global_solution(self, data_dict={}): - from cert_tools.sdp_solvers import options_cvxpy - - assert self.solver_vars is not None - - A_b_list_all = self.get_A_b_list() - options_cvxpy["accept_unknown"] = True - - # find or certify global solution - if self.lifter.TIGHTNESS == "rank": - X = self.solver_vars["X"] - x, info = rank_project(X, p=1) - x = x.flatten() - else: - """Try to solve dual problem.""" - xhat = self.solver_vars["xhat"] - - H, info = solve_feasibility_sdp( - self.solver_vars["Q"], - A_b_list_all, - xhat, - adjust=True, - options=options_cvxpy, - tol=1e-10, - # soft_epsilon=False, - # eps_tol=1e-4, - soft_epsilon=True, - ) - if info["eps"] is not None: - cert = abs(info["eps"]) <= GLOBAL_THRESH - print(f"global solution eps: {info['eps']:.2e}, cert: {cert}") - data_dict["global solution cert"] = cert - - if info["eps"] and cert: - x = xhat - else: - x = None - - # sanity check: try to certify local minima (should fail) - keys = [key for key in data_dict.keys() if key.startswith("local solution")] - for key in keys: - x_local = data_dict[key] - x_local = self.lifter.get_x(theta=x_local) - H, info = solve_feasibility_sdp( - self.solver_vars["Q"], - A_b_list_all, - x_local, - adjust=True, - tol=1e-10, - options=options_cvxpy, - ) - if info["eps"] is not None: - print(f"local solution eps: {info['eps']:.2e}") - cert = abs(info["eps"]) <= GLOBAL_THRESH - data_dict[key + " cert"] = cert - - if x is not None: - theta = self.lifter.get_theta(x) - cost = self.lifter.get_cost(theta, self.solver_vars["y"]) - data_dict["global theta"] = theta - data_dict["global cost"] = cost - return True - return False - - def _test_tightness(self, A_b_list_all, B_list=[], verbose=False): - from cert_tools.sdp_solvers import options_cvxpy - - if self.solver_vars is None: - self.find_local_solution(verbose=verbose) - assert self.solver_vars is not None - - options_cvxpy["accept_unknown"] = True - X, info = solve_sdp_cvxpy( - self.solver_vars["Q"], - A_b_list_all, - B_list=B_list, - adjust=ADJUST_Q, - verbose=verbose, - primal=PRIMAL, - tol=TOL, - options=options_cvxpy, - ) - return X, info - - def update_variables(self): - # add new variable to the list of variables to study - try: - self.mat_vars = next(self.variable_iter) - return True - except StopIteration: - return False - - def extract_known_templates(self): - """Find which of the known constraints are relevant for the current variables.""" - templates_known_sub = [] - for c in self.templates_known: - var_subset = set(c.A_poly_.get_variables()) - if var_subset.issubset(self.mat_vars): - templates_known_sub.append(c) - - new_index_set = set([t.index for t in templates_known_sub]) - old_index_set = set([t.index for t in self.templates_known_sub]) - diff_index_set = new_index_set.difference(old_index_set) - - self.templates_known_sub = templates_known_sub - return len(diff_index_set) - - def learn_templates(self, plot=False, data_dict=None): - from popcor import AutoTight - - templates = [] - mat_var_dict = self.lifter.get_var_dict(self.mat_vars) - param_dict = self.lifter.get_involved_param_dict(self.mat_vars) - - t1 = time.time() - Y = AutoTight.generate_Y( - self.lifter, - var_subset=self.mat_vars, - param_subset=param_dict, - factor=FACTOR, - ) - a_vectors = [] - if self.use_incremental: - for c in self.templates: - ai = get_vec(c.A_poly_.get_matrix(mat_var_dict)) - bi = self.lifter.augment_using_zero_padding(ai, param_dict) - a_vectors.append(bi) - if self.use_known: - for c in self.templates_known_sub: - ai = get_vec(c.A_poly_.get_matrix(mat_var_dict)) - bi = self.lifter.augment_using_zero_padding(ai, param_dict) - a_vectors.append(bi) - Y = np.vstack([Y] + a_vectors) - - if plot: - fig, ax = plt.subplots() - - print(f"data matrix Y has shape {Y.shape} ") - for i in range(AutoTight.N_CLEANING_STEPS + 1): - if i == 0: - print(f"getting basis...", end="") - else: - print(f"cleaning step {i}/{AutoTight.N_CLEANING_STEPS+1}...", end="") - basis_new, S = AutoTight.get_basis(self.lifter, Y, method=METHOD_NULL) - print("...done, analyzing...", end="") - corank = basis_new.shape[0] - if corank > 0: - AutoTight.test_S_cutoff(S, corank) - - bad_idx = AutoTight.clean_Y(basis_new, Y, S, plot=False) - - if plot: - if len(bad_idx): - plot_singular_values( - S, eps=AutoTight.EPS_SVD, label=f"run {i}", ax=ax - ) - else: - plot_singular_values(S, eps=AutoTight.EPS_SVD, ax=ax, label=None) - - if len(bad_idx) > 0: - print(f"there are {len(bad_idx)} bad basis vectors (with high error).") - Y = np.delete(Y, bad_idx, axis=0) - else: - print(f"no bad basis vectors found.") - break - - if basis_new.shape[0]: - for i, b in enumerate(basis_new): - constraint = Constraint.init_from_b( - index=self.constraint_index, - mat_var_dict=mat_var_dict, - mat_param_dict=param_dict, - b=b, - lifter=self.lifter, - convert_to_polyrow=self.APPLY_TEMPLATES_TO_OTHERS, - known=False, - ) - if constraint is None: - print("Warning: found an all-zero constraint; not adding it.") - continue - templates.append(constraint) - self.constraint_index += 1 - - if len(templates + self.templates): - # we assume that all known constraints are linearly independent, and also - # that all known+previously found constraints are linearly independent. - indep_templates = self.clean_constraints( - constraints=templates + self.templates, - remove_dependent=True, - remove_imprecise=False, - ) - else: - indep_templates = [] - - if data_dict is not None: - ttot = time.time() - t1 - data_dict["t learn templates"] = ttot - data_dict["n rank"] = Y.shape[1] - corank - data_dict["n nullspace"] = corank - - if len(templates) > 0: - n_all = len(indep_templates) - n_new = n_all - len(self.templates) - self.templates = indep_templates - return n_new, n_all - return 0, len(self.constraints) - - def apply_templates(self): - # the new templates are all the ones corresponding to the new matrix variables. - new_constraints = self.lifter.apply_templates( - self.templates, self.constraint_index - ) - self.constraint_index += len(new_constraints) - if not len(new_constraints): - return 0, 0 - - n_all = len(new_constraints) - n_new = n_all - len(self.constraints) - self.constraints = new_constraints - return n_new, n_all - - def clean_constraints( - self, - constraints, - remove_dependent=True, - remove_imprecise=True, - ): - """ - This function is used in two different ways. - - First use case: Given the new templates, in b-PolyRow form, we determine which of the templates are actually - independent to a_current. We only want to augment the independent ones, otherwise we waste computing effort. - - Second use case: After applying the templates to as many variable pairs as we wish, we call this function again, - to make sure all the matrices going into the SDP are in fact linearly independent. - """ - if remove_dependent: - # find which constraints are lin. dep. - A_vec = sp.vstack( - [constraint.a_full_ for constraint in constraints], format="coo" - ).T - - # make sure that matrix is tall (we have less constraints than number of dimensions of x) - if A_vec.shape[0] < A_vec.shape[1]: - print("Warning: fat matrix.") - - bad_idx = find_dependent_columns(A_vec) - if len(bad_idx): - for idx in sorted(bad_idx)[::-1]: - del constraints[idx] - - if remove_imprecise: - error, bad_idx = self.lifter.test_constraints( - [c.A_sparse_ for c in constraints if c.index not in self.index_tested], - errors="ignore", - n_seeds=2, - ) - self.index_tested = self.index_tested.union([c.index for c in constraints]) - if len(bad_idx): - print(f"removing {bad_idx} because high error, up to {error:.2e}") - for idx in list(sorted(bad_idx))[ - ::-1 - ]: # reverse order to not mess up indexing - del constraints[idx] - return constraints - - def get_known_templates(self, unroll=False): - templates_known = [] - if not self.use_known: - return templates_known - - # TODO(FD) we should not always recompute from scratch, but it's not very expensive so it's okay for now. - target_dict = self.lifter.get_var_dict(unroll_keys=unroll) - for i, Ai in enumerate( - self.lifter.get_A_known(var_dict=target_dict, output_poly=True) - ): - template = Constraint.init_from_A_poly( - lifter=self.lifter, - A_poly=Ai, - known=True, - index=self.constraint_index, - template_idx=self.constraint_index, - mat_var_dict=self.lifter.var_dict, - compute_polyrow_b=True, - ) - self.constraint_index += 1 - templates_known.append(template) - return templates_known - - def get_sufficient_templates(self, new_order, new_lifter): - """Use the templates in learner to populate the own templates and constraints.""" - template_indices = sorted( - [t.index for t in self.templates + self.templates_known] - ) - new_templates = [] - template_unique_idx = set() - - # The index list new_order contains the indices of constraints, but we want to track back - # which templates those corresponded to. - # We thus create the set of all template indices that are represented in the - # sufficient constraints. - all_constraints = self.templates_known + self.constraints - for i in new_order: - # the first constraint ALWAYS corresponds to A0, whichs not part of our templates. - if i > 0: - new_index = all_constraints[i - 1].template_idx - assert new_index in template_indices # just a sanity check - template_unique_idx.add(new_index) - - # now we can create the new templates by looping through the sufficent template list. - for t in template_unique_idx: - # find the template of the requested index - other_templates = self.templates + self.templates_known - template_indices = [temp.index for temp in other_templates] - idx = template_indices.index(t) # raises Error if t is not in list. - template = other_templates[idx] - - assert isinstance(template, Constraint) - - # scale the template to the dimensions of the new learner. - # (not the known ones as those where already through other_learner.templates_known) - if not template.known: - scaled_template = template.scale_to_new_lifter(new_lifter) - new_templates.append(scaled_template) - return new_templates - - def get_sorted_df(self, templates_poly=None, add_columns={}): - def sort_fun_sparsity(series): - # This is a bit complicated because we don't want the order to change - # because of the values, only isna() should matter. - # To make this work, we temporarily change the non-nan values to the order in which they appear - index = pd.MultiIndex.from_product([[0], series.index]) - series.index = index - scipy_sparse = series.sparse.to_coo()[0] - # don't start at 0 because it's considered empty by scipy - scipy_sparse.data = np.arange(1, 1 + scipy_sparse.nnz) - pd_sparse = pd.Series.sparse.from_coo(scipy_sparse, dense_index=True) - return pd_sparse - - if templates_poly is None: - templates_poly = self.templates_poly - - series = [] - - variable_dict_j = list(templates_poly.variable_dict_j.keys()) - for i, key_i in enumerate(templates_poly.variable_dict_i): - data = {j: float(val) for j, val in templates_poly.matrix[key_i].items()} - for key, idx_list in add_columns.items(): - # if the list is not empty, then indicate which constraints are required. - if idx_list is not None and len(idx_list): - idx_list = list(idx_list) - try: - data[key] = idx_list.index(i) - except Exception: - data[key] = -1 - # if the list is empty, all of them are required (and more) - else: - data[key] = 1.0 - series.append( - pd.Series( - data, - index=variable_dict_j + list(add_columns.keys()), - dtype="Sparse[float]", - ) - ) - df = pd.DataFrame( - series, dtype="Sparse[float]", index=templates_poly.variable_dict_i - ) - df.dropna(axis=1, how="all", inplace=True) - - try: - df_sorted = df.sort_values( - key=sort_fun_sparsity, - by=list(df.columns), - axis=0, - na_position="last", - inplace=False, - ) - df_sorted["order_sparsity"] = range(len(df_sorted)) - return df_sorted - except Exception as e: - print("failed to sort:", e) - return df - - def generate_templates_poly(self, constraints=None, factor_out_parameters=False): - if constraints is None: - constraints = self.templates_known + self.constraints - - plot_rows = [] - plot_row_labels = [] - j = -1 - old_mat_vars = "" - for constraint in constraints: - mat_vars = constraint.mat_var_dict - i = constraint.index - if factor_out_parameters: # use a and not b. - if constraint.polyrow_a_ is not None: - plot_rows.append(constraint.polyrow_a_) - else: - if constraint.a_ is not None: - polyrow_a = self.lifter.convert_a_to_polyrow( - constraint.a_, mat_vars - ) - elif constraint.a_full_ is not None: - polyrow_a = self.lifter.convert_a_to_polyrow( - constraint.a_full_, mat_vars - ) - plot_rows.append(polyrow_a) - else: - if constraint.polyrow_b_ is not None: - plot_rows.append(constraint.polyrow_b_) - else: - plot_rows.append( - self.lifter.convert_b_to_polyrow(constraint.b_, mat_vars) - ) - - if mat_vars != old_mat_vars: - j += 1 - plot_row_labels.append(f"{j}:b{i}") - # plot_row_labels.append(f"{j}{mat_vars}:b{i}") - old_mat_vars = mat_vars - else: - plot_row_labels.append(f"{j}:b{i}") - - templates_poly = PolyMatrix.init_from_row_list( - plot_rows, row_labels=plot_row_labels - ) - - # make sure variable_dict_j is ordered correctly. - templates_poly.variable_dict_j = self.lifter.var_dict_row( - mat_vars, force_parameters_off=factor_out_parameters - ) - return templates_poly - - def save_sorted_templates( - self, df, fname_root="", title="", drop_zero=False, simplify=True - ): - - # convert to poly matrix for plotting purposes only. - poly_matrix = PolyMatrix(symmetric=False) - keys_j = [] - keys_i = [] - for i, row in df.iterrows(): - for k, val in row[~row.isna()].items(): - if "order" in k or "required" in k: - continue - poly_matrix[i, k] = val - keys_j.append(k) - keys_i.append(i) - - variables_j = self.lifter.var_dict_row( - var_subset=self.lifter.var_dict, force_parameters_off=False - ) - assert set(keys_j).issubset(variables_j) - if drop_zero: - variables_j = {k: v for k, v in variables_j.items() if k in keys_j} - variables_i = {i: 1 for i in keys_i} - fig, ax = plot_basis( - poly_matrix, - variables_j=variables_j, - variables_i=variables_i, - discrete=True, - ) - ax.set_yticklabels([]) - ax.set_yticks([]) - if simplify: - ax.set_xticks([]) - ax.set_xticklabels([]) - else: - new_xticks = [] - for lbl in ax.get_xticklabels(): - lbl = lbl.get_text() - if "_" in lbl: # avoid double subscript - new_lbl = f"${lbl.replace('h-', '').replace(':', '^')}$" - else: - new_lbl = f"${lbl.replace('h-', '').replace(':', '_')}$" - new_xticks.append(new_lbl) - ax.set_xticklabels(new_xticks, fontsize=7) - - # plot a red vertical line at each new block of parameters. - params = [v.split("-")[0] for v in variables_j] - old_param = params[0] - for i, p in enumerate(params): - if p != old_param: - ax.axvline(i, color="red", linewidth=1.0) - ax.annotate( - text=f"${p.replace(':0', '^x').replace(':1', '^y').replace('l.','').replace('.','')}$", - xy=(float(i), 0.0), - fontsize=8, - color="red", - ) - old_param = p - ax.set_title(title) - if "required (sorted)" in df.columns: - for i, (_, row) in enumerate(df.iterrows()): - if row["required (sorted)"] < 0: - ax.add_patch( - matplotlib.patches.Rectangle( - (ax.get_xlim()[0], i - 0.5), - ax.get_xlim()[1] + 0.5, - 1.0, - fc="white", - alpha=0.5, - lw=0.0, - ) - ) - if fname_root != "": - savefig(fig, fname_root + "_templates-sorted.png") - return fig, ax - - def save_templates(self, fname_root="", title="", with_parameters=False): - - templates_poly = self.generate_templates_poly( - factor_out_parameters=not with_parameters - ) - variables_j = self.lifter.var_dict_row( - self.mat_vars, force_parameters_off=not with_parameters - ) - fig, ax = plot_basis(templates_poly, variables_j=variables_j, discrete=True) - if with_parameters: - for p in range(1, self.lifter.get_dim_P(self.mat_vars)): - ax.axvline(p * self.lifter.get_dim_X(self.mat_vars) - 0.5, color="red") - - ax.set_title(title) - if fname_root != "": - savefig(fig, fname_root + "_templates.png") - return fig, ax - - def save_tightness(self, fname_root, title=""): - labels = self.variable_list - assert self.solver_vars is not None - - fig, ax = plt.subplots() - xticks = range(len(self.dual_costs)) - ax.semilogy(xticks, self.dual_costs, marker="o") - ax.set_xticks(xticks, labels) - if self.solver_vars["qcqp_cost"] is not None: - ax.axhline(float(self.solver_vars["qcqp_cost"]), color="k", ls=":") - ax.set_title(title) - if fname_root != "": - savefig(fig, fname_root + "_tightness.png") - - fig, ax = plt.subplots() - for eig, label in zip(self.ranks, labels): - ax.semilogy(eig, label=label) - ax.legend(loc="upper right") - ax.set_title(title) - if fname_root != "": - savefig(fig, fname_root + "_eigs.png") - return - - def save_matrices_sparsity(self, A_matrices=None, fname_root="", title=""): - assert self.solver_vars is not None - - if A_matrices is None: - A_matrices = self.A_matrices - - Q = self.solver_vars["Q"].toarray() # type:ignore - - sorted_i = self.lifter.get_var_dict(unroll_keys=True) - A_matrices_sparse = [ - A_poly.get_matrix(variables=sorted_i) for A_poly in A_matrices - ] - - A_agg = get_aggregate_sparsity(A_matrices_sparse) - - fig, axs = plt.subplots(1, 2) - fig.set_size_inches(6, 3) - im0 = axs[0].matshow( - 1 - A_agg.toarray(), vmin=0, vmax=1, cmap="gray" - ) # 1 (white) is empty, 0 (black) is nonempty - - vmin = min(-np.max(Q), np.min(Q)) - vmax = max(np.max(Q), -np.min(Q)) - norm = matplotlib.colors.SymLogNorm(10**-5, vmin=vmin, vmax=vmax) - im1 = axs[1].matshow(Q, norm=norm) - - for ax in axs: - add_rectangles(ax, self.lifter.var_dict) - - add_colorbar(fig, axs[1], im1, nticks=3) - # only for dimensions - add_colorbar(fig, axs[0], im0, visible=False) - if fname_root != "": - savefig(fig, fname_root + "_matrices-sparisty.png") - return fig, axs - - def save_matrices_poly( - self, - A_matrices=None, - n_matrices=5, - fname_root="", - reduced_mode=False, - save_individual=False, - max_matrices=PLOT_MAX_MATRICES, - ): - if A_matrices is None: - A_matrices = self.A_matrices - - n_rows = n_matrices // 10 + 1 - n_cols = min(n_matrices, 10) - fig, axs = plt.subplots(n_rows, n_cols, squeeze=False) - fig.set_size_inches(5 * n_cols / n_rows, 5) - axs = axs.flatten() - i = 0 - for i, A_poly in enumerate(A_matrices): - if reduced_mode: - sorted_i = sorted(A_poly.variable_dict_i.keys()) - else: - sorted_i = self.lifter.get_var_dict(unroll_keys=True) - - plot_axs = [] - if i < n_matrices: - plot_axs.append(axs[i]) - - if save_individual and (i < max_matrices): - figi, axi = plt.subplots() - figi.set_size_inches(3, 3) - plot_axs.append(axi) - - if isinstance(A_poly, PolyMatrix): - A_sparse = A_poly.get_matrix(sorted_i) - else: - A_sparse = A_poly - cmap, norm, colorbar_yticks = initialize_discrete_cbar(A_sparse.data) # type: ignore - - for ax in plot_axs: - if sp.isspmatrix(A_sparse): - arr = A_sparse.toarray() # type: ignore - else: - arr = A_sparse - im = ax.matshow(arr, cmap=cmap, norm=norm) - # Use sp.isspmatrix to check if A_sparse is a scipy sparse matrix - if sp.isspmatrix(A_sparse): - add_rectangles(ax, self.lifter.var_dict) - cax = add_colorbar(fig, ax, im, size=0.1) - cax.set_yticklabels(colorbar_yticks) - - if save_individual and (i < max_matrices): - savefig(figi, fname_root + f"_matrix{i}.pdf") - for ax in axs[i + 1 :]: - ax.axis("off") - return fig, axs - -
-[docs] - def run( - self, - use_known: bool = USE_KNOWN, - use_incremental: bool = USE_INCREMENTAL, - variable_list: list[list[str]] | None = None, - verbose: bool = False, - plot: bool = False, - ): - """Run the template learning algorithm until we reach tightness, or run out of variables to add. - - :param use_known: whether to use the known constraints of the lfiter (must have get_A_known). - :param use_incremental: whether to keep adding the learned tempaltes to the set of known constraints, to enforce we find orthogonal ones. - :param variable_list: list of lists of variables to consider. If not given, will use the VARIABLE_LIST parameter of the lifter class. - - """ - data = [] - success = False - self.use_known = use_known - self.use_incremental = use_incremental - - if use_known: - self.templates_known = self.get_known_templates() - n_known = len(self.templates_known) - print(f"there are total {n_known} known constraints") - - if variable_list is None: - variable_list = self.lifter.VARIABLE_LIST - self.variable_iter = iter(variable_list) - - while 1: - # add one more variable to the list of variables to vary - if not self.update_variables(): - print("no more variables to add") - break - print(f"======== {self.mat_vars} ========") - - n_new = 0 - if use_known: - n_known_here = self.extract_known_templates() - n_new += n_known_here - print( - f"using {n_known_here}/{n_known} known constraints (only the ones that contain the current variables)" - ) - - data_dict = {"variables": self.mat_vars} - param_dict = self.lifter.get_involved_param_dict(self.mat_vars) - - # Set the type expectation for the dictionary if using type hints - data_dict: dict[str, float | int | list | None] - data_dict["n dims"] = self.lifter.get_dim_Y( - var_subset=self.mat_vars, param_subset=param_dict - ) - - print("-------- templates learning --------") - # learn new templates, orthogonal to the ones found so far. - n_new_learned, n_all = self.learn_templates(plot=plot, data_dict=data_dict) - n_new += n_new_learned - print( - f"found {n_new_learned} learned templates, new total learned: {n_all} " - ) - data_dict["n templates"] = ( - len(self.templates) + len(self.templates_known) + 1 - ) - if n_new == 0: - data.append(data_dict) - continue - - if plot: - # turn the current list of templates into a poly matrix. - templates = self.templates_known + self.templates - poly_matrix = generate_poly_matrix(templates, lifter=self.lifter) - - # make sure we use sorted column names - variables_j_all = self.lifter.var_dict_row() - variables_j = { - key: val - for key, val in variables_j_all.items() - if key in poly_matrix.variable_dict_j - } - - fig, ax = plot_poly_matrix( - poly_matrix, variables_j, simplify=False, hom="l" - ) - w, h = fig.get_size_inches() - fig.set_size_inches(10, 10 * h / w) - - # apply the pattern to all landmarks - if self.APPLY_TEMPLATES_TO_OTHERS: - print("------- applying templates ---------") - t1 = time.time() - n_new, n_all = self.apply_templates() - print( - f"found {n_new} independent learned constraints, new total: {n_all} " - ) - ttot = time.time() - t1 - - data_dict["n constraints"] = n_all + len(self.templates_known) + 1 - print( - f"total including known and homogenization:", - data_dict["n constraints"], - ) - data_dict["t apply templates"] = ttot - else: - self.constraints = [] - for temp in self.templates: - con = deepcopy(temp) - con.template_idx = temp.index - self.constraints.append(con) - - t1 = time.time() - print("-------- checking tightness ----------") - self.reset_tightness_dict() - is_tight = self.is_tight(verbose=verbose, data_dict=data_dict) - ttot = time.time() - t1 - data_dict["t check tightness"] = ttot - data.append(data_dict) - if is_tight: - success = True - break - return data, success
- - -
-[docs] - def apply(self, lifter: StateLifter, use_known: bool = False) -> list: - """Apply the learned templates to a new lifter.""" - constraints = lifter.apply_templates(self.templates) - - if use_known: - # if we set use_known=True in running AutoTemplate, then we learned only - # constraints that were not already known, so we need to add them to the - # overall set of constraints. - A_known = lifter.get_A_known() - assert isinstance(A_known, list) - return A_known + [c.A_sparse_ for c in constraints] # type: ignore
-
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/auto_tight.html b/docs/build/_modules/popcor/auto_tight.html deleted file mode 100644 index 78601b7..0000000 --- a/docs/build/_modules/popcor/auto_tight.html +++ /dev/null @@ -1,657 +0,0 @@ - - - - - - - - popcor.auto_tight — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.auto_tight

-import matplotlib.pylab as plt
-import numpy as np
-import scipy.sparse as sp
-import scipy.sparse.linalg as splinalg
-from cert_tools.linalg_tools import find_dependent_columns, get_nullspace
-
-from popcor.utils.common import get_vec
-from popcor.utils.constraint import Constraint
-from popcor.utils.plotting_tools import add_colorbar, initialize_discrete_cbar
-
-
-
-[docs] -class AutoTight(object): - """Class for automatic constraint generation.""" - - # consider singular value zero below this - EPS_SVD = 1e-5 - - # basis pursuit method, can be - # - qr: qr decomposition - # - qrp: qr decomposition with permutations (sparser), recommended - # - svd: svd - METHOD = "qrp" - - # normalize learned Ai or not - NORMALIZE = False - - # how much to oversample (>= 1) - FACTOR = 1.2 - - # number of times we remove bad samples from data matrix - N_CLEANING_STEPS = 1 # was 3 - - # maximum number of iterations of local solver - LOCAL_MAXITER = 100 - - # find and remove linearly dependent constraints - REDUCE_DEPENDENT = False - - def __init__(self): - pass - - @staticmethod - def clean_Y(basis_new, Y, s, plot=False): - errors = np.abs(basis_new @ Y.T) # Nb x n x n x Ns = Nb x Ns - if np.all(errors < 1e-10): - return [] - bad_bins = np.unique(np.argmax(errors, axis=1)) - if plot: - fig, ax = plt.subplots() - ax.semilogy(np.min(errors, axis=1)) - ax.semilogy(np.max(errors, axis=1)) - ax.semilogy(np.median(errors, axis=1)) - ax.semilogy(s) - return bad_bins - - @staticmethod - def test_S_cutoff(S, corank, eps_svd=None): - if eps_svd is None: - eps_svd = AutoTight.EPS_SVD - if corank > 1: - try: - assert abs(S[-corank]) / eps_svd < 1e-1 # 1e-1 1e-10 - assert abs(S[-corank - 1]) / eps_svd > 10 # 1e-11 1e-10 - except AssertionError: - print(f"there might be a problem with the chosen threshold {eps_svd}:") - print(S[-corank], eps_svd, S[-corank - 1]) - - @staticmethod - def get_basis_sparse( - lifter, var_list, param_list, A_known=[], plot=False, eps_svd=None - ): - - Y = AutoTight.generate_Y_sparse( - lifter, var_subset=var_list, param_subset=param_list, factor=1.0 - ) - basis, S = AutoTight.get_basis(lifter, Y, A_known=A_known, eps_svd=eps_svd) - AutoTight.test_S_cutoff(S, corank=basis.shape[0], eps_svd=eps_svd) - constraints = [] - for i, b in enumerate(basis): - constraints.append( - Constraint.init_from_b( - i, - b, - mat_var_dict=var_list, - mat_param_dict=param_list, - convert_to_polyrow=False, - known=False, - ) - ) - if plot: - plot_matrix = np.vstack([t.b_[None, :] for t in constraints]) - - cmap, norm, colorbar_yticks = initialize_discrete_cbar(plot_matrix) - X_dim = lifter.get_dim_X(var_list) - fig, ax = plt.subplots() - ax.axvline(X_dim - 0.5, color="red") - im = ax.matshow(plot_matrix, cmap=cmap, norm=norm) - ax.set_title(f"{var_list}, {param_list}") - cax = add_colorbar(fig, ax, im) - if colorbar_yticks is not None: - cax.set_yticklabels(colorbar_yticks) - plt.show(block=False) - return constraints - -
-[docs] - @staticmethod - def get_A_learned( - lifter, - A_known=[], - var_dict=None, - method=METHOD, - verbose=False, - ) -> list: - """Generate list of learned constraints by sampling the lifter. - - :param lifter: StateLifter object - :param A_known: list of known constraints, if given, will generate basis that is orthogonal to these given constraints. - :param var_dict: variable dictionary, if None, will use all variables - :param method: method to use for basis generation, can be 'qr', 'qrp', or 'svd'. 'qrp' is recommended. - :param verbose: if True, will print timing information - - :return: list of learned constraints. - """ - import time - - t1 = time.time() - Y = AutoTight.generate_Y(lifter, var_subset=var_dict, factor=1.0) - if verbose: - print(f"generate Y ({Y.shape}): {time.time() - t1:4.4f}") - t1 = time.time() - basis, S = AutoTight.get_basis(lifter, Y, A_known=A_known, method=method) - if verbose: - print(f"get basis ({basis.shape})): {time.time() - t1:4.4f}") - t1 = time.time() - A_learned = AutoTight.generate_matrices(lifter, basis, var_dict=var_dict) - if verbose: - print(f"get matrices ({len(A_learned)}): {time.time() - t1:4.4f}") - return A_learned
- - - @staticmethod - def get_A_learned_simple( - lifter, - A_known=[], - var_dict=None, - method=METHOD, - verbose=False, - ) -> list: - """Simplified version of get_A_learned that does not consider parameters.""" - import time - - t1 = time.time() - Y = AutoTight.generate_Y_simple(lifter, var_subset=var_dict, factor=1.5) - if verbose: - print(f"generate Y ({Y.shape}): {time.time() - t1:4.4f}") - t1 = time.time() - if len(A_known): - basis_known = np.vstack( - [ - np.asarray(get_vec(Ai.get_matrix(var_dict))) - for Ai in A_known - if get_vec(Ai.get_matrix(var_dict)) is not None - ] - ).T - else: - basis_known = None - basis, S = AutoTight.get_basis( - lifter, Y, basis_known=basis_known, method=method - ) - if verbose: - print(f"get basis ({basis.shape})): {time.time() - t1:4.4f}") - t1 = time.time() - A_learned = AutoTight.generate_matrices_simple(lifter, basis, var_dict=var_dict) - if verbose: - print(f"get matrices ({len(A_learned)}): {time.time() - t1:4.4f}") - return A_learned - - @staticmethod - def generate_Y_simple(lifter, var_subset, factor): - # need at least dim_Y different random setups - dim_Y = lifter.get_dim_X(var_subset) - n_seeds = int(dim_Y * factor) - Y = np.empty((n_seeds, dim_Y)) - for seed in range(n_seeds): - np.random.seed(seed) - - theta = lifter.sample_theta() - x = lifter.get_x(theta=theta, parameters=None, var_subset=var_subset) - X = np.outer(x, x) - Y[seed, :] = get_vec(X) - return Y - - @staticmethod - def generate_Y_sparse(lifter, var_subset, param_subset, factor=FACTOR, ax=None): - from popcor.base_lifters import StateLifter - - assert isinstance(lifter, StateLifter) - assert lifter.HOM in param_subset - - # need at least dim_Y different random setups - dim_Y = lifter.get_dim_Y(var_subset, param_subset) - n_seeds = int(dim_Y * factor) - Y = np.empty((n_seeds, dim_Y)) - for seed in range(n_seeds): - np.random.seed(seed) - - theta = lifter.sample_theta() - parameters = lifter.sample_parameters(theta) - - if seed < 10 and ax is not None: - if np.ndim(lifter.theta) == 1: - ax.scatter(np.arange(len(theta)), theta) - else: - ax.scatter(*theta[:, :2].T) - - x = lifter.get_x(theta=theta, parameters=parameters, var_subset=var_subset) - X = np.outer(x, x) - - # generates [1*x, a1*x, ..., aK*x] - p = lifter.get_p(parameters=parameters, param_subset=param_subset) - Y[seed, :] = np.kron(p, get_vec(X)) - return Y - - @staticmethod - def generate_Y(lifter, factor=FACTOR, ax=None, var_subset=None, param_subset=None): - # need at least dim_Y different random setups - dim_Y = lifter.get_dim_Y(var_subset, param_subset) - n_seeds = int(dim_Y * factor) - Y = np.empty((n_seeds, dim_Y)) - for seed in range(n_seeds): - np.random.seed(seed) - - theta = lifter.sample_theta() - parameters = lifter.sample_parameters(theta) - if seed < 10 and ax is not None: - if np.ndim(lifter.theta) == 1: - ax.scatter(np.arange(len(theta)), theta) - else: - ax.scatter(*theta[:, :2].T) - - x = lifter.get_x(theta=theta, parameters=parameters, var_subset=var_subset) - X = np.outer(x, x) - - # generates [1*x, a1*x, ..., aK*x] - p = lifter.get_p(parameters=parameters, param_subset=param_subset) - assert p[0] == 1 - Y[seed, :] = np.kron(p, get_vec(X)) - return Y - - @staticmethod - def get_basis( - lifter, - Y, - A_known: list = [], - basis_known: np.ndarray | None = None, - method=METHOD, - eps_svd=None, - ): - """Generate basis from lifted state matrix Y. - - :param A_known: if given, will generate basis that is orthogonal to these given constraints. - - :return: basis, S - """ - if eps_svd is None: - eps_svd = AutoTight.EPS_SVD - - # if there is a known list of constraints, add them to the Y so that resulting nullspace is orthogonal to them - if basis_known is not None: - if len(A_known): - print( - "Warning: ignoring given A_known because basis_all is also given." - ) - Y = np.vstack([Y, basis_known.T]) - elif len(A_known): - A = np.vstack( - [lifter.augment_using_zero_padding(get_vec(a)) for a in A_known] - ) - Y = np.vstack([Y, A]) - - basis, info = get_nullspace(Y, method=method, tolerance=eps_svd) - - basis[np.abs(basis) < lifter.EPS_SPARSE] = 0.0 - return basis, info["values"] - - @staticmethod - def generate_matrices_simple( - lifter, - basis, - normalize=NORMALIZE, - sparse=True, - trunc_tol=1e-10, - var_dict=None, - ): - """ - Generate constraint matrices from the rows of the nullspace basis matrix. - """ - try: - n_basis = len(basis) - except Exception: - n_basis = basis.shape[0] - - if isinstance(var_dict, list): - var_dict = lifter.get_var_dict(var_dict) - - from popcor.base_lifters import StateLifter - - assert isinstance(lifter, StateLifter) - - A_list = [] - for i in range(n_basis): - ai = basis[i] - Ai = lifter.get_mat(ai, sparse=sparse, correct=True, var_dict=None) - # Normalize the matrix - if normalize and not sparse: - # Ai /= np.max(np.abs(Ai)) - assert isinstance(Ai, np.ndarray) - Ai /= np.linalg.norm(Ai, p=2) # type: ignore - elif normalize and sparse: - Ai /= splinalg.norm(Ai, ord="fro") - # Sparsify and truncate - if sparse: - Ai.eliminate_zeros() # type: ignore - else: - Ai[np.abs(Ai) < trunc_tol] = 0.0 # type: ignore - # add to list - A_list.append(Ai) - return A_list - - @staticmethod - def generate_matrices( - lifter, - basis, - normalize=NORMALIZE, - sparse=True, - trunc_tol=1e-10, - var_dict=None, - ): - """ - Generate constraint matrices from the rows of the nullspace basis matrix. - """ - from popcor.base_lifters import StateLifter - - assert isinstance(lifter, StateLifter) - - try: - n_basis = len(basis) - except Exception: - n_basis = basis.shape[0] - - if isinstance(var_dict, list): - var_dict = lifter.get_var_dict(var_dict) - - A_list = [] - basis_reduced = [] - for i in range(n_basis): - ai = lifter.get_reduced_a(bi=basis[i], var_subset=var_dict, sparse=True) - basis_reduced.append(ai) - basis_reduced = sp.vstack(basis_reduced) - - if AutoTight.REDUCE_DEPENDENT: - bad_idx = find_dependent_columns(basis_reduced.T, tolerance=1e-6) - else: - bad_idx = [] - - for i in range(basis_reduced.shape[0]): # type: ignore - if i in bad_idx: - continue - ai = basis_reduced[[i], :].toarray().flatten() # type: ignore - Ai = lifter.get_mat(ai, sparse=sparse, correct=True, var_dict=None) - # Normalize the matrix - if normalize and not sparse: - # Ai /= np.max(np.abs(Ai)) - Ai /= np.linalg.norm(Ai, p=2) # type: ignore - elif normalize and sparse: - Ai /= splinalg.norm(Ai, ord="fro") - # Sparsify and truncate - if sparse: - Ai.eliminate_zeros() # type: ignore - else: - Ai[np.abs(Ai) < trunc_tol] = 0.0 # type: ignore - # add to list - A_list.append(Ai) - return A_list - -
-[docs] - @staticmethod - def get_duality_gap(cost_local, cost_sdp): - return (cost_local - cost_sdp) / abs(cost_sdp)
-
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/base_lifters/poly_lifters.html b/docs/build/_modules/popcor/base_lifters/poly_lifters.html deleted file mode 100644 index cae85fe..0000000 --- a/docs/build/_modules/popcor/base_lifters/poly_lifters.html +++ /dev/null @@ -1,325 +0,0 @@ - - - - - - - - popcor.base_lifters.poly_lifters — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.base_lifters.poly_lifters

-import numpy as np
-
-from .state_lifter import StateLifter
-
-
-
-[docs] -class PolyLifter(StateLifter): - def __init__(self, degree, param_level="no"): - """Simple univariate polynomial lifter, mostly for testing and pedagogical purposes.""" - self.degree = degree - super().__init__(d=1, param_level=param_level) - - @property - def var_dict(self): - if self.var_dict_ is None: - self.var_dict_ = {self.HOM: 1, "t": 1} - self.var_dict_.update({f"z{i}": 1 for i in range(self.M)}) - return self.var_dict_ - - @property - def M(self): - return self.degree // 2 - 1 - - def sample_theta(self): - return np.random.rand(1) - - def get_error(self, t): - return {"MAE": float(abs(self.theta - t)), "error": float(abs(self.theta - t))} - - def get_x(self, theta=None, parameters=None, var_subset=None): - if theta is None: - theta = self.theta - return np.array([theta**i for i in range(self.degree // 2 + 1)]) - - def get_cost(self, theta, *args, **kwargs) -> float: - Q = self.get_Q() - x = self.get_x(theta).flatten() - return float(x.T @ Q @ x) - - def get_hess(self, *args, **kwargs): - raise NotImplementedError - - def local_solver(self, t0, *args, **kwargs): - from scipy.optimize import minimize - - sol = minimize(self.get_cost, t0) - info = {"success": sol.success} - return sol.x, info, sol.fun - - def plot(self, thetas, label=None): - from popcor.utils.plotting_tools_poly import coordinate_system - - fig, ax = coordinate_system() - ys = [self.get_cost(t) for t in thetas] - ax.plot(thetas, ys, label=label) - ymin = min(-max(ys) / 3, min(ys)) - ax.set_ylim(ymin, max(ys)) - return fig, ax - - def __repr__(self): - return f"poly{self.degree}"
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/base_lifters/robust_pose_lifter.html b/docs/build/_modules/popcor/base_lifters/robust_pose_lifter.html deleted file mode 100644 index 4731549..0000000 --- a/docs/build/_modules/popcor/base_lifters/robust_pose_lifter.html +++ /dev/null @@ -1,775 +0,0 @@ - - - - - - - - popcor.base_lifters.robust_pose_lifter — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.base_lifters.robust_pose_lifter

-from abc import ABC, abstractmethod
-from copy import deepcopy
-
-import numpy as np
-from poly_matrix.poly_matrix import PolyMatrix
-from scipy.spatial.transform import Rotation as R
-
-from popcor.utils.geometry import (
-    get_C_r_from_theta,
-    get_noisy_pose,
-    get_pose_errors_from_theta,
-    get_theta_from_C_r,
-)
-
-from .state_lifter import StateLifter
-
-N_TRYS = 10
-
-METHOD = "CG"
-SOLVER_KWARGS = dict(
-    min_gradient_norm=1e-7, max_iterations=10000, min_step_size=1e-8, verbosity=1
-)
-
-# TODO(FD) we need to add a penalty here, otherwise the local solution is not good.
-# However, the penalty results in inequality constraints etc. and that's not easy to deal with.
-PENALTY_RHO = 10
-PENALTY_U = 1e-3
-
-# the cutoff parameter of least squares. If residuals are >= BETA, they are considered outliers.
-BETA = 0.1
-
-
-
-[docs] -class RobustPoseLifter(StateLifter, ABC): - LEVELS = ["no", "xwT", "xxT"] - PARAM_LEVELS = ["no", "p", "ppT"] - LEVEL_NAMES = {"no": "no", "xwT": "x kron w", "xxT": "x kron x"} - MAX_DIST = 10.0 # maximum of norm of t. - - @property - def VARIABLE_LIST(self): - if not self.robust: - return [[self.HOM, "t", "c"]] - else: - base = [self.HOM, "t", "c"] - return [ - base, - base + ["w_0"], - base + ["z_0"], - base + ["w_0", "w_1"], - base + ["w_0", "z_0"], - base + ["z_0", "z_1"], - # base + ["w_0", "w_1", "z_0"], - # base + ["w_0", "w_1", "z_0", "z_1"], - ] - - # Add any parameters here that describe the problem (e.g. number of landmarks etc.) - def __init__( - self, - n_outliers=0, - level="no", - param_level="no", - d=2, - n_landmarks=3, - variable_list=None, - robust=False, - beta=BETA, - ): - """RobustPoseLifter is a general class for point-to-point, point-to-line, and point-to-plane registration problems, - with starndard or robust loss functions. - - The goal is to regress an unknown pose based on extrinsic measurements. - - See :class:`~popcor.examples.WahbaLifter` for point-to-point registration and :class:`~popcor.examples.MonoLifter`) for point-to-line registration. - - Implemented lifting functions are: - - - xwT: :math:`x \\otimes w` - - xxT: :math:`x \\otimes x` - """ - self.beta = beta - self.n_landmarks = n_landmarks - - self.robust = robust - self.level = level - if variable_list == "all": - variable_list = self.get_all_variables() - # elif variable_list is None: - # self.variable_list = self.VARIABLE_LIST - - if not robust: - assert level == "no" - - self.landmarks_ = None # will be initialized later - super().__init__( - level=level, - param_level=param_level, - d=d, - variable_list=variable_list, - n_outliers=n_outliers, - robust=robust, - ) - - def penalty(self, t, rho=PENALTY_RHO, u=PENALTY_U): - import autograd.numpy as anp - - try: - return anp.sum( # type: ignore - [rho * u * anp.log10(1 + anp.exp(hi / u)) for hi in self.h_list(t)] # type: ignore - ) - except RuntimeWarning: - u = PENALTY_U * 0.1 - return anp.sum( # type: ignore - [rho * u * anp.log10(1 + anp.exp(hi / u)) for hi in self.h_list(t)] # type: ignore - ) - - @property - def var_dict(self): - """Return key,size pairs of all variables.""" - var_dict = {self.HOM: 1, "t": self.d, "c": self.d**2} - if not self.robust: - return var_dict - - n = self.d**2 + self.d - if self.level == "xwT": - for i in range(self.n_landmarks): - var_dict.update({f"w_{i}": 1, f"z_{i}": n}) - elif self.level == "xxT": - var_dict.update({f"w_{i}": 1 for i in range(self.n_landmarks)}) - var_dict.update({"z_0": n**2}) - return var_dict - - @property - def param_dict(self): - return self.param_dict_landmarks - - def get_all_variables(self): - all_variables = [self.HOM, "t", "c"] - if self.robust: - if self.level == "xxT": - all_variables += [f"w_{i}" for i in range(self.n_landmarks)] - all_variables += ["z_0"] - elif self.level == "xwT": - for i in range(self.n_landmarks): - all_variables += [f"w_{i}", f"z_{i}"] - variable_list = [all_variables] - return variable_list - - def sample_theta(self): - """Generate a random new feasible point.""" - - # make sure random pose is looking at world centre (where landmarks are) - success = False - i = 0 - while not success: - pc_cw = self.get_random_position() - success = np.all(np.array(self.h_list(pc_cw)) <= 0) - if success: - break - i += 1 - if i >= N_TRYS: - raise ValueError("didn't find valid initialization") - - if self.d == 2: - angle = np.random.uniform(0, 2 * np.pi) - C = R.from_euler("z", angle).as_matrix()[:2, :2] - else: - C = R.random().as_matrix() - theta_x = get_theta_from_C_r(C, pc_cw) - - if self.robust: - outlier_index = np.random.choice( - self.n_landmarks, replace=False, size=self.n_outliers - ) - w = np.ones(self.n_landmarks) - w[outlier_index] = -1 - return np.hstack([theta_x, w]) - else: - return theta_x - - @property - def landmarks(self): - if self.landmarks_ is None: - self.landmarks_ = np.random.normal( - loc=0, scale=1.0, size=(self.n_landmarks, self.d) - ) - return self.landmarks_ - - def sample_parameters(self, theta=None): - if self.parameters_ is None: - return self.sample_parameters_landmarks(self.landmarks) - landmarks = np.random.normal(loc=0, scale=1.0, size=(self.n_landmarks, self.d)) - return self.sample_parameters_landmarks(landmarks) - - def get_x(self, theta=None, parameters=None, var_subset=None) -> np.ndarray: - """Get the lifted vector x given theta and parameters.""" - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - if var_subset is None: - var_subset = self.var_dict.keys() - - if self.robust: - theta_here = theta[: -self.n_landmarks] - else: - theta_here = theta - - # RT below is R_cw. (c=camera, w=world) - RT, t = get_C_r_from_theta(theta_here, self.d) - R = RT.T - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - elif key == "t": - x_data += list(t) - elif key == "c": - x_data += list(R.flatten("C")) - elif "w" in key: - j = int(key.split("_")[-1]) - w_j = theta[-self.n_landmarks + j] - x_data.append(w_j) - elif (self.level == "xxT") and (key == "z_0"): - x_vec = list(get_theta_from_C_r(R, t)) - x_data += list(np.kron(x_vec, x_vec).flatten()) - elif (self.level == "xwT") and ("z" in key): - j = int(key.split("_")[-1]) - w_j = theta[-self.n_landmarks + j] - x_vec = get_theta_from_C_r(R, t) - x_data += list(x_vec * w_j) - dim_x = self.get_dim_x(var_subset=var_subset) - assert len(x_data) == dim_x - return np.array(x_data) - - def get_outlier_index(self): - if self.robust: - return np.where(self.theta[-self.n_landmarks :] == -1)[0] - else: - return [] - - def get_error(self, theta_hat): - - theta_hat_pose = theta_hat[: self.d + self.d**2] - theta_gt_pose = self.theta[: self.d + self.d**2] - return get_pose_errors_from_theta(theta_hat_pose, theta_gt_pose, self.d) - - def get_vec_around_gt(self, delta: float = 0): - """Sample around ground truth. - :param delta: sample from gt + std(delta) (set to 0 to start from gt.) - """ - if self.robust: - theta = deepcopy(self.theta[: self.d + self.d**2]) - C, r = get_C_r_from_theta(theta, self.d) - theta_noisy = get_noisy_pose(C, r, delta=delta) - theta_w = self.theta[self.d + self.d**2 :] - return np.r_[theta_noisy, theta_w] - else: - C, r = get_C_r_from_theta(self.theta, self.d) - theta_noisy = get_noisy_pose(C, r, delta=delta) - return theta_noisy - - def get_cost(self, theta, y): - if self.robust: - x = theta[: -self.n_landmarks] - w = theta[-self.n_landmarks :] - assert np.all(w**2 == 1.0) - else: - x = theta - - R, t = get_C_r_from_theta(x, self.d) - - cost = 0 - for i in range(self.n_landmarks): - res = self.residual_sq(R, t, self.landmarks[i], y[i]) - if self.robust: - cost += (1 + w[i]) / self.beta**2 * res + 1 - w[i] - else: - cost += res - return 0.5 * cost - - def local_solver( - self, t0, y, verbose=False, method=METHOD, solver_kwargs=SOLVER_KWARGS - ): - import pymanopt - from pymanopt.manifolds import Euclidean, Product, SpecialOrthogonalGroup - - if method == "CG": - from pymanopt.optimizers import ConjugateGradient as Optimizer # fastest - elif method == "SD": - from pymanopt.optimizers import SteepestDescent as Optimizer # slow - elif method == "TR": - from pymanopt.optimizers import TrustRegions as Optimizer # okay - else: - raise ValueError(method) - - if verbose: - solver_kwargs["verbosity"] = 2 - else: - solver_kwargs["verbosity"] = 0 - - # We assume that we know w! If we wanted to solve for w too we would need - # IRLS or similar. Since we just care about getting the global solution - # with a local sovler that's not necessary. - if self.robust: - w = self.theta[-self.n_landmarks :] - - manifold = Product((SpecialOrthogonalGroup(self.d, k=1), Euclidean(self.d))) - - @pymanopt.function.autograd(manifold) - def cost(R, t): - cost = 0 - for i in range(self.n_landmarks): - residual = self.residual_sq(R, t, self.landmarks[i], y[i]) - if self.robust: - cost += (1 + w[i]) / self.beta**2 * residual + 1 - w[i] - else: - cost += residual - return 0.5 * cost + self.penalty(t) - - @pymanopt.function.autograd(manifold) - def euclidean_gradient_unused(R, t): - grad_R = np.zeros(R.shape) - grad_t = np.zeros(t.shape) - for i in range(self.n_landmarks): - Wi = np.eye(self.d) - np.outer(y[i], y[i]) - # residual = (R @ pi + t).T @ Wi @ (R @ pi + t) - term = self.term_in_norm(R, t, self.landmarks[i], y[i]) - if self.robust: - grad_R += ( - 2 - * w[i] - / self.beta**2 - * np.outer(Wi.T @ term, self.landmarks[i]) - ) - grad_t += 2 * w[i] / self.beta**2 * Wi.T @ term - else: - grad_R += np.outer(Wi.T @ term, self.landmarks[i]) - grad_t += Wi.T @ term - return grad_R, grad_t - - euclidean_gradient = None - problem = pymanopt.Problem( - manifold, cost, euclidean_gradient=euclidean_gradient - ) - optimizer = Optimizer(**solver_kwargs) - - R_0, t_0 = get_C_r_from_theta(t0[: self.d + self.d**2], self.d) - res = optimizer.run(problem, initial_point=(R_0, t_0)) - R, t = res.point - - if verbose: - print("local solver sanity check:") - print("final penalty:", self.penalty(t)) - w = self.theta[-self.n_landmarks :] - for i in range(self.n_landmarks): - residual = self.residual_sq(R, t, self.landmarks[i], y[i]) - if w[i] == -1: - if verbose: - print(f"outlier residual: {residual:.4e}") - assert ( - residual > self.beta - ), f"outlier residual too small: {residual} <= {self.beta}" - else: - if verbose: - print(f"inlier residual: {residual:.4e}") - assert ( - residual < self.beta - ), f"inlier residual too large: {residual} > {self.beta}" - if verbose: - print("qcqp cost:", res.cost) - - if self.robust: - theta_hat = np.r_[get_theta_from_C_r(R, t), w] - else: - theta_hat = get_theta_from_C_r(R, t) - - cost_penalized = res.cost - if self.robust: - pen = self.penalty(t) - if abs(res.cost) > 1e-10: - assert abs(pen) / res.cost <= 1e-1, (pen, res.cost) - cost_penalized -= pen - - success = ("min step_size" in res.stopping_criterion) or ( - "min grad norm" in res.stopping_criterion - ) - info = { - "success": success, - "msg": res.stopping_criterion, - } - if success: - return theta_hat, info, cost_penalized - else: - return None, info, cost_penalized - - def test_and_add(self, A_list, Ai, output_poly): - x = self.get_x() - Ai_sparse = Ai.get_matrix(self.var_dict) - err = x.T @ Ai_sparse @ x - assert abs(err) <= 1e-10, err - if output_poly: - A_list.append(Ai) - else: - A_list.append(Ai_sparse) - - def get_A_known(self, var_dict=None, output_poly=False): - A_list = [] - if var_dict is None: - var_dict = self.var_dict - - if "c" in var_dict: - # enforce diagonal == 1 - for i in range(self.d): - Ei = np.zeros((self.d, self.d)) - Ei[i, i] = 1.0 - constraint = np.kron(Ei, np.eye(self.d)) - Ai = PolyMatrix(symmetric=True) - Ai["c", "c"] = constraint - Ai[self.HOM, self.HOM] = -1 - self.test_and_add(A_list, Ai, output_poly=output_poly) - - # enforce off-diagonal == 0 - for i in range(self.d): - for j in range(i + 1, self.d): - Ei = np.zeros((self.d, self.d)) - Ei[i, j] = 1.0 - Ei[j, i] = 1.0 - constraint = np.kron(Ei, np.eye(self.d)) - Ai = PolyMatrix(symmetric=True) - Ai["c", "c"] = constraint - self.test_and_add(A_list, Ai, output_poly=output_poly) - if self.robust: - for key in var_dict: - if "w" in key: - i = key.split("_")[-1] - Ai = PolyMatrix(symmetric=True) - Ai[self.HOM, self.HOM] = -1.0 - Ai[f"w_{i}", f"w_{i}"] = 1.0 - self.test_and_add(A_list, Ai, output_poly=output_poly) - - # below doesn't hold: w_i*w_j = += 1 - # for key_other in [k for k in var_dict if (k.startswith("w") and (k!= key))]: - # Ai = PolyMatrix(symmetric=True) - # Ai[self.HOM, self.HOM] = -1.0 - # Ai[key, key_other] = 0.5 - # self.test_and_add(A_list, Ai, output_poly=output_poly) - - if "z" in key: - if self.level == "xwT": - i = key.split("_")[-1] - """ each z_i equals x * w_i""" - - for j in range(self.d): - Ai = PolyMatrix(symmetric=True) - constraint = np.zeros((self.d + self.d**2)) - constraint[j] = 1.0 - Ai[self.HOM, f"z_{i}"] = constraint[None, :] - constraint = np.zeros((self.d)) - constraint[j] = -1.0 - Ai[f"t", f"w_{i}"] = constraint[:, None] - self.test_and_add(A_list, Ai, output_poly=output_poly) - - for j in range(self.d**2): - Ai = PolyMatrix(symmetric=True) - constraint = np.zeros((self.d + self.d**2)) - constraint[self.d + j] = 1.0 - Ai[self.HOM, f"z_{i}"] = constraint[None, :] - constraint = np.zeros((self.d**2)) - constraint[j] = -1.0 - Ai[f"c", f"w_{i}"] = constraint[:, None] - self.test_and_add(A_list, Ai, output_poly=output_poly) - return A_list - - def get_B_known(self): - """Get inequality constraints of the form x.T @ B @ x <= 0. - By default, we always add ||t|| <= MAX_DIST - """ - B1 = PolyMatrix(symmetric=True) - B1[self.HOM, self.HOM] = -self.MAX_DIST - B1["t", "t"] = np.eye(self.d) - return [B1.get_matrix(self.var_dict)] - - @abstractmethod - def h_list(self, t): - """ - Any inequality constraints to enforce, returned as a list [h_1(t), h_2(t), ...] - We use the convention h_i(t) <= 0. - - By default, we always add |t| <= MAX_DIST - """ - try: - import autograd.numpy as anp - - return [anp.sqrt(anp.sum(t[: self.d] ** 2)) - self.MAX_DIST] # type: ignore - except ModuleNotFoundError: - return [np.sqrt(np.sum(t[: self.d] ** 2)) - self.MAX_DIST] - - @abstractmethod - def get_random_position(self): - """Generate a new random position. Orientation angles will be drawn uniformly from [0, pi].""" - return None - - @abstractmethod - def term_in_norm(self, R, t, pi, ui) -> np.ndarray: - pass - - @abstractmethod - def residual_sq(self, R, t, pi, ui) -> float: - pass
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/base_lifters/state_lifter.html b/docs/build/_modules/popcor/base_lifters/state_lifter.html deleted file mode 100644 index 5d5b124..0000000 --- a/docs/build/_modules/popcor/base_lifters/state_lifter.html +++ /dev/null @@ -1,501 +0,0 @@ - - - - - - - - popcor.base_lifters.state_lifter — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.base_lifters.state_lifter

-from abc import abstractmethod
-
-import numpy as np
-
-from ._base_class import BaseClass
-
-
-
-[docs] -class StateLifter(BaseClass): - # sparse hierarchy: define the levels that are implemented - LEVELS = ["no"] - - # used for AutoTemplate - VARIABLE_LIST = [["h"]] - TIGHTNESS = "cost" - - # to be overwritten by inheriting class - NOISE = 1e-2 - - def __init__( - self, - level="no", - param_level="no", - d=2, - variable_list=None, - robust=False, - n_outliers=0, - n_parameters=1, - ): - - # variables that get overwritten upon initialization - self.parameters_ = None - self.theta_ = None - self.var_dict_ = None - self.y_ = None - - self.robust = robust - self.n_outliers = n_outliers - - assert level in self.LEVELS - self.level = level - - if variable_list is not None: - self.variable_list = variable_list - else: - self.variable_list = self.VARIABLE_LIST - - if (param_level != "no") and (n_parameters == 1): - print("Warning: make sure to give the correct n_parameters for the level.") - - super().__init__(d, param_level, n_parameters) - - # MUST OVERWRITE THESE - - @property - def var_dict(self): - raise ValueError("Inheriting class must implement this!") - -
-[docs] - @abstractmethod - def sample_theta(self) -> np.ndarray: - """Randomly sample a feasible state theta. This function must be - implemented by the inheriting class.""" - raise NotImplementedError("need to implement sample_theta")
- - - # MUST OVERWRITE THESE FOR TIGHTNESS CHECKS - -
-[docs] - def get_Q(self, output_poly=False, noise=None): - """Construct the cost matrix Q. - - :param noise: set the noise level, if appropriate. - :param output_poly: if True, return the matrix in PolyMatrix format. - - :returns: the cost matrix as a sparse matrix or PolyMatrix. - """ - raise NotImplementedError( - "Need to impelement get_Q in inheriting class if you want to use it." - )
- - - def get_Q_from_y(self, y): - raise NotImplementedError( - "Need to impelement get_Q_from_y in inheriting class if you want to use it." - ) - -
-[docs] - def get_A_known( - self, - add_redundant: bool = False, - var_dict: dict | None = None, - output_poly: bool = False, - ) -> list: - """Construct the matrices defining the known equality constraints. - - :param add_redundant: if True, add redundant constraints. - :param var_dict: if provided, return only the matrices that involve these variables. - :param output_poly: if True, return the matrices in PolyMatrix format. - """ - return []
- - -
-[docs] - def get_B_known(self) -> list: - """Construct the matrices defining the known inequality constraints.""" - return []
- - - # MUST OVERWRITE THESE FOR ADDING PARAMETERS - -
-[docs] - def sample_parameters(self, theta=None) -> dict: - """Create random set of parameters. By default, there are no parameters - so this function just returns `{self.HOM: 1.0}`.""" - assert ( - self.param_level == "no" - ), "Need to overwrite sample_parameters to use level different than 'no'" - return {self.HOM: 1.0}
- - - @property - def param_dict(self): - assert ( - self.param_level == "no" - ), "Need to overwrite param_dict to use level different than 'no'" - return {self.HOM: 1} - - def get_involved_param_dict(self, var_subset): - """Find which parameters to include, given the current var_subset. Here we implicitly assume - that each parameter is associated with a variable. This is true for parameters that involve - substitution variables.""" - keys = [self.HOM] - for v in var_subset: - index = v.split("_") - if len(index) > 1: - index = int(index[-1]) - key = f"p_{index}" - if key not in keys: - keys.append(key) - return {k: self.param_dict[k] for k in keys if k in self.param_dict} - - # CAN OPTINALLY OVERWRITE THESE FOR BETTER PERFORMANCE - - def get_grad(self, theta, y=None) -> float: - raise NotImplementedError("must define get_grad if you want to use it.") - - def get_hess(self, theta, y=None) -> float: - raise NotImplementedError("must define get_hess if you want to use it.") - -
-[docs] - def get_cost(self, theta, y=None) -> float: - """Compute the cost of the given state theta. This uses the simple form - x.T @ Q @ x. Consider overwriting this for more efficient computations.""" - print( - "Warning: using default get_cost, which may be less efficient than a custom one." - ) - x = self.get_x(theta=theta).flatten("C") - if y is not None: - Q = self.get_Q_from_y(y) - else: - Q = self.get_Q() - return float(x.T @ Q @ x)
- - -
-[docs] - def local_solver(self, t0, y=None, *args, **kwargs): - """ - Default local solver that uses IPOPT to solve the QCQP problem defined by Q and the constraints matrices. - Consider overwriting this for more efficient solvers. - """ - print( - "Warning: using default local_solver, which may be less efficient than a custom one." - ) - print("Ignoring args and kwargs:", args, kwargs) - from cert_tools.sdp_solvers import solve_low_rank_sdp - - if y is not None: - Q = self.get_Q_from_y(y) - else: - Q = self.get_Q() - - B = self.get_B_known() - if len(B) > 0: - raise NotImplementedError( - "Inequality constraints are not currently considered by default solver. Must implement your own." - ) - - Constraints = self.get_A_b_list(A_list=self.get_A_known()) - x0 = self.get_x(theta=t0) - X, info = solve_low_rank_sdp( - Q, Constraints=Constraints, rank=1, verbose=True, x_cand=x0 - ) - # TODO(FD) identify when the solve is not successful. - info["success"] = True - try: - theta = self.get_theta(X[:, 0]) - except AttributeError: - theta = X[1 : 1 + self.d, 0] - return theta, info, info["cost"]
- - - @property - def param_dict_landmarks(self): - assert self.n_parameters is not None - - param_dict = {self.HOM: 1} - if self.param_level == "no": - return param_dict - if self.param_level == "p": - param_dict.update({f"p_{i}": self.d for i in range(self.n_parameters)}) - if self.param_level == "ppT": - # Note that ppT is actually - # [p; vech(ppT)] (linear and quadratic terms) - # TODO(FD): rename ppT to quadratic - param_dict.update( - { - f"p_{i}": self.d + int(self.d * (self.d + 1) / 2) - for i in range(self.n_parameters) - } - ) - return param_dict - -
-[docs] - def get_theta(self, x): - """Inverse of get_x: given lifted vector x, extract elements corresponding to theta.""" - assert np.ndim(x) == 1 or x.shape[1] == 1 - return x.flatten()[1 : 1 + self.d]
-
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/base_lifters/stereo_lifter.html b/docs/build/_modules/popcor/base_lifters/stereo_lifter.html deleted file mode 100644 index f7c9cff..0000000 --- a/docs/build/_modules/popcor/base_lifters/stereo_lifter.html +++ /dev/null @@ -1,746 +0,0 @@ - - - - - - - - popcor.base_lifters.stereo_lifter — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.base_lifters.stereo_lifter

-from abc import ABC, abstractmethod
-
-import numpy as np
-import scipy.sparse as sp
-from poly_matrix.poly_matrix import PolyMatrix
-
-from popcor.utils.geometry import (
-    generate_random_pose,
-    get_C_r_from_theta,
-    get_noisy_pose,
-    get_pose_errors_from_theta,
-    get_T,
-    get_theta_from_C_r,
-)
-
-from .state_lifter import StateLifter
-
-NOISE = 1.0  #
-
-
-SOLVER_KWARGS = dict(
-    min_gradient_norm=1e-6, max_iterations=10000, min_step_size=1e-10, verbosity=1
-)
-
-
-
-[docs] -class StereoLifter(StateLifter, ABC): - """StereoLifter is a general lifter class for the stereo localization problem, supporting both 2D and 3D cases. - - See :class:`~popcor.examples.Stereo2DLifter` for 2D and :class:`~popcor.examples.Stereo3DLifter` for 3D. - """ - - NORMALIZE = True - - # Levels that were experimented with for creating a tight relaxation. - LEVELS = [ - "no", - "u@u", # ... - "u2", - "u@r", - "uuT", - "urT", - "uxT", - ] - PARAM_LEVELS = ["no", "p", "ppT"] - LEVEL_NAMES = { - "no": "$\\boldsymbol{u}_n$", - "urT": "$\\boldsymbol{u}\\boldsymbol{t}^\\top_n$", - "uxT": "$\\boldsymbol{u}\\boldsymbol{x}^\\top_n$", - } - - EPS_ERROR = 1e-7 # for constraints test. default is 1e-8 - - def __init__( - self, n_landmarks, d, level="no", param_level="no", variable_list=None - ): - self.y_ = None - self.n_landmarks = n_landmarks - self.landmarks_ = None # will be initialized on first access - super().__init__( - d=d, - level=level, - param_level=param_level, - variable_list=variable_list, - n_parameters=n_landmarks, - ) - - @property - @abstractmethod - def M_matrix(self): - raise NotImplementedError("Inheriting class must initialize M_matrix.") - - def get_all_variables(self): - return [[self.HOM, "x"] + [f"z_{i}" for i in range(self.n_landmarks)]] - - def get_level_dims(self, n=1): - """ - :param n: number of landmarks to consider - """ - return { - "no": 0, - "u@u": n, # ... - "u2": n * self.d, - "u@r": n, - "uuT": n * self.d**2, - "urT": n * self.d**2, - "uxT": n * (self.d * (self.d + self.d**2)), - } - - @property - def landmarks(self): - if self.landmarks_ is None: - landmarks = self.generate_random_landmarks(self.theta) - self.landmarks_ = landmarks - return self.landmarks_ - - def generate_random_landmarks(self, theta=None): - if theta is not None: - C, r = get_C_r_from_theta(theta, self.d) - if self.d == 3: - # sample left u, v coordinates in left image, and compute landmark coordinates from that. - fu, cu, b = self.M_matrix[0, [0, 2, 3]] - fv, cv = self.M_matrix[1, [1, 2]] - u = np.random.uniform(0, cu * 2, self.n_landmarks) - v = np.random.uniform(0, cv * 2, self.n_landmarks) - z = np.random.uniform(0, 5, self.n_landmarks) - x = 1 / fu * (z * (u - cu) - b) - y = 1 / fv * z * (v - cv) - points_cam = np.c_[x, y, z] # N x 3 - else: - # sample left u in left image, and compute landmark coordinates from that. - fu, cu, b = self.M_matrix[0, :] - u = np.random.uniform(0, cu * 2, self.n_landmarks) - y = np.random.uniform(1, 5, self.n_landmarks) - x = 1 / fu * (y * (u - cu) - b) - points_cam = np.c_[x, y] - # transform points from camera to world - return (C.T @ (points_cam.T - r[:, None])).T - else: - return np.random.rand(self.n_landmarks, self.d) - - def sample_parameters(self, theta=None): - landmarks = self.generate_random_landmarks(theta=self.theta) - return self.sample_parameters_landmarks(landmarks) - - def get_parameters(self, var_subset=None): - return self.get_p(param_subset=var_subset) - - @property - def VARIABLE_LIST(self): - return [ - [self.HOM, "x"], - [self.HOM, "z_0"], - [self.HOM, "x", "z_0"], - [self.HOM, "z_0", "z_1"], # should achieve tightness here - ] - - @property - def param_dict(self): - return self.param_dict_landmarks - - @property - def var_dict(self): - level_dim = self.get_level_dims()[self.level] - if self.var_dict_ is None: - self.var_dict_ = {self.HOM: 1} - self.var_dict.update({"x": self.d**2 + self.d}) - self.var_dict.update( - {f"z_{k}": self.d + level_dim for k in range(self.n_landmarks)} - ) - return self.var_dict_ - - def get_x(self, theta=None, parameters=None, var_subset=None): - """ - :param var_subset: list of variables to include in x vector. Set to None for all. - """ - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - if var_subset is None: - var_subset = self.var_dict.keys() - - # TODO(FD) below is a bit hacky, these two variables should not both be called theta. - # theta is either (x, y, alpha) or (x, y, z, a1, a2, a3) - C, r = get_C_r_from_theta(theta, self.d) - if (self.param_level != "no") and (len(parameters) > 1): - landmarks = parameters - else: - landmarks = { - f"p_{i}": self.landmarks[i, :] for i in range(self.landmarks.shape[0]) - } - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - elif key == "x": - x_data += list(r) + list(C.flatten("C")) # row-wise flatten - elif "z" in key: - j = int(key.split("_")[-1]) - - pj = landmarks[f"p_{j}"][: self.d] # - - zj = C[self.d - 1, :] @ pj + r[self.d - 1] - u = 1 / zj * np.r_[C[: self.d - 1, :] @ pj + r[: self.d - 1], 1] - x_data += list(u) - - if self.level == "no": - continue - elif self.level == "u2": - x_data += list(u**2) - elif self.level == "u@u": - x_data += [u @ u] - elif self.level == "u@r": - x_data += [u @ r] - elif self.level == "uuT": - x_data += list(np.outer(u, u).flatten()) - elif self.level == "urT": - # this works - x_data += list(np.outer(u, r).flatten()) - elif self.level == "uxT": - x = np.r_[r, C.flatten("C")] - x_data += list(np.outer(u, x).flatten()) - dim_x = self.get_dim_x(var_subset=var_subset) - assert len(x_data) == dim_x - return np.array(x_data) - - def get_A_known(self, var_dict=None, output_poly=False): - """ - T = | cx' tx | - | cy' ty | - | cz' tz | - | 0 0 0 1 | - Let pj be the j-th landmark coordinate. - [xj] [cx @ pj + tx] - [yj] = [cy @ pj + ty] - [zj] [cz @ pj + tz] - - Let u be the substitution variable, which has d-1 elements. - Then we want to enforce that: - u_xj = 1/zj * xj -> u_xj * zj = xj -> (cz @ pj + tz) * u_xj - (cx @ pj + tx) = 0 - u_yj = 1/zj * yj -> u_yj * zj = yj -> same as above - u_zj = 1/zj -> u_zj * zj = 1 -> u_zj * (cz @ pj + tz) -1 = 0 - Writing things as homogeneous constraints: - a1) cz @ pj * u_xj + tz*u_xj - cx @ pj - h * tx = 0 - a2) -----1x------- --2x--- -- 3 -- --4--- - a3) cz @ pj * u_zj + tz*u_zj - h*h = 0 - ------1z------- --2z--- - """ - print("not using known stereo templates because they depend on the landmarks.") - return [] - - # x contains: [c1, c2, c3, t] - # z contains: [u_xj, u_yj, u_zj, H.O.T.] - if self.d == 2: - x = self.get_x() - _, tx, tz, cx1, cx2, cz1, cz2, u_xj, u_zj, *_ = x - cz = np.array([cz1, cz2]) - cx = np.array([cx1, cx2]) - pj = self.landmarks[0] - assert abs(cz @ pj * u_xj + tz * u_xj - cx @ pj - tx) < 1e-10 - assert abs(u_zj * cz @ pj + u_zj * tz - 1) < 1e-10 - elif self.d == 3: - x = self.get_x() - # fmt: off - (_, tx, ty, tz, cx1, cx2, cx3, cy1, cy2, cy3, cz1, cz2, cz3, u_x1, u_y1, u_z1, *_) = x - # fmt: on - p1 = self.landmarks[0] - assert ( - abs(u_z1 * (cx1 * p1[0] + cx2 * p1[1] + cx3 * p1[2]) + u_z1 * tx - u_x1) - < 1e-10 - ) - assert ( - abs(u_z1 * (cy1 * p1[0] + cy2 * p1[1] + cy3 * p1[2]) + u_z1 * ty - u_y1) - < 1e-10 - ) - assert ( - abs(u_z1 * (cz1 * p1[0] + cz2 * p1[1] + cz3 * p1[2]) + u_z1 * tz - 1) - < 1e-10 - ) - - if var_dict is None: - var_dict = self.var_dict - - A_known = [] - z_dim = self.get_level_dims()[self.level] - - if "x" not in var_dict or self.HOM not in var_dict: - return A_known - landmarks = [j for j in range(self.n_landmarks) if f"z_{j}" in var_dict] - for j in landmarks: - # one complete constraint has x, z_j and h. - pj = self.landmarks[j] - for i in range(self.d): - A = PolyMatrix() - # -----1i------- --2i--- -- 3 -- --4--- - # a1) cz @ pj * u_xj + tz*u_xj - cx @ pj - h * tx = 0 - # a2) cz @ pj * u_yj + tz*u_yj - cy @ pj - h * ty = 0 - # a3) cz @ pj * u_zj + tz*u_zj - h*h = 0 - # ------1i------- --2i--- - # --- 1i --- - fill_mat = np.zeros((self.d + self.d**2, self.d + z_dim)) - # chooses cz of x, and u_xj, u_yj or u_zj of z - fill_mat[-self.d :, i] = pj - - # --- 2 --- u_zj * tx - # chooses tz of x, and u_ij of z - fill_mat[self.d - 1, i] = 1.0 - A[f"x", f"z_{j}"] = fill_mat - - if i < self.d - 1: # u, (v) - fill_mat = np.zeros((self.d + self.d**2, 1)) - # chooses ci of x - fill_mat[(i + 1) * self.d : (i + 2) * self.d, 0] = -pj - - # chooses ti of x - fill_mat[i, 0] = -1 - A["x", self.HOM] = fill_mat - elif i == self.d - 1: # z - A[self.HOM, self.HOM] = -0 # 2.0 - if output_poly: - A_known.append(A) - else: - A_known.append(A.get_matrix(var_dict)) - self.test_constraints(A_known) - return A_known - - def sample_theta(self): - return generate_random_pose(d=self.d).flatten() - - def simulate_y(self, noise: float | None = None): - if noise is None: - noise = NOISE - - T = get_T(theta=self.theta, d=self.d) - - y_sim = np.zeros((self.n_landmarks, self.M_matrix.shape[0])) - for j in range(self.n_landmarks): - y_gt = T @ np.r_[self.landmarks[j], 1.0] - - # in 2d: y_gt[1] - # in 3d: y_gt[2] - y_gt /= y_gt[self.d - 1] - y_gt = self.M_matrix @ y_gt - y_sim[j, :] = y_gt + np.random.normal(loc=0, scale=noise, size=len(y_gt)) - return y_sim - - def get_Q( - self, - noise: float | None = None, - output_poly: bool = False, - use_cliques: list = [], - ) -> PolyMatrix | sp.csr_matrix | sp.csc_matrix: - if self.y_ is None: - if noise is None: - noise = NOISE - self.y_ = self.simulate_y(noise=noise) - - Q = self.get_Q_from_y(self.y_, output_poly=output_poly, use_cliques=use_cliques) - return Q - - def get_Q_from_y( - self, y, output_poly=False, use_cliques=[] - ) -> PolyMatrix | sp.csr_matrix | sp.csc_matrix: - """ - The least squares problem reads - min_T sum_{n=0}^{N-1} || y - Mtilde@z || - where the first d elements of z correspond to u, and Mtilde contains the first d-1 and last element of M - Mtilde is thus of shape d*2 by dim_z, where dim_z=d+dL (the additional Lasserre variables) - y is of length d*2, corresponding to the measured pixel values in left and right image. - """ - from poly_matrix.least_squares_problem import LeastSquaresProblem - - if len(use_cliques): - js = use_cliques - else: - js = range(y.shape[0]) - - # when using lifting (level=urT), then we have - # in 2d: M_tilde is 2 by 6, with first 2 columns: M[:, [0, 2]] - # in 3d: M_tilde is 4 by 12, with first 3 columns: M[:, [0, 1, 3]] - M_tilde = np.zeros((len(y[0]), self.var_dict["z_0"])) - M_tilde[:, : self.d] = self.M_matrix[:, list(range(self.d - 1)) + [self.d]] - - # in 2d: M[:, 1] - # in 3d: M[:, 2] - m = self.M_matrix[:, self.d - 1] - - ls_problem = LeastSquaresProblem() - for j in js: - ls_problem.add_residual({self.HOM: (y[j] - m), f"z_{j}": -M_tilde}) - - if output_poly: - Q = ls_problem.get_Q() - else: - Q = ls_problem.get_Q().get_matrix(self.var_dict) - if self.NORMALIZE: - Q /= self.n_landmarks * self.d - - # sanity check - x = self.get_x() - - # sanity checks. Below is the best conditioned because we don't have to compute B.T @ B, which - # can contain very large values. - B = ls_problem.get_B_matrix(self.var_dict) - errors = B @ x - cost_test = errors.T @ errors - if self.NORMALIZE: - cost_test /= self.n_landmarks * self.d - - if output_poly: - assert isinstance(Q, PolyMatrix) - cost_Q = x.T @ Q.get_matrix(self.var_dict, output_type="csr") @ x - else: - cost_Q = x.T @ Q @ x - assert abs(cost_test - cost_Q) < 1e-6, (cost_test, cost_Q) - if not len(use_cliques): - cost_raw = self.get_cost(self.theta, y) - assert abs(cost_test - cost_raw) < 1e-6, (cost_test, cost_raw) - assert isinstance(Q, (PolyMatrix, sp.csr_matrix, sp.csc_matrix)), type(Q) - return Q - - def get_theta(self, x): - return x[1 : 1 + self.d + self.d**2] - - def get_vec_around_gt(self, delta: float = 0): - if delta == 0: - return self.theta - - C, r = get_C_r_from_theta(self.theta, self.d) - if self.d == 2: - return super().get_vec_around_gt(delta=delta) - else: - return get_noisy_pose(C, r, delta) - - def get_C_cw(self, theta=None): - C_cw, __ = get_C_r_from_theta(theta, self.d) - return C_cw - - def get_position(self, theta=None): - C_cw, r_wc_c = get_C_r_from_theta(theta, self.d) - return (-C_cw.T @ r_wc_c)[None, :] - - def get_error(self, theta_hat): - return get_pose_errors_from_theta(theta_hat, self.theta, self.d) - - def local_solver_manopt(self, t0, y, W=None, verbose=False, method="CG", **kwargs): - """Alternative solver using Pymanopt. By default, :ref:`StateLifter.local_solver` by is used.""" - import pymanopt - from pymanopt.manifolds import Euclidean, Product, SpecialOrthogonalGroup - - if method == "CG": - from pymanopt.optimizers import ConjugateGradient as Optimizer # fastest - elif method == "SD": - from pymanopt.optimizers import SteepestDescent as Optimizer # slow - elif method == "TR": - from pymanopt.optimizers import TrustRegions as Optimizer # okay - else: - raise ValueError(method) - - solver_kwargs = SOLVER_KWARGS - solver_kwargs.update(kwargs) - - if verbose: - solver_kwargs["verbosity"] = 2 - else: - solver_kwargs["verbosity"] = 1 - - manifold = Product((SpecialOrthogonalGroup(self.d, k=1), Euclidean(self.d))) - - if W is None: - W = np.eye(4) if self.d == 3 else np.eye(2) - - @pymanopt.function.autograd(manifold) - def cost(R, t): - cost = 0 - for i in range(self.n_landmarks): - pi_cam = np.concatenate([R @ self.landmarks[i] + t, [1]], axis=0) # type: ignore - y_gt = self.M_matrix @ (pi_cam / pi_cam[self.d - 1]) - residual = y[i] - y_gt - cost += residual.T @ W @ residual - if self.NORMALIZE: - return cost / (self.n_landmarks * self.d) - return cost - - euclidean_gradient = None # set to None - problem = pymanopt.Problem( - manifold, cost, euclidean_gradient=euclidean_gradient # - ) - optimizer = Optimizer(**solver_kwargs) # type: ignore - - R_0, t_0 = get_C_r_from_theta(t0[: self.d + self.d**2], self.d) - res = optimizer.run(problem, initial_point=(R_0, t_0)) - R, t = res.point - - theta_hat = get_theta_from_C_r(R, t) - return theta_hat, res.stopping_criterion, res.cost - - def __repr__(self): - level_str = str(self.level).replace(".", "-") - return f"stereo{self.d}d_{level_str}_{self.param_level}"
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/examples/example_lifter.html b/docs/build/_modules/popcor/examples/example_lifter.html deleted file mode 100644 index ed04821..0000000 --- a/docs/build/_modules/popcor/examples/example_lifter.html +++ /dev/null @@ -1,324 +0,0 @@ - - - - - - - - popcor.examples.example_lifter — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.examples.example_lifter

-import numpy as np
-
-from popcor.base_lifters import StateLifter
-
-
-
-[docs] -class ExampleLifter(StateLifter): - """Example Lifter class. - - This class implements the bare minimum to use AutoTight. - - To create a new Lifter for your problem formulation, create - a copy of this file and fill in the missing parts. - - You can take a look at the :ref:`Examples` for inspiration. - """ - - # choose your homogenization variable here - HOM = "h" - - # chose the "lifting" levels when going up in the sparse Lasserre's hierarchy. - LEVELS = ["no"] - - def __init__(self, param_level="no"): - # you can choose if you want to use parameters. Otherwise remove param_level or set to "no" - super().__init__(param_level=param_level) - - @property - def var_dict(self): - var_dict = {self.HOM: 1} - return var_dict - - @property - def param_dict(self): - param_dict = {self.HOM: 1} - return param_dict - - def get_x(self, theta=None, parameters=None, var_subset=None) -> np.ndarray: - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - if var_subset is None: - var_subset = self.var_dict - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - # fill in the rest of x according to var_subset. - # elif "x" in key: - # elif "z" in key: - assert len(x_data) == self.get_dim_x(var_subset) - return np.array(x_data) - - def sample_parameters(self, theta: np.ndarray | None = None) -> dict: - pass - - def sample_theta(self) -> np.ndarray: - pass
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/examples/mono_lifter.html b/docs/build/_modules/popcor/examples/mono_lifter.html deleted file mode 100644 index 84bccb4..0000000 --- a/docs/build/_modules/popcor/examples/mono_lifter.html +++ /dev/null @@ -1,526 +0,0 @@ - - - - - - - - popcor.examples.mono_lifter — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.examples.mono_lifter

-from copy import deepcopy
-
-# import autograd.numpy as np
-import numpy as np
-from poly_matrix.poly_matrix import PolyMatrix
-
-from popcor.base_lifters import RobustPoseLifter
-from popcor.utils.geometry import get_C_r_from_theta
-from popcor.utils.plotting_tools import plot_frame
-
-FOV = np.pi / 2  # camera field of view
-
-N_TRYS = 10
-
-# TODO(FD) for some reason this is not required as opposed to what is stated in Heng's paper
-# and it currently breaks tightness (might be a bug in my implementation though)
-USE_INEQ = False
-
-NORMALIZE = False
-
-
-
-[docs] -class MonoLifter(RobustPoseLifter): - """This example is treated in more details in `this paper <https://arxiv.org/abs/2308.05783>`_, - under the name "PLR" (point-to-line registration). - """ - - NOISE = 1e-3 # inlier noise - NOISE_OUT = 0.1 # outlier noise - - @property - def TIGHTNESS(self): - return "cost" if self.robust else "rank" - - def h_list(self, t): - """ - We want to inforce that - - norm(t) <= 10 (default) - - tan(a/2)*t3 >= sqrt(t1**2 + t2**2) - as constraints h_j(t)<=0 - """ - default = super().h_list(t) - try: - import autograd.numpy as anp - - return default + [ - anp.sum(t[:-1] ** 2) - anp.tan(FOV / 2) ** 2 * t[-1] ** 2, # type: ignore - -t[-1], - ] - except ModuleNotFoundError: - return default + [ - np.sum(t[:-1] ** 2) - np.tan(FOV / 2) ** 2 * t[-1] ** 2, - -t[-1], - ] - - def get_random_position(self): - pc_cw = np.random.rand(self.d) * 0.1 - # make sure all landmarks are in field of view: - # min_dist = max(np.linalg.norm(self.landmarks[:, :self.d-1], axis=1)) - pc_cw[self.d - 1] = np.random.uniform(1, self.MAX_DIST) - return pc_cw - - def get_B_known(self): - """Get inequality constraints of the form x.T @ B @ x <= 0""" - - # TODO(FD) for some reason this is not required as opposed to what is stated in Heng's paper - # and it currently breaks tightness (might be a bug in my implementation though) - if not USE_INEQ: - return [] - - default = super().get_B_known() - # B2 and B3 enforce that tan(FOV/2)*t3 >= sqrt(t1**2 + t2**2) - # 0 <= - tan**2(FOV/2)*t3**2 + t1**2 + t2**2 - B3 = PolyMatrix(symmetric=True) - constraint = np.zeros((self.d, self.d)) - constraint[range(self.d - 1), range(self.d - 1)] = 1.0 - constraint[self.d - 1, self.d - 1] = -np.tan(FOV / 2) ** 2 - B3["t", "t"] = constraint - - # t3 >= 0 - constraint = np.zeros(self.d) - constraint[self.d - 1] = -1 - B2 = PolyMatrix(symmetric=True) - B2[self.HOM, "t"] = constraint[None, :] - return default + [ - B2.get_matrix(self.var_dict), - B3.get_matrix(self.var_dict), - ] - - def term_in_norm(self, R, t, pi, ui): - return R @ pi + t - - def residual_sq(self, R, t, pi, ui): - W = np.eye(self.d) - np.outer(ui, ui) - term = self.term_in_norm(R, t, pi, ui) - if NORMALIZE: - return term.T @ W @ term / (self.n_landmarks * self.d) ** 2 - else: - return term.T @ W @ term - - def plot_setup(self): - if self.d != 2: - print("Plotting currently only supported for d=2") - return - import matplotlib.pylab as plt - - assert self.landmarks is not None - - fig, ax = plt.subplots() - - # R, t = get_C_r_from_theta(self.theta, self.d) - # ax.scatter(*t, color="k", label="pose") - - ax.axis("equal") - t_wc_w, C_cw = plot_frame(ax, self.theta, label="pose", color="gray", d=2) - - if self.y_ is not None: - for i in range(self.y_.shape[0]): - ax.scatter( - self.landmarks[i][0], - self.landmarks[i][1], - color=f"C{i}", - label="landmarks", - ) - - # this vector is in camera coordinates - ui_c = self.y_[i] - assert abs(np.linalg.norm(ui_c) - 1.0) < 1e-10 - - ax.plot( - [t_wc_w[0], self.landmarks[i][0]], - [t_wc_w[1], self.landmarks[i][1]], - color=f"C{i}", - ls=":", - ) - if C_cw is not None: - ui_w = C_cw.T @ ui_c - ax.plot( - [t_wc_w[0], t_wc_w[0] + ui_w[0]], - [t_wc_w[1], t_wc_w[1] + ui_w[1]], - color=f"r" if i < self.n_outliers else "g", - ) - - def get_Q( - self, - noise: float | None = None, - output_poly: bool = False, - use_cliques: list = [], - ): - assert self.landmarks is not None, "landmarks must be set before calling get_Q" - if noise is None: - noise = self.NOISE - - if self.y_ is None: - self.y_ = np.zeros((self.n_landmarks, self.d)) - theta = self.theta[: self.d + self.d**2] - outlier_index = self.get_outlier_index() - - R, t = get_C_r_from_theta(theta, self.d) - for i in range(self.n_landmarks): - pi = self.landmarks[i] - # ui = deepcopy(pi) #R @ pi + t - ui = R @ pi + t - ui /= ui[self.d - 1] - - # random unit vector inside the FOV cone - # tan(a/2)*t3 >= sqrt(t1**2 + t2**2) or t3 >= 1 - if np.tan(FOV / 2) * ui[self.d - 1] < np.sqrt( - np.sum(ui[: self.d - 1] ** 2) - ): - print("warning: inlier not in FOV!!") - - if i in outlier_index: - # randomly sample a vector - success = False - for _ in range(N_TRYS): - ui_test = deepcopy(ui) - ui_test[: self.d - 1] += np.random.normal( - scale=self.NOISE_OUT, loc=0, size=self.d - 1 - ) - if np.tan(FOV / 2) * ui_test[self.d - 1] >= np.sqrt( - np.sum(ui_test[: self.d - 1] ** 2) - ): - success = True - ui = ui_test - break - if not success: - raise ValueError("did not find valid outlier ui") - else: - ui[: self.d - 1] += np.random.normal( - scale=noise, loc=0, size=self.d - 1 - ) - assert ui[self.d - 1] == 1.0 - ui /= np.linalg.norm(ui) - self.y_[i] = ui - - Q = self.get_Q_from_y(self.y_, output_poly=output_poly, use_cliques=use_cliques) - return Q - - def get_Q_from_y(self, y, output_poly: bool = False, use_cliques: list = []): - """ - every cost term can be written as - (1 + wi)/b**2 [l x'] Qi [l; x] / norm + 1 - wi - = [l x'] Qi/b**2 [l; x] /norm + wi * [l x']Qi/b**2[l;x] / norm + 1 - wi - - cost term: - (Rpi + t) (I - uiui') (Rpi + t) - """ - assert ( - self.landmarks is not None - ), "landmarks must be set before calling get_Q_from_y" - - Q = PolyMatrix(symmetric=True) - if NORMALIZE: - norm = (self.n_landmarks * self.d) ** 2 - - if len(use_cliques): - js = use_cliques - else: - js = list(range(self.n_landmarks)) - - for i in js: - pi = self.landmarks[i] - ui = y[i] - Pi = np.c_[np.eye(self.d), np.kron(pi, np.eye(self.d))] # I, pi x I - Wi = np.eye(self.d) - np.outer(ui, ui) - Qi = Pi.T @ Wi @ Pi # "t,t, t,c, c,c: Wi, Wi @ kron, kron.T @ Wi @ kron - if NORMALIZE: - Qi /= norm - - if self.robust: - Qi /= self.beta**2 - # last two terms, should not be affected by norm - Q[self.HOM, self.HOM] += 1 - Q[self.HOM, f"w_{i}"] += -0.5 - if self.level == "xwT": - # Q[f"z_{i}", "x"] += 0.5 * Qi - Q[f"z_{i}", "t"] += 0.5 * Qi[:, : self.d] - Q[f"z_{i}", "c"] += 0.5 * Qi[:, self.d :] - # Q["x", "x"] += Qi - Q["t", "t"] += Qi[: self.d, : self.d] - Q["t", "c"] += Qi[: self.d, self.d :] - Q["c", "c"] += Qi[self.d :, self.d :] - elif self.level == "xxT": - Q["z_0", f"w_{i}"] += 0.5 * Qi.flatten()[:, None] - # Q["x", "x"] += Qi - Q["t", "t"] += Qi[: self.d, : self.d] - Q["t", "c"] += Qi[: self.d, self.d :] - Q["c", "c"] += Qi[self.d :, self.d :] - else: - # Q["x", "x"] += Qi - Q["t", "t"] += Qi[: self.d, : self.d] - Q["t", "c"] += Qi[: self.d, self.d :] - Q["c", "c"] += Qi[self.d :, self.d :] - if output_poly: - return 0.5 * Q - Q_sparse = 0.5 * Q.get_matrix(variables=self.var_dict) - return Q_sparse - - def __repr__(self): - appendix = "_robust" if self.robust else "" - return f"mono_{self.d}d_{self.level}_{self.param_level}{appendix}"
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/examples/poly4_lifter.html b/docs/build/_modules/popcor/examples/poly4_lifter.html deleted file mode 100644 index 73909c2..0000000 --- a/docs/build/_modules/popcor/examples/poly4_lifter.html +++ /dev/null @@ -1,361 +0,0 @@ - - - - - - - - popcor.examples.poly4_lifter — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.examples.poly4_lifter

-import numpy as np
-
-from popcor.base_lifters import PolyLifter
-
-
-
-[docs] -class Poly4Lifter(PolyLifter): - """Fourth-degree polynomial examples. - - Two types are provided: - - - poly_type="A": one global minimum, one local minimum - - poly_type="B": two global minima - """ - - @property - def VARIABLE_LIST(self): - return [[self.HOM, "t", "z0"]] - - def __init__(self, poly_type="A"): - # actual minimum - assert poly_type in ["A", "B"] - self.poly_type = poly_type - super().__init__(degree=4) - - def get_Q(self, *args, **kwargs): - if self.poly_type == "A": - # fmt: off - # noqa - Q = np.r_[ - np.c_[2, 1, 0], - np.c_[1, -1 / 2, -1 / 3], - np.c_[0, -1 / 3, 1 / 4] - ] - # fmt: on - elif self.poly_type == "B": - # below is constructed such that f'(t) = (t-1)*(t-2)*(t-3) - # fmt: off - # noqa - Q = np.r_[ - np.c_[3, -3, 0], - np.c_[-3, 11 / 2, -1], - np.c_[0, -1, 1 / 4] - ] - # fmt: on - return Q - - def get_A_known(self, output_poly=False, add_redundant=False): - from poly_matrix import PolyMatrix - - if add_redundant: - print("No redundant constraitns for 4-degree polynomial.") - - # z_0 = t^2 - A_1 = PolyMatrix(symmetric=True) - A_1[self.HOM, "z0"] = -1 - A_1["t", "t"] = 2 - if output_poly: - return [A_1] - else: - return [A_1.get_matrix(self.var_dict)] - - def generate_random_setup(self): - self.theta_ = np.array([-1]) - - def get_D(self, that): - """Not currently used.""" - D = np.array( - [ - [1.0, 0.0, 0.0], - [that, 1.0, 0.0], - [that**2, 2 * that, 1.0], - ] - ) - return D
- - - -if __name__ == "__main__": - import os - - import matplotlib.pylab as plt - - # Get the directory two levels up from this file - base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) - - thetas = np.linspace(-2, 3, 100) - poly_lifter = Poly4Lifter(poly_type="A") - fig, ax = poly_lifter.plot(thetas) - fig.savefig(os.path.join(base_dir, "docs", "figures", "poly4_lifter_A.png")) - - thetas = np.linspace(0, 4, 100) - poly_lifter = Poly4Lifter(poly_type="B") - fig, ax = poly_lifter.plot(thetas) - fig.savefig(os.path.join(base_dir, "docs", "figures", "poly4_lifter_B.png")) - - plt.show(block=False) - print("done") -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/examples/poly6_lifter.html b/docs/build/_modules/popcor/examples/poly6_lifter.html deleted file mode 100644 index 64d5845..0000000 --- a/docs/build/_modules/popcor/examples/poly6_lifter.html +++ /dev/null @@ -1,372 +0,0 @@ - - - - - - - - popcor.examples.poly6_lifter — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.examples.poly6_lifter

-import numpy as np
-
-from popcor.base_lifters import PolyLifter
-
-
-
-[docs] -class Poly6Lifter(PolyLifter): - """Sixth-degree polynomial examples. - - Two types are provided: - - - poly_type="A": one global minimum, two local minima, 2 local maxima - - poly_type="B": one global minimum, one local minimum, one local maximum - """ - - @property - def VARIABLE_LIST(self): - return [[self.HOM, "t", "z0", "z1"]] - - def __init__(self, poly_type="A"): - assert poly_type in ["A", "B"] - self.poly_type = poly_type - super().__init__(degree=6) - - def get_Q(self, *args, **kwargs): - if self.poly_type == "A": - return 0.1 * np.array( - [ - [25, 12, 0, 0], - [12, -13, -2.5, 0], - [0, -2.5, 6.25, -0.9], - [0, 0, -0.9, 1 / 6], - ] - ) - elif self.poly_type == "B": - return np.array( - [ - [5.0000, 1.3167, -1.4481, 0], - [1.3167, -1.4481, 0, 0.2685], - [-1.4481, 0, 0.2685, -0.0667], - [0, 0.2685, -0.0667, 0.0389], - ] - ) - - def get_A_known(self, output_poly=False, add_redundant=True): - from poly_matrix import PolyMatrix - - A_list = [] - - # z_0 = t^2 - A_1 = PolyMatrix(symmetric=True) - A_1[self.HOM, "z0"] = -1 - A_1["t", "t"] = 2 - A_list.append(A_1) - - # z_1 = t^3 = t z_0 - A_2 = PolyMatrix(symmetric=True) - A_2[self.HOM, "z1"] = -1 - A_2["t", "z0"] = 1 - A_list.append(A_2) - - # t^4 = z_1 t = z_0 z_0 - if add_redundant: - B_0 = PolyMatrix(symmetric=True) - B_0["z0", "z0"] = 2 - B_0["t", "z1"] = -1 - A_list.append(B_0) - - if output_poly: - return A_list - else: - return [A_i.get_matrix(self.var_dict) for A_i in A_list] - - def get_D(self, that): - D = np.array( - [ - [1.0, 0.0, 0.0, 0.0], - [that, 1.0, 0.0, 0.0], - [that**2, 2 * that, 1.0, 0.0], - [that**3, 3 * that**2, 3 * that, 1.0], - ] - ) - return D - - def generate_random_setup(self): - self.theta_ = np.array([-1])
- - - -if __name__ == "__main__": - import os - - import matplotlib.pylab as plt - - # Get the directory two levels up from this file - base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) - - thetas = np.linspace(-1.5, 4.5, 100) - poly_lifter = Poly6Lifter(poly_type="A") - fig, ax = poly_lifter.plot(thetas) - fig.savefig(os.path.join(base_dir, "docs", "figures", "poly6_lifter_A.png")) - - thetas = np.linspace(-3, 3, 100) - poly_lifter = Poly6Lifter(poly_type="B") - fig, ax = poly_lifter.plot(thetas) - fig.savefig(os.path.join(base_dir, "docs", "figures", "poly6_lifter_B.png")) - - plt.show(block=False) - print("done") -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/examples/range_only_lifters.html b/docs/build/_modules/popcor/examples/range_only_lifters.html deleted file mode 100644 index 73f5196..0000000 --- a/docs/build/_modules/popcor/examples/range_only_lifters.html +++ /dev/null @@ -1,763 +0,0 @@ - - - - - - - - popcor.examples.range_only_lifters — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.examples.range_only_lifters

-import matplotlib.pylab as plt
-import numpy as np
-import scipy.sparse as sp
-from poly_matrix.least_squares_problem import LeastSquaresProblem
-from scipy.optimize import minimize
-
-from popcor.base_lifters import StateLifter
-from popcor.utils.common import diag_indices, upper_triangular
-
-plt.ion()
-
-NOISE = 1e-2  # std deviation of distance noise
-
-METHOD = "BFGS"
-NORMALIZE = True
-
-# TODO(FD): parameters below are not all equivalent.
-SOLVER_KWARGS = {
-    "BFGS": dict(gtol=1e-6, xrtol=1e-10),  # relative step size
-    "Nelder-Mead": dict(xatol=1e-10),  # absolute step size
-    "Powell": dict(ftol=1e-6, xtol=1e-10),
-    "TNC": dict(gtol=1e-6, xtol=1e-10),
-}
-
-
-
-[docs] -class RangeOnlyLocLifter(StateLifter): - """Range-only localization in 2D or 3D. - - We minimize the cost function - - .. math:: f(\\theta) = \\sum_{n=0}^{N-1} \\sum_{k=0}^{K-1} w_{nk} (d_{nk}^2 - ||p_n - a_k||^2)^2 - - where - - - :math:`w_{nk}` is the weight for the nth point and kth landmark (currently assumed binary to mark missing edges). - - :math:`\\theta` is the flattened vector of positions :math:`p_n`. - - :math:`d_{nk}` is the distance measurement from point n to landmark k. - - :math:`a_k` is the kth landmark. - - Note that in the current implementation, there is no regularization term so the problem could be split into individual points. - - We experiment with two different substitutions to turn the cost function into aquadratic form: - - - level "no" uses substitution :math:`z_i=||p_i||^2=x_i^2 + y_i^2` (or equivalent 3D version). - - level "quad" uses substitution :math:`y_i=[x_i^2, x_iy_i, y_i^2]` (or equivalent 3D version). - - This example is treated in more details in `this paper <https://arxiv.org/abs/2308.05783>`_. - """ - - TIGHTNESS = "rank" - LEVELS = ["no", "quad"] - LEVEL_NAMES = { - "no": "$z_n$", - "quad": "$\\boldsymbol{y}_n$", - } - - def get_vec_around_gt(self, delta: float = 0): - """Sample around ground truth. - :param delta: sample from gt + std(delta) (set to 0 to start from gt.) - """ - assert self.landmarks is not None, "landmarks must be set before sampling" - - if delta == 0: - return self.theta - else: - bbox_max = np.max(self.landmarks, axis=0) * 2 - bbox_min = np.min(self.landmarks, axis=0) * 2 - pos = ( - np.random.rand(self.n_positions, self.d) - * (bbox_max - bbox_min)[None, :] - + bbox_min[None, :] - ) - return pos.flatten() - - def __init__( - self, - n_positions, - n_landmarks, - d, - W=None, - level="no", - variable_list=None, - param_level="no", - ): - self.n_positions = n_positions - self.n_landmarks = n_landmarks - self.landmarks_ = None # will be set later - - if W is not None: - assert W.shape == (n_landmarks, n_positions) - self.W = W - else: - self.W = np.ones((n_positions, n_landmarks)) - self.y_ = None - - if variable_list == "all": - variable_list = self.get_all_variables() - super().__init__( - level=level, d=d, variable_list=variable_list, param_level=param_level - ) - - @property - def landmarks(self): - landmarks = np.random.rand(self.n_landmarks, self.d) - if self.landmarks_ is None: - self.landmarks_ = landmarks - return self.landmarks_ - - @property - def VARIABLE_LIST(self): - return [ - [self.HOM, "x_0"], - [self.HOM, "x_0", "z_0"], - [self.HOM, "x_0", "z_0", "z_1"], - [self.HOM, "x_0", "x_1", "z_0", "z_1"], - ] - - def get_all_variables(self): - vars = [self.HOM] - vars += [f"x_{i}" for i in range(self.n_positions)] - vars += [f"z_{i}" for i in range(self.n_positions)] - return [vars] - - def sample_parameters(self, theta=None): - landmarks = np.random.rand(self.n_landmarks, self.d) - return self.sample_parameters_landmarks(landmarks) - - def sample_theta(self): - return np.random.rand(self.n_positions, self.d).flatten() - - def get_A_known(self, var_dict=None, output_poly=False): - from poly_matrix.poly_matrix import PolyMatrix - - if var_dict is None: - var_dict = self.var_dict - positions = self.get_variable_indices(var_dict) - - A_list = [] - for n in positions: - if self.level == "no": - A = PolyMatrix(symmetric=True) - A[f"x_{n}", f"x_{n}"] = np.eye(self.d) - A[self.HOM, f"z_{n}"] = -0.5 - if output_poly: - A_list.append(A) - else: - A_list.append(A.get_matrix(self.var_dict)) - - elif self.level == "quad": - count = 0 - for i in range(self.d): - for j in range(i, self.d): - A = PolyMatrix(symmetric=True) - mat_x = np.zeros((self.d, self.d)) - mat_z = np.zeros((1, self.size_z)) - if i == j: - mat_x[i, i] = 1.0 - else: - mat_x[i, j] = 0.5 - mat_x[j, i] = 0.5 - mat_z[0, count] = -0.5 - A[f"x_{n}", f"x_{n}"] = mat_x - A[self.HOM, f"z_{n}"] = mat_z - count += 1 - if output_poly: - A_list.append(A) - else: - A_list.append(A.get_matrix(self.var_dict)) - return A_list - - def get_x(self, theta=None, parameters=None, var_subset=None): - if var_subset is None: - var_subset = self.var_dict - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - - positions = theta.reshape(self.n_positions, -1) - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - elif "x" in key: - n = int(key.split("_")[-1]) - x_data += list(positions[n]) - elif "z" in key: - n = int(key.split("_")[-1]) - if self.level == "no": - x_data.append(np.linalg.norm(positions[n]) ** 2) - elif self.level == "quad": - x_data += list(upper_triangular(positions[n])) - assert len(x_data) == self.get_dim_x(var_subset) - return np.array(x_data) - - def get_J_lifting(self, t): - pos = t.reshape((-1, self.d)) - ii = [] - jj = [] - data = [] - - idx = 0 - for n in range(self.n_positions): - if self.level == "no": - ii += [n] * self.d - jj += list(range(n * self.d, (n + 1) * self.d)) - data += list(2 * pos[n]) - elif self.level == "quad": - # it seemed easier to do this manually that programtically - if self.d == 3: - x, y, z = pos[n] - jj += [n * self.d + j for j in [0, 0, 1, 0, 2, 1, 1, 2, 2]] - data += [2 * x, y, x, z, x, 2 * y, z, y, 2 * z] - ii += [idx + i for i in [0, 1, 1, 2, 2, 3, 4, 4, 5]] - elif self.d == 2: - x, y = pos[n] - jj += [n * self.d + j for j in [0, 0, 1, 1]] - data += [2 * x, y, x, 2 * y] - ii += [idx + i for i in [0, 1, 1, 2]] - idx += self.size_z - J_lifting = sp.csr_array( - (data, (ii, jj)), - shape=(self.M, self.N), - ) - return J_lifting - - def get_hess_lifting(self, t): - """return list of the hessians of the M lifting functions.""" - hessians = [] - for n in range(self.n_positions): - idx = range(n * self.d, (n + 1) * self.d) - if self.level == "no": - hessian = sp.csr_array( - ([2] * self.d, (idx, idx)), - shape=(self.N, self.N), - ) - hessians.append(hessian) - elif self.level == "quad": - for h in self.fixed_hessian_list: - ii, jj = np.meshgrid(idx, idx) - hessian = sp.csr_array( - (h.flatten(), (ii.flatten(), jj.flatten())), - shape=(self.N, self.N), - ) - hessians.append(hessian) - return hessians - - @property - def fixed_hessian_list(self): - if self.d == 2: - return [ - np.array([[2, 0], [0, 0]]), - np.array([[0, 1], [1, 0]]), - np.array([[0, 0], [0, 2]]), - ] - elif self.d == 3: - return [ - np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]]), - np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]), - np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]]), - np.array([[0, 0, 0], [0, 2, 0], [0, 0, 0]]), - np.array([[0, 0, 0], [0, 0, 1], [0, 1, 0]]), - np.array([[0, 0, 0], [0, 0, 0], [0, 0, 2]]), - ] - else: - raise ValueError(f"Unsupported dimension {self.d} for fixed hessians.") - - def get_residuals(self, t, y): - positions = t.reshape((-1, self.d)) - y_current = ( - np.linalg.norm(self.landmarks[None, :, :] - positions[:, None, :], axis=2) - ** 2 - ) - return self.W * (y - y_current) - - def get_cost(self, t, y, sub_idx=None): - """ - get cost for given positions, landmarks and noise. - - :param t: flattened positions of length Nd - :param y: N x K distance measurements - """ - residuals = self.get_residuals(t, y) - if sub_idx is None: - cost = np.sum(residuals**2) - else: - cost = np.sum(residuals[sub_idx] ** 2) - if NORMALIZE: - return cost / np.sum(self.W > 0) - return cost - - def get_grad(self, t, y, sub_idx=None): - """get gradient""" - J = self.get_J(t, y) - x = self.get_x(t) - Q = self.get_Q_from_y(y) - if sub_idx is None: - return 2 * J.T @ Q @ x - else: - sub_idx_x = self.get_sub_idx_x(sub_idx) - return 2 * J.T[:, sub_idx_x] @ Q[sub_idx_x, :][:, sub_idx_x] @ x[sub_idx_x] - - def get_J(self, t, y): - J = sp.csr_array( - (np.ones(self.N), (range(1, self.N + 1), range(self.N))), - shape=(self.N + 1, self.N), - ) - J_lift = self.get_J_lifting(t) - J = sp.vstack([J, J_lift]) - return J - - def get_hess(self, t, y): - """get Hessian""" - x = self.get_x(t) - Q = self.get_Q_from_y(y) - J = self.get_J(t, y) - hess = 2 * J.T @ Q @ J - - hessians = self.get_hess_lifting(t) - B = self.ls_problem.get_B_matrix(self.var_dict) - residuals = B @ x - for m, h in enumerate(hessians): - bm_tilde = B[:, -self.M + m] - factor = float(bm_tilde.T @ residuals) - hess += 2 * factor * h - return hess - - def get_Q_from_y(self, y): - import itertools - - self.ls_problem = LeastSquaresProblem() - - if self.level == "quad": - diag_idx = diag_indices(self.d) - - for n, k in itertools.product(range(self.n_positions), range(self.n_landmarks)): - if self.W[n, k] > 0: - ak = self.landmarks[k] - if self.level == "no": - self.ls_problem.add_residual( - { - self.HOM: y[n, k] - np.linalg.norm(ak) ** 2, - f"x_{n}": 2 * ak.reshape((1, -1)), - f"z_{n}": -1, - } - ) - elif self.level == "quad": - mat = np.zeros((1, self.size_z)) - mat[0, diag_idx] = -1 - res_dict = { - self.HOM: y[n, k] - np.linalg.norm(ak) ** 2, - f"x_{n}": 2 * ak.reshape((1, -1)), - f"z_{n}": mat, - } - self.ls_problem.add_residual(res_dict) - Q = self.ls_problem.get_Q().get_matrix(self.var_dict) - if NORMALIZE: - return Q / np.sum(self.W > 0) - return Q - - def simulate_y(self, noise: float | None = None): - assert self.landmarks is not None - # N x K matrix - if noise is None: - noise = NOISE - positions = self.theta.reshape(self.n_positions, -1) - y_gt = ( - np.linalg.norm(self.landmarks[None, :, :] - positions[:, None, :], axis=2) - ** 2 - ) - return y_gt + np.random.normal(loc=0, scale=noise, size=y_gt.shape) - - def get_Q(self, noise: float | None = None) -> tuple: - if self.y_ is None: - self.y_ = self.simulate_y(noise=noise) - Q = self.get_Q_from_y(self.y_) - - # DEBUGGING - x = self.get_x() - cost1 = x.T @ Q @ x - cost3 = self.get_cost(self.theta, self.y_) - assert abs(cost1 - cost3) < 1e-10 - return Q - - def get_D(self, that): - D = np.eye(1 + self.n_positions * self.d + self.size_z) - x = self.get_x(theta=that) - J = self.get_J_lifting(t=that) - - D = sp.lil_array((len(x), len(x))) - D[range(len(x)), range(len(x))] = 1.0 - D[:, 0] = x - D[-J.shape[0] :, 1 : 1 + J.shape[1]] = J # type: ignore - return D.tocsc() - - def get_sub_idx_x(self, sub_idx, add_z=True): - sub_idx_x = [0] - for idx in sub_idx: - sub_idx_x += [1 + idx * self.d + d for d in range(self.d)] - if not add_z: - return sub_idx_x - for idx in sub_idx: - sub_idx_x += [ - 1 + self.n_positions * self.d + idx * self.size_z + d - for d in range(self.size_z) - ] - return sub_idx_x - - def get_theta(self, x): - return x[1 : self.d + 1] - - def get_position(self, theta=None): - if theta is not None: - return theta.reshape(self.n_positions, self.d) - - def get_error(self, that): - err = np.sqrt(np.mean((self.theta - that) ** 2)) - return {"total error": err, "error": err} - - def local_solver( - self, - t_init, - y, - verbose=False, - method="BFGS", - solver_kwargs=SOLVER_KWARGS, - ): - """ - :param t_init: (positions, landmarks) tuple - """ - - # TODO(FD): split problem into individual points. - options = solver_kwargs[method] - options["disp"] = verbose - sol = minimize( - self.get_cost, - x0=t_init, - args=y, - jac=self.get_grad, - # hess=self.get_hess, not used by any solvers. - method=method, - options=options, - ) - - info = {} - info["success"] = sol.success - info["msg"] = sol.message + f"(# iterations: {sol.nit})" - if sol.success: - that = sol.x - rel_error = self.get_cost(that, y) - self.get_cost(sol.x, y) - assert abs(rel_error) < 1e-10, rel_error - residuals = self.get_residuals(that, y) - cost = sol.fun - info["max res"] = np.max(np.abs(residuals)) - hess = self.get_hess(that, y) - eigs = np.linalg.eigvalsh(hess.toarray()) - info["cond Hess"] = eigs[-1] / eigs[0] - else: - that = cost = None - info["max res"] = None - info["cond Hess"] = None - return that, info, cost - - @property - def var_dict(self): - var_dict = {self.HOM: 1} - var_dict.update({f"x_{n}": self.d for n in range(self.n_positions)}) - var_dict.update({f"z_{n}": self.size_z for n in range(self.n_positions)}) - return var_dict - - @property - def param_dict(self): - return self.param_dict_landmarks - - @property - def size_z(self): - if self.level == "no": - return 1 - elif self.level == "quad": - return int(self.d * (self.d + 1) / 2) - else: - raise ValueError(f"Unknown level {self.level}") - - @property - def N(self): - return self.n_positions * self.d - - @property - def M(self): - return self.n_positions * self.size_z - - def __repr__(self): - return f"rangeonlyloc{self.d}d_{self.level}"
- - - -if __name__ == "__main__": - lifter = RangeOnlyLocLifter(n_positions=3, n_landmarks=4, d=2) -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/examples/rotation_lifter.html b/docs/build/_modules/popcor/examples/rotation_lifter.html deleted file mode 100644 index 29b4d97..0000000 --- a/docs/build/_modules/popcor/examples/rotation_lifter.html +++ /dev/null @@ -1,484 +0,0 @@ - - - - - - - - popcor.examples.rotation_lifter — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.examples.rotation_lifter

-import numpy as np
-from poly_matrix.poly_matrix import PolyMatrix
-from scipy.spatial.transform import Rotation as R
-
-from popcor.base_lifters import StateLifter
-
-METHOD = "CG"
-SOLVER_KWARGS = dict(
-    min_gradient_norm=1e-7, max_iterations=10000, min_step_size=1e-8, verbosity=1
-)
-
-
-
-[docs] -class RotationLifter(StateLifter): - """Rotation averaging problem.""" - - LEVELS = ["no"] - HOM = "h" - VARIABLE_LIST = [["h", "c"]] - - # whether or not to include the determinant constraints in the known constraints. - ADD_DETERMINANT = False - - NOISE = 1e-3 - - # Add any parameters here that describe the problem (e.g. number of landmarks etc.) - def __init__(self, level="no", param_level="no", d=2, n_meas=2): - self.n_meas = n_meas - self.level = level - super().__init__( - level=level, - param_level=param_level, - d=d, - ) - - @property - def var_dict(self): - return {self.HOM: 1, "c": self.d**2} - - def sample_theta(self): - """Generate a random new feasible point.""" - - if self.d == 2: - angle = np.random.uniform(0, 2 * np.pi) - C = R.from_euler("z", angle).as_matrix()[:2, :2] - elif self.d == 3: - C = R.random().as_matrix() - return C - - def get_x(self, theta=None, parameters=None, var_subset=None) -> np.ndarray: - """Get the lifted vector x given theta and parameters.""" - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - if var_subset is None: - var_subset = self.var_dict.keys() - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - elif key == "c": - x_data += list(theta.flatten("C")) - dim_x = self.get_dim_x(var_subset=var_subset) - assert len(x_data) == dim_x - return np.array(x_data) - - def get_theta(self, x: np.ndarray) -> np.ndarray: - assert np.ndim(x) == 1 - C_flat = x[1 : 1 + self.d**2] - return C_flat.reshape((self.d, self.d)) - - def get_Q(self, noise: float | None = None): - if noise is None: - noise = self.NOISE - if self.y_ is None: - self.y_ = [] - for i in range(self.n_meas): - # noise model: R_i = R.T @ Rnoise - if noise > 0: - # Generate a random small rotation as noise and apply it - noise_rotvec = np.random.normal(scale=noise, size=(self.d,)) - Rnoise = ( - R.from_rotvec(noise_rotvec).as_matrix() - if self.d == 3 - else R.from_euler("z", noise_rotvec[0]).as_matrix()[:2, :2] - ) - Ri = self.theta.T @ Rnoise - else: - Ri = self.theta.T - self.y_.append(Ri) - - return self.get_Q_from_y(self.y_) - - def get_Q_from_y(self, y, output_poly=False): - # f(R) = sum_i || R @ R_i - I ||_F^2 - # argmin f(R) = argmin sum_i || R_i.T @ R_i ||^2 - 2 tr(R.T @ R_i) + ||I||_F^2 - # = argmin sum_i -2 tr(R.T @ R_i) + sum_i d - # = argmin sum_i -2 vec(R).T @ vec(R_i.T) + N * d - # sanity check for zero noise: - # || R @ R.T - I ||_F^2 = 0 - """param y: list of noisy rotation matrices.""" - Q = PolyMatrix() - for Ri in y: - Q[self.HOM, "c"] -= Ri.T.flatten("C")[None, :] - Q[self.HOM, self.HOM] += len(y) * self.d - if output_poly: - return Q - else: - return Q.get_matrix(self.var_dict) - - def local_solver_old( - self, t0, y, verbose=False, method=METHOD, solver_kwargs=SOLVER_KWARGS - ): - import pymanopt - from pymanopt.manifolds import SpecialOrthogonalGroup - - if method == "CG": - from pymanopt.optimizers import ConjugateGradient as Optimizer # fastest - elif method == "SD": - from pymanopt.optimizers import SteepestDescent as Optimizer # slow - elif method == "TR": - from pymanopt.optimizers import TrustRegions as Optimizer # okay - else: - raise ValueError(method) - - if verbose: - solver_kwargs["verbosity"] = 2 - else: - solver_kwargs["verbosity"] = 0 - - manifold = SpecialOrthogonalGroup(self.d, k=1) - - @pymanopt.function.autograd(manifold) - def cost(R): - cost = 0 - for Ri in y: - cost += np.sum((R.T @ Ri - np.eye(self.d)) ** 2) - return cost - - euclidean_gradient = None - problem = pymanopt.Problem( - manifold, cost, euclidean_gradient=euclidean_gradient - ) - optimizer = Optimizer(**solver_kwargs) - - res = optimizer.run(problem, initial_point=t0) - theta_hat = res.point - - success = ("min step_size" in res.stopping_criterion) or ( - "min grad norm" in res.stopping_criterion - ) - info = { - "success": success, - "msg": res.stopping_criterion, - } - if success: - return theta_hat, info, cost - - def test_and_add(self, A_list, Ai, output_poly): - x = self.get_x() - Ai_sparse = Ai.get_matrix(self.var_dict) - err = x.T @ Ai_sparse @ x - assert abs(err) <= 1e-10, err - if output_poly: - A_list.append(Ai) - else: - A_list.append(Ai_sparse) - - def get_A_known(self, var_dict=None, output_poly=False, add_redundant=False): - A_list = [] - if var_dict is None: - var_dict = self.var_dict - - if "c" in var_dict: - # enforce diagonal == 1 - for i in range(self.d): - Ei = np.zeros((self.d, self.d)) - Ei[i, i] = 1.0 - constraint = np.kron(Ei, np.eye(self.d)) - Ai = PolyMatrix(symmetric=True) - Ai["c", "c"] = constraint - Ai[self.HOM, self.HOM] = -1 - self.test_and_add(A_list, Ai, output_poly=output_poly) - - # enforce off-diagonal == 0 - for i in range(self.d): - for j in range(i + 1, self.d): - Ei = np.zeros((self.d, self.d)) - Ei[i, j] = 1.0 - Ei[j, i] = 1.0 - constraint = np.kron(Ei, np.eye(self.d)) - Ai = PolyMatrix(symmetric=True) - Ai["c", "c"] = constraint - self.test_and_add(A_list, Ai, output_poly=output_poly) - - # enforce that determinant is one. - if self.d == 2 and self.ADD_DETERMINANT: - # C = [a b; c d]; ad - bc - 1 = 0 - # a b c d - # a 1 - # b -1 - # c -1 - # d 1 - Ai = PolyMatrix(symmetric=True) - constraint = np.zeros((self.d**2, self.d**2)) - constraint[0, 3] = constraint[3, 0] = 1.0 - constraint[1, 2] = constraint[2, 1] = -1.0 - Ai[self.HOM, self.HOM] = -2 - Ai["c", "c"] = constraint - self.test_and_add(A_list, Ai, output_poly=output_poly) - elif self.d == 3 and self.ADD_DETERMINANT: - # c11 c12 c13 c21 * c32 - c31 * c22 = c13 - # C = [c21, c22, c23]; c1 x c2 = c3: c31 * c12 - c11 * c12 = c23 - # c31 c32 c33 c11 * c22 - c21 * c12 = c33 - print( - "Warning: consider implementing the determinant constraint for RobustPoseLifter, d=3" - ) - return A_list
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/examples/stereo1d_lifter.html b/docs/build/_modules/popcor/examples/stereo1d_lifter.html deleted file mode 100644 index a4f6034..0000000 --- a/docs/build/_modules/popcor/examples/stereo1d_lifter.html +++ /dev/null @@ -1,471 +0,0 @@ - - - - - - - - popcor.examples.stereo1d_lifter — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.examples.stereo1d_lifter

-from typing import Optional
-
-import numpy as np
-from poly_matrix.least_squares_problem import LeastSquaresProblem
-from poly_matrix.poly_matrix import PolyMatrix
-
-from popcor.base_lifters import StateLifter
-
-
-
-[docs] -class Stereo1DLifter(StateLifter): - """Toy example for stereo localization in 1D. We minimize the following cost function: - - .. math:: - f(\\theta) = \\sum_{j=0}^{N-1} (u_j - 1 / (\\theta - a_j))^2 - - where :math:`a_j` are the landmarks and :math:`u_j` are the measurements. - - This is the pedagogical running example of `this paper <https://arxiv.org/abs/2308.05783>`_. - and also used in the :ref:`Quick Start Guide`. - """ - - PARAM_LEVELS = ["no", "p", "ppT"] - VARIABLE_LIST = [["h", "x"], ["h", "x", "z_0"], ["h", "x", "z_0", "z_1"]] - - NOISE = 0.1 - - def __init__(self, n_landmarks, param_level="no"): - self.n_landmarks = n_landmarks - self.d = 1 - self.W = 1.0 - - # will be initialized later - self.landmarks_ = None - - super().__init__(param_level=param_level, d=self.d, n_parameters=n_landmarks) - - @property - def landmarks(self): - if self.landmarks_ is None: - self.landmarks_ = np.random.rand(self.n_landmarks, self.d) - return self.landmarks_ - - def sample_parameters(self, theta=None): - if self.parameters_ is None: - return self.sample_parameters_landmarks(self.landmarks) - landmarks = np.random.rand(self.n_landmarks, self.d) - return self.sample_parameters_landmarks(landmarks) - - def sample_theta(self): - x_try = np.random.rand(1) - counter = 0 - while np.min(np.abs(x_try - self.landmarks)) <= 1e-2: - x_try = np.random.rand(1) - counter += 1 - if counter >= 1000: - print("Warning: couldn't find valid setup") - return - return x_try - - def get_x(self, theta=None, parameters=None, var_subset=None): - """ - :param var_subset: list of variables to include in x vector. Set to None for all. - """ - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - - if var_subset is None: - var_subset = self.var_dict.keys() - - if self.param_level == "no": - landmarks = { - f"p_{i}": self.landmarks[i] for i in range(self.landmarks.shape[0]) - } - else: - landmarks = { - f"p_{i}": parameters[f"p_{i}"][: self.d] - for i in range(self.landmarks.shape[0]) - } - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - elif key == "x": - x_data.append(theta[0]) - elif "z" in key: - idx = int(key.split("_")[-1]) - x_data.append(1 / (theta[0] - landmarks[f"p_{idx}"])) - else: - raise ValueError("unknown key in get_x", key) - return np.hstack(x_data) - - @property - def var_dict(self): - vars = [self.HOM, "x"] + [f"z_{j}" for j in range(self.n_landmarks)] - return {v: 1 for v in vars} - - @property - def param_dict(self): - return self.param_dict_landmarks - - def get_Q(self, noise: Optional[float] = None): - if self.landmarks is None: - raise ValueError("self.landmarks must be initialized before calling get_Q.") - if noise is None: - noise = self.NOISE - - y = 1 / (self.theta - self.landmarks.flatten()) + np.random.normal( - scale=noise, loc=0, size=self.n_landmarks - ) - if self.y_ is None: - self.y_ = y - - return self.get_Q_from_y(y) - - def get_Q_from_y(self, y): - ls_problem = LeastSquaresProblem() - for j in range(len(y)): - ls_problem.add_residual({self.HOM: -y[j], f"z_{j}": 1}) - return ls_problem.get_Q().get_matrix(self.var_dict) - - def get_A_known(self, var_dict=None, output_poly=False): - if var_dict is None: - var_dict = self.var_dict - - # if self.add_parameters: - # raise ValueError("can't extract known matrices yet when using parameters.") - - A_known = [] - - # enforce that z_j = 1/(x - a_j) <=> 1 - z_j*x + a_j*z_j = 0 - if not ("x" in var_dict and self.HOM in var_dict): - return [] - - landmark_indices = [ - int(key.split("_")[-1]) for key in var_dict if key.startswith("z_") - ] - for j in landmark_indices: - A = PolyMatrix() - A[self.HOM, f"z_{j}"] = 0.5 * self.landmarks[j] - A["x", f"z_{j}"] = -0.5 - A[self.HOM, self.HOM] = 1.0 - if output_poly: - A_known.append(A) - else: - A_known.append(A.get_matrix(variables=self.var_dict)) - return A_known - - def get_A_known_redundant(self, var_dict=None, output_poly=False): - import itertools - - if var_dict is None: - var_dict = self.var_dict - - assert self.HOM in var_dict, "homogenization variable must be in var_dict" - - landmark_indices = [ - int(key.split("_")[-1]) for key in var_dict if key.startswith("z_") - ] - # add known redundant constraints: - # enforce that z_j - z_i = (a_j - a_i) * z_j * z_i - A_known = [] - for i, j in itertools.combinations(landmark_indices, 2): - A = PolyMatrix() - A[self.HOM, f"z_{j}"] = 1 - A[self.HOM, f"z_{i}"] = -1 - A[f"z_{i}", f"z_{j}"] = self.landmarks[i] - self.landmarks[j] - if output_poly: - A_known.append(A) - else: - A_known.append(A.get_matrix(variables=self.var_dict)) - return A_known - - def get_cost(self, t, y): - return np.sum((y - (1 / (t - self.landmarks.flatten()))) ** 2) - - def local_solver( - self, t_init, y, num_iters=100, eps=1e-5, W=None, verbose=False, **kwargs - ): - info = {} - a = self.landmarks.flatten() - x_op = t_init - for i in range(num_iters): - u = y - (1 / (x_op - a)) - if verbose: - print(f"cost {i}", np.sum(u**2)) - du = 1 / ((x_op - a) ** 2) - if np.linalg.norm(du) > 1e-10: - dx = -np.sum(u * du) / np.sum(du * du) - x_op = x_op + dx - if np.abs(dx) < eps: - msg = f"converged in dx after {i} it" - cost = self.get_cost(x_op, y) - info = {"msg": msg, "cost": cost, "success": True} - return x_op, info, cost - else: - msg = f"converged in du after {i} it" - cost = self.get_cost(x_op, y) - info = {"msg": msg, "cost": self.get_cost(x_op, y), "success": True} - return x_op, info, cost - return None, {"msg": "didn't converge", "cost": None, "success": False}, None - - def __repr__(self): - return f"stereo1d_{self.param_level}"
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/examples/stereo2d_lifter.html b/docs/build/_modules/popcor/examples/stereo2d_lifter.html deleted file mode 100644 index 0aeca23..0000000 --- a/docs/build/_modules/popcor/examples/stereo2d_lifter.html +++ /dev/null @@ -1,365 +0,0 @@ - - - - - - - - popcor.examples.stereo2d_lifter — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.examples.stereo2d_lifter

-# import autograd.numpy as np
-import numpy as np
-
-from popcor.base_lifters import StereoLifter
-from popcor.utils.geometry import convert_phi_to_theta, convert_theta_to_phi
-from popcor.utils.stereo2d_problem import _cost, local_solver
-
-
-def change_dimensions(a, y, x):
-    p_w = np.concatenate([a, np.ones((a.shape[0], 1))], axis=1)
-    y_mat = np.c_[[*y]]  # N x 2
-    return p_w[:, :, None], y_mat[:, :, None], x[:, None]
-
-
-GTOL = 1e-6
-
-
-
-[docs] -class Stereo2DLifter(StereoLifter): - """Stereo-camera localization in 2D. - - We minimize the following cost function: - - .. math:: - f(\\theta) = \\sum_{j=0}^{n} (u_j - M q_j / q_j[1])^2 - - where - - - :math:`p_j` are known landmarks (in homogeneous coordinates), - - :math:`u_j` are pixel measurements (2 elements: one pixel in left "image" and one in right "image"), - - :math:`q_j = T(\\theta) p_j` are the (homogeneous) coordinates of landmark j in the (unknown) camera frame, parameterized by :math:`T(\\theta)`, and - - :math:`M` is the stereo camera calibration matrix. Here, it is given by - - .. math:: - - \\begin{bmatrix} - f_u & c_u & \\frac{b f_u}{2} \\\\ - f_v & c_v & -\\frac{b f_v}{2} \\\\ - \\end{bmatrix} - - where :math:`f_u, f_v` are horizontal and vertical focal lengths, :math:`c_u,c_v` are image center points in pixels and :math:`b` is the camera baseline. - - This example is treated in more details in `this paper <https://arxiv.org/abs/2308.05783>`_. - """ - - def __init__(self, n_landmarks, level="no", param_level="no", variable_list=None): - self.W = np.stack([np.eye(2)] * n_landmarks) - - super().__init__( - n_landmarks=n_landmarks, - level=level, - param_level=param_level, - d=2, - variable_list=variable_list, - ) - - @property - def M_matrix(self): - f_u = 484.5 - c_u = 322 - b = 0.24 - return np.array([[f_u, c_u, f_u * b / 2], [f_u, c_u, -f_u * b / 2]]) - - def get_cost(self, t, y, W=None): - - if W is None: - W = self.W - a = self.landmarks - - phi = convert_theta_to_phi(t) - p_w, y, phi = change_dimensions(a, y, phi) - cost = _cost(phi, p_w, y, W, self.M_matrix) - if StereoLifter.NORMALIZE: - return cost / (self.n_landmarks * self.d) - else: - return cost - - def local_solver(self, t_init, y, W=None, verbose=False, **kwargs): - - if W is None: - W = self.W - a = self.landmarks - - init_phi = convert_theta_to_phi(t_init) - p_w, y, __ = change_dimensions(a, y, init_phi) - success, phi_hat, cost = local_solver( - p_w=p_w, y=y, W=W, init_phi=init_phi, log=verbose, gtol=GTOL - ) - if StereoLifter.NORMALIZE: - cost /= self.n_landmarks * self.d - # cost /= self.n_landmarks * self.d - theta_hat = convert_phi_to_theta(phi_hat) - info = {"success": success, "msg": "converged"} - if success: - return theta_hat, info, cost - else: - return None, info, cost
- - - -if __name__ == "__main__": - lifter = Stereo2DLifter(n_landmarks=3) -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/examples/stereo3d_lifter.html b/docs/build/_modules/popcor/examples/stereo3d_lifter.html deleted file mode 100644 index 14dbb3e..0000000 --- a/docs/build/_modules/popcor/examples/stereo3d_lifter.html +++ /dev/null @@ -1,438 +0,0 @@ - - - - - - - - popcor.examples.stereo3d_lifter — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.examples.stereo3d_lifter

-import pickle
-
-import numpy as np
-
-from popcor.base_lifters import StereoLifter
-from popcor.utils.geometry import get_T, get_theta_from_T
-from popcor.utils.stereo3d_problem import _cost, local_solver
-
-
-def change_dimensions(a, y):
-    p_w = np.concatenate([a, np.ones((a.shape[0], 1))], axis=1)
-    y_mat = np.c_[[*y]]  # N x 2
-    return p_w[:, :, None], y_mat[:, :, None]
-
-
-GTOL = 1e-6
-
-
-
-[docs] -class Stereo3DLifter(StereoLifter): - """Stereo-camera localization in 3D. - - Analogously to :class:`Stereo2DLifter`, we minimize the following cost function: - - .. math:: - f(\\theta) = \\sum_{j=0}^{n} (u_j - M q_j / q_j[2])^2 - - where - - - :math:`p_j` are known landmarks (in homogeneous coordinates), - - :math:`u_j` are pixel measurements (4 elements: two pixel coordinates in left image and two in right image), - - :math:`q_j = T(\\theta) p_j` are the (homogeneous) coordinates of landmark j in the (unknown) camera frame, parameterized by :math:`T(\\theta)`, and - - :math:`M` is the stereo camera calibration matrix. Here, it is given by - - .. math:: - - \\begin{bmatrix} - f_u & 0 & c_u & \\frac{b f_u}{2} \\\\ - 0 & f_v & c_v & 0 \\\\ - f_u & 0 & c_u & -\\frac{b f_u}{2} \\\\ - 0 & f_v & c_v & 0 \\\\ - \\end{bmatrix} - - where :math:`f_u, f_v` are horizontal and vertical focal lengths, :math:`c_u,c_v` are image center points in pixels and :math:`b` is the camera baseline. - - This example is treated in more details in `this paper <https://arxiv.org/abs/2308.05783>`_. - """ - - def __init__(self, n_landmarks, level="no", param_level="no", variable_list=None): - self.W = np.stack([np.eye(4)] * n_landmarks) - - super().__init__( - n_landmarks=n_landmarks, - level=level, - param_level=param_level, - d=3, - variable_list=variable_list, - ) - - @property - def M_matrix(self): - f_u = 484.5 - f_v = 484.5 - c_u = 322 - c_v = 247 - b = 0.24 - return np.array( - [ - [f_u, 0, c_u, f_u * b / 2], - [0, f_v, c_v, 0], - [f_u, 0, c_u, -f_u * b / 2], - [0, f_v, c_v, 0], - ] - ) - - @staticmethod - def from_file(fname): - with open(fname, "rb") as f: - y_ = pickle.load(f) - landmarks = pickle.load(f) - theta = pickle.load(f) - - level = pickle.load(f) - param_level = pickle.load(f) - variable_list = pickle.load(f) - lifter = Stereo3DLifter( - n_landmarks=landmarks.shape[0], - level=level, - param_level=param_level, - variable_list=variable_list, - ) - lifter.y_ = y_ - lifter.landmarks_ = landmarks - lifter.parameters = np.r_[1, landmarks.flatten()] - lifter.theta = theta - return lifter - - def to_file(self, fname): - with open(fname, "wb") as f: - pickle.dump(self.y_, f) - pickle.dump(self.landmarks, f) - - pickle.dump(self.theta, f) - pickle.dump(self.level, f) - pickle.dump(self.param_level, f) - pickle.dump(self.variable_list, f) - - def get_cost(self, t, y, W=None): - """ - :param t: can be either - - x, y, z, yaw, pitch roll: vector of unknowns, or - - [c1, c2, c3, x, y, z], the theta vector (flattened C and x, y, z) - """ - - if W is None: - W = self.W - a = self.landmarks - - p_w, y = change_dimensions(a, y) - - T = get_T(theta=t, d=3) - - cost = _cost(p_w=p_w, y=y, T=T, M=self.M_matrix, W=W) - if StereoLifter.NORMALIZE: - return cost / (self.n_landmarks * self.d) - else: - return cost - - def local_solver(self, t_init, y, W=None, verbose=False, **kwargs): - """ - :param t_init: same options asfor t in cost. - """ - - if W is None: - W = self.W - - a = self.landmarks - p_w, y = change_dimensions(a, y) - T_init = get_T(theta=t_init, d=3) - - info, T_hat, cost = local_solver( - T_init=T_init, - y=y, - p_w=p_w, - W=W, - M=self.M_matrix, - log=False, - gtol=GTOL, - min_update_norm=-1, # makes this inactive - ) - - if verbose: - print("Stereo3D local solver:", info["msg"]) - - if StereoLifter.NORMALIZE: - - cost /= self.n_landmarks * self.d - - x_hat = get_theta_from_T(T_hat) - x = self.get_x(theta=x_hat) - Q = self.get_Q_from_y(y[:, :, 0]) - cost_Q = x.T @ Q @ x - if abs(cost) > 1e-10: - if not (abs(cost_Q - cost) / cost < 1e-8): - print(f"Warning, cost not equal {cost_Q:.2e} {cost:.2e}") - - if info["success"]: - return x_hat, info, cost - else: - return None, info, cost
- - - -if __name__ == "__main__": - lifter = Stereo3DLifter(n_landmarks=4) -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/examples/wahba_lifter.html b/docs/build/_modules/popcor/examples/wahba_lifter.html deleted file mode 100644 index 7936905..0000000 --- a/docs/build/_modules/popcor/examples/wahba_lifter.html +++ /dev/null @@ -1,496 +0,0 @@ - - - - - - - - popcor.examples.wahba_lifter — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.examples.wahba_lifter

-# import autograd.numpy as np
-import numpy as np
-
-from popcor.base_lifters import RobustPoseLifter
-from popcor.utils.geometry import get_C_r_from_theta
-from popcor.utils.plotting_tools import plot_frame
-
-N_TRYS = 10
-
-# TODO(FD) for some reason this is not required as opposed to what is stated in Heng's paper
-# and it currently breaks tightness (might be a bug in my implementation though)
-USE_INEQ = False
-
-NORMALIZE = False
-
-
-
-[docs] -class WahbaLifter(RobustPoseLifter): - """This example is treated in more details in `this paper <https://arxiv.org/abs/2308.05783>`_, - under the name "PPR" (point-to-point registration). - """ - - NOISE = 1e-2 # inlier noise - NOISE_OUT = 1.0 # outlier noise - - def h_list(self, t): - """ - We want to inforce that - - norm(t) <= 10 (default) - as constraints h_j(t)<=0 - """ - default = super().h_list(t) - return default - - def get_random_position(self): - return np.random.uniform( - -0.5 * self.MAX_DIST ** (1 / self.d), - 0.5 * self.MAX_DIST ** (1 / self.d), - size=self.d, - ) - - def get_B_known(self): - """Get inequality constraints of the form x.T @ B @ x <= 0""" - if not USE_INEQ: - return [] - - default = super().get_B_known() - return default - - def term_in_norm(self, R, t, pi, ui): - return R @ pi + t - ui - - def residual_sq(self, R, t, pi, ui): - # TODO: can easily extend below to matrix-weighted - W = np.eye(self.d) - res_sq = (R @ pi + t - ui).T @ W @ (R @ pi + t - ui) - if NORMALIZE: - return res_sq / (self.n_landmarks * self.d) ** 2 - return res_sq - - def plot_setup(self): - if self.d != 2: - print("Plotting currently only supported for d=2") - return - import matplotlib.pylab as plt - - fig, ax = plt.subplots() - - # R, t = get_C_r_from_theta(self.theta, self.d) - # ax.scatter(*t, color="k", label="pose") - - ax.axis("equal") - t_wc_w, C_cw = plot_frame(ax, self.theta, label="pose", color="gray", d=2) - - if self.y_ is not None: - for i in range(self.y_.shape[0]): - ax.scatter(*self.landmarks[i], color=f"C{i}", label="landmarks") - - # this vector is in camera coordinates - t_cpi_c = self.y_[i] - # t_cpi_w: vector from camera to pi in world coordinates - - ax.plot( - [t_wc_w[0], self.landmarks[i][0]], - [t_wc_w[1], self.landmarks[i][1]], - color=f"C{i}", - ls=":", - ) - if C_cw is not None: - t_cpi_w = C_cw.T @ t_cpi_c - ax.plot( - [t_wc_w[0], t_wc_w[0] + t_cpi_w[0]], - [t_wc_w[1], t_wc_w[1] + t_cpi_w[1]], - color=f"r" if i < self.n_outliers else "g", - ) - - def get_Q( - self, - noise: float | None = None, - output_poly: bool = False, - use_cliques: list = [], - ): - if noise is None: - noise = self.NOISE - - if self.y_ is None: - theta = self.theta[: self.d + self.d**2] - outlier_index = self.get_outlier_index() - - self.y_ = np.empty((self.n_landmarks, self.d)) - R, t = get_C_r_from_theta(theta, self.d) - for i in range(self.n_landmarks): - valid_measurement = False - for _ in range(N_TRYS): - outlier = i in outlier_index - y_i = R @ self.landmarks[i] + t - if outlier: - y_i += np.random.normal( - scale=self.NOISE_OUT, loc=0, size=self.d - ) - else: - y_i += np.random.normal(scale=noise, loc=0, size=self.d) - - residual = self.residual_sq(R, t, self.landmarks[i], y_i) - if not self.robust: - valid_measurement = True - else: - if outlier: - valid_measurement = residual > self.beta - else: - valid_measurement = residual < self.beta - if valid_measurement: - break - if not valid_measurement and self.robust: - self.plot_setup() - raise ValueError("did not find a valid measurement.") - self.y_[i] = y_i - Q = self.get_Q_from_y(self.y_, output_poly=output_poly, use_cliques=use_cliques) - return Q - - def get_Q_from_y(self, y, output_poly: bool = False, use_cliques: list = []): - """ - every cost term can be written as - (1 + wi)/b^2 r^2(x, zi) + (1 - wi) - - residual term: - (Rpi + t - ui).T Wi (Rpi + t - ui) = - [t', vec(R)'] @ [I (pi x I)]' @ Wi @ [I (pi x I)] @ [t ; vec(R)] - ------x'----- -----Pi'----- - - 2 [t', vec(R)'] @ [I (pi x I)]' Wi @ ui - -----x'------ ---------Pi_xl-------- - + ui.T @ Wi @ ui - -----Pi_ll------ - """ - - if len(use_cliques): - js = use_cliques - else: - js = list(range(self.n_landmarks)) - - from poly_matrix import PolyMatrix - - Q = PolyMatrix(symmetric=True) - if NORMALIZE: - norm = (self.n_landmarks * self.d) ** 2 - - Wi = np.eye(self.d) - for i in js: - pi = self.landmarks[i] - ui = y[i] - Pi = np.c_[np.eye(self.d), np.kron(pi, np.eye(self.d))] - - Pi_ll = ui.T @ Wi @ ui - Pi_xl = -(Pi.T @ Wi @ ui)[:, None] - Qi = Pi.T @ Wi @ Pi - if NORMALIZE: - Pi_ll /= norm - Pi_xl /= norm - Qi /= norm - - if self.robust: - Qi /= self.beta**2 - Pi_ll /= self.beta**2 - Pi_xl /= self.beta**2 - # Q["x", "x"] += Qi - Q["t", "t"] += Qi[: self.d, : self.d] - Q["t", "c"] += Qi[: self.d, self.d :] - Q["c", "c"] += Qi[self.d :, self.d :] - - # Q["x", self.HOM] += Pi_xl - Q["t", self.HOM] += Pi_xl[: self.d, :] - Q["c", self.HOM] += Pi_xl[self.d :, :] - Q[self.HOM, self.HOM] += ( - 1 + Pi_ll - ) # 1 from (1 - wi), Pi_ll from first term. - Q[ - self.HOM, f"w_{i}" - ] += -0.5 # from (1 - wi), 0.5 cause on off-diagonal - if self.level == "xwT": - # Q[f"z_{i}", "x"] += 0.5 * Qi - Q[f"z_{i}", "t"] += 0.5 * Qi[:, : self.d] - Q[f"z_{i}", "c"] += 0.5 * Qi[:, self.d :] - - Q[self.HOM, f"w_{i}"] += 0.5 * Pi_ll # from first term - - Q[f"z_{i}", self.HOM] += Pi_xl - elif self.level == "xxT": - Q["z_0", f"w_{i}"] += 0.5 * Qi.flatten()[:, None] - - # Q["x", f"w_{i}"] += Pi_xl - Q["t", f"w_{i}"] += Pi_xl[: self.d, :] - Q["c", f"w_{i}"] += Pi_xl[self.d :, :] - - Q[self.HOM, f"w_{i}"] += 0.5 * Pi_ll - else: - # Q["x", "x"] += Qi - Q["t", "t"] += Qi[: self.d, : self.d] - Q["t", "c"] += Qi[: self.d, self.d :] - Q["c", "c"] += Qi[self.d :, self.d :] - - # Q["x", self.HOM] += Pi_xl - Q["t", self.HOM] += Pi_xl[: self.d, :] - Q["c", self.HOM] += Pi_xl[self.d :, :] - Q[self.HOM, self.HOM] += Pi_ll # on diagonal - if output_poly: - return 0.5 * Q - Q_sparse = 0.5 * Q.get_matrix(variables=self.var_dict) - return Q_sparse - - def __repr__(self): - appendix = "_robust" if self.robust else "" - return f"wahba_{self.d}d_{self.level}_{self.param_level}{appendix}"
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/utils/common.html b/docs/build/_modules/popcor/utils/common.html deleted file mode 100644 index 7824722..0000000 --- a/docs/build/_modules/popcor/utils/common.html +++ /dev/null @@ -1,456 +0,0 @@ - - - - - - - - popcor.utils.common — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.utils.common

-import itertools
-
-import numpy as np
-import scipy.sparse as sp
-
-
-
-[docs] -def upper_triangular(p): - """Given vector, get the half kronecker product.""" - return np.outer(p, p)[np.triu_indices(len(p))]
- - - -
-[docs] -def diag_indices(n): - """Given the half kronecker product, return diagonal elements""" - z = np.empty((n, n)) - z[np.triu_indices(n)] = range(int(n * (n + 1) / 2)) - return np.diag(z).astype(int)
- - - -def get_aggregate_sparsity(matrix_list_sparse): - agg_ii = [] - agg_jj = [] - for i, A_sparse in enumerate(matrix_list_sparse): - assert isinstance(A_sparse, sp.spmatrix) - ii, jj = A_sparse.nonzero() # type: ignore - agg_ii += list(ii) - agg_jj += list(jj) - return sp.csr_matrix(([1.0] * len(agg_ii), (agg_ii, agg_jj)), A_sparse.shape) - - -
-[docs] -def unravel_multi_index_triu(flat_indices, shape): - """Equivalent of np.multi_index_triu, but using only the upper-triangular part of matrix.""" - i_upper = [] - j_upper = [] - - # for 4 x 4, this would give [4, 7, 9, 11] - cutoffs = np.cumsum(list(range(1, shape[0] + 1))[::-1]) - for idx in flat_indices: - i = np.where(idx < cutoffs)[0][0] - if i == 0: - j = idx - else: - j = idx - cutoffs[i - 1] + i - i_upper.append(i) - j_upper.append(j) - return np.array(i_upper), np.array(j_upper)
- - - -
-[docs] -def ravel_multi_index_triu(index_tuple, shape): - """Equivalent of np.multi_index_triu, but using only the upper-triangular part of matrix.""" - ii, jj = index_tuple - - triu_mask = jj >= ii - i_upper = ii[triu_mask] - j_upper = jj[triu_mask] - flat_indices = [] - for i, j in zip(i_upper, j_upper): - # for i == 0: idx = j - # for i == 1: idx = shape[0] + j - # for i == 2: idx = shape[0] + shape[0]-1 + j - idx = np.sum(range(shape[0] - i, shape[0])) + j - flat_indices.append(idx) - return flat_indices
- - - -
-[docs] -def create_symmetric(vec, eps_sparse, correct=False, sparse=False): - """Create a symmetric matrix from the vectorized elements of the upper half""" - - def get_dim_x(len_vec): - return int(0.5 * (-1 + np.sqrt(1 + 8 * len_vec))) - - try: - # vec is dense - len_vec = len(vec) - dim_x = get_dim_x(len_vec) - triu = np.triu_indices(n=dim_x) - mask = np.abs(vec) > eps_sparse - triu_i_nnz = triu[0][mask] - triu_j_nnz = triu[1][mask] - vec_nnz = vec[mask] - except Exception: - # vec is sparse - len_vec = vec.shape[1] - dim_x = get_dim_x(len_vec) - vec.data[np.abs(vec.data) < eps_sparse] = 0 - vec.eliminate_zeros() - ii, jj = vec.nonzero() # vec is 1 x jj - triu_i_nnz, triu_j_nnz = unravel_multi_index_triu(jj, (dim_x, dim_x)) - vec_nnz = np.array(vec[ii, jj]).flatten() - # assert dim_x == self.get_dim_x(var_dict) - - if sparse: - offdiag = triu_i_nnz != triu_j_nnz - diag = triu_i_nnz == triu_j_nnz - triu_i = triu_i_nnz[offdiag] - triu_j = triu_j_nnz[offdiag] - diag_i = triu_i_nnz[diag] - if correct: - # divide off-diagonal elements by sqrt(2) - vec_nnz_off = vec_nnz[offdiag] / np.sqrt(2) - else: - vec_nnz_off = vec_nnz[offdiag] - vec_nnz_diag = vec_nnz[diag] - Ai = sp.csr_array( - ( - np.r_[vec_nnz_diag, vec_nnz_off, vec_nnz_off], - (np.r_[diag_i, triu_i, triu_j], np.r_[diag_i, triu_j, triu_i]), - ), - (dim_x, dim_x), - dtype=float, - ) - else: - Ai = np.zeros((dim_x, dim_x)) - - if correct: - # divide all elements by sqrt(2) - Ai[triu_i_nnz, triu_j_nnz] = vec_nnz / np.sqrt(2) - Ai[triu_j_nnz, triu_i_nnz] = vec_nnz / np.sqrt(2) - # undo operation for diagonal - Ai[range(dim_x), range(dim_x)] *= np.sqrt(2) - else: - Ai[triu_i_nnz, triu_j_nnz] = vec_nnz - Ai[triu_j_nnz, triu_i_nnz] = vec_nnz - return Ai
- - - -
-[docs] -def get_vec(mat, correct=True, sparse=False) -> np.ndarray | sp.csr_matrix | None: - """Convert NxN Symmetric matrix to (N+1)N/2 vectorized version that preserves inner product. - - :param mat: (spmatrix or ndarray) symmetric matrix - :return: ndarray - """ - from copy import deepcopy - - mat = deepcopy(mat) - if correct: - if isinstance(mat, sp.csc_matrix): - ii, jj = mat.nonzero() - mat[ii, jj] *= np.sqrt(2.0) - diag = ii == jj - mat[ii[diag], jj[diag]] /= np.sqrt(2) # type: ignore - else: - mat *= np.sqrt(2.0) - mat[range(mat.shape[0]), range(mat.shape[0])] /= np.sqrt(2) - if sparse: - assert isinstance(mat, sp.csc_matrix) - ii, jj = mat.nonzero() - if len(ii) == 0: - # got an empty matrix -- this can happen depending on the parameter values. - return None - triu_mask = jj >= ii - - flat_indices = ravel_multi_index_triu([ii[triu_mask], jj[triu_mask]], mat.shape) # type: ignore - data = np.array(mat[ii[triu_mask], jj[triu_mask]]).flatten() # type: ignore - vec_size = int(mat.shape[0] * (mat.shape[0] + 1) / 2) # type: ignore - return sp.csr_matrix( - (data, ([0] * len(flat_indices), flat_indices)), (1, vec_size) - ) - else: - return np.array(mat[np.triu_indices(n=mat.shape[0])]).flatten() # type: ignore
- - - -def get_labels(p, zi, zj, var_dict): - labels = [] - size_i = var_dict[zi] - size_j = var_dict[zj] - if zi == zj: - # only upper diagonal for i == j - key_pairs = itertools.combinations_with_replacement(range(size_i), 2) - else: - key_pairs = itertools.product(range(size_i), range(size_j)) - for i, j in key_pairs: - label = f"{p}-" - label += f"{zi}:{i}." if size_i > 1 else f"{zi}." - label += f"{zj}:{j}" if size_j > 1 else f"{zj}" - labels.append(label) - return labels -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popcor/utils/constraint.html b/docs/build/_modules/popcor/utils/constraint.html deleted file mode 100644 index bcfe205..0000000 --- a/docs/build/_modules/popcor/utils/constraint.html +++ /dev/null @@ -1,550 +0,0 @@ - - - - - - - - popcor.utils.constraint — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- -

Source code for popcor.utils.constraint

-import numpy as np
-import scipy.sparse as sp
-from poly_matrix.poly_matrix import PolyMatrix
-
-from popcor.utils.common import get_vec
-from popcor.utils.plotting_tools import plot_basis
-
-
-def remove_dependent_constraints(constraints, verbose=False):
-    from cert_tools.linalg_tools import find_dependent_columns
-
-    # find which constraints are lin. dep.
-    A_vec = sp.vstack(
-        [constraint.a_full_ for constraint in constraints], format="coo"
-    ).T
-
-    bad_idx = find_dependent_columns(A_vec, verbose=verbose)
-    if len(bad_idx):
-        np.testing.assert_allclose(bad_idx, sorted(bad_idx))
-        # important: by changing the order we
-        for idx in sorted(bad_idx)[::-1]:
-            del constraints[idx]
-
-
-def generate_poly_matrix(constraints, factor_out_parameters=False, lifter=None):
-    plot_rows = []
-    plot_row_labels = []
-    j = -1
-    old_mat_vars = ""
-    for constraint in constraints:
-        mat_vars = constraint.mat_var_dict
-        i = constraint.index
-        if factor_out_parameters:  # use a and not b.
-            if constraint.polyrow_a_ is not None:
-                plot_rows.append(constraint.polyrow_a_)
-            else:
-                if constraint.a_ is not None:
-                    assert (
-                        lifter is not None
-                    ), "Need to provide lifter because a_ is not defined"
-                    polyrow_a = lifter.convert_a_to_polyrow(
-                        constraint.a_, constraint.mat_var_dict
-                    )
-                elif constraint.a_full_ is not None:
-                    assert (
-                        lifter is not None
-                    ), "Need to provide lifter because a_full_ is not defined"
-                    polyrow_a = lifter.convert_a_to_polyrow(
-                        constraint.a_full_, constraint.mat_var_dict
-                    )
-                plot_rows.append(polyrow_a)
-        else:
-            if constraint.polyrow_b_ is not None:
-                plot_rows.append(constraint.polyrow_b_)
-            else:
-                assert (
-                    lifter is not None
-                ), "Need to provide lifter because polyrow_b_ is not defined."
-                plot_rows.append(
-                    lifter.convert_b_to_polyrow(
-                        constraint.b_, mat_vars, constraint.mat_param_dict
-                    )
-                )
-
-        if mat_vars != old_mat_vars:
-            j += 1
-            plot_row_labels.append(f"{j}:b{i}")
-            # plot_row_labels.append(f"{j}{mat_vars}:b{i}")
-            old_mat_vars = mat_vars
-        else:
-            plot_row_labels.append(f"{j}:b{i}")
-
-    templates_poly = PolyMatrix.init_from_row_list(
-        plot_rows, row_labels=plot_row_labels
-    )
-    return templates_poly
-
-
-def plot_poly_matrix(
-    poly_matrix, variables_j=None, variables_i=None, simplify=True, hom="h"
-):
-    if variables_i is None:
-        variables_i = poly_matrix.variable_dict_i
-    if variables_j is None:
-        variables_j = poly_matrix.variable_dict_j
-
-    # plot the templates stored in poly_matrix.
-    fig, ax = plot_basis(
-        poly_matrix,
-        variables_j=variables_j,
-        variables_i=variables_i,
-        discrete=True,
-    )
-    ax.set_yticklabels([])
-    ax.set_yticks([])
-    if simplify:
-        ax.set_xticks([])
-        ax.set_xticklabels([])
-    else:
-        new_xticks = []
-        for lbl in ax.get_xticklabels():
-            lbl = lbl.get_text()
-            if "_" in lbl:  # avoid double subscript
-                new_lbl = f"${lbl.replace(f'{hom}.', '').replace(':', '^')}$"
-            else:
-                new_lbl = f"${lbl.replace(f'{hom}.', '').replace(':', '_')}$"
-            new_xticks.append(new_lbl)
-        ax.set_xticklabels(new_xticks, fontsize=7)
-
-    # plot a red vertical line at each new block of parameters.
-    params = [v.split("-")[0] for v in variables_j]
-    old_param = params[0]
-    for i, p in enumerate(params):
-        if p != old_param:
-            ax.axvline(i - 0.5, color="red", linewidth=1.0)
-            ax.annotate(
-                text=f"${p.replace(':0', '^x').replace(':1', '^y').replace('l.','').replace('.','')}$",
-                xy=(float(i - 0.4), 0.0),
-                fontsize=8,
-                color="red",
-            )
-            old_param = p
-    return fig, ax
-
-
-
-[docs] -class Constraint(object): - """ - This class serves the main purpose of not recomputing representations of constraints more than once. - """ - - def __init__( - self, - index=0, - polyrow_a=None, - polyrow_b=None, - A_poly=None, - A_sparse=None, - b=None, - a=None, - a_full=None, - b_full=None, - mat_var_dict=None, - mat_param_dict=None, - known=False, - template_idx=0, - ): - self.index = index - self.mat_var_dict = mat_var_dict - self.mat_param_dict = mat_param_dict - - self.b_ = b - self.polyrow_b_ = polyrow_b - self.polyrow_a_ = polyrow_a - self.A_poly_ = A_poly - self.A_sparse_ = A_sparse - self.a_ = a - self.b_full_ = b_full - self.a_full_ = a_full - - self.known = known - self.template_idx = template_idx - - # list of applied constraints derived from this constraint. - self.applied_list = [] - - @staticmethod - # @profile - def init_from_b( - index: int, - b: np.ndarray, - mat_var_dict: dict, - lifter=None, - mat_param_dict: dict | None = None, - convert_to_polyrow: bool = True, - known: bool = True, - template_idx: int = 0, - ): - a = None - A_sparse = None - a_full = None - if lifter is not None: - a = lifter.get_reduced_a( - b, var_subset=mat_var_dict, param_subset=mat_param_dict, sparse=True - ) - A_sparse = lifter.get_mat(a, var_dict=mat_var_dict, sparse=True) - a_full = get_vec(A_sparse, sparse=True) - if a_full is None: - return None - if convert_to_polyrow: - assert lifter is not None - A_poly, __ = PolyMatrix.init_from_sparse( - A_sparse, var_dict=lifter.var_dict, unfold=True - ) - polyrow_b = lifter.convert_b_to_polyrow( - b, mat_var_dict, param_subset=mat_param_dict - ) - else: - A_poly = None - polyrow_b = None - return Constraint( - index=index, - a=a, - b=b, - A_sparse=A_sparse, - A_poly=A_poly, - polyrow_b=polyrow_b, - a_full=a_full, - mat_var_dict=mat_var_dict, - mat_param_dict=mat_param_dict, - known=known, - template_idx=template_idx, - ) - - @staticmethod - def init_from_A_poly( - lifter, - A_poly: PolyMatrix, - mat_var_dict: dict, - known: bool = False, - index: int = 0, - template_idx: int = 0, - compute_polyrow_b=False, - ): - Ai_sparse_small = A_poly.get_matrix(variables=mat_var_dict) - ai = get_vec(Ai_sparse_small, correct=True) - bi = lifter.augment_using_zero_padding(ai) - if compute_polyrow_b: - polyrow_b = lifter.convert_b_to_polyrow(bi, mat_var_dict) - else: - polyrow_b = None - polyrow_a = lifter.convert_a_to_polyrow(ai, mat_var_dict) - Ai_sparse = A_poly.get_matrix(variables=lifter.var_dict) - return Constraint( - a=ai, - polyrow_a=polyrow_a, - b=bi, - polyrow_b=polyrow_b, - A_poly=A_poly, - A_sparse=Ai_sparse, - known=known, - index=index, - mat_var_dict=mat_var_dict, - template_idx=template_idx, - ) - - @staticmethod - def init_from_polyrow_b( - polyrow_b: PolyMatrix, - lifter, - index: int = 0, - known: bool = False, - template_idx: int = 0, - mat_var_dict: dict | None = None, - ): - if mat_var_dict is None: - mat_var_dict = lifter.var_dict - A_poly = lifter.convert_polyrow_to_Apoly(polyrow_b) - dict_unroll = lifter.get_var_dict(mat_var_dict, unroll_keys=True) - A_sparse = A_poly.get_matrix(dict_unroll) - a_full = get_vec(A_sparse, sparse=True) - return Constraint( - index=index, - A_poly=A_poly, - polyrow_b=polyrow_b, - A_sparse=A_sparse, - a_full=a_full, - known=known, - template_idx=template_idx, - mat_var_dict=mat_var_dict, - ) - - def scale_to_new_lifter(self, lifter): - if self.known: - assert self.A_poly_ is not None - # known matrices are stored in origin variables, not unrolled form - self.A_sparse_ = self.A_poly_.get_matrix(lifter.var_dict) - self.a_full_ = get_vec(self.A_sparse_, sparse=True) - - else: - assert self.A_poly_ is not None - # known matrices are stored in origin variables, not unrolled form - target_dict_unroll = lifter.get_var_dict(unroll_keys=True) - self.A_sparse_ = self.A_poly_.get_matrix(target_dict_unroll) - self.a_full_ = get_vec(self.A_sparse_, sparse=True) - return self
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/auto_template.html b/docs/build/_modules/popr/auto_template.html deleted file mode 100644 index 79ec10c..0000000 --- a/docs/build/_modules/popr/auto_template.html +++ /dev/null @@ -1,1354 +0,0 @@ - - - - - - - - popr.auto_template — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for popr.auto_template

-import time
-from copy import deepcopy
-
-import matplotlib
-import matplotlib.patches
-import numpy as np
-import pandas as pd
-import scipy.sparse as sp
-from cert_tools.linalg_tools import find_dependent_columns, rank_project
-from cert_tools.sdp_solvers import solve_feasibility_sdp
-from cert_tools.sdp_solvers import solve_lambda_cvxpy as solve_lambda
-from cert_tools.sdp_solvers import solve_sdp_cvxpy
-from poly_matrix import PolyMatrix
-
-from popr.base_lifters import StateLifter
-from popr.solvers.common import find_local_minimum
-from popr.solvers.sparse import bisection, brute_force
-from popr.utils.common import get_aggregate_sparsity, get_vec
-from popr.utils.constraint import Constraint, generate_poly_matrix, plot_poly_matrix
-from popr.utils.plotting_tools import (
-    add_colorbar,
-    add_rectangles,
-    import_plt,
-    initialize_discrete_cbar,
-    plot_basis,
-    plot_singular_values,
-    savefig,
-)
-
-plt = import_plt()
-
-# parameter of SDP solver
-TOL = 1e-10
-
-NOISE_SEED = 5
-
-ADJUST_Q = True  # rescale Q matrix
-PRIMAL = False  # use primal or dual formulation of SDP. Recommended is False, because of how MOSEK handles this parameter.
-
-FACTOR = 1.2  # oversampling factor.
-
-PLOT_MAX_MATRICES = 10  # set to np.inf to plot all individual matrices.
-
-USE_KNOWN = True
-USE_INCREMENTAL = False
-
-GLOBAL_THRESH = 1e-3  # consider dual problem optimal when eps<GLOBAL_THRESH
-
-METHOD_NULL = "qrp"  # use svd or qp for comparison only, otherwise leave it at qrp
-
-EPSILON = 1e-4  # fixed epsilon for sparsity-promoting SDP
-
-
-
-[docs] -class AutoTemplate(object): - """ - Class to incrementally learn and augment constraint templates until we reach tightness. - """ - - TOL_RANK_ONE = 1e7 - TOL_REL_GAP = 1e-3 - - # number of random inits to find the global solution - # we always try from ground truth, so for low-enough noise - # that should be good enough. - N_INITS = 1 - - APPLY_TEMPLATES_TO_OTHERS = True - - def __init__( - self, - lifter: StateLifter, - ): - self.lifter = lifter - - # templates contains the learned "templates" of the form: - # ((i, mat_vars), <i-th learned vector for these mat_vars variables, PolyRow form>) - self.templates_poly_ = None # for plotting only: all templats stacked in one - - # constraints after applying templates to the parameters. - self.constraints = [] - - # templates contains all the learned templates - self.templates = [] - - # contains all known constraints, computed only once. - self.templates_known = [] - - # contains the currently relevant known constraints - self.templates_known_sub = [] - self.constraint_index = 0 - - # keep track of which constraints have been tested any constraint twice. - self.index_tested = set() - - # solver results - self.solver_vars = None - self.df_tight = None - self.ranks = [] - self.dual_costs = [] - - # tightness dict makes sure we don't compute tightness twice. - self.reset_tightness_dict() - - # currently used variables - self.mat_vars = [] - - # so-far used variables - self.variable_list = [] - - # can be overwritten later - self.use_known = USE_KNOWN - self.use_incremental = USE_INCREMENTAL - - def reset_tightness_dict(self): - self.tightness_dict = {"rank": None, "cost": None} - - @property - def templates_poly(self): - if self.templates_poly_ is None: - self.templates_poly_ = self.generate_templates_poly( - factor_out_parameters=True - ) - return self.templates_poly_ - - @property - def A_matrices(self): - return [c.A_sparse_ for c in self.constraints] - - def check_violation(self, dual_cost): - assert self.solver_vars is not None - primal_cost = self.solver_vars["qcqp_cost"] - if primal_cost is None: - print("warning can't check violation, no primal cost.") - return False - return (dual_cost - primal_cost) / abs(dual_cost) > self.TOL_REL_GAP - - def duality_gap_is_zero(self, dual_cost, verbose=False, data_dict={}): - assert self.solver_vars is not None - primal_cost = self.solver_vars["qcqp_cost"] - RDG = (primal_cost - dual_cost) / abs(dual_cost) - if RDG < -1e-2: - print( - f"Warning: dual is significantly larger than primal: d={dual_cost:.3e} > p={primal_cost:.3e}, diff={dual_cost-primal_cost:.3e}" - ) - return False - res = RDG < self.TOL_REL_GAP - data_dict["RDG"] = RDG - if not verbose: - return res - - if res: - print("achieved cost tightness:") - else: - print("no cost tightness yet:") - print(f"qcqp cost={primal_cost:.4e}, dual cost={dual_cost:.4e}") - return res - - def is_rank_one(self, eigs, verbose=False, data_dict={}): - SVR = eigs[0] / eigs[1] - data_dict["SVR"] = SVR - res = SVR > self.TOL_RANK_ONE - if not verbose: - return res - if res: - print("achieved rank tightness:") - else: - print("no rank tightness yet:") - print( - f"first two eigenvalues: {eigs[0]:.2e}, {eigs[1]:.2e}, ratio:{eigs[0] / eigs[1]:.2e}" - ) - return res - - def is_tight(self, verbose=False, data_dict={}, tightness=None): - - if tightness is None: - tightness = self.lifter.TIGHTNESS - - if self.tightness_dict[tightness] is not None: - return self.tightness_dict[tightness] - - A_b_list_all = self.get_A_b_list() - A_list = [A for A, __ in A_b_list_all[1:]] # for debugging only - - B_list = self.lifter.get_B_known() - X, info = self._test_tightness(A_b_list_all, B_list, verbose=verbose) - - assert self.solver_vars is not None - self.solver_vars["X"] = X # type: ignore - - self.dual_costs.append(info["cost"]) - self.variable_list.append(self.mat_vars) - - data_dict["q"] = self.solver_vars["qcqp_cost"] - data_dict.update(self.solver_vars) - - if info["cost"] is None: - self.ranks.append(np.zeros(self.lifter.get_dim_x())) - print(f"Warning: solver failed with message: {info['msg']}") - max_error, bad_list = self.lifter.test_constraints(A_list, errors="print") - print("Maximum error:", max_error) - return False - elif self.check_violation(info["cost"]): - self.ranks.append(np.zeros(A_list[0].shape[0])) # type: ignore - print( - f"Warning: dual cost higher than QCQP, d={info['cost']:.2e}, q={self.solver_vars['qcqp_cost']:.2e}" - ) - print( - "Usually this means that MOSEK tolerances are too loose, or that there is a mistake in the constraints." - ) - print( - "It can also mean that we are not sampling enough of the space close to the true solution." - ) - max_error, bad_list = self.lifter.test_constraints(A_list, errors="print") - print("Maximum feasibility error at random x:", max_error) - - tol = 1e-10 - xhat = self.solver_vars["xhat"] - max_error = -np.inf - - assert xhat is not None - assert X is not None - for Ai in A_list: - assert isinstance(Ai, np.ndarray) or isinstance(Ai, sp.spmatrix) - assert isinstance(xhat, np.ndarray) - error = xhat.T @ Ai @ xhat - - errorX = np.trace(X @ Ai) - max_error = max(errorX, max_error) - if abs(error) > tol: - print( - f"Feasibility error too high! xAx:{error:.2e}, <X,A>:{errorX:.2e}" - ) - print(f"Maximum feasibility error at solution x: {max_error}") - - assert X is not None - - data_dict["d"] = info["cost"] - - # sanity check - final_cost = np.trace(self.solver_vars["Q"] @ X) - if abs(final_cost - info["cost"]) / info["cost"] >= 1e-1: - print( - f"Warning: cost is inconsistent: {final_cost:.3e}, {info['cost']:.3e}" - ) - - eigs = np.linalg.eigvalsh(X)[::-1] - self.ranks.append(eigs) - - if self.lifter.robust: - x_dim = self.lifter.d + self.lifter.d**2 + 1 - wi = X[0, x_dim::x_dim] - print("should be plus or minus ones:", wi.round(4)) - - if self.solver_vars["qcqp_cost"] is not None: - cost_tight = self.duality_gap_is_zero( - info["cost"], verbose=tightness == "cost", data_dict=data_dict - ) - else: - cost_tight = False - rank_tight = self.is_rank_one( - eigs, verbose=tightness == "rank", data_dict=data_dict - ) - self.tightness_dict["rank"] = rank_tight - self.tightness_dict["cost"] = cost_tight # type: ignore - if tightness == "rank": - return rank_tight - elif tightness == "cost": - return cost_tight - - def get_A_list(self, var_dict=None): - if var_dict is None: - A_known = [] - if self.use_known: - A_known += [constraint.A_sparse_ for constraint in self.templates_known] - return A_known + [constraint.A_sparse_ for constraint in self.constraints] - else: - A_known = [] - if self.use_known: - A_known += [constraint.A_poly_ for constraint in self.templates_known] - A_list_poly = A_known + [ - constraint.A_poly_ for constraint in self.constraints - ] - return [A.get_matrix(var_dict) for A in A_list_poly] - - def get_A_b_list(self): - A_list = self.get_A_list() - A_b_list_all = self.lifter.get_A_b_list(A_list) - return A_b_list_all - - def generate_minimal_subset( - self, - reorder=False, - tightness="rank", - use_last=None, - use_bisection=False, - tol=TOL, - ): - def function(A_b_list_here, df_data, verbose=False): - """Function for bisection or brute_force""" - if (len(A_b_list_here) in df_data.keys()) and not verbose: - new_data = df_data[len(A_b_list_here)] - else: - new_data = {"lifter": str(self.lifter), "reorder": reorder} - X, info = self._test_tightness( - A_b_list_here, B_list=B_list, verbose=False - ) - dual_cost = info["cost"] - new_data["dual cost"] = dual_cost - if dual_cost is None: - print(f"{len(A_b_list_here)}: solver error? msg: {info['msg']}") - new_data["eigs"] = np.full(self.lifter.get_dim_X(), np.nan) - new_data["cost_tight"] = False - new_data["rank_tight"] = False - df_data[len(A_b_list_here)] = deepcopy(new_data) - return False - - elif self.duality_gap_is_zero(dual_cost): - print(f"{len(A_b_list_here)}: cost-tight") - new_data["cost_tight"] = True - else: - print(f"{len(A_b_list_here)}: not cost-tight yet") - new_data["cost_tight"] = False - - assert X is not None - assert self.solver_vars is not None - - eigs = np.linalg.eigvalsh(X)[::-1] - new_data["eigs"] = eigs - if self.is_rank_one(eigs): - print(f"{len(A_b_list_here)}: rank-tight") - new_data["rank_tight"] = True - else: - new_data["rank_tight"] = False - print(f"{len(A_b_list_here)}: not rank-tight yet") - df_data[len(A_b_list_here)] = deepcopy(new_data) - - if verbose: - print( - f"dual cost: {dual_cost:4.4e}, primal cost: {self.solver_vars['qcqp_cost']:4.4f}" - ) - print(f"largest 10 eigenvalues: {eigs[:10]}") - - if tightness == "rank": - return new_data["rank_tight"] - else: - return new_data["cost_tight"] - - A_b_list_all = self.get_A_b_list() - B_list = self.lifter.get_B_known() - - force_first = 1 - if self.use_known: - force_first += len(self.templates_known) - - if reorder: - if self.solver_vars is None: - self.find_local_solution() - assert self.solver_vars is not None - - # find the importance of each constraint - _, lamdas = solve_lambda( - self.solver_vars["Q"], - A_b_list_all, - self.solver_vars["xhat"], - B_list=B_list, - force_first=force_first, - tol=tol, - adjust=True, - primal=False, - verbose=False, - fixed_epsilon=EPSILON, - ) - if lamdas is None: - print("Warning: problem doesn't have feasible solution!") - print("Sanity checks:") - B_list = self.lifter.get_B_known() - X, info = self._test_tightness(A_b_list_all, B_list, verbose=False) - xhat_from_X, _ = rank_project(X, p=1) - xhat = self.solver_vars["xhat"] - - assert xhat is not None - print("max xhat error:", np.min(xhat - xhat_from_X)) - print("max Hx", np.max(np.abs(info["H"] @ xhat))) - print("max Hx_from_X", np.max(np.abs(info["H"] @ xhat_from_X))) - eigs = np.linalg.eigvalsh(info["H"].toarray()) - print("min eig of H", np.min(eigs)) - return None - print("found valid lamdas") - - # order the redundant constraints by importance - redundant_idx = np.argsort(np.abs(lamdas[force_first:]))[::-1] - sorted_idx = np.r_[np.arange(force_first), force_first + redundant_idx] - else: - sorted_idx = range(len(A_b_list_all)) - - inputs = [A_b_list_all[idx] for idx in sorted_idx] - - B_list = self.lifter.get_B_known() - df_data = [] - - if use_last is None: - start_idx = force_first - else: - start_idx = max(len(inputs) - use_last, force_first) - - df_data = {} - if use_bisection: - bisection( - function, (inputs, df_data), left_num=start_idx, right_num=len(inputs) - ) - else: - brute_force( - function, (inputs, df_data), left_num=start_idx, right_num=len(inputs) - ) - - df_tight = pd.DataFrame(df_data.values(), index=list(df_data.keys())) - if self.df_tight is None: - self.df_tight = df_tight - else: - self.df_tight = pd.concat([self.df_tight, df_tight], axis=0) - - minimal_indices = [] - if tightness == "cost": - min_num = df_tight[df_tight.cost_tight == 1].index.min() - elif tightness == "rank": - min_num = df_tight[df_tight.rank_tight == 1].index.min() - if not np.isnan(min_num): - minimal_indices = list(sorted_idx[:min_num]) - return minimal_indices - - def find_local_solution(self, n_inits=None, verbose=False, plot=False): - if n_inits is None: - n_inits = self.N_INITS - np.random.seed(NOISE_SEED) - Q = self.lifter.get_Q() - y = self.lifter.y_ - qcqp_that, qcqp_cost, info = find_local_minimum( - self.lifter, y=y, verbose=verbose, n_inits=n_inits, plot=plot - ) - self.solver_vars = dict(Q=Q, y=y, qcqp_cost=qcqp_cost, xhat=None) - self.solver_vars.update(info) # type: ignore - if qcqp_cost is not None: - xhat = self.lifter.get_x(qcqp_that) - self.solver_vars["xhat"] = xhat # type: ignore - - # calculate error for global estimate - self.solver_vars.update(self.lifter.get_error(qcqp_that)) - # calculate errors for local estimates - for key, qcqp_that_local in info.items(): - if key.startswith("local solution"): - solution_idx = key.strip("local solution ") - error_dict = self.lifter.get_error(qcqp_that_local) - self.solver_vars.update( - { - f"local {solution_idx} {error_name}": err - for error_name, err in error_dict.items() - } - ) - - return True - - def find_global_solution(self, data_dict={}): - from cert_tools.sdp_solvers import options_cvxpy - - assert self.solver_vars is not None - - A_b_list_all = self.get_A_b_list() - options_cvxpy["accept_unknown"] = True - - # find or certify global solution - if self.lifter.TIGHTNESS == "rank": - X = self.solver_vars["X"] - x, info = rank_project(X, p=1) - x = x.flatten() - else: - """Try to solve dual problem.""" - xhat = self.solver_vars["xhat"] - - H, info = solve_feasibility_sdp( - self.solver_vars["Q"], - A_b_list_all, - xhat, - adjust=True, - options=options_cvxpy, - tol=1e-10, - # soft_epsilon=False, - # eps_tol=1e-4, - soft_epsilon=True, - ) - if info["eps"] is not None: - cert = abs(info["eps"]) <= GLOBAL_THRESH - print(f"global solution eps: {info['eps']:.2e}, cert: {cert}") - data_dict["global solution cert"] = cert - - if info["eps"] and cert: - x = xhat - else: - x = None - - # sanity check: try to certify local minima (should fail) - keys = [key for key in data_dict.keys() if key.startswith("local solution")] - for key in keys: - x_local = data_dict[key] - x_local = self.lifter.get_x(theta=x_local) - H, info = solve_feasibility_sdp( - self.solver_vars["Q"], - A_b_list_all, - x_local, - adjust=True, - tol=1e-10, - options=options_cvxpy, - ) - if info["eps"] is not None: - print(f"local solution eps: {info['eps']:.2e}") - cert = abs(info["eps"]) <= GLOBAL_THRESH - data_dict[key + " cert"] = cert - - if x is not None: - theta = self.lifter.get_theta(x) - cost = self.lifter.get_cost(theta, self.solver_vars["y"]) - data_dict["global theta"] = theta - data_dict["global cost"] = cost - return True - return False - - def _test_tightness(self, A_b_list_all, B_list=[], verbose=False): - from cert_tools.sdp_solvers import options_cvxpy - - if self.solver_vars is None: - self.find_local_solution(verbose=verbose) - assert self.solver_vars is not None - - options_cvxpy["accept_unknown"] = True - X, info = solve_sdp_cvxpy( - self.solver_vars["Q"], - A_b_list_all, - B_list=B_list, - adjust=ADJUST_Q, - verbose=verbose, - primal=PRIMAL, - tol=TOL, - options=options_cvxpy, - ) - return X, info - - def update_variables(self): - # add new variable to the list of variables to study - try: - self.mat_vars = next(self.variable_iter) - return True - except StopIteration: - return False - - def extract_known_templates(self): - """Find which of the known constraints are relevant for the current variables.""" - templates_known_sub = [] - for c in self.templates_known: - var_subset = set(c.A_poly_.get_variables()) - if var_subset.issubset(self.mat_vars): - templates_known_sub.append(c) - - new_index_set = set([t.index for t in templates_known_sub]) - old_index_set = set([t.index for t in self.templates_known_sub]) - diff_index_set = new_index_set.difference(old_index_set) - - self.templates_known_sub = templates_known_sub - return len(diff_index_set) - - def learn_templates(self, plot=False, data_dict=None): - from popr import AutoTight - - templates = [] - mat_var_dict = self.lifter.get_var_dict(self.mat_vars) - param_dict = self.lifter.get_involved_param_dict(self.mat_vars) - - t1 = time.time() - Y = AutoTight.generate_Y( - self.lifter, - var_subset=self.mat_vars, - param_subset=param_dict, - factor=FACTOR, - ) - a_vectors = [] - if self.use_incremental: - for c in self.templates: - ai = get_vec(c.A_poly_.get_matrix(mat_var_dict)) - bi = self.lifter.augment_using_zero_padding(ai, param_dict) - a_vectors.append(bi) - if self.use_known: - for c in self.templates_known_sub: - ai = get_vec(c.A_poly_.get_matrix(mat_var_dict)) - bi = self.lifter.augment_using_zero_padding(ai, param_dict) - a_vectors.append(bi) - Y = np.vstack([Y] + a_vectors) - - if plot: - fig, ax = plt.subplots() - - print(f"data matrix Y has shape {Y.shape} ") - for i in range(AutoTight.N_CLEANING_STEPS + 1): - if i == 0: - print(f"getting basis...", end="") - else: - print(f"cleaning step {i}/{AutoTight.N_CLEANING_STEPS+1}...", end="") - basis_new, S = AutoTight.get_basis(self.lifter, Y, method=METHOD_NULL) - print("...done, analyzing...", end="") - corank = basis_new.shape[0] - if corank > 0: - AutoTight.test_S_cutoff(S, corank) - - bad_idx = AutoTight.clean_Y(basis_new, Y, S, plot=False) - - if plot: - if len(bad_idx): - plot_singular_values( - S, eps=AutoTight.EPS_SVD, label=f"run {i}", ax=ax - ) - else: - plot_singular_values(S, eps=AutoTight.EPS_SVD, ax=ax, label=None) - - if len(bad_idx) > 0: - print(f"there are {len(bad_idx)} bad basis vectors (with high error).") - Y = np.delete(Y, bad_idx, axis=0) - else: - print(f"no bad basis vectors found.") - break - - if basis_new.shape[0]: - for i, b in enumerate(basis_new): - constraint = Constraint.init_from_b( - index=self.constraint_index, - mat_var_dict=mat_var_dict, - mat_param_dict=param_dict, - b=b, - lifter=self.lifter, - convert_to_polyrow=self.APPLY_TEMPLATES_TO_OTHERS, - known=False, - ) - if constraint is None: - print("Warning: found an all-zero constraint; not adding it.") - continue - templates.append(constraint) - self.constraint_index += 1 - - if len(templates + self.templates): - # we assume that all known constraints are linearly independent, and also - # that all known+previously found constraints are linearly independent. - indep_templates = self.clean_constraints( - constraints=templates + self.templates, - remove_dependent=True, - remove_imprecise=False, - ) - else: - indep_templates = [] - - if data_dict is not None: - ttot = time.time() - t1 - data_dict["t learn templates"] = ttot - data_dict["n rank"] = Y.shape[1] - corank - data_dict["n nullspace"] = corank - - if len(templates) > 0: - n_all = len(indep_templates) - n_new = n_all - len(self.templates) - self.templates = indep_templates - return n_new, n_all - return 0, len(self.constraints) - - def apply_templates(self): - # the new templates are all the ones corresponding to the new matrix variables. - new_constraints = self.lifter.apply_templates( - self.templates, self.constraint_index - ) - self.constraint_index += len(new_constraints) - if not len(new_constraints): - return 0, 0 - - n_all = len(new_constraints) - n_new = n_all - len(self.constraints) - self.constraints = new_constraints - return n_new, n_all - - def clean_constraints( - self, - constraints, - remove_dependent=True, - remove_imprecise=True, - ): - """ - This function is used in two different ways. - - First use case: Given the new templates, in b-PolyRow form, we determine which of the templates are actually - independent to a_current. We only want to augment the independent ones, otherwise we waste computing effort. - - Second use case: After applying the templates to as many variable pairs as we wish, we call this function again, - to make sure all the matrices going into the SDP are in fact linearly independent. - """ - if remove_dependent: - # find which constraints are lin. dep. - A_vec = sp.vstack( - [constraint.a_full_ for constraint in constraints], format="coo" - ).T - - # make sure that matrix is tall (we have less constraints than number of dimensions of x) - if A_vec.shape[0] < A_vec.shape[1]: - print("Warning: fat matrix.") - - bad_idx = find_dependent_columns(A_vec) - if len(bad_idx): - for idx in sorted(bad_idx)[::-1]: - del constraints[idx] - - if remove_imprecise: - error, bad_idx = self.lifter.test_constraints( - [c.A_sparse_ for c in constraints if c.index not in self.index_tested], - errors="ignore", - n_seeds=2, - ) - self.index_tested = self.index_tested.union([c.index for c in constraints]) - if len(bad_idx): - print(f"removing {bad_idx} because high error, up to {error:.2e}") - for idx in list(sorted(bad_idx))[ - ::-1 - ]: # reverse order to not mess up indexing - del constraints[idx] - return constraints - - def get_known_templates(self, unroll=False): - templates_known = [] - if not self.use_known: - return templates_known - - # TODO(FD) we should not always recompute from scratch, but it's not very expensive so it's okay for now. - target_dict = self.lifter.get_var_dict(unroll_keys=unroll) - for i, Ai in enumerate( - self.lifter.get_A_known(var_dict=target_dict, output_poly=True) - ): - template = Constraint.init_from_A_poly( - lifter=self.lifter, - A_poly=Ai, - known=True, - index=self.constraint_index, - template_idx=self.constraint_index, - mat_var_dict=self.lifter.var_dict, - compute_polyrow_b=True, - ) - self.constraint_index += 1 - templates_known.append(template) - return templates_known - - def get_sufficient_templates(self, new_order, new_lifter): - """Use the templates in learner to populate the own templates and constraints.""" - template_indices = sorted( - [t.index for t in self.templates + self.templates_known] - ) - new_templates = [] - template_unique_idx = set() - - # The index list new_order contains the indices of constraints, but we want to track back - # which templates those corresponded to. - # We thus create the set of all template indices that are represented in the - # sufficient constraints. - all_constraints = self.templates_known + self.constraints - for i in new_order: - # the first constraint ALWAYS corresponds to A0, whichs not part of our templates. - if i > 0: - new_index = all_constraints[i - 1].template_idx - assert new_index in template_indices # just a sanity check - template_unique_idx.add(new_index) - - # now we can create the new templates by looping through the sufficent template list. - for t in template_unique_idx: - # find the template of the requested index - other_templates = self.templates + self.templates_known - template_indices = [temp.index for temp in other_templates] - idx = template_indices.index(t) # raises Error if t is not in list. - template = other_templates[idx] - - assert isinstance(template, Constraint) - - # scale the template to the dimensions of the new learner. - # (not the known ones as those where already through other_learner.templates_known) - if not template.known: - scaled_template = template.scale_to_new_lifter(new_lifter) - new_templates.append(scaled_template) - return new_templates - - def get_sorted_df(self, templates_poly=None, add_columns={}): - def sort_fun_sparsity(series): - # This is a bit complicated because we don't want the order to change - # because of the values, only isna() should matter. - # To make this work, we temporarily change the non-nan values to the order in which they appear - index = pd.MultiIndex.from_product([[0], series.index]) - series.index = index - scipy_sparse = series.sparse.to_coo()[0] - # don't start at 0 because it's considered empty by scipy - scipy_sparse.data = np.arange(1, 1 + scipy_sparse.nnz) - pd_sparse = pd.Series.sparse.from_coo(scipy_sparse, dense_index=True) - return pd_sparse - - if templates_poly is None: - templates_poly = self.templates_poly - - series = [] - - variable_dict_j = list(templates_poly.variable_dict_j.keys()) - for i, key_i in enumerate(templates_poly.variable_dict_i): - data = {j: float(val) for j, val in templates_poly.matrix[key_i].items()} - for key, idx_list in add_columns.items(): - # if the list is not empty, then indicate which constraints are required. - if idx_list is not None and len(idx_list): - idx_list = list(idx_list) - try: - data[key] = idx_list.index(i) - except Exception: - data[key] = -1 - # if the list is empty, all of them are required (and more) - else: - data[key] = 1.0 - series.append( - pd.Series( - data, - index=variable_dict_j + list(add_columns.keys()), - dtype="Sparse[float]", - ) - ) - df = pd.DataFrame( - series, dtype="Sparse[float]", index=templates_poly.variable_dict_i - ) - df.dropna(axis=1, how="all", inplace=True) - - try: - df_sorted = df.sort_values( - key=sort_fun_sparsity, - by=list(df.columns), - axis=0, - na_position="last", - inplace=False, - ) - df_sorted["order_sparsity"] = range(len(df_sorted)) - return df_sorted - except Exception as e: - print("failed to sort:", e) - return df - - def generate_templates_poly(self, constraints=None, factor_out_parameters=False): - if constraints is None: - constraints = self.templates_known + self.constraints - - plot_rows = [] - plot_row_labels = [] - j = -1 - old_mat_vars = "" - for constraint in constraints: - mat_vars = constraint.mat_var_dict - i = constraint.index - if factor_out_parameters: # use a and not b. - if constraint.polyrow_a_ is not None: - plot_rows.append(constraint.polyrow_a_) - else: - if constraint.a_ is not None: - polyrow_a = self.lifter.convert_a_to_polyrow( - constraint.a_, mat_vars - ) - elif constraint.a_full_ is not None: - polyrow_a = self.lifter.convert_a_to_polyrow( - constraint.a_full_, mat_vars - ) - plot_rows.append(polyrow_a) - else: - if constraint.polyrow_b_ is not None: - plot_rows.append(constraint.polyrow_b_) - else: - plot_rows.append( - self.lifter.convert_b_to_polyrow(constraint.b_, mat_vars) - ) - - if mat_vars != old_mat_vars: - j += 1 - plot_row_labels.append(f"{j}:b{i}") - # plot_row_labels.append(f"{j}{mat_vars}:b{i}") - old_mat_vars = mat_vars - else: - plot_row_labels.append(f"{j}:b{i}") - - templates_poly = PolyMatrix.init_from_row_list( - plot_rows, row_labels=plot_row_labels - ) - - # make sure variable_dict_j is ordered correctly. - templates_poly.variable_dict_j = self.lifter.var_dict_row( - mat_vars, force_parameters_off=factor_out_parameters - ) - return templates_poly - - def save_sorted_templates( - self, df, fname_root="", title="", drop_zero=False, simplify=True - ): - - # convert to poly matrix for plotting purposes only. - poly_matrix = PolyMatrix(symmetric=False) - keys_j = [] - keys_i = [] - for i, row in df.iterrows(): - for k, val in row[~row.isna()].items(): - if "order" in k or "required" in k: - continue - poly_matrix[i, k] = val - keys_j.append(k) - keys_i.append(i) - - variables_j = self.lifter.var_dict_row( - var_subset=self.lifter.var_dict, force_parameters_off=False - ) - assert set(keys_j).issubset(variables_j) - if drop_zero: - variables_j = {k: v for k, v in variables_j.items() if k in keys_j} - variables_i = {i: 1 for i in keys_i} - fig, ax = plot_basis( - poly_matrix, - variables_j=variables_j, - variables_i=variables_i, - discrete=True, - ) - ax.set_yticklabels([]) - ax.set_yticks([]) - if simplify: - ax.set_xticks([]) - ax.set_xticklabels([]) - else: - new_xticks = [] - for lbl in ax.get_xticklabels(): - lbl = lbl.get_text() - if "_" in lbl: # avoid double subscript - new_lbl = f"${lbl.replace('h-', '').replace(':', '^')}$" - else: - new_lbl = f"${lbl.replace('h-', '').replace(':', '_')}$" - new_xticks.append(new_lbl) - ax.set_xticklabels(new_xticks, fontsize=7) - - # plot a red vertical line at each new block of parameters. - params = [v.split("-")[0] for v in variables_j] - old_param = params[0] - for i, p in enumerate(params): - if p != old_param: - ax.axvline(i, color="red", linewidth=1.0) - ax.annotate( - text=f"${p.replace(':0', '^x').replace(':1', '^y').replace('l.','').replace('.','')}$", - xy=(float(i), 0.0), - fontsize=8, - color="red", - ) - old_param = p - ax.set_title(title) - if "required (sorted)" in df.columns: - for i, (_, row) in enumerate(df.iterrows()): - if row["required (sorted)"] < 0: - ax.add_patch( - matplotlib.patches.Rectangle( - (ax.get_xlim()[0], i - 0.5), - ax.get_xlim()[1] + 0.5, - 1.0, - fc="white", - alpha=0.5, - lw=0.0, - ) - ) - if fname_root != "": - savefig(fig, fname_root + "_templates-sorted.png") - return fig, ax - - def save_templates(self, fname_root="", title="", with_parameters=False): - - templates_poly = self.generate_templates_poly( - factor_out_parameters=not with_parameters - ) - variables_j = self.lifter.var_dict_row( - self.mat_vars, force_parameters_off=not with_parameters - ) - fig, ax = plot_basis(templates_poly, variables_j=variables_j, discrete=True) - if with_parameters: - for p in range(1, self.lifter.get_dim_P(self.mat_vars)): - ax.axvline(p * self.lifter.get_dim_X(self.mat_vars) - 0.5, color="red") - - ax.set_title(title) - if fname_root != "": - savefig(fig, fname_root + "_templates.png") - return fig, ax - - def save_tightness(self, fname_root, title=""): - labels = self.variable_list - assert self.solver_vars is not None - - fig, ax = plt.subplots() - xticks = range(len(self.dual_costs)) - ax.semilogy(xticks, self.dual_costs, marker="o") - ax.set_xticks(xticks, labels) - if self.solver_vars["qcqp_cost"] is not None: - ax.axhline(float(self.solver_vars["qcqp_cost"]), color="k", ls=":") - ax.set_title(title) - if fname_root != "": - savefig(fig, fname_root + "_tightness.png") - - fig, ax = plt.subplots() - for eig, label in zip(self.ranks, labels): - ax.semilogy(eig, label=label) - ax.legend(loc="upper right") - ax.set_title(title) - if fname_root != "": - savefig(fig, fname_root + "_eigs.png") - return - - def save_matrices_sparsity(self, A_matrices=None, fname_root="", title=""): - assert self.solver_vars is not None - - if A_matrices is None: - A_matrices = self.A_matrices - - Q = self.solver_vars["Q"].toarray() # type:ignore - - sorted_i = self.lifter.get_var_dict(unroll_keys=True) - A_matrices_sparse = [ - A_poly.get_matrix(variables=sorted_i) for A_poly in A_matrices - ] - - A_agg = get_aggregate_sparsity(A_matrices_sparse) - - fig, axs = plt.subplots(1, 2) - fig.set_size_inches(6, 3) - im0 = axs[0].matshow( - 1 - A_agg.toarray(), vmin=0, vmax=1, cmap="gray" - ) # 1 (white) is empty, 0 (black) is nonempty - - vmin = min(-np.max(Q), np.min(Q)) - vmax = max(np.max(Q), -np.min(Q)) - norm = matplotlib.colors.SymLogNorm(10**-5, vmin=vmin, vmax=vmax) - im1 = axs[1].matshow(Q, norm=norm) - - for ax in axs: - add_rectangles(ax, self.lifter.var_dict) - - add_colorbar(fig, axs[1], im1, nticks=3) - # only for dimensions - add_colorbar(fig, axs[0], im0, visible=False) - if fname_root != "": - savefig(fig, fname_root + "_matrices-sparisty.png") - return fig, axs - - def save_matrices_poly( - self, - A_matrices=None, - n_matrices=5, - fname_root="", - reduced_mode=False, - save_individual=False, - max_matrices=PLOT_MAX_MATRICES, - ): - if A_matrices is None: - A_matrices = self.A_matrices - - n_rows = n_matrices // 10 + 1 - n_cols = min(n_matrices, 10) - fig, axs = plt.subplots(n_rows, n_cols, squeeze=False) - fig.set_size_inches(5 * n_cols / n_rows, 5) - axs = axs.flatten() - i = 0 - for i, A_poly in enumerate(A_matrices): - if reduced_mode: - sorted_i = sorted(A_poly.variable_dict_i.keys()) - else: - sorted_i = self.lifter.get_var_dict(unroll_keys=True) - - plot_axs = [] - if i < n_matrices: - plot_axs.append(axs[i]) - - if save_individual and (i < max_matrices): - figi, axi = plt.subplots() - figi.set_size_inches(3, 3) - plot_axs.append(axi) - - if isinstance(A_poly, PolyMatrix): - A_sparse = A_poly.get_matrix(sorted_i) - else: - A_sparse = A_poly - cmap, norm, colorbar_yticks = initialize_discrete_cbar(A_sparse.data) # type: ignore - - for ax in plot_axs: - if sp.isspmatrix(A_sparse): - arr = A_sparse.toarray() # type: ignore - else: - arr = A_sparse - im = ax.matshow(arr, cmap=cmap, norm=norm) - # Use sp.isspmatrix to check if A_sparse is a scipy sparse matrix - if sp.isspmatrix(A_sparse): - add_rectangles(ax, self.lifter.var_dict) - cax = add_colorbar(fig, ax, im, size=0.1) - cax.set_yticklabels(colorbar_yticks) - - if save_individual and (i < max_matrices): - savefig(figi, fname_root + f"_matrix{i}.pdf") - for ax in axs[i + 1 :]: - ax.axis("off") - return fig, axs - -
-[docs] - def run( - self, - use_known: bool = USE_KNOWN, - use_incremental: bool = USE_INCREMENTAL, - variable_list: list[list[str]] | None = None, - verbose: bool = False, - plot: bool = False, - ): - """Run the template learning algorithm until we reach tightness, or run out of variables to add. - - :param use_known: whether to use the known constraints of the lfiter (must have get_A_known). - :param use_incremental: whether to keep adding the learned tempaltes to the set of known constraints, to enforce we find orthogonal ones. - :param variable_list: list of lists of variables to consider. If not given, will use the VARIABLE_LIST parameter of the lifter class. - - """ - data = [] - success = False - self.use_known = use_known - self.use_incremental = use_incremental - - if use_known: - self.templates_known = self.get_known_templates() - n_known = len(self.templates_known) - print(f"there are total {n_known} known constraints") - - if variable_list is None: - variable_list = self.lifter.VARIABLE_LIST - self.variable_iter = iter(variable_list) - - while 1: - # add one more variable to the list of variables to vary - if not self.update_variables(): - print("no more variables to add") - break - print(f"======== {self.mat_vars} ========") - - n_new = 0 - if use_known: - n_known_here = self.extract_known_templates() - n_new += n_known_here - print( - f"using {n_known_here}/{n_known} known constraints (only the ones that contain the current variables)" - ) - - data_dict = {"variables": self.mat_vars} - param_dict = self.lifter.get_involved_param_dict(self.mat_vars) - - # Set the type expectation for the dictionary if using type hints - data_dict: dict[str, float | int | list | None] - data_dict["n dims"] = self.lifter.get_dim_Y( - var_subset=self.mat_vars, param_subset=param_dict - ) - - print("-------- templates learning --------") - # learn new templates, orthogonal to the ones found so far. - n_new_learned, n_all = self.learn_templates(plot=plot, data_dict=data_dict) - n_new += n_new_learned - print( - f"found {n_new_learned} learned templates, new total learned: {n_all} " - ) - data_dict["n templates"] = ( - len(self.templates) + len(self.templates_known) + 1 - ) - if n_new == 0: - data.append(data_dict) - continue - - if plot: - # turn the current list of templates into a poly matrix. - templates = self.templates_known + self.templates - poly_matrix = generate_poly_matrix(templates, lifter=self.lifter) - - # make sure we use sorted column names - variables_j_all = self.lifter.var_dict_row() - variables_j = { - key: val - for key, val in variables_j_all.items() - if key in poly_matrix.variable_dict_j - } - - fig, ax = plot_poly_matrix( - poly_matrix, variables_j, simplify=False, hom="l" - ) - w, h = fig.get_size_inches() - fig.set_size_inches(10, 10 * h / w) - - # apply the pattern to all landmarks - if self.APPLY_TEMPLATES_TO_OTHERS: - print("------- applying templates ---------") - t1 = time.time() - n_new, n_all = self.apply_templates() - print( - f"found {n_new} independent learned constraints, new total: {n_all} " - ) - ttot = time.time() - t1 - - data_dict["n constraints"] = n_all + len(self.templates_known) + 1 - print( - f"total including known and homogenization:", - data_dict["n constraints"], - ) - data_dict["t apply templates"] = ttot - else: - self.constraints = [] - for temp in self.templates: - con = deepcopy(temp) - con.template_idx = temp.index - self.constraints.append(con) - - t1 = time.time() - print("-------- checking tightness ----------") - self.reset_tightness_dict() - is_tight = self.is_tight(verbose=verbose, data_dict=data_dict) - ttot = time.time() - t1 - data_dict["t check tightness"] = ttot - data.append(data_dict) - if is_tight: - success = True - break - return data, success
- - -
-[docs] - def apply(self, lifter: StateLifter, use_known: bool = False) -> list: - """Apply the learned templates to a new lifter.""" - constraints = lifter.apply_templates(self.templates) - - if use_known: - # if we set use_known=True in running AutoTemplate, then we learned only - # constraints that were not already known, so we need to add them to the - # overall set of constraints. - A_known = lifter.get_A_known() - assert isinstance(A_known, list) - return A_known + [c.A_sparse_ for c in constraints] # type: ignore
-
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/auto_tight.html b/docs/build/_modules/popr/auto_tight.html deleted file mode 100644 index 6226fdc..0000000 --- a/docs/build/_modules/popr/auto_tight.html +++ /dev/null @@ -1,521 +0,0 @@ - - - - - - - - popr.auto_tight — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for popr.auto_tight

-import matplotlib.pylab as plt
-import numpy as np
-import scipy.sparse as sp
-import scipy.sparse.linalg as splinalg
-from cert_tools.linalg_tools import find_dependent_columns, get_nullspace
-
-from popr.utils.common import get_vec
-from popr.utils.constraint import Constraint
-from popr.utils.plotting_tools import add_colorbar, initialize_discrete_cbar
-
-
-
-[docs] -class AutoTight(object): - """Class for automatic constraint generation.""" - - # consider singular value zero below this - EPS_SVD = 1e-5 - - # basis pursuit method, can be - # - qr: qr decomposition - # - qrp: qr decomposition with permutations (sparser), recommended - # - svd: svd - METHOD = "qrp" - - # normalize learned Ai or not - NORMALIZE = False - - # how much to oversample (>= 1) - FACTOR = 1.2 - - # number of times we remove bad samples from data matrix - N_CLEANING_STEPS = 1 # was 3 - - # maximum number of iterations of local solver - LOCAL_MAXITER = 100 - - # find and remove linearly dependent constraints - REDUCE_DEPENDENT = False - - def __init__(self): - pass - - @staticmethod - def clean_Y(basis_new, Y, s, plot=False): - errors = np.abs(basis_new @ Y.T) # Nb x n x n x Ns = Nb x Ns - if np.all(errors < 1e-10): - return [] - bad_bins = np.unique(np.argmax(errors, axis=1)) - if plot: - fig, ax = plt.subplots() - ax.semilogy(np.min(errors, axis=1)) - ax.semilogy(np.max(errors, axis=1)) - ax.semilogy(np.median(errors, axis=1)) - ax.semilogy(s) - return bad_bins - - @staticmethod - def test_S_cutoff(S, corank, eps_svd=None): - if eps_svd is None: - eps_svd = AutoTight.EPS_SVD - if corank > 1: - try: - assert abs(S[-corank]) / eps_svd < 1e-1 # 1e-1 1e-10 - assert abs(S[-corank - 1]) / eps_svd > 10 # 1e-11 1e-10 - except AssertionError: - print(f"there might be a problem with the chosen threshold {eps_svd}:") - print(S[-corank], eps_svd, S[-corank - 1]) - - @staticmethod - def get_basis_sparse( - lifter, var_list, param_list, A_known=[], plot=False, eps_svd=None - ): - - Y = AutoTight.generate_Y_sparse( - lifter, var_subset=var_list, param_subset=param_list, factor=1.0 - ) - basis, S = AutoTight.get_basis(lifter, Y, A_known=A_known, eps_svd=eps_svd) - AutoTight.test_S_cutoff(S, corank=basis.shape[0], eps_svd=eps_svd) - constraints = [] - for i, b in enumerate(basis): - constraints.append( - Constraint.init_from_b( - i, - b, - mat_var_dict=var_list, - mat_param_dict=param_list, - convert_to_polyrow=False, - known=False, - ) - ) - if plot: - plot_matrix = np.vstack([t.b_[None, :] for t in constraints]) - - cmap, norm, colorbar_yticks = initialize_discrete_cbar(plot_matrix) - X_dim = lifter.get_dim_X(var_list) - fig, ax = plt.subplots() - ax.axvline(X_dim - 0.5, color="red") - im = ax.matshow(plot_matrix, cmap=cmap, norm=norm) - ax.set_title(f"{var_list}, {param_list}") - cax = add_colorbar(fig, ax, im) - if colorbar_yticks is not None: - cax.set_yticklabels(colorbar_yticks) - plt.show(block=False) - return constraints - -
-[docs] - @staticmethod - def get_A_learned( - lifter, - A_known=[], - var_dict=None, - method=METHOD, - verbose=False, - ) -> list: - """Generate list of learned constraints by sampling the lifter. - - :param lifter: StateLifter object - :param A_known: list of known constraints, if given, will generate basis that is orthogonal to these given constraints. - :param var_dict: variable dictionary, if None, will use all variables - :param method: method to use for basis generation, can be 'qr', 'qrp', or 'svd'. 'qrp' is recommended. - :param verbose: if True, will print timing information - - :return: list of learned constraints. - """ - import time - - t1 = time.time() - Y = AutoTight.generate_Y(lifter, var_subset=var_dict, factor=1.0) - if verbose: - print(f"generate Y ({Y.shape}): {time.time() - t1:4.4f}") - t1 = time.time() - basis, S = AutoTight.get_basis(lifter, Y, A_known=A_known, method=method) - if verbose: - print(f"get basis ({basis.shape})): {time.time() - t1:4.4f}") - t1 = time.time() - A_learned = AutoTight.generate_matrices(lifter, basis, var_dict=var_dict) - if verbose: - print(f"get matrices ({len(A_learned)}): {time.time() - t1:4.4f}") - return A_learned
- - - @staticmethod - def get_A_learned_simple( - lifter, - A_known=[], - var_dict=None, - method=METHOD, - verbose=False, - ) -> list: - """Simplified version of get_A_learned that does not consider parameters.""" - import time - - t1 = time.time() - Y = AutoTight.generate_Y_simple(lifter, var_subset=var_dict, factor=1.5) - if verbose: - print(f"generate Y ({Y.shape}): {time.time() - t1:4.4f}") - t1 = time.time() - if len(A_known): - basis_known = np.vstack( - [ - np.asarray(get_vec(Ai.get_matrix(var_dict))) - for Ai in A_known - if get_vec(Ai.get_matrix(var_dict)) is not None - ] - ).T - else: - basis_known = None - basis, S = AutoTight.get_basis( - lifter, Y, basis_known=basis_known, method=method - ) - if verbose: - print(f"get basis ({basis.shape})): {time.time() - t1:4.4f}") - t1 = time.time() - A_learned = AutoTight.generate_matrices_simple(lifter, basis, var_dict=var_dict) - if verbose: - print(f"get matrices ({len(A_learned)}): {time.time() - t1:4.4f}") - return A_learned - - @staticmethod - def generate_Y_simple(lifter, var_subset, factor): - # need at least dim_Y different random setups - dim_Y = lifter.get_dim_X(var_subset) - n_seeds = int(dim_Y * factor) - Y = np.empty((n_seeds, dim_Y)) - for seed in range(n_seeds): - np.random.seed(seed) - - theta = lifter.sample_theta() - x = lifter.get_x(theta=theta, parameters=None, var_subset=var_subset) - X = np.outer(x, x) - Y[seed, :] = get_vec(X) - return Y - - @staticmethod - def generate_Y_sparse(lifter, var_subset, param_subset, factor=FACTOR, ax=None): - from popr.base_lifters import StateLifter - - assert isinstance(lifter, StateLifter) - assert lifter.HOM in param_subset - - # need at least dim_Y different random setups - dim_Y = lifter.get_dim_Y(var_subset, param_subset) - n_seeds = int(dim_Y * factor) - Y = np.empty((n_seeds, dim_Y)) - for seed in range(n_seeds): - np.random.seed(seed) - - theta = lifter.sample_theta() - parameters = lifter.sample_parameters(theta) - - if seed < 10 and ax is not None: - if np.ndim(lifter.theta) == 1: - ax.scatter(np.arange(len(theta)), theta) - else: - ax.scatter(*theta[:, :2].T) - - x = lifter.get_x(theta=theta, parameters=parameters, var_subset=var_subset) - X = np.outer(x, x) - - # generates [1*x, a1*x, ..., aK*x] - p = lifter.get_p(parameters=parameters, param_subset=param_subset) - Y[seed, :] = np.kron(p, get_vec(X)) - return Y - - @staticmethod - def generate_Y(lifter, factor=FACTOR, ax=None, var_subset=None, param_subset=None): - # need at least dim_Y different random setups - dim_Y = lifter.get_dim_Y(var_subset, param_subset) - n_seeds = int(dim_Y * factor) - Y = np.empty((n_seeds, dim_Y)) - for seed in range(n_seeds): - np.random.seed(seed) - - theta = lifter.sample_theta() - parameters = lifter.sample_parameters(theta) - if seed < 10 and ax is not None: - if np.ndim(lifter.theta) == 1: - ax.scatter(np.arange(len(theta)), theta) - else: - ax.scatter(*theta[:, :2].T) - - x = lifter.get_x(theta=theta, parameters=parameters, var_subset=var_subset) - X = np.outer(x, x) - - # generates [1*x, a1*x, ..., aK*x] - p = lifter.get_p(parameters=parameters, param_subset=param_subset) - assert p[0] == 1 - Y[seed, :] = np.kron(p, get_vec(X)) - return Y - - @staticmethod - def get_basis( - lifter, - Y, - A_known: list = [], - basis_known: np.ndarray | None = None, - method=METHOD, - eps_svd=None, - ): - """Generate basis from lifted state matrix Y. - - :param A_known: if given, will generate basis that is orthogonal to these given constraints. - - :return: basis, S - """ - if eps_svd is None: - eps_svd = AutoTight.EPS_SVD - - # if there is a known list of constraints, add them to the Y so that resulting nullspace is orthogonal to them - if basis_known is not None: - if len(A_known): - print( - "Warning: ignoring given A_known because basis_all is also given." - ) - Y = np.vstack([Y, basis_known.T]) - elif len(A_known): - A = np.vstack( - [lifter.augment_using_zero_padding(get_vec(a)) for a in A_known] - ) - Y = np.vstack([Y, A]) - - basis, info = get_nullspace(Y, method=method, tolerance=eps_svd) - - basis[np.abs(basis) < lifter.EPS_SPARSE] = 0.0 - return basis, info["values"] - - @staticmethod - def generate_matrices_simple( - lifter, - basis, - normalize=NORMALIZE, - sparse=True, - trunc_tol=1e-10, - var_dict=None, - ): - """ - Generate constraint matrices from the rows of the nullspace basis matrix. - """ - try: - n_basis = len(basis) - except Exception: - n_basis = basis.shape[0] - - if isinstance(var_dict, list): - var_dict = lifter.get_var_dict(var_dict) - - from popr.base_lifters import StateLifter - - assert isinstance(lifter, StateLifter) - - A_list = [] - for i in range(n_basis): - ai = basis[i] - Ai = lifter.get_mat(ai, sparse=sparse, correct=True, var_dict=None) - # Normalize the matrix - if normalize and not sparse: - # Ai /= np.max(np.abs(Ai)) - assert isinstance(Ai, np.ndarray) - Ai /= np.linalg.norm(Ai, p=2) # type: ignore - elif normalize and sparse: - Ai /= splinalg.norm(Ai, ord="fro") - # Sparsify and truncate - if sparse: - Ai.eliminate_zeros() # type: ignore - else: - Ai[np.abs(Ai) < trunc_tol] = 0.0 # type: ignore - # add to list - A_list.append(Ai) - return A_list - - @staticmethod - def generate_matrices( - lifter, - basis, - normalize=NORMALIZE, - sparse=True, - trunc_tol=1e-10, - var_dict=None, - ): - """ - Generate constraint matrices from the rows of the nullspace basis matrix. - """ - from popr.base_lifters import StateLifter - - assert isinstance(lifter, StateLifter) - - try: - n_basis = len(basis) - except Exception: - n_basis = basis.shape[0] - - if isinstance(var_dict, list): - var_dict = lifter.get_var_dict(var_dict) - - A_list = [] - basis_reduced = [] - for i in range(n_basis): - ai = lifter.get_reduced_a(bi=basis[i], var_subset=var_dict, sparse=True) - basis_reduced.append(ai) - basis_reduced = sp.vstack(basis_reduced) - - if AutoTight.REDUCE_DEPENDENT: - bad_idx = find_dependent_columns(basis_reduced.T, tolerance=1e-6) - else: - bad_idx = [] - - for i in range(basis_reduced.shape[0]): # type: ignore - if i in bad_idx: - continue - ai = basis_reduced[[i], :].toarray().flatten() # type: ignore - Ai = lifter.get_mat(ai, sparse=sparse, correct=True, var_dict=None) - # Normalize the matrix - if normalize and not sparse: - # Ai /= np.max(np.abs(Ai)) - Ai /= np.linalg.norm(Ai, p=2) # type: ignore - elif normalize and sparse: - Ai /= splinalg.norm(Ai, ord="fro") - # Sparsify and truncate - if sparse: - Ai.eliminate_zeros() # type: ignore - else: - Ai[np.abs(Ai) < trunc_tol] = 0.0 # type: ignore - # add to list - A_list.append(Ai) - return A_list - -
-[docs] - @staticmethod - def get_duality_gap(cost_local, cost_sdp): - return (cost_local - cost_sdp) / abs(cost_sdp)
-
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/base_lifters/_base_class.html b/docs/build/_modules/popr/base_lifters/_base_class.html deleted file mode 100644 index a3f9dc4..0000000 --- a/docs/build/_modules/popr/base_lifters/_base_class.html +++ /dev/null @@ -1,884 +0,0 @@ - - - - - - - - popr.base_lifters._base_class — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.base_lifters._base_class

-"""
-The BaseClass contains all the functionalities that are required by StateLifter but that
-are tedious and uninteresting, such as converting between different formats.
-
-Ideally, the user never has to look at this code.
-"""
-
-import itertools
-from abc import abstractmethod
-from collections.abc import Iterable
-
-import numpy as np
-import scipy.sparse as sp
-from poly_matrix import PolyMatrix, unroll
-
-from popr.utils.common import create_symmetric, get_labels, get_vec
-from popr.utils.constraint import Constraint, remove_dependent_constraints
-
-
-class BaseClass(object):
-    # Homogenization variable name
-    HOM = "h"
-
-    # set elements below this threshold to zero.
-    EPS_SPARSE = 1e-9
-
-    # properties of template scaling
-    ALL_PAIRS = True
-    # Below only have effect if ALL_PAIRS is False.
-    # Then, they determine the clique size hierarchy.
-    CLIQUE_SIZE = 5
-    STEP_SIZE = 1
-
-    # tolerance for feasibility error of learned constraints
-    EPS_ERROR = 1e-8
-
-    PARAM_LEVELS = ["no", "p", "ppT"]
-
-    @property
-    @abstractmethod
-    def var_dict(self) -> dict:
-        pass
-
-    @property
-    @abstractmethod
-    def param_dict(self) -> dict:
-        pass
-
-    @abstractmethod
-    def sample_theta(self) -> np.ndarray:
-        pass
-
-    @abstractmethod
-    def sample_parameters(self) -> np.ndarray:
-        pass
-
-    @staticmethod
-    def get_variable_indices(var_subset, variable="z"):
-        return np.unique(
-            [int(v.split("_")[-1]) for v in var_subset if v.startswith(f"{variable}_")]
-        )
-
-    def __init__(self, d, param_level, n_parameters):
-        assert param_level in self.PARAM_LEVELS
-        self.param_level = param_level
-        self.d = d
-
-        if param_level == "no":
-            self.n_parameters = 1
-        elif param_level in ["p", "ppT"]:
-            self.n_parameters = n_parameters
-        else:
-            raise ValueError(f"Unknown param_level: {param_level}")
-        # n_parameters * (self.d + (self.d * (self.d + 1)) // 2)
-        self.generate_random_setup()
-
-    ### Functionalities related to var_dict
-    def get_var_dict(self, var_subset=None, unroll_keys=False):
-        if var_subset is not None:
-            var_dict = {k: v for k, v in self.var_dict.items() if k in var_subset}
-            if unroll_keys:
-                return unroll(var_dict)
-            else:
-                return var_dict
-        if unroll_keys:
-            return unroll(self.var_dict)
-        return self.var_dict
-
-    def get_param_dict(self, param_subset=None):
-        if param_subset is not None:
-            return {k: v for k, v in self.param_dict.items() if k in param_subset}
-        return self.param_dict
-
-    ### Functionalities related to random setups
-    @property
-    def theta(self):
-        if self.theta_ is None:
-            self.theta_ = self.sample_theta()
-        return self.theta_
-
-    @theta.setter
-    def theta(self, t):
-        assert (
-            self.theta_ is None
-        ), "The property self.theta is only meant to be set once!"
-        self.theta_ = t
-
-    @property
-    def parameters(self) -> dict:
-        if self.parameters_ is None:
-            self.parameters_ = self.sample_parameters()
-            assert isinstance(self.parameters_, dict)
-        return self.parameters_  # type: ignore
-
-    @parameters.setter
-    def parameters(self, p):
-        assert (
-            self.parameters_ is None
-        ), "The property self.parameters is only meant to be set once!"
-        assert isinstance(p, dict)
-        self.parameters_ = p
-
-    def extract_A_known(self, A_known, var_subset, output_type="csc"):
-        """
-        Extract from the list of constraint matrices only the ones that
-        touch only a subset of var_subset.
-        """
-        if output_type == "dense":
-            sub_A_known = np.empty((0, self.get_dim_Y(var_subset)))
-        else:
-            sub_A_known = []
-        for A in A_known:
-            A_poly, var_dict = PolyMatrix.init_from_sparse(A, self.var_dict)
-
-            assert len(A_poly.get_variables()) > 0
-
-            # if all of the non-zero elements of A_poly are in var_subset,
-            # we can use this matrix.
-            if np.all([v in var_subset for v in A_poly.get_variables()]):
-                Ai = A_poly.get_matrix(
-                    self.get_var_dict(var_subset), output_type=output_type
-                )
-                if output_type == "dense":
-                    ai = self.augment_using_zero_padding(
-                        get_vec(Ai, correct=True), var_subset
-                    )
-                    sub_A_known = np.r_[sub_A_known, ai[None, :]]
-                else:
-                    assert isinstance(sub_A_known, list)
-                    sub_A_known.append(Ai)
-        return sub_A_known
-
-    def get_param_idx_dict(self, var_subset=None):
-        """
-        Give the current subset of variables, extract the parameter dictionary to use.
-        Example:
-            var_subset = ['z_0', 'x_1']
-            -> Parameters to include:  self.HOM (always), p_0, p_1
-        - if param_level == 'no': {'l': 0}
-        - if param_level == 'p': {'l': 0, 'p_0:0': 1, ..., 'p_0:d-1': d}
-        - if param_level == 'ppT': {'l': 0, 'p_0:0.p_0:0': 1, ..., 'p_0:d-1:.p_0:d-1': 1}
-        """
-        # TODO(FD) change to use  param_subset instead.
-        param_subset = [self.HOM] + [
-            f"p_{i}" for i in self.get_variable_indices(var_subset)
-        ]
-        return unroll(self.get_param_dict(param_subset))
-
-    def get_mat(self, vec, sparse=False, var_dict=None, correct=True):
-        """Convert (N+1)N/2 vectorized matrix to NxN Symmetric matrix in a way that preserves inner products.
-
-        In particular, this means that we divide the off-diagonal elements by sqrt(2).
-
-        :param vec (ndarray): vector of upper-diagonal elements
-        :return: symmetric matrix filled with vec.
-        """
-        # len(vec) = k = n(n+1)/2 -> dim_x = n =
-        if var_dict is None:
-            pass
-
-        elif not isinstance(var_dict, dict):
-            var_dict = {k: v for k, v in self.var_dict.items() if k in var_dict}
-
-        Ai = create_symmetric(
-            vec, correct=correct, sparse=sparse, eps_sparse=self.EPS_SPARSE
-        )
-        if var_dict is None:
-            return Ai
-
-        # if var_dict is not None, then Ai corresponds to the subblock
-        # defined by var_dict, of the full constraint matrix.
-        Ai_poly, __ = PolyMatrix.init_from_sparse(Ai, var_dict, unfold=True)
-        from poly_matrix.poly_matrix import augment
-
-        augment_var_dict = augment(self.var_dict)
-        all_var_dict = {key[2]: 1 for key in augment_var_dict.values()}
-        return Ai_poly.get_matrix(all_var_dict)
-
-    def var_list_row(
-        self, var_subset=None, param_subset=None, force_parameters_off=False
-    ):
-        if var_subset is None:
-            var_subset = list(self.var_dict.keys())
-        elif isinstance(var_subset, dict):
-            var_subset = list(var_subset.keys())
-
-        if param_subset is None:
-            param_subset = self.param_dict
-
-        label_list = []
-        if force_parameters_off:
-            param_dict = {self.HOM: 0}
-        else:
-            param_dict = unroll(
-                self.get_param_dict(param_subset)
-            )  # self.get_param_idx_dict(var_subset)
-
-        for idx, key in enumerate(param_dict.keys()):
-            for i in range(len(var_subset)):
-                zi = var_subset[i]
-                sizei = self.var_dict[zi]
-                for di in range(sizei):
-                    keyi = f"{zi}:{di}" if sizei > 1 else f"{zi}"
-                    for j in range(i, len(var_subset)):
-                        zj = var_subset[j]
-                        sizej = self.var_dict[zj]
-                        if zi == zj:
-                            djs = range(di, sizej)
-                        else:
-                            djs = range(sizej)
-
-                        for dj in djs:
-                            keyj = f"{zj}:{dj}" if sizej > 1 else f"{zj}"
-                            label_list.append(f"{key}-{keyi}.{keyj}")
-            # for zi, zj in vectorized_var_list:
-            # label_list += self.get_labels(key, zi, zj)
-            assert len(label_list) == (idx + 1) * self.get_dim_X(var_subset)
-        return label_list
-
-    def var_dict_row(self, var_subset=None, force_parameters_off=False):
-        return {
-            label: 1
-            for label in self.var_list_row(
-                var_subset, force_parameters_off=force_parameters_off
-            )
-        }
-
-    def get_basis_from_poly_rows(self, basis_poly_list, var_subset=None):
-        var_dict = self.get_var_dict(var_subset=var_subset)
-
-        all_dict = {label: 1 for label in self.var_list_row(var_subset)}
-        basis_reduced = np.empty((0, len(all_dict)))
-        for i, bi_poly in enumerate(basis_poly_list):
-            # test that this constraint holds
-
-            bi = bi_poly.get_matrix(([self.HOM], all_dict))
-
-            if bi.shape[1] == self.get_dim_X(var_subset) * self.get_dim_P():
-                ai = self.get_reduced_a(bi, var_subset=var_subset)
-                Ai = self.get_mat(ai, var_dict=var_dict)
-            elif bi.shape[1] == self.get_dim_X():
-                Ai = self.get_mat(bi, var_dict=var_subset)
-
-            err, idx = self.test_constraints([Ai], errors="print")
-            if len(idx):
-                print(f"found b{i} has error: {err}")
-                continue
-
-            # test that this constraint is lin. independent of previous ones.
-            basis_reduced_test = np.vstack([basis_reduced, bi.toarray()])
-            rank = np.linalg.matrix_rank(basis_reduced_test)
-            if rank == basis_reduced_test.shape[0]:
-                basis_reduced = basis_reduced_test
-            else:
-                print(f"b{i} is linearly dependant after factoring out parameters.")
-        print(f"left with {basis_reduced.shape} total constraints")
-        return basis_reduced
-
-    def convert_polyrow_to_Apoly(self, poly_row, correct=True):
-        """Convert poly-row to reduced a.
-
-        poly-row has elements with keys "pk:l.xi:m.xj:n",
-        meaning this element corresponds to the l-th element of parameter i,
-        and the m-n-th element of xj times xk.
-        """
-        parameters = self.get_p()
-        param_dict = dict(zip(unroll(self.param_dict), parameters))
-
-        poly_mat = PolyMatrix(symmetric=True)
-        for key in poly_row.variable_dict_j:
-            param, var_keys = key.split("-")
-            param_val = param_dict[param]
-
-            keyi_m, keyj_n = var_keys.split(".")
-            m = keyi_m.split(":")[-1]
-            n = keyj_n.split(":")[-1]
-
-            # divide off-diagonal elements by sqrt(2)
-            newval = poly_row[self.HOM, key] * param_val
-            if correct and not ((keyi_m == keyj_n) and (m == n)):
-                newval /= np.sqrt(2)
-
-            poly_mat[keyi_m, keyj_n] += newval
-        return poly_mat
-
-    def convert_polyrow_to_Asparse(self, poly_row, var_subset=None):
-        poly_mat = self.convert_polyrow_to_Apoly(poly_row, correct=False)
-
-        var_dict = self.get_var_dict(var_subset)
-        mat_var_list = []
-        for var, size in var_dict.items():
-            if size == 1:
-                mat_var_list.append(var)
-            else:
-                mat_var_list += [f"{var}:{i}" for i in range(size)]
-        mat_sparse = poly_mat.get_matrix({m: 1 for m in mat_var_list})
-        return mat_sparse
-
-    def convert_polyrow_to_a(self, poly_row, var_subset=None, sparse=False):
-        """Convert poly-row to reduced a.
-
-        poly-row has elements with keys "pk:l.xi:m.xj:n",
-        meaning this element corresponds to the l-th element of parameter i,
-        and the m-n-th element of xj times xk.
-        """
-        mat_sparse = self.convert_polyrow_to_Asparse(poly_row, var_subset)
-        return get_vec(mat_sparse, correct=False, sparse=sparse)
-
-    # TODO(FD) consider removing this cause only used in tests.
-    def convert_a_to_polyrow(
-        self,
-        a,
-        var_subset=None,
-    ) -> PolyMatrix:
-        """Convert a array to poly-row."""
-        if var_subset is None:
-            var_subset = self.var_dict
-        var_dict = self.get_var_dict(var_subset)
-        dim_X = self.get_dim_X(var_subset)
-
-        try:
-            dim_a = len(a)
-        except Exception:
-            dim_a = a.shape[1]
-        assert dim_a == dim_X
-
-        mat = create_symmetric(a, sparse=True, eps_sparse=self.EPS_SPARSE)
-        poly_mat, __ = PolyMatrix.init_from_sparse(mat, var_dict)
-        poly_row = PolyMatrix(symmetric=False)
-        for keyi, keyj in itertools.combinations_with_replacement(var_dict, 2):
-            if keyi in poly_mat.matrix and keyj in poly_mat.matrix[keyi]:
-                val = poly_mat.matrix[keyi][keyj]
-                labels = get_labels(self.HOM, keyi, keyj, self.var_dict)
-                if keyi != keyj:
-                    vals = val.flatten()
-                else:
-                    # TODO: use get_vec instead?
-                    vals = val[np.triu_indices(val.shape[0])]
-                assert len(labels) == len(vals)
-                for label, v in zip(labels, vals):
-                    if np.any(np.abs(v) > self.EPS_SPARSE):
-                        poly_row[self.HOM, label] = v
-        return poly_row
-
-    def convert_b_to_polyrow(
-        self, b, var_subset, param_subset=None, tol=1e-10
-    ) -> PolyMatrix:
-        """Convert (augmented) b array to poly-row."""
-        if isinstance(b, PolyMatrix):
-            raise NotImplementedError(
-                "can't call convert_b_to_polyrow with PolyMatrix yet."
-            )
-
-        assert len(b) == self.get_dim_Y(var_subset, param_subset)
-        poly_row = PolyMatrix(symmetric=False)
-        mask = np.abs(b) > tol
-
-        # get the variable names such as p_0:0-x:0.x:4 whch corresponds to p_0[0]*x[0]*x[4]
-        var_list_row = self.var_list_row(var_subset, param_subset)
-        assert len(b) == len(var_list_row)
-
-        for idx in np.where(mask == True)[0]:
-            poly_row[self.HOM, var_list_row[idx]] = b[idx]
-        return poly_row
-
-    def get_dim_x(self, var_subset=None):
-        var_dict = self.get_var_dict(var_subset)
-        return sum([val for val in var_dict.values()])
-
-    def get_dim_p(self, param_subset=None):
-        param_dict = self.get_param_dict(param_subset)
-        return sum([val for val in param_dict.values()])
-
-    def get_dim_Y(self, var_subset=None, param_subset=None):
-        dim_X = self.get_dim_X(var_subset=var_subset)
-        dim_P = self.get_dim_P(param_subset=param_subset)
-        return int(dim_X * dim_P)
-
-    def get_dim_X(self, var_subset=None):
-        dim_x = self.get_dim_x(var_subset)
-        return int(dim_x * (dim_x + 1) / 2)
-
-    def get_dim_P(self, param_subset=None):
-        return len(self.get_p(param_subset=param_subset))
-
-    def get_reduced_a(
-        self, bi, param_here=None, var_subset=None, param_subset=None, sparse=False
-    ):
-        """
-        Extract first block of bi by summing over other blocks times the parameters.
-        """
-        if param_here is None:
-            param_here = self.get_p(param_subset=param_subset)
-
-        if isinstance(bi, np.ndarray):
-            len_b = len(bi)
-        elif isinstance(bi, PolyMatrix):
-            bi = bi.get_matrix(([self.HOM], self.var_dict_row(var_subset)))
-            len_b = bi.shape[1]  # type: ignore
-        else:
-            # bi can be a scipy sparse matrix,
-            len_b = bi.shape[1]
-
-        n_params = self.get_dim_P(param_subset=param_subset)
-        dim_X = self.get_dim_X(var_subset)
-        n_parts = len_b / dim_X
-        assert (
-            n_parts == n_params
-        ), f"{len_b} does not not split in dim_P={n_params} parts of size dim_X={dim_X}"
-
-        ai = np.zeros(dim_X)
-        for i, p in enumerate(param_here):
-            if isinstance(bi, np.ndarray):
-                ai += p * bi[i * dim_X : (i + 1) * dim_X]
-            elif isinstance(bi, sp.csc_array):
-                ai += p * bi[0, i * dim_X : (i + 1) * dim_X].toarray().flatten()
-            else:
-                raise ValueError("untreated case:", type(bi))
-        if sparse:
-            ai_sparse = sp.csr_array(ai[None, :])
-            ai_sparse.eliminate_zeros()
-            return ai_sparse
-        else:
-            return ai
-
-    def augment_using_zero_padding(self, ai, param_subset=None):
-        n_parameters = self.get_dim_P(param_subset)
-        return np.hstack([ai, np.zeros((n_parameters - 1) * len(ai))])
-
-    def augment_using_parameters(self, x, param_subset=None):
-        p = self.get_p(param_subset)
-        return np.kron(p, x)
-
-    def compute_Ai(self, templates, var_dict, param_dict):
-        """
-        Take all elements from the list of templates and apply them
-        to the given pair of var_list and param_list.
-        """
-
-        A_list = []
-        for k, template in enumerate(templates):
-            assert template.b_ is not None
-            assert isinstance(template, Constraint)
-            # First, we find the current parameters, so that we can factor
-            # them into b and compute a from it.
-            p_here = self.get_p(param_subset=param_dict)
-
-            # We need to partition the vector b into its subblocks
-            # so that we can compute a from it.
-            X_dim = self.get_dim_X(template.mat_var_dict)
-            assert self.get_dim_X(var_dict) == X_dim
-            p_dim = self.get_dim_p(template.mat_param_dict)
-            assert self.get_dim_p(param_dict) == p_dim
-            n_blocks = int(len(template.b_) / X_dim)
-            assert n_blocks == p_dim
-
-            a = p_here[0] * template.b_[:X_dim]
-            for i in range(n_blocks - 1):
-                a += p_here[i + 1] * template.b_[(i + 1) * X_dim : (i + 2) * X_dim]
-
-            if not np.any(np.abs(a) > self.EPS_SPARSE):
-                print(f"matrix {k} is zero")
-                continue
-
-            a_test = self.get_reduced_a(
-                template.b_,
-                var_subset=template.mat_var_dict,
-                param_subset=template.mat_param_dict,
-                param_here=p_here,
-                sparse=False,
-            )
-            assert isinstance(a_test, np.ndarray)
-            np.testing.assert_allclose(a, a_test)
-
-            # Get a symmetric matrix where the upper and lower parts have been filled with a,
-            # and applying the correction to the diagonal.
-            # Note that we do not set var_dict because otherwise A would already
-            # be the zero-padded large matrix.
-            A = self.get_mat(a, sparse=True, correct=True)
-
-            # Get the corresponding PolyMatrix.
-            A_poly, __ = PolyMatrix.init_from_sparse(
-                A,
-                var_dict=self.get_var_dict(var_dict),
-                symmetric=True,
-                unfold=False,
-            )
-            A_list.append(A_poly)
-        return A_list
-
-    def get_constraint_rank(self, A_list_poly, output_sorted=False):
-        """Find the number of independent constraints when they are of the form A_i @ x = 0.
-
-        :param A_list_poly: list of constraints matrices
-
-        :return: rank (int) and sorted matrices (list, if output_sorted=True) where the first rank matrices correspond to
-                 linearly independent matrices.
-        """
-        x = self.get_x()
-
-        current_rank = 0
-        independent_indices = []
-        dependent_indices = []
-        basis_incremental = np.zeros((len(x), 0))
-        for i, Ai in enumerate(A_list_poly):
-            if isinstance(Ai, PolyMatrix):
-                new_candidate = (Ai.get_matrix(self.var_dict) @ x).reshape((-1, 1))
-            else:
-                new_candidate = (Ai @ x).reshape((-1, 1))
-            basis_candidate = np.hstack([basis_incremental, new_candidate])
-            new_rank = np.linalg.matrix_rank(basis_candidate)
-            if new_rank == current_rank + 1:
-                independent_indices.append(i)
-                basis_incremental = basis_candidate
-                current_rank = new_rank
-            else:
-                dependent_indices.append(i)
-        if not output_sorted:
-            return current_rank
-        A_list_sorted = [A_list_poly[i] for i in independent_indices] + [
-            A_list_poly[i] for i in dependent_indices
-        ]
-        return current_rank, A_list_sorted
-
-    def apply_template(self, bi_poly, n_parameters=None, verbose=False):
-        if n_parameters is None:
-            n_parameters = len(self.parameters)
-
-        new_poly_rows = []
-        # find the number of variables that this constraint touches.
-        unique_idx = set()
-        for key in bi_poly.variable_dict_j:
-            param, var_keys = key.split("-")
-            vars = var_keys.split(".")
-            vars += param.split(".")
-            for var in vars:
-                var_base = var.split(":")[0]
-                if "_" in var_base:
-                    i = int(var_base.split("_")[-1])
-                    unique_idx.add(i)
-
-        if len(unique_idx) == 0:
-            return [bi_poly]
-        elif len(unique_idx) > 3:
-            raise ValueError("unexpected triple dependencies!")
-
-        variable_indices = self.get_variable_indices(self.var_dict)
-        # if z_0 is in this constraint, repeat the constraint for each landmark.
-        for idx in itertools.combinations(variable_indices, len(unique_idx)):
-            new_poly_row = PolyMatrix(symmetric=False)
-            for key in bi_poly.variable_dict_j:
-                # need intermediate variables cause otherwise z_0 -> z_1 -> z_2 etc. can happen.
-                key_ij = key
-                for from_, to_ in zip(unique_idx, idx):
-                    key_ij = key_ij.replace(f"x_{from_}", f"xi_{to_}")
-                    key_ij = key_ij.replace(f"w_{from_}", f"wi_{to_}")
-                    key_ij = key_ij.replace(f"z_{from_}", f"zi_{to_}")
-                    key_ij = key_ij.replace(f"p_{from_}", f"pi_{to_}")
-                key_ij = (
-                    key_ij.replace("zi", "z")
-                    .replace("pi", "p")
-                    .replace("xi", "x")
-                    .replace("wi", "w")
-                )
-                if verbose and (key != key_ij):
-                    print("changed", key, "to", key_ij)
-
-                try:
-                    params = key_ij.split("-")[0]
-                    pi, pj = params.split(".")
-                    pi, di = pi.split(":")
-                    pj, dj = pj.split(":")
-                    if pi == pj:
-                        if not (int(dj) >= int(di)):
-                            raise IndexError(
-                                "something went wrong in augment_basis_list"
-                            )
-                except ValueError as e:
-                    pass
-                new_poly_row[self.HOM, key_ij] = bi_poly[self.HOM, key]
-            new_poly_rows.append(new_poly_row)
-        return new_poly_rows
-
-    def apply_templates(
-        self, templates, starting_index=0, var_dict=None, all_pairs=None
-    ):
-
-        if all_pairs is None:
-            all_pairs = self.ALL_PAIRS
-        if var_dict is None:
-            var_dict = self.var_dict
-
-        new_constraints = []
-        index = starting_index
-        for template in templates:
-            constraints = self.apply_template(template.polyrow_b_)
-            template.applied_list = []
-            for new_constraint in constraints:
-                template.applied_list.append(
-                    Constraint.init_from_polyrow_b(
-                        index=index,
-                        polyrow_b=new_constraint,
-                        lifter=self,
-                        template_idx=template.index,
-                        known=template.known,
-                        mat_var_dict=var_dict,
-                    )
-                )
-                new_constraints += template.applied_list
-                index += 1
-
-        if len(new_constraints):
-            remove_dependent_constraints(new_constraints)
-        return new_constraints
-
-    def get_vec_around_gt(self, delta: float = 0):
-        """Sample around ground truth.
-        :param delta: sample from gt + std(delta) (set to 0 to start from gt.)
-        """
-        return self.theta + np.random.normal(size=self.theta.shape, scale=delta)
-
-    def test_constraints(self, A_list, errors: str = "raise", n_seeds: int = 3):
-        """
-        :param A_list: can be either list of sparse matrices, or poly matrices
-        :param errors: "raise" or "print" detected violations.
-        """
-        max_violation = -np.inf
-        j_bad = set()
-
-        for j, A in enumerate(A_list):
-            if isinstance(A, PolyMatrix):
-                A = A.get_matrix(self.get_var_dict(unroll_keys=True))
-
-            for i in range(n_seeds):
-                if i == 0:
-                    x = self.get_x().flatten()
-                else:
-                    np.random.seed(i)
-                    t = self.sample_theta()
-                    p = self.sample_parameters()
-                    x = self.get_x(theta=t, parameters=p).flatten()
-
-                constraint_violation = abs(float(x.T @ A @ x))
-                max_violation = max(max_violation, constraint_violation)
-                if constraint_violation > self.EPS_ERROR:
-                    msg = f"big violation at {j}: {constraint_violation:.1e}"
-                    j_bad.add(j)
-                    if errors == "raise":
-                        raise ValueError(msg)
-                    elif errors == "print":
-                        print(msg)
-                    elif errors == "ignore":
-                        pass
-                    else:
-                        raise ValueError(errors)
-        return max_violation, j_bad
-
-    def get_A0(self, var_subset=None):
-        if var_subset is not None:
-            var_dict = {k: self.var_dict[k] for k in var_subset}
-        else:
-            var_dict = self.var_dict
-        A0 = PolyMatrix()
-        A0[self.HOM, self.HOM] = 1.0
-        return A0.get_matrix(var_dict)
-
-    def get_A_b_list(self, A_list, var_subset=None):
-        return [(self.get_A0(var_subset), 1.0)] + [(A, 0.0) for A in A_list]
-
-    def sample_parameters_landmarks(self, landmarks: np.ndarray):
-        """Used by RobustPoseLifter, RangeOnlyLocLifter: the default way of adding landmarks to parameters."""
-        parameters = {self.HOM: 1.0}
-        n_landmarks = landmarks.shape[0]
-
-        if self.param_level == "no":
-            return parameters
-
-        for i in range(n_landmarks):
-            if self.param_level == "p":
-                parameters[f"p_{i}"] = landmarks[i]
-            elif self.param_level == "ppT":
-                parameters[f"p_{i}"] = np.hstack(  # type: ignore
-                    [
-                        landmarks[i],
-                        get_vec(  # type: ignore
-                            np.outer(landmarks[i], landmarks[i]),
-                            correct=False,
-                            sparse=False,
-                        ),
-                    ]
-                )
-        return parameters
-
-    def get_error(self, t) -> dict:
-        err = np.linalg.norm(t - self.theta) ** 2 / self.theta.size
-        return {"MSE": err, "error": err}
-
-    def generate_random_setup(self):
-        if self.parameters is None:
-            self.parameters = self.sample_parameters()
-        if self.theta is None:
-            self.theta = self.sample_theta()
-
-    def get_x(
-        self,
-        theta: np.ndarray | None = None,
-        var_subset: Iterable | None = None,
-        **kwargs,
-    ) -> np.ndarray:
-        """
-        Get the lifted state vector x.
-
-        :param theta: if given, use this theta instead of the ground truth one.
-        :param var_subset: list of parameter keys to use. If None, use all.
-
-        :returns: lifted vector x
-        """
-        if theta is None:
-            theta = self.theta
-        if var_subset is None:
-            var_subset = self.var_dict
-
-        assert theta is not None
-
-        x_data = []
-        for key in var_subset:
-            if key == self.HOM:
-                x_data.append(1.0)
-            else:
-                print(
-                    "Warning: just using theta in x because there is no specific implementation."
-                )
-                x_data += list(theta)
-        return np.hstack(x_data)
-
-    def get_p(
-        self, parameters: dict | None = None, param_subset: dict | list | None = None
-    ):
-        if parameters is None:
-            parameters = self.parameters
-        if param_subset is None:
-            param_subset = self.param_dict
-
-        assert isinstance(parameters, dict)
-
-        p_data = []
-        for key in param_subset:
-            if key == self.HOM:
-                p_data.append(1.0)
-            else:
-                param = parameters[key]
-                if np.ndim(param) == 0:
-                    p_data.append(param)
-                else:
-                    p_data += list(param)
-        return np.array(p_data)
-
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/base_lifters/poly_lifters.html b/docs/build/_modules/popr/base_lifters/poly_lifters.html deleted file mode 100644 index f60fc61..0000000 --- a/docs/build/_modules/popr/base_lifters/poly_lifters.html +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - - popr.base_lifters.poly_lifters — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.base_lifters.poly_lifters

-import numpy as np
-
-from .state_lifter import StateLifter
-
-
-
-[docs] -class PolyLifter(StateLifter): - def __init__(self, degree, param_level="no"): - """Simple univariate polynomial lifter, mostly for testing and pedagogical purposes.""" - self.degree = degree - super().__init__(d=1, param_level=param_level) - - @property - def var_dict(self): - if self.var_dict_ is None: - self.var_dict_ = {self.HOM: 1, "t": 1} - self.var_dict_.update({f"z{i}": 1 for i in range(self.M)}) - return self.var_dict_ - - @property - def M(self): - return self.degree // 2 - 1 - - def sample_theta(self): - return np.random.rand(1) - - def get_error(self, t): - return {"MAE": float(abs(self.theta - t)), "error": float(abs(self.theta - t))} - - def get_x(self, theta=None, parameters=None, var_subset=None): - if theta is None: - theta = self.theta - return np.array([theta**i for i in range(self.degree // 2 + 1)]) - - def get_cost(self, theta, *args, **kwargs) -> float: - Q = self.get_Q() - x = self.get_x(theta).flatten() - return float(x.T @ Q @ x) - - def get_hess(self, *args, **kwargs): - raise NotImplementedError - - def local_solver(self, t0, *args, **kwargs): - from scipy.optimize import minimize - - sol = minimize(self.get_cost, t0) - info = {"success": sol.success} - return sol.x, info, sol.fun - - def plot(self, thetas, label=None): - from popr.utils.plotting_tools_poly import coordinate_system - - fig, ax = coordinate_system() - ys = [self.get_cost(t) for t in thetas] - ax.plot(thetas, ys, label=label) - ymin = min(-max(ys) / 3, min(ys)) - ax.set_ylim(ymin, max(ys)) - return fig, ax - - def __repr__(self): - return f"poly{self.degree}"
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/base_lifters/robust_pose_lifter.html b/docs/build/_modules/popr/base_lifters/robust_pose_lifter.html deleted file mode 100644 index 1700557..0000000 --- a/docs/build/_modules/popr/base_lifters/robust_pose_lifter.html +++ /dev/null @@ -1,639 +0,0 @@ - - - - - - - - popr.base_lifters.robust_pose_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.base_lifters.robust_pose_lifter

-from abc import ABC, abstractmethod
-from copy import deepcopy
-
-import numpy as np
-from poly_matrix.poly_matrix import PolyMatrix
-from scipy.spatial.transform import Rotation as R
-
-from popr.utils.geometry import (
-    get_C_r_from_theta,
-    get_noisy_pose,
-    get_pose_errors_from_theta,
-    get_theta_from_C_r,
-)
-
-from .state_lifter import StateLifter
-
-N_TRYS = 10
-
-METHOD = "CG"
-SOLVER_KWARGS = dict(
-    min_gradient_norm=1e-7, max_iterations=10000, min_step_size=1e-8, verbosity=1
-)
-
-# TODO(FD) we need to add a penalty here, otherwise the local solution is not good.
-# However, the penalty results in inequality constraints etc. and that's not easy to deal with.
-PENALTY_RHO = 10
-PENALTY_U = 1e-3
-
-# the cutoff parameter of least squares. If residuals are >= BETA, they are considered outliers.
-BETA = 0.1
-
-
-
-[docs] -class RobustPoseLifter(StateLifter, ABC): - LEVELS = ["no", "xwT", "xxT"] - PARAM_LEVELS = ["no", "p", "ppT"] - LEVEL_NAMES = {"no": "no", "xwT": "x kron w", "xxT": "x kron x"} - MAX_DIST = 10.0 # maximum of norm of t. - - @property - def VARIABLE_LIST(self): - if not self.robust: - return [[self.HOM, "t", "c"]] - else: - base = [self.HOM, "t", "c"] - return [ - base, - base + ["w_0"], - base + ["z_0"], - base + ["w_0", "w_1"], - base + ["w_0", "z_0"], - base + ["z_0", "z_1"], - # base + ["w_0", "w_1", "z_0"], - # base + ["w_0", "w_1", "z_0", "z_1"], - ] - - # Add any parameters here that describe the problem (e.g. number of landmarks etc.) - def __init__( - self, - n_outliers=0, - level="no", - param_level="no", - d=2, - n_landmarks=3, - variable_list=None, - robust=False, - beta=BETA, - ): - """RobustPoseLifter is a general class for point-to-point, point-to-line, and point-to-plane registration problems, - with starndard or robust loss functions. - - The goal is to regress an unknown pose based on extrinsic measurements. - - See :class:`~popr.examples.WahbaLifter` for point-to-point registration and :class:`~popr.examples.MonoLifter`) for point-to-line registration. - - Implemented lifting functions are: - - - xwT: :math:`x \\otimes w` - - xxT: :math:`x \\otimes x` - """ - self.beta = beta - self.n_landmarks = n_landmarks - - self.robust = robust - self.level = level - if variable_list == "all": - variable_list = self.get_all_variables() - # elif variable_list is None: - # self.variable_list = self.VARIABLE_LIST - - if not robust: - assert level == "no" - - self.landmarks_ = None # will be initialized later - super().__init__( - level=level, - param_level=param_level, - d=d, - variable_list=variable_list, - n_outliers=n_outliers, - robust=robust, - ) - - def penalty(self, t, rho=PENALTY_RHO, u=PENALTY_U): - import autograd.numpy as anp - - try: - return anp.sum( # type: ignore - [rho * u * anp.log10(1 + anp.exp(hi / u)) for hi in self.h_list(t)] # type: ignore - ) - except RuntimeWarning: - u = PENALTY_U * 0.1 - return anp.sum( # type: ignore - [rho * u * anp.log10(1 + anp.exp(hi / u)) for hi in self.h_list(t)] # type: ignore - ) - - @property - def var_dict(self): - """Return key,size pairs of all variables.""" - var_dict = {self.HOM: 1, "t": self.d, "c": self.d**2} - if not self.robust: - return var_dict - - n = self.d**2 + self.d - if self.level == "xwT": - for i in range(self.n_landmarks): - var_dict.update({f"w_{i}": 1, f"z_{i}": n}) - elif self.level == "xxT": - var_dict.update({f"w_{i}": 1 for i in range(self.n_landmarks)}) - var_dict.update({"z_0": n**2}) - return var_dict - - @property - def param_dict(self): - return self.param_dict_landmarks - - def get_all_variables(self): - all_variables = [self.HOM, "t", "c"] - if self.robust: - if self.level == "xxT": - all_variables += [f"w_{i}" for i in range(self.n_landmarks)] - all_variables += ["z_0"] - elif self.level == "xwT": - for i in range(self.n_landmarks): - all_variables += [f"w_{i}", f"z_{i}"] - variable_list = [all_variables] - return variable_list - - def sample_theta(self): - """Generate a random new feasible point.""" - - # make sure random pose is looking at world centre (where landmarks are) - success = False - i = 0 - while not success: - pc_cw = self.get_random_position() - success = np.all(np.array(self.h_list(pc_cw)) <= 0) - if success: - break - i += 1 - if i >= N_TRYS: - raise ValueError("didn't find valid initialization") - - if self.d == 2: - angle = np.random.uniform(0, 2 * np.pi) - C = R.from_euler("z", angle).as_matrix()[:2, :2] - else: - C = R.random().as_matrix() - theta_x = get_theta_from_C_r(C, pc_cw) - - if self.robust: - outlier_index = np.random.choice( - self.n_landmarks, replace=False, size=self.n_outliers - ) - w = np.ones(self.n_landmarks) - w[outlier_index] = -1 - return np.hstack([theta_x, w]) - else: - return theta_x - - @property - def landmarks(self): - if self.landmarks_ is None: - self.landmarks_ = np.random.normal( - loc=0, scale=1.0, size=(self.n_landmarks, self.d) - ) - return self.landmarks_ - - def sample_parameters(self, theta=None): - if self.parameters_ is None: - return self.sample_parameters_landmarks(self.landmarks) - landmarks = np.random.normal(loc=0, scale=1.0, size=(self.n_landmarks, self.d)) - return self.sample_parameters_landmarks(landmarks) - - def get_x(self, theta=None, parameters=None, var_subset=None) -> np.ndarray: - """Get the lifted vector x given theta and parameters.""" - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - if var_subset is None: - var_subset = self.var_dict.keys() - - if self.robust: - theta_here = theta[: -self.n_landmarks] - else: - theta_here = theta - - # RT below is R_cw. (c=camera, w=world) - RT, t = get_C_r_from_theta(theta_here, self.d) - R = RT.T - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - elif key == "t": - x_data += list(t) - elif key == "c": - x_data += list(R.flatten("C")) - elif "w" in key: - j = int(key.split("_")[-1]) - w_j = theta[-self.n_landmarks + j] - x_data.append(w_j) - elif (self.level == "xxT") and (key == "z_0"): - x_vec = list(get_theta_from_C_r(R, t)) - x_data += list(np.kron(x_vec, x_vec).flatten()) - elif (self.level == "xwT") and ("z" in key): - j = int(key.split("_")[-1]) - w_j = theta[-self.n_landmarks + j] - x_vec = get_theta_from_C_r(R, t) - x_data += list(x_vec * w_j) - dim_x = self.get_dim_x(var_subset=var_subset) - assert len(x_data) == dim_x - return np.array(x_data) - - def get_outlier_index(self): - if self.robust: - return np.where(self.theta[-self.n_landmarks :] == -1)[0] - else: - return [] - - def get_error(self, theta_hat): - - theta_hat_pose = theta_hat[: self.d + self.d**2] - theta_gt_pose = self.theta[: self.d + self.d**2] - return get_pose_errors_from_theta(theta_hat_pose, theta_gt_pose, self.d) - - def get_vec_around_gt(self, delta: float = 0): - """Sample around ground truth. - :param delta: sample from gt + std(delta) (set to 0 to start from gt.) - """ - if self.robust: - theta = deepcopy(self.theta[: self.d + self.d**2]) - C, r = get_C_r_from_theta(theta, self.d) - theta_noisy = get_noisy_pose(C, r, delta=delta) - theta_w = self.theta[self.d + self.d**2 :] - return np.r_[theta_noisy, theta_w] - else: - C, r = get_C_r_from_theta(self.theta, self.d) - theta_noisy = get_noisy_pose(C, r, delta=delta) - return theta_noisy - - def get_cost(self, theta, y): - if self.robust: - x = theta[: -self.n_landmarks] - w = theta[-self.n_landmarks :] - assert np.all(w**2 == 1.0) - else: - x = theta - - R, t = get_C_r_from_theta(x, self.d) - - cost = 0 - for i in range(self.n_landmarks): - res = self.residual_sq(R, t, self.landmarks[i], y[i]) - if self.robust: - cost += (1 + w[i]) / self.beta**2 * res + 1 - w[i] - else: - cost += res - return 0.5 * cost - - def local_solver( - self, t0, y, verbose=False, method=METHOD, solver_kwargs=SOLVER_KWARGS - ): - import pymanopt - from pymanopt.manifolds import Euclidean, Product, SpecialOrthogonalGroup - - if method == "CG": - from pymanopt.optimizers import ConjugateGradient as Optimizer # fastest - elif method == "SD": - from pymanopt.optimizers import SteepestDescent as Optimizer # slow - elif method == "TR": - from pymanopt.optimizers import TrustRegions as Optimizer # okay - else: - raise ValueError(method) - - if verbose: - solver_kwargs["verbosity"] = 2 - else: - solver_kwargs["verbosity"] = 0 - - # We assume that we know w! If we wanted to solve for w too we would need - # IRLS or similar. Since we just care about getting the global solution - # with a local sovler that's not necessary. - if self.robust: - w = self.theta[-self.n_landmarks :] - - manifold = Product((SpecialOrthogonalGroup(self.d, k=1), Euclidean(self.d))) - - @pymanopt.function.autograd(manifold) - def cost(R, t): - cost = 0 - for i in range(self.n_landmarks): - residual = self.residual_sq(R, t, self.landmarks[i], y[i]) - if self.robust: - cost += (1 + w[i]) / self.beta**2 * residual + 1 - w[i] - else: - cost += residual - return 0.5 * cost + self.penalty(t) - - @pymanopt.function.autograd(manifold) - def euclidean_gradient_unused(R, t): - grad_R = np.zeros(R.shape) - grad_t = np.zeros(t.shape) - for i in range(self.n_landmarks): - Wi = np.eye(self.d) - np.outer(y[i], y[i]) - # residual = (R @ pi + t).T @ Wi @ (R @ pi + t) - term = self.term_in_norm(R, t, self.landmarks[i], y[i]) - if self.robust: - grad_R += ( - 2 - * w[i] - / self.beta**2 - * np.outer(Wi.T @ term, self.landmarks[i]) - ) - grad_t += 2 * w[i] / self.beta**2 * Wi.T @ term - else: - grad_R += np.outer(Wi.T @ term, self.landmarks[i]) - grad_t += Wi.T @ term - return grad_R, grad_t - - euclidean_gradient = None - problem = pymanopt.Problem( - manifold, cost, euclidean_gradient=euclidean_gradient - ) - optimizer = Optimizer(**solver_kwargs) - - R_0, t_0 = get_C_r_from_theta(t0[: self.d + self.d**2], self.d) - res = optimizer.run(problem, initial_point=(R_0, t_0)) - R, t = res.point - - if verbose: - print("local solver sanity check:") - print("final penalty:", self.penalty(t)) - w = self.theta[-self.n_landmarks :] - for i in range(self.n_landmarks): - residual = self.residual_sq(R, t, self.landmarks[i], y[i]) - if w[i] == -1: - if verbose: - print(f"outlier residual: {residual:.4e}") - assert ( - residual > self.beta - ), f"outlier residual too small: {residual} <= {self.beta}" - else: - if verbose: - print(f"inlier residual: {residual:.4e}") - assert ( - residual < self.beta - ), f"inlier residual too large: {residual} > {self.beta}" - if verbose: - print("qcqp cost:", res.cost) - - if self.robust: - theta_hat = np.r_[get_theta_from_C_r(R, t), w] - else: - theta_hat = get_theta_from_C_r(R, t) - - cost_penalized = res.cost - if self.robust: - pen = self.penalty(t) - if abs(res.cost) > 1e-10: - assert abs(pen) / res.cost <= 1e-1, (pen, res.cost) - cost_penalized -= pen - - success = ("min step_size" in res.stopping_criterion) or ( - "min grad norm" in res.stopping_criterion - ) - info = { - "success": success, - "msg": res.stopping_criterion, - } - if success: - return theta_hat, info, cost_penalized - else: - return None, info, cost_penalized - - def test_and_add(self, A_list, Ai, output_poly): - x = self.get_x() - Ai_sparse = Ai.get_matrix(self.var_dict) - err = x.T @ Ai_sparse @ x - assert abs(err) <= 1e-10, err - if output_poly: - A_list.append(Ai) - else: - A_list.append(Ai_sparse) - - def get_A_known(self, var_dict=None, output_poly=False): - A_list = [] - if var_dict is None: - var_dict = self.var_dict - - if "c" in var_dict: - # enforce diagonal == 1 - for i in range(self.d): - Ei = np.zeros((self.d, self.d)) - Ei[i, i] = 1.0 - constraint = np.kron(Ei, np.eye(self.d)) - Ai = PolyMatrix(symmetric=True) - Ai["c", "c"] = constraint - Ai[self.HOM, self.HOM] = -1 - self.test_and_add(A_list, Ai, output_poly=output_poly) - - # enforce off-diagonal == 0 - for i in range(self.d): - for j in range(i + 1, self.d): - Ei = np.zeros((self.d, self.d)) - Ei[i, j] = 1.0 - Ei[j, i] = 1.0 - constraint = np.kron(Ei, np.eye(self.d)) - Ai = PolyMatrix(symmetric=True) - Ai["c", "c"] = constraint - self.test_and_add(A_list, Ai, output_poly=output_poly) - if self.robust: - for key in var_dict: - if "w" in key: - i = key.split("_")[-1] - Ai = PolyMatrix(symmetric=True) - Ai[self.HOM, self.HOM] = -1.0 - Ai[f"w_{i}", f"w_{i}"] = 1.0 - self.test_and_add(A_list, Ai, output_poly=output_poly) - - # below doesn't hold: w_i*w_j = += 1 - # for key_other in [k for k in var_dict if (k.startswith("w") and (k!= key))]: - # Ai = PolyMatrix(symmetric=True) - # Ai[self.HOM, self.HOM] = -1.0 - # Ai[key, key_other] = 0.5 - # self.test_and_add(A_list, Ai, output_poly=output_poly) - - if "z" in key: - if self.level == "xwT": - i = key.split("_")[-1] - """ each z_i equals x * w_i""" - - for j in range(self.d): - Ai = PolyMatrix(symmetric=True) - constraint = np.zeros((self.d + self.d**2)) - constraint[j] = 1.0 - Ai[self.HOM, f"z_{i}"] = constraint[None, :] - constraint = np.zeros((self.d)) - constraint[j] = -1.0 - Ai[f"t", f"w_{i}"] = constraint[:, None] - self.test_and_add(A_list, Ai, output_poly=output_poly) - - for j in range(self.d**2): - Ai = PolyMatrix(symmetric=True) - constraint = np.zeros((self.d + self.d**2)) - constraint[self.d + j] = 1.0 - Ai[self.HOM, f"z_{i}"] = constraint[None, :] - constraint = np.zeros((self.d**2)) - constraint[j] = -1.0 - Ai[f"c", f"w_{i}"] = constraint[:, None] - self.test_and_add(A_list, Ai, output_poly=output_poly) - return A_list - - def get_B_known(self): - """Get inequality constraints of the form x.T @ B @ x <= 0. - By default, we always add ||t|| <= MAX_DIST - """ - B1 = PolyMatrix(symmetric=True) - B1[self.HOM, self.HOM] = -self.MAX_DIST - B1["t", "t"] = np.eye(self.d) - return [B1.get_matrix(self.var_dict)] - - @abstractmethod - def h_list(self, t): - """ - Any inequality constraints to enforce, returned as a list [h_1(t), h_2(t), ...] - We use the convention h_i(t) <= 0. - - By default, we always add |t| <= MAX_DIST - """ - try: - import autograd.numpy as anp - - return [anp.sqrt(anp.sum(t[: self.d] ** 2)) - self.MAX_DIST] # type: ignore - except ModuleNotFoundError: - return [np.sqrt(np.sum(t[: self.d] ** 2)) - self.MAX_DIST] - - @abstractmethod - def get_random_position(self): - """Generate a new random position. Orientation angles will be drawn uniformly from [0, pi].""" - return None - - @abstractmethod - def term_in_norm(self, R, t, pi, ui) -> np.ndarray: - pass - - @abstractmethod - def residual_sq(self, R, t, pi, ui) -> float: - pass
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/base_lifters/state_lifter.html b/docs/build/_modules/popr/base_lifters/state_lifter.html deleted file mode 100644 index 206bf24..0000000 --- a/docs/build/_modules/popr/base_lifters/state_lifter.html +++ /dev/null @@ -1,365 +0,0 @@ - - - - - - - - popr.base_lifters.state_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.base_lifters.state_lifter

-from abc import abstractmethod
-
-import numpy as np
-
-from ._base_class import BaseClass
-
-
-
-[docs] -class StateLifter(BaseClass): - # sparse hierarchy: define the levels that are implemented - LEVELS = ["no"] - - # used for AutoTemplate - VARIABLE_LIST = [["h"]] - TIGHTNESS = "cost" - - # to be overwritten by inheriting class - NOISE = 1e-2 - - def __init__( - self, - level="no", - param_level="no", - d=2, - variable_list=None, - robust=False, - n_outliers=0, - n_parameters=1, - ): - - # variables that get overwritten upon initialization - self.parameters_ = None - self.theta_ = None - self.var_dict_ = None - self.y_ = None - - self.robust = robust - self.n_outliers = n_outliers - - assert level in self.LEVELS - self.level = level - - if variable_list is not None: - self.variable_list = variable_list - else: - self.variable_list = self.VARIABLE_LIST - - if (param_level != "no") and (n_parameters == 1): - print("Warning: make sure to give the correct n_parameters for the level.") - - super().__init__(d, param_level, n_parameters) - - # MUST OVERWRITE THESE - - @property - def var_dict(self): - raise ValueError("Inheriting class must implement this!") - -
-[docs] - @abstractmethod - def sample_theta(self) -> np.ndarray: - """Randomly sample a feasible state theta. This function must be - implemented by the inheriting class.""" - raise NotImplementedError("need to implement sample_theta")
- - - # MUST OVERWRITE THESE FOR TIGHTNESS CHECKS - -
-[docs] - def get_Q(self, output_poly=False, noise=None): - """Construct the cost matrix Q. - - :param noise: set the noise level, if appropriate. - :param output_poly: if True, return the matrix in PolyMatrix format. - - :returns: the cost matrix as a sparse matrix or PolyMatrix. - """ - raise NotImplementedError( - "Need to impelement get_Q in inheriting class if you want to use it." - )
- - - def get_Q_from_y(self, y): - raise NotImplementedError( - "Need to impelement get_Q_from_y in inheriting class if you want to use it." - ) - -
-[docs] - def get_A_known( - self, - add_redundant: bool = False, - var_dict: dict | None = None, - output_poly: bool = False, - ) -> list: - """Construct the matrices defining the known equality constraints. - - :param add_redundant: if True, add redundant constraints. - :param var_dict: if provided, return only the matrices that involve these variables. - :param output_poly: if True, return the matrices in PolyMatrix format. - """ - return []
- - -
-[docs] - def get_B_known(self) -> list: - """Construct the matrices defining the known inequality constraints.""" - return []
- - - # MUST OVERWRITE THESE FOR ADDING PARAMETERS - -
-[docs] - def sample_parameters(self, theta=None) -> dict: - """Create random set of parameters. By default, there are no parameters - so this function just returns `{self.HOM: 1.0}`.""" - assert ( - self.param_level == "no" - ), "Need to overwrite sample_parameters to use level different than 'no'" - return {self.HOM: 1.0}
- - - @property - def param_dict(self): - assert ( - self.param_level == "no" - ), "Need to overwrite param_dict to use level different than 'no'" - return {self.HOM: 1} - - def get_involved_param_dict(self, var_subset): - """Find which parameters to include, given the current var_subset. Here we implicitly assume - that each parameter is associated with a variable. This is true for parameters that involve - substitution variables.""" - keys = [self.HOM] - for v in var_subset: - index = v.split("_") - if len(index) > 1: - index = int(index[-1]) - key = f"p_{index}" - if key not in keys: - keys.append(key) - return {k: self.param_dict[k] for k in keys if k in self.param_dict} - - # CAN OPTINALLY OVERWRITE THESE FOR BETTER PERFORMANCE - - def get_grad(self, theta, y=None) -> float: - raise NotImplementedError("must define get_grad if you want to use it.") - - def get_hess(self, theta, y=None) -> float: - raise NotImplementedError("must define get_hess if you want to use it.") - -
-[docs] - def get_cost(self, theta, y=None) -> float: - """Compute the cost of the given state theta. This uses the simple form - x.T @ Q @ x. Consider overwriting this for more efficient computations.""" - print( - "Warning: using default get_cost, which may be less efficient than a custom one." - ) - x = self.get_x(theta=theta).flatten("C") - if y is not None: - Q = self.get_Q_from_y(y) - else: - Q = self.get_Q() - return float(x.T @ Q @ x)
- - -
-[docs] - def local_solver(self, t0, y=None, *args, **kwargs): - """ - Default local solver that uses IPOPT to solve the QCQP problem defined by Q and the constraints matrices. - Consider overwriting this for more efficient solvers. - """ - print( - "Warning: using default local_solver, which may be less efficient than a custom one." - ) - print("Ignoring args and kwargs:", args, kwargs) - from cert_tools.sdp_solvers import solve_low_rank_sdp - - if y is not None: - Q = self.get_Q_from_y(y) - else: - Q = self.get_Q() - - B = self.get_B_known() - if len(B) > 0: - raise NotImplementedError( - "Inequality constraints are not currently considered by default solver. Must implement your own." - ) - - Constraints = self.get_A_b_list(A_list=self.get_A_known()) - x0 = self.get_x(theta=t0) - X, info = solve_low_rank_sdp( - Q, Constraints=Constraints, rank=1, verbose=True, x_cand=x0 - ) - # TODO(FD) identify when the solve is not successful. - info["success"] = True - try: - theta = self.get_theta(X[:, 0]) - except AttributeError: - theta = X[1 : 1 + self.d, 0] - return theta, info, info["cost"]
- - - @property - def param_dict_landmarks(self): - assert self.n_parameters is not None - - param_dict = {self.HOM: 1} - if self.param_level == "no": - return param_dict - if self.param_level == "p": - param_dict.update({f"p_{i}": self.d for i in range(self.n_parameters)}) - if self.param_level == "ppT": - # Note that ppT is actually - # [p; vech(ppT)] (linear and quadratic terms) - # TODO(FD): rename ppT to quadratic - param_dict.update( - { - f"p_{i}": self.d + int(self.d * (self.d + 1) / 2) - for i in range(self.n_parameters) - } - ) - return param_dict - -
-[docs] - def get_theta(self, x): - """Inverse of get_x: given lifted vector x, extract elements corresponding to theta.""" - assert np.ndim(x) == 1 or x.shape[1] == 1 - return x.flatten()[1 : 1 + self.d]
-
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/base_lifters/stereo_lifter.html b/docs/build/_modules/popr/base_lifters/stereo_lifter.html deleted file mode 100644 index 8748b96..0000000 --- a/docs/build/_modules/popr/base_lifters/stereo_lifter.html +++ /dev/null @@ -1,610 +0,0 @@ - - - - - - - - popr.base_lifters.stereo_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.base_lifters.stereo_lifter

-from abc import ABC, abstractmethod
-
-import numpy as np
-import scipy.sparse as sp
-from poly_matrix.poly_matrix import PolyMatrix
-
-from popr.utils.geometry import (
-    generate_random_pose,
-    get_C_r_from_theta,
-    get_noisy_pose,
-    get_pose_errors_from_theta,
-    get_T,
-    get_theta_from_C_r,
-)
-
-from .state_lifter import StateLifter
-
-NOISE = 1.0  #
-
-
-SOLVER_KWARGS = dict(
-    min_gradient_norm=1e-6, max_iterations=10000, min_step_size=1e-10, verbosity=1
-)
-
-
-
-[docs] -class StereoLifter(StateLifter, ABC): - """StereoLifter is a general lifter class for the stereo localization problem, supporting both 2D and 3D cases. - - See :class:`~popr.examples.Stereo2DLifter` for 2D and :class:`~popr.examples.Stereo3DLifter` for 3D. - """ - - NORMALIZE = True - - # Levels that were experimented with for creating a tight relaxation. - LEVELS = [ - "no", - "u@u", # ... - "u2", - "u@r", - "uuT", - "urT", - "uxT", - ] - PARAM_LEVELS = ["no", "p", "ppT"] - LEVEL_NAMES = { - "no": "$\\boldsymbol{u}_n$", - "urT": "$\\boldsymbol{u}\\boldsymbol{t}^\\top_n$", - "uxT": "$\\boldsymbol{u}\\boldsymbol{x}^\\top_n$", - } - - EPS_ERROR = 1e-7 # for constraints test. default is 1e-8 - - def __init__( - self, n_landmarks, d, level="no", param_level="no", variable_list=None - ): - self.y_ = None - self.n_landmarks = n_landmarks - self.landmarks_ = None # will be initialized on first access - super().__init__( - d=d, - level=level, - param_level=param_level, - variable_list=variable_list, - n_parameters=n_landmarks, - ) - - @property - @abstractmethod - def M_matrix(self): - raise NotImplementedError("Inheriting class must initialize M_matrix.") - - def get_all_variables(self): - return [[self.HOM, "x"] + [f"z_{i}" for i in range(self.n_landmarks)]] - - def get_level_dims(self, n=1): - """ - :param n: number of landmarks to consider - """ - return { - "no": 0, - "u@u": n, # ... - "u2": n * self.d, - "u@r": n, - "uuT": n * self.d**2, - "urT": n * self.d**2, - "uxT": n * (self.d * (self.d + self.d**2)), - } - - @property - def landmarks(self): - if self.landmarks_ is None: - landmarks = self.generate_random_landmarks(self.theta) - self.landmarks_ = landmarks - return self.landmarks_ - - def generate_random_landmarks(self, theta=None): - if theta is not None: - C, r = get_C_r_from_theta(theta, self.d) - if self.d == 3: - # sample left u, v coordinates in left image, and compute landmark coordinates from that. - fu, cu, b = self.M_matrix[0, [0, 2, 3]] - fv, cv = self.M_matrix[1, [1, 2]] - u = np.random.uniform(0, cu * 2, self.n_landmarks) - v = np.random.uniform(0, cv * 2, self.n_landmarks) - z = np.random.uniform(0, 5, self.n_landmarks) - x = 1 / fu * (z * (u - cu) - b) - y = 1 / fv * z * (v - cv) - points_cam = np.c_[x, y, z] # N x 3 - else: - # sample left u in left image, and compute landmark coordinates from that. - fu, cu, b = self.M_matrix[0, :] - u = np.random.uniform(0, cu * 2, self.n_landmarks) - y = np.random.uniform(1, 5, self.n_landmarks) - x = 1 / fu * (y * (u - cu) - b) - points_cam = np.c_[x, y] - # transform points from camera to world - return (C.T @ (points_cam.T - r[:, None])).T - else: - return np.random.rand(self.n_landmarks, self.d) - - def sample_parameters(self, theta=None): - landmarks = self.generate_random_landmarks(theta=self.theta) - return self.sample_parameters_landmarks(landmarks) - - def get_parameters(self, var_subset=None): - return self.get_p(param_subset=var_subset) - - @property - def VARIABLE_LIST(self): - return [ - [self.HOM, "x"], - [self.HOM, "z_0"], - [self.HOM, "x", "z_0"], - [self.HOM, "z_0", "z_1"], # should achieve tightness here - ] - - @property - def param_dict(self): - return self.param_dict_landmarks - - @property - def var_dict(self): - level_dim = self.get_level_dims()[self.level] - if self.var_dict_ is None: - self.var_dict_ = {self.HOM: 1} - self.var_dict.update({"x": self.d**2 + self.d}) - self.var_dict.update( - {f"z_{k}": self.d + level_dim for k in range(self.n_landmarks)} - ) - return self.var_dict_ - - def get_x(self, theta=None, parameters=None, var_subset=None): - """ - :param var_subset: list of variables to include in x vector. Set to None for all. - """ - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - if var_subset is None: - var_subset = self.var_dict.keys() - - # TODO(FD) below is a bit hacky, these two variables should not both be called theta. - # theta is either (x, y, alpha) or (x, y, z, a1, a2, a3) - C, r = get_C_r_from_theta(theta, self.d) - if (self.param_level != "no") and (len(parameters) > 1): - landmarks = parameters - else: - landmarks = { - f"p_{i}": self.landmarks[i, :] for i in range(self.landmarks.shape[0]) - } - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - elif key == "x": - x_data += list(r) + list(C.flatten("C")) # row-wise flatten - elif "z" in key: - j = int(key.split("_")[-1]) - - pj = landmarks[f"p_{j}"][: self.d] # - - zj = C[self.d - 1, :] @ pj + r[self.d - 1] - u = 1 / zj * np.r_[C[: self.d - 1, :] @ pj + r[: self.d - 1], 1] - x_data += list(u) - - if self.level == "no": - continue - elif self.level == "u2": - x_data += list(u**2) - elif self.level == "u@u": - x_data += [u @ u] - elif self.level == "u@r": - x_data += [u @ r] - elif self.level == "uuT": - x_data += list(np.outer(u, u).flatten()) - elif self.level == "urT": - # this works - x_data += list(np.outer(u, r).flatten()) - elif self.level == "uxT": - x = np.r_[r, C.flatten("C")] - x_data += list(np.outer(u, x).flatten()) - dim_x = self.get_dim_x(var_subset=var_subset) - assert len(x_data) == dim_x - return np.array(x_data) - - def get_A_known(self, var_dict=None, output_poly=False): - """ - T = | cx' tx | - | cy' ty | - | cz' tz | - | 0 0 0 1 | - Let pj be the j-th landmark coordinate. - [xj] [cx @ pj + tx] - [yj] = [cy @ pj + ty] - [zj] [cz @ pj + tz] - - Let u be the substitution variable, which has d-1 elements. - Then we want to enforce that: - u_xj = 1/zj * xj -> u_xj * zj = xj -> (cz @ pj + tz) * u_xj - (cx @ pj + tx) = 0 - u_yj = 1/zj * yj -> u_yj * zj = yj -> same as above - u_zj = 1/zj -> u_zj * zj = 1 -> u_zj * (cz @ pj + tz) -1 = 0 - Writing things as homogeneous constraints: - a1) cz @ pj * u_xj + tz*u_xj - cx @ pj - h * tx = 0 - a2) -----1x------- --2x--- -- 3 -- --4--- - a3) cz @ pj * u_zj + tz*u_zj - h*h = 0 - ------1z------- --2z--- - """ - print("not using known stereo templates because they depend on the landmarks.") - return [] - - # x contains: [c1, c2, c3, t] - # z contains: [u_xj, u_yj, u_zj, H.O.T.] - if self.d == 2: - x = self.get_x() - _, tx, tz, cx1, cx2, cz1, cz2, u_xj, u_zj, *_ = x - cz = np.array([cz1, cz2]) - cx = np.array([cx1, cx2]) - pj = self.landmarks[0] - assert abs(cz @ pj * u_xj + tz * u_xj - cx @ pj - tx) < 1e-10 - assert abs(u_zj * cz @ pj + u_zj * tz - 1) < 1e-10 - elif self.d == 3: - x = self.get_x() - # fmt: off - (_, tx, ty, tz, cx1, cx2, cx3, cy1, cy2, cy3, cz1, cz2, cz3, u_x1, u_y1, u_z1, *_) = x - # fmt: on - p1 = self.landmarks[0] - assert ( - abs(u_z1 * (cx1 * p1[0] + cx2 * p1[1] + cx3 * p1[2]) + u_z1 * tx - u_x1) - < 1e-10 - ) - assert ( - abs(u_z1 * (cy1 * p1[0] + cy2 * p1[1] + cy3 * p1[2]) + u_z1 * ty - u_y1) - < 1e-10 - ) - assert ( - abs(u_z1 * (cz1 * p1[0] + cz2 * p1[1] + cz3 * p1[2]) + u_z1 * tz - 1) - < 1e-10 - ) - - if var_dict is None: - var_dict = self.var_dict - - A_known = [] - z_dim = self.get_level_dims()[self.level] - - if "x" not in var_dict or self.HOM not in var_dict: - return A_known - landmarks = [j for j in range(self.n_landmarks) if f"z_{j}" in var_dict] - for j in landmarks: - # one complete constraint has x, z_j and h. - pj = self.landmarks[j] - for i in range(self.d): - A = PolyMatrix() - # -----1i------- --2i--- -- 3 -- --4--- - # a1) cz @ pj * u_xj + tz*u_xj - cx @ pj - h * tx = 0 - # a2) cz @ pj * u_yj + tz*u_yj - cy @ pj - h * ty = 0 - # a3) cz @ pj * u_zj + tz*u_zj - h*h = 0 - # ------1i------- --2i--- - # --- 1i --- - fill_mat = np.zeros((self.d + self.d**2, self.d + z_dim)) - # chooses cz of x, and u_xj, u_yj or u_zj of z - fill_mat[-self.d :, i] = pj - - # --- 2 --- u_zj * tx - # chooses tz of x, and u_ij of z - fill_mat[self.d - 1, i] = 1.0 - A[f"x", f"z_{j}"] = fill_mat - - if i < self.d - 1: # u, (v) - fill_mat = np.zeros((self.d + self.d**2, 1)) - # chooses ci of x - fill_mat[(i + 1) * self.d : (i + 2) * self.d, 0] = -pj - - # chooses ti of x - fill_mat[i, 0] = -1 - A["x", self.HOM] = fill_mat - elif i == self.d - 1: # z - A[self.HOM, self.HOM] = -0 # 2.0 - if output_poly: - A_known.append(A) - else: - A_known.append(A.get_matrix(var_dict)) - self.test_constraints(A_known) - return A_known - - def sample_theta(self): - return generate_random_pose(d=self.d).flatten() - - def simulate_y(self, noise: float | None = None): - if noise is None: - noise = NOISE - - T = get_T(theta=self.theta, d=self.d) - - y_sim = np.zeros((self.n_landmarks, self.M_matrix.shape[0])) - for j in range(self.n_landmarks): - y_gt = T @ np.r_[self.landmarks[j], 1.0] - - # in 2d: y_gt[1] - # in 3d: y_gt[2] - y_gt /= y_gt[self.d - 1] - y_gt = self.M_matrix @ y_gt - y_sim[j, :] = y_gt + np.random.normal(loc=0, scale=noise, size=len(y_gt)) - return y_sim - - def get_Q( - self, - noise: float | None = None, - output_poly: bool = False, - use_cliques: list = [], - ) -> PolyMatrix | sp.csr_matrix | sp.csc_matrix: - if self.y_ is None: - if noise is None: - noise = NOISE - self.y_ = self.simulate_y(noise=noise) - - Q = self.get_Q_from_y(self.y_, output_poly=output_poly, use_cliques=use_cliques) - return Q - - def get_Q_from_y( - self, y, output_poly=False, use_cliques=[] - ) -> PolyMatrix | sp.csr_matrix | sp.csc_matrix: - """ - The least squares problem reads - min_T sum_{n=0}^{N-1} || y - Mtilde@z || - where the first d elements of z correspond to u, and Mtilde contains the first d-1 and last element of M - Mtilde is thus of shape d*2 by dim_z, where dim_z=d+dL (the additional Lasserre variables) - y is of length d*2, corresponding to the measured pixel values in left and right image. - """ - from poly_matrix.least_squares_problem import LeastSquaresProblem - - if len(use_cliques): - js = use_cliques - else: - js = range(y.shape[0]) - - # when using lifting (level=urT), then we have - # in 2d: M_tilde is 2 by 6, with first 2 columns: M[:, [0, 2]] - # in 3d: M_tilde is 4 by 12, with first 3 columns: M[:, [0, 1, 3]] - M_tilde = np.zeros((len(y[0]), self.var_dict["z_0"])) - M_tilde[:, : self.d] = self.M_matrix[:, list(range(self.d - 1)) + [self.d]] - - # in 2d: M[:, 1] - # in 3d: M[:, 2] - m = self.M_matrix[:, self.d - 1] - - ls_problem = LeastSquaresProblem() - for j in js: - ls_problem.add_residual({self.HOM: (y[j] - m), f"z_{j}": -M_tilde}) - - if output_poly: - Q = ls_problem.get_Q() - else: - Q = ls_problem.get_Q().get_matrix(self.var_dict) - if self.NORMALIZE: - Q /= self.n_landmarks * self.d - - # sanity check - x = self.get_x() - - # sanity checks. Below is the best conditioned because we don't have to compute B.T @ B, which - # can contain very large values. - B = ls_problem.get_B_matrix(self.var_dict) - errors = B @ x - cost_test = errors.T @ errors - if self.NORMALIZE: - cost_test /= self.n_landmarks * self.d - - if output_poly: - assert isinstance(Q, PolyMatrix) - cost_Q = x.T @ Q.get_matrix(self.var_dict, output_type="csr") @ x - else: - cost_Q = x.T @ Q @ x - assert abs(cost_test - cost_Q) < 1e-6, (cost_test, cost_Q) - if not len(use_cliques): - cost_raw = self.get_cost(self.theta, y) - assert abs(cost_test - cost_raw) < 1e-6, (cost_test, cost_raw) - assert isinstance(Q, (PolyMatrix, sp.csr_matrix, sp.csc_matrix)), type(Q) - return Q - - def get_theta(self, x): - return x[1 : 1 + self.d + self.d**2] - - def get_vec_around_gt(self, delta: float = 0): - if delta == 0: - return self.theta - - C, r = get_C_r_from_theta(self.theta, self.d) - if self.d == 2: - return super().get_vec_around_gt(delta=delta) - else: - return get_noisy_pose(C, r, delta) - - def get_C_cw(self, theta=None): - C_cw, __ = get_C_r_from_theta(theta, self.d) - return C_cw - - def get_position(self, theta=None): - C_cw, r_wc_c = get_C_r_from_theta(theta, self.d) - return (-C_cw.T @ r_wc_c)[None, :] - - def get_error(self, theta_hat): - return get_pose_errors_from_theta(theta_hat, self.theta, self.d) - - def local_solver_manopt(self, t0, y, W=None, verbose=False, method="CG", **kwargs): - """Alternative solver using Pymanopt. By default, :ref:`StateLifter.local_solver` by is used.""" - import pymanopt - from pymanopt.manifolds import Euclidean, Product, SpecialOrthogonalGroup - - if method == "CG": - from pymanopt.optimizers import ConjugateGradient as Optimizer # fastest - elif method == "SD": - from pymanopt.optimizers import SteepestDescent as Optimizer # slow - elif method == "TR": - from pymanopt.optimizers import TrustRegions as Optimizer # okay - else: - raise ValueError(method) - - solver_kwargs = SOLVER_KWARGS - solver_kwargs.update(kwargs) - - if verbose: - solver_kwargs["verbosity"] = 2 - else: - solver_kwargs["verbosity"] = 1 - - manifold = Product((SpecialOrthogonalGroup(self.d, k=1), Euclidean(self.d))) - - if W is None: - W = np.eye(4) if self.d == 3 else np.eye(2) - - @pymanopt.function.autograd(manifold) - def cost(R, t): - cost = 0 - for i in range(self.n_landmarks): - pi_cam = np.concatenate([R @ self.landmarks[i] + t, [1]], axis=0) # type: ignore - y_gt = self.M_matrix @ (pi_cam / pi_cam[self.d - 1]) - residual = y[i] - y_gt - cost += residual.T @ W @ residual - if self.NORMALIZE: - return cost / (self.n_landmarks * self.d) - return cost - - euclidean_gradient = None # set to None - problem = pymanopt.Problem( - manifold, cost, euclidean_gradient=euclidean_gradient # - ) - optimizer = Optimizer(**solver_kwargs) # type: ignore - - R_0, t_0 = get_C_r_from_theta(t0[: self.d + self.d**2], self.d) - res = optimizer.run(problem, initial_point=(R_0, t_0)) - R, t = res.point - - theta_hat = get_theta_from_C_r(R, t) - return theta_hat, res.stopping_criterion, res.cost - - def __repr__(self): - level_str = str(self.level).replace(".", "-") - return f"stereo{self.d}d_{level_str}_{self.param_level}"
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/examples/example_lifter.html b/docs/build/_modules/popr/examples/example_lifter.html deleted file mode 100644 index 1f201ae..0000000 --- a/docs/build/_modules/popr/examples/example_lifter.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - popr.examples.example_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for popr.examples.example_lifter

-import numpy as np
-
-from popr.base_lifters import StateLifter
-
-
-
-[docs] -class ExampleLifter(StateLifter): - """Example Lifter class. - - This class implements the bare minimum to use AutoTight. - - To create a new Lifter for your problem formulation, create - a copy of this file and fill in the missing parts. - - You can take a look at the :ref:`Examples` for inspiration. - """ - - # choose your homogenization variable here - HOM = "h" - - # chose the "lifting" levels when going up in the sparse Lasserre's hierarchy. - LEVELS = ["no"] - - def __init__(self, param_level="no"): - # you can choose if you want to use parameters. Otherwise remove param_level or set to "no" - super().__init__(param_level=param_level) - - @property - def var_dict(self): - var_dict = {self.HOM: 1} - return var_dict - - @property - def param_dict(self): - param_dict = {self.HOM: 1} - return param_dict - - def get_x(self, theta=None, parameters=None, var_subset=None) -> np.ndarray: - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - if var_subset is None: - var_subset = self.var_dict - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - # fill in the rest of x according to var_subset. - # elif "x" in key: - # elif "z" in key: - assert len(x_data) == self.get_dim_x(var_subset) - return np.array(x_data) - - def sample_parameters(self, theta: np.ndarray | None = None) -> dict: - pass - - def sample_theta(self) -> np.ndarray: - pass
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/examples/mono_lifter.html b/docs/build/_modules/popr/examples/mono_lifter.html deleted file mode 100644 index 4787a9e..0000000 --- a/docs/build/_modules/popr/examples/mono_lifter.html +++ /dev/null @@ -1,390 +0,0 @@ - - - - - - - - popr.examples.mono_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for popr.examples.mono_lifter

-from copy import deepcopy
-
-# import autograd.numpy as np
-import numpy as np
-from poly_matrix.poly_matrix import PolyMatrix
-
-from popr.base_lifters import RobustPoseLifter
-from popr.utils.geometry import get_C_r_from_theta
-from popr.utils.plotting_tools import plot_frame
-
-FOV = np.pi / 2  # camera field of view
-
-N_TRYS = 10
-
-# TODO(FD) for some reason this is not required as opposed to what is stated in Heng's paper
-# and it currently breaks tightness (might be a bug in my implementation though)
-USE_INEQ = False
-
-NORMALIZE = False
-
-
-
-[docs] -class MonoLifter(RobustPoseLifter): - """This example is treated in more details in `this paper <https://arxiv.org/abs/2308.05783>`_, - under the name "PLR" (point-to-line registration). - """ - - NOISE = 1e-3 # inlier noise - NOISE_OUT = 0.1 # outlier noise - - @property - def TIGHTNESS(self): - return "cost" if self.robust else "rank" - - def h_list(self, t): - """ - We want to inforce that - - norm(t) <= 10 (default) - - tan(a/2)*t3 >= sqrt(t1**2 + t2**2) - as constraints h_j(t)<=0 - """ - default = super().h_list(t) - try: - import autograd.numpy as anp - - return default + [ - anp.sum(t[:-1] ** 2) - anp.tan(FOV / 2) ** 2 * t[-1] ** 2, # type: ignore - -t[-1], - ] - except ModuleNotFoundError: - return default + [ - np.sum(t[:-1] ** 2) - np.tan(FOV / 2) ** 2 * t[-1] ** 2, - -t[-1], - ] - - def get_random_position(self): - pc_cw = np.random.rand(self.d) * 0.1 - # make sure all landmarks are in field of view: - # min_dist = max(np.linalg.norm(self.landmarks[:, :self.d-1], axis=1)) - pc_cw[self.d - 1] = np.random.uniform(1, self.MAX_DIST) - return pc_cw - - def get_B_known(self): - """Get inequality constraints of the form x.T @ B @ x <= 0""" - - # TODO(FD) for some reason this is not required as opposed to what is stated in Heng's paper - # and it currently breaks tightness (might be a bug in my implementation though) - if not USE_INEQ: - return [] - - default = super().get_B_known() - # B2 and B3 enforce that tan(FOV/2)*t3 >= sqrt(t1**2 + t2**2) - # 0 <= - tan**2(FOV/2)*t3**2 + t1**2 + t2**2 - B3 = PolyMatrix(symmetric=True) - constraint = np.zeros((self.d, self.d)) - constraint[range(self.d - 1), range(self.d - 1)] = 1.0 - constraint[self.d - 1, self.d - 1] = -np.tan(FOV / 2) ** 2 - B3["t", "t"] = constraint - - # t3 >= 0 - constraint = np.zeros(self.d) - constraint[self.d - 1] = -1 - B2 = PolyMatrix(symmetric=True) - B2[self.HOM, "t"] = constraint[None, :] - return default + [ - B2.get_matrix(self.var_dict), - B3.get_matrix(self.var_dict), - ] - - def term_in_norm(self, R, t, pi, ui): - return R @ pi + t - - def residual_sq(self, R, t, pi, ui): - W = np.eye(self.d) - np.outer(ui, ui) - term = self.term_in_norm(R, t, pi, ui) - if NORMALIZE: - return term.T @ W @ term / (self.n_landmarks * self.d) ** 2 - else: - return term.T @ W @ term - - def plot_setup(self): - if self.d != 2: - print("Plotting currently only supported for d=2") - return - import matplotlib.pylab as plt - - assert self.landmarks is not None - - fig, ax = plt.subplots() - - # R, t = get_C_r_from_theta(self.theta, self.d) - # ax.scatter(*t, color="k", label="pose") - - ax.axis("equal") - t_wc_w, C_cw = plot_frame(ax, self.theta, label="pose", color="gray", d=2) - - if self.y_ is not None: - for i in range(self.y_.shape[0]): - ax.scatter( - self.landmarks[i][0], - self.landmarks[i][1], - color=f"C{i}", - label="landmarks", - ) - - # this vector is in camera coordinates - ui_c = self.y_[i] - assert abs(np.linalg.norm(ui_c) - 1.0) < 1e-10 - - ax.plot( - [t_wc_w[0], self.landmarks[i][0]], - [t_wc_w[1], self.landmarks[i][1]], - color=f"C{i}", - ls=":", - ) - if C_cw is not None: - ui_w = C_cw.T @ ui_c - ax.plot( - [t_wc_w[0], t_wc_w[0] + ui_w[0]], - [t_wc_w[1], t_wc_w[1] + ui_w[1]], - color=f"r" if i < self.n_outliers else "g", - ) - - def get_Q( - self, - noise: float | None = None, - output_poly: bool = False, - use_cliques: list = [], - ): - assert self.landmarks is not None, "landmarks must be set before calling get_Q" - if noise is None: - noise = self.NOISE - - if self.y_ is None: - self.y_ = np.zeros((self.n_landmarks, self.d)) - theta = self.theta[: self.d + self.d**2] - outlier_index = self.get_outlier_index() - - R, t = get_C_r_from_theta(theta, self.d) - for i in range(self.n_landmarks): - pi = self.landmarks[i] - # ui = deepcopy(pi) #R @ pi + t - ui = R @ pi + t - ui /= ui[self.d - 1] - - # random unit vector inside the FOV cone - # tan(a/2)*t3 >= sqrt(t1**2 + t2**2) or t3 >= 1 - if np.tan(FOV / 2) * ui[self.d - 1] < np.sqrt( - np.sum(ui[: self.d - 1] ** 2) - ): - print("warning: inlier not in FOV!!") - - if i in outlier_index: - # randomly sample a vector - success = False - for _ in range(N_TRYS): - ui_test = deepcopy(ui) - ui_test[: self.d - 1] += np.random.normal( - scale=self.NOISE_OUT, loc=0, size=self.d - 1 - ) - if np.tan(FOV / 2) * ui_test[self.d - 1] >= np.sqrt( - np.sum(ui_test[: self.d - 1] ** 2) - ): - success = True - ui = ui_test - break - if not success: - raise ValueError("did not find valid outlier ui") - else: - ui[: self.d - 1] += np.random.normal( - scale=noise, loc=0, size=self.d - 1 - ) - assert ui[self.d - 1] == 1.0 - ui /= np.linalg.norm(ui) - self.y_[i] = ui - - Q = self.get_Q_from_y(self.y_, output_poly=output_poly, use_cliques=use_cliques) - return Q - - def get_Q_from_y(self, y, output_poly: bool = False, use_cliques: list = []): - """ - every cost term can be written as - (1 + wi)/b**2 [l x'] Qi [l; x] / norm + 1 - wi - = [l x'] Qi/b**2 [l; x] /norm + wi * [l x']Qi/b**2[l;x] / norm + 1 - wi - - cost term: - (Rpi + t) (I - uiui') (Rpi + t) - """ - assert ( - self.landmarks is not None - ), "landmarks must be set before calling get_Q_from_y" - - Q = PolyMatrix(symmetric=True) - if NORMALIZE: - norm = (self.n_landmarks * self.d) ** 2 - - if len(use_cliques): - js = use_cliques - else: - js = list(range(self.n_landmarks)) - - for i in js: - pi = self.landmarks[i] - ui = y[i] - Pi = np.c_[np.eye(self.d), np.kron(pi, np.eye(self.d))] # I, pi x I - Wi = np.eye(self.d) - np.outer(ui, ui) - Qi = Pi.T @ Wi @ Pi # "t,t, t,c, c,c: Wi, Wi @ kron, kron.T @ Wi @ kron - if NORMALIZE: - Qi /= norm - - if self.robust: - Qi /= self.beta**2 - # last two terms, should not be affected by norm - Q[self.HOM, self.HOM] += 1 - Q[self.HOM, f"w_{i}"] += -0.5 - if self.level == "xwT": - # Q[f"z_{i}", "x"] += 0.5 * Qi - Q[f"z_{i}", "t"] += 0.5 * Qi[:, : self.d] - Q[f"z_{i}", "c"] += 0.5 * Qi[:, self.d :] - # Q["x", "x"] += Qi - Q["t", "t"] += Qi[: self.d, : self.d] - Q["t", "c"] += Qi[: self.d, self.d :] - Q["c", "c"] += Qi[self.d :, self.d :] - elif self.level == "xxT": - Q["z_0", f"w_{i}"] += 0.5 * Qi.flatten()[:, None] - # Q["x", "x"] += Qi - Q["t", "t"] += Qi[: self.d, : self.d] - Q["t", "c"] += Qi[: self.d, self.d :] - Q["c", "c"] += Qi[self.d :, self.d :] - else: - # Q["x", "x"] += Qi - Q["t", "t"] += Qi[: self.d, : self.d] - Q["t", "c"] += Qi[: self.d, self.d :] - Q["c", "c"] += Qi[self.d :, self.d :] - if output_poly: - return 0.5 * Q - Q_sparse = 0.5 * Q.get_matrix(variables=self.var_dict) - return Q_sparse - - def __repr__(self): - appendix = "_robust" if self.robust else "" - return f"mono_{self.d}d_{self.level}_{self.param_level}{appendix}"
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/examples/poly4_lifter.html b/docs/build/_modules/popr/examples/poly4_lifter.html deleted file mode 100644 index 03341fc..0000000 --- a/docs/build/_modules/popr/examples/poly4_lifter.html +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - - - popr.examples.poly4_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for popr.examples.poly4_lifter

-import numpy as np
-
-from popr.base_lifters import PolyLifter
-
-
-
-[docs] -class Poly4Lifter(PolyLifter): - """Fourth-degree polynomial examples. - - Two types are provided: - - - poly_type="A": one global minimum, one local minimum - - poly_type="B": two global minima - """ - - @property - def VARIABLE_LIST(self): - return [[self.HOM, "t", "z0"]] - - def __init__(self, poly_type="A"): - # actual minimum - assert poly_type in ["A", "B"] - self.poly_type = poly_type - super().__init__(degree=4) - - def get_Q(self, *args, **kwargs): - if self.poly_type == "A": - # fmt: off - # noqa - Q = np.r_[ - np.c_[2, 1, 0], - np.c_[1, -1 / 2, -1 / 3], - np.c_[0, -1 / 3, 1 / 4] - ] - # fmt: on - elif self.poly_type == "B": - # below is constructed such that f'(t) = (t-1)*(t-2)*(t-3) - # fmt: off - # noqa - Q = np.r_[ - np.c_[3, -3, 0], - np.c_[-3, 11 / 2, -1], - np.c_[0, -1, 1 / 4] - ] - # fmt: on - return Q - - def get_A_known(self, output_poly=False, add_redundant=False): - from poly_matrix import PolyMatrix - - if add_redundant: - print("No redundant constraitns for 4-degree polynomial.") - - # z_0 = t^2 - A_1 = PolyMatrix(symmetric=True) - A_1[self.HOM, "z0"] = -1 - A_1["t", "t"] = 2 - if output_poly: - return [A_1] - else: - return [A_1.get_matrix(self.var_dict)] - - def generate_random_setup(self): - self.theta_ = np.array([-1]) - - def get_D(self, that): - """Not currently used.""" - D = np.array( - [ - [1.0, 0.0, 0.0], - [that, 1.0, 0.0], - [that**2, 2 * that, 1.0], - ] - ) - return D
- - - -if __name__ == "__main__": - import os - - import matplotlib.pylab as plt - - # Get the directory two levels up from this file - base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) - - thetas = np.linspace(-2, 3, 100) - poly_lifter = Poly4Lifter(poly_type="A") - fig, ax = poly_lifter.plot(thetas) - fig.savefig(os.path.join(base_dir, "docs", "figures", "poly4_lifter_A.png")) - - thetas = np.linspace(0, 4, 100) - poly_lifter = Poly4Lifter(poly_type="B") - fig, ax = poly_lifter.plot(thetas) - fig.savefig(os.path.join(base_dir, "docs", "figures", "poly4_lifter_B.png")) - - plt.show(block=False) - print("done") -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/examples/poly6_lifter.html b/docs/build/_modules/popr/examples/poly6_lifter.html deleted file mode 100644 index 4d8d947..0000000 --- a/docs/build/_modules/popr/examples/poly6_lifter.html +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - - - popr.examples.poly6_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for popr.examples.poly6_lifter

-import numpy as np
-
-from popr.base_lifters import PolyLifter
-
-
-
-[docs] -class Poly6Lifter(PolyLifter): - """Sixth-degree polynomial examples. - - Two types are provided: - - - poly_type="A": one global minimum, two local minima, 2 local maxima - - poly_type="B": one global minimum, one local minimum, one local maximum - """ - - @property - def VARIABLE_LIST(self): - return [[self.HOM, "t", "z0", "z1"]] - - def __init__(self, poly_type="A"): - assert poly_type in ["A", "B"] - self.poly_type = poly_type - super().__init__(degree=6) - - def get_Q(self, *args, **kwargs): - if self.poly_type == "A": - return 0.1 * np.array( - [ - [25, 12, 0, 0], - [12, -13, -2.5, 0], - [0, -2.5, 6.25, -0.9], - [0, 0, -0.9, 1 / 6], - ] - ) - elif self.poly_type == "B": - return np.array( - [ - [5.0000, 1.3167, -1.4481, 0], - [1.3167, -1.4481, 0, 0.2685], - [-1.4481, 0, 0.2685, -0.0667], - [0, 0.2685, -0.0667, 0.0389], - ] - ) - - def get_A_known(self, output_poly=False, add_redundant=True): - from poly_matrix import PolyMatrix - - A_list = [] - - # z_0 = t^2 - A_1 = PolyMatrix(symmetric=True) - A_1[self.HOM, "z0"] = -1 - A_1["t", "t"] = 2 - A_list.append(A_1) - - # z_1 = t^3 = t z_0 - A_2 = PolyMatrix(symmetric=True) - A_2[self.HOM, "z1"] = -1 - A_2["t", "z0"] = 1 - A_list.append(A_2) - - # t^4 = z_1 t = z_0 z_0 - if add_redundant: - B_0 = PolyMatrix(symmetric=True) - B_0["z0", "z0"] = 2 - B_0["t", "z1"] = -1 - A_list.append(B_0) - - if output_poly: - return A_list - else: - return [A_i.get_matrix(self.var_dict) for A_i in A_list] - - def get_D(self, that): - D = np.array( - [ - [1.0, 0.0, 0.0, 0.0], - [that, 1.0, 0.0, 0.0], - [that**2, 2 * that, 1.0, 0.0], - [that**3, 3 * that**2, 3 * that, 1.0], - ] - ) - return D - - def generate_random_setup(self): - self.theta_ = np.array([-1])
- - - -if __name__ == "__main__": - import os - - import matplotlib.pylab as plt - - # Get the directory two levels up from this file - base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) - - thetas = np.linspace(-1.5, 4.5, 100) - poly_lifter = Poly6Lifter(poly_type="A") - fig, ax = poly_lifter.plot(thetas) - fig.savefig(os.path.join(base_dir, "docs", "figures", "poly6_lifter_A.png")) - - thetas = np.linspace(-3, 3, 100) - poly_lifter = Poly6Lifter(poly_type="B") - fig, ax = poly_lifter.plot(thetas) - fig.savefig(os.path.join(base_dir, "docs", "figures", "poly6_lifter_B.png")) - - plt.show(block=False) - print("done") -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/examples/range_only_lifters.html b/docs/build/_modules/popr/examples/range_only_lifters.html deleted file mode 100644 index de26270..0000000 --- a/docs/build/_modules/popr/examples/range_only_lifters.html +++ /dev/null @@ -1,609 +0,0 @@ - - - - - - - - popr.examples.range_only_lifters — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.examples.range_only_lifters

-import matplotlib.pylab as plt
-import numpy as np
-import scipy.sparse as sp
-from poly_matrix.least_squares_problem import LeastSquaresProblem
-from scipy.optimize import minimize
-
-from popr.base_lifters import StateLifter
-from popr.utils.common import diag_indices, upper_triangular
-
-plt.ion()
-
-NOISE = 1e-2  # std deviation of distance noise
-
-METHOD = "BFGS"
-NORMALIZE = True
-
-# TODO(FD): parameters below are not all equivalent.
-SOLVER_KWARGS = {
-    "BFGS": dict(gtol=1e-6, xrtol=1e-10),  # relative step size
-    "Nelder-Mead": dict(xatol=1e-10),  # absolute step size
-    "Powell": dict(ftol=1e-6, xtol=1e-10),
-    "TNC": dict(gtol=1e-6, xtol=1e-10),
-}
-
-
-
-[docs] -class RangeOnlyLocLifter(StateLifter): - """Range-only localization in 2D or 3D. - - We minimize the cost function - - .. math:: f(\\theta) = \\sum_{n=0}^{N-1} \\sum_{k=0}^{K-1} w_{nk} (d_{nk}^2 - ||p_n - a_k||^2)^2 - - where - - - :math:`w_{nk}` is the weight for the nth point and kth landmark (currently assumed binary to mark missing edges). - - :math:`\\theta` is the flattened vector of positions :math:`p_n`. - - :math:`d_{nk}` is the distance measurement from point n to landmark k. - - :math:`a_k` is the kth landmark. - - Note that in the current implementation, there is no regularization term so the problem could be split into individual points. - - We experiment with two different substitutions to turn the cost function into aquadratic form: - - - level "no" uses substitution :math:`z_i=||p_i||^2=x_i^2 + y_i^2` (or equivalent 3D version). - - level "quad" uses substitution :math:`y_i=[x_i^2, x_iy_i, y_i^2]` (or equivalent 3D version). - - This example is treated in more details in `this paper <https://arxiv.org/abs/2308.05783>`_. - """ - - TIGHTNESS = "rank" - LEVELS = ["no", "quad"] - LEVEL_NAMES = { - "no": "$z_n$", - "quad": "$\\boldsymbol{y}_n$", - } - - def get_vec_around_gt(self, delta: float = 0): - """Sample around ground truth. - :param delta: sample from gt + std(delta) (set to 0 to start from gt.) - """ - assert self.landmarks is not None, "landmarks must be set before sampling" - - if delta == 0: - return self.theta - else: - bbox_max = np.max(self.landmarks, axis=0) * 2 - bbox_min = np.min(self.landmarks, axis=0) * 2 - pos = ( - np.random.rand(self.n_positions, self.d) - * (bbox_max - bbox_min)[None, :] - + bbox_min[None, :] - ) - return pos.flatten() - - def __init__( - self, - n_positions, - n_landmarks, - d, - W=None, - level="no", - variable_list=None, - param_level="no", - ): - self.n_positions = n_positions - self.n_landmarks = n_landmarks - self.landmarks_ = None # will be set later - - if W is not None: - assert W.shape == (n_landmarks, n_positions) - self.W = W - else: - self.W = np.ones((n_positions, n_landmarks)) - self.y_ = None - - if variable_list == "all": - variable_list = self.get_all_variables() - super().__init__( - level=level, d=d, variable_list=variable_list, param_level=param_level - ) - - @property - def landmarks(self): - landmarks = np.random.rand(self.n_landmarks, self.d) - if self.landmarks_ is None: - self.landmarks_ = landmarks - return self.landmarks_ - - @property - def VARIABLE_LIST(self): - return [ - [self.HOM, "x_0"], - [self.HOM, "x_0", "z_0"], - [self.HOM, "x_0", "z_0", "z_1"], - [self.HOM, "x_0", "x_1", "z_0", "z_1"], - ] - - def get_all_variables(self): - vars = [self.HOM] - vars += [f"x_{i}" for i in range(self.n_positions)] - vars += [f"z_{i}" for i in range(self.n_positions)] - return [vars] - - def sample_parameters(self, theta=None): - landmarks = np.random.rand(self.n_landmarks, self.d) - return self.sample_parameters_landmarks(landmarks) - - def sample_theta(self): - return np.random.rand(self.n_positions, self.d).flatten() - - def get_A_known(self, var_dict=None, output_poly=False): - from poly_matrix.poly_matrix import PolyMatrix - - if var_dict is None: - var_dict = self.var_dict - positions = self.get_variable_indices(var_dict) - - A_list = [] - for n in positions: - if self.level == "no": - A = PolyMatrix(symmetric=True) - A[f"x_{n}", f"x_{n}"] = np.eye(self.d) - A[self.HOM, f"z_{n}"] = -0.5 - if output_poly: - A_list.append(A) - else: - A_list.append(A.get_matrix(self.var_dict)) - - elif self.level == "quad": - count = 0 - for i in range(self.d): - for j in range(i, self.d): - A = PolyMatrix(symmetric=True) - mat_x = np.zeros((self.d, self.d)) - mat_z = np.zeros((1, self.size_z)) - if i == j: - mat_x[i, i] = 1.0 - else: - mat_x[i, j] = 0.5 - mat_x[j, i] = 0.5 - mat_z[0, count] = -0.5 - A[f"x_{n}", f"x_{n}"] = mat_x - A[self.HOM, f"z_{n}"] = mat_z - count += 1 - if output_poly: - A_list.append(A) - else: - A_list.append(A.get_matrix(self.var_dict)) - return A_list - - def get_x(self, theta=None, parameters=None, var_subset=None): - if var_subset is None: - var_subset = self.var_dict - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - - positions = theta.reshape(self.n_positions, -1) - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - elif "x" in key: - n = int(key.split("_")[-1]) - x_data += list(positions[n]) - elif "z" in key: - n = int(key.split("_")[-1]) - if self.level == "no": - x_data.append(np.linalg.norm(positions[n]) ** 2) - elif self.level == "quad": - x_data += list(upper_triangular(positions[n])) - assert len(x_data) == self.get_dim_x(var_subset) - return np.array(x_data) - - def get_J_lifting(self, t): - pos = t.reshape((-1, self.d)) - ii = [] - jj = [] - data = [] - - idx = 0 - for n in range(self.n_positions): - if self.level == "no": - ii += [n] * self.d - jj += list(range(n * self.d, (n + 1) * self.d)) - data += list(2 * pos[n]) - elif self.level == "quad": - # it seemed easier to do this manually that programtically - if self.d == 3: - x, y, z = pos[n] - jj += [n * self.d + j for j in [0, 0, 1, 0, 2, 1, 1, 2, 2]] - data += [2 * x, y, x, z, x, 2 * y, z, y, 2 * z] - ii += [idx + i for i in [0, 1, 1, 2, 2, 3, 4, 4, 5]] - elif self.d == 2: - x, y = pos[n] - jj += [n * self.d + j for j in [0, 0, 1, 1]] - data += [2 * x, y, x, 2 * y] - ii += [idx + i for i in [0, 1, 1, 2]] - idx += self.size_z - J_lifting = sp.csr_array( - (data, (ii, jj)), - shape=(self.M, self.N), - ) - return J_lifting - - def get_hess_lifting(self, t): - """return list of the hessians of the M lifting functions.""" - hessians = [] - for n in range(self.n_positions): - idx = range(n * self.d, (n + 1) * self.d) - if self.level == "no": - hessian = sp.csr_array( - ([2] * self.d, (idx, idx)), - shape=(self.N, self.N), - ) - hessians.append(hessian) - elif self.level == "quad": - for h in self.fixed_hessian_list: - ii, jj = np.meshgrid(idx, idx) - hessian = sp.csr_array( - (h.flatten(), (ii.flatten(), jj.flatten())), - shape=(self.N, self.N), - ) - hessians.append(hessian) - return hessians - - @property - def fixed_hessian_list(self): - if self.d == 2: - return [ - np.array([[2, 0], [0, 0]]), - np.array([[0, 1], [1, 0]]), - np.array([[0, 0], [0, 2]]), - ] - elif self.d == 3: - return [ - np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]]), - np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]), - np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]]), - np.array([[0, 0, 0], [0, 2, 0], [0, 0, 0]]), - np.array([[0, 0, 0], [0, 0, 1], [0, 1, 0]]), - np.array([[0, 0, 0], [0, 0, 0], [0, 0, 2]]), - ] - else: - raise ValueError(f"Unsupported dimension {self.d} for fixed hessians.") - - def get_residuals(self, t, y): - positions = t.reshape((-1, self.d)) - y_current = ( - np.linalg.norm(self.landmarks[None, :, :] - positions[:, None, :], axis=2) - ** 2 - ) - return self.W * (y - y_current) - - def get_cost(self, t, y, sub_idx=None): - """ - get cost for given positions, landmarks and noise. - - :param t: flattened positions of length Nd - :param y: N x K distance measurements - """ - residuals = self.get_residuals(t, y) - if sub_idx is None: - cost = np.sum(residuals**2) - else: - cost = np.sum(residuals[sub_idx] ** 2) - if NORMALIZE: - return cost / np.sum(self.W > 0) - return cost - - def get_grad(self, t, y, sub_idx=None): - """get gradient""" - J = self.get_J(t, y) - x = self.get_x(t) - Q = self.get_Q_from_y(y) - if sub_idx is None: - return 2 * J.T @ Q @ x - else: - sub_idx_x = self.get_sub_idx_x(sub_idx) - return 2 * J.T[:, sub_idx_x] @ Q[sub_idx_x, :][:, sub_idx_x] @ x[sub_idx_x] - - def get_J(self, t, y): - J = sp.csr_array( - (np.ones(self.N), (range(1, self.N + 1), range(self.N))), - shape=(self.N + 1, self.N), - ) - J_lift = self.get_J_lifting(t) - J = sp.vstack([J, J_lift]) - return J - - def get_hess(self, t, y): - """get Hessian""" - x = self.get_x(t) - Q = self.get_Q_from_y(y) - J = self.get_J(t, y) - hess = 2 * J.T @ Q @ J - - hessians = self.get_hess_lifting(t) - B = self.ls_problem.get_B_matrix(self.var_dict) - residuals = B @ x - for m, h in enumerate(hessians): - bm_tilde = B[:, -self.M + m] - factor = float(bm_tilde.T @ residuals) - hess += 2 * factor * h - return hess - - def get_Q_from_y(self, y): - import itertools - - self.ls_problem = LeastSquaresProblem() - - if self.level == "quad": - diag_idx = diag_indices(self.d) - - for n, k in itertools.product(range(self.n_positions), range(self.n_landmarks)): - if self.W[n, k] > 0: - ak = self.landmarks[k] - if self.level == "no": - self.ls_problem.add_residual( - { - self.HOM: y[n, k] - np.linalg.norm(ak) ** 2, - f"x_{n}": 2 * ak.reshape((1, -1)), - f"z_{n}": -1, - } - ) - elif self.level == "quad": - mat = np.zeros((1, self.size_z)) - mat[0, diag_idx] = -1 - res_dict = { - self.HOM: y[n, k] - np.linalg.norm(ak) ** 2, - f"x_{n}": 2 * ak.reshape((1, -1)), - f"z_{n}": mat, - } - self.ls_problem.add_residual(res_dict) - Q = self.ls_problem.get_Q().get_matrix(self.var_dict) - if NORMALIZE: - return Q / np.sum(self.W > 0) - return Q - - def simulate_y(self, noise: float | None = None): - assert self.landmarks is not None - # N x K matrix - if noise is None: - noise = NOISE - positions = self.theta.reshape(self.n_positions, -1) - y_gt = ( - np.linalg.norm(self.landmarks[None, :, :] - positions[:, None, :], axis=2) - ** 2 - ) - return y_gt + np.random.normal(loc=0, scale=noise, size=y_gt.shape) - - def get_Q(self, noise: float | None = None) -> tuple: - if self.y_ is None: - self.y_ = self.simulate_y(noise=noise) - Q = self.get_Q_from_y(self.y_) - - # DEBUGGING - x = self.get_x() - cost1 = x.T @ Q @ x - cost3 = self.get_cost(self.theta, self.y_) - assert abs(cost1 - cost3) < 1e-10 - return Q - - def get_D(self, that): - D = np.eye(1 + self.n_positions * self.d + self.size_z) - x = self.get_x(theta=that) - J = self.get_J_lifting(t=that) - - D = sp.lil_array((len(x), len(x))) - D[range(len(x)), range(len(x))] = 1.0 - D[:, 0] = x - D[-J.shape[0] :, 1 : 1 + J.shape[1]] = J # type: ignore - return D.tocsc() - - def get_sub_idx_x(self, sub_idx, add_z=True): - sub_idx_x = [0] - for idx in sub_idx: - sub_idx_x += [1 + idx * self.d + d for d in range(self.d)] - if not add_z: - return sub_idx_x - for idx in sub_idx: - sub_idx_x += [ - 1 + self.n_positions * self.d + idx * self.size_z + d - for d in range(self.size_z) - ] - return sub_idx_x - - def get_theta(self, x): - return x[1 : self.d + 1] - - def get_position(self, theta=None): - if theta is not None: - return theta.reshape(self.n_positions, self.d) - - def get_error(self, that): - err = np.sqrt(np.mean((self.theta - that) ** 2)) - return {"total error": err, "error": err} - - def local_solver( - self, - t_init, - y, - verbose=False, - method="BFGS", - solver_kwargs=SOLVER_KWARGS, - ): - """ - :param t_init: (positions, landmarks) tuple - """ - - # TODO(FD): split problem into individual points. - options = solver_kwargs[method] - options["disp"] = verbose - sol = minimize( - self.get_cost, - x0=t_init, - args=y, - jac=self.get_grad, - # hess=self.get_hess, not used by any solvers. - method=method, - options=options, - ) - - info = {} - info["success"] = sol.success - info["msg"] = sol.message + f"(# iterations: {sol.nit})" - if sol.success: - that = sol.x - rel_error = self.get_cost(that, y) - self.get_cost(sol.x, y) - assert abs(rel_error) < 1e-10, rel_error - residuals = self.get_residuals(that, y) - cost = sol.fun - info["max res"] = np.max(np.abs(residuals)) - hess = self.get_hess(that, y) - eigs = np.linalg.eigvalsh(hess.toarray()) - info["cond Hess"] = eigs[-1] / eigs[0] - else: - that = cost = None - info["max res"] = None - info["cond Hess"] = None - return that, info, cost - - @property - def var_dict(self): - var_dict = {self.HOM: 1} - var_dict.update({f"x_{n}": self.d for n in range(self.n_positions)}) - var_dict.update({f"z_{n}": self.size_z for n in range(self.n_positions)}) - return var_dict - - @property - def param_dict(self): - return self.param_dict_landmarks - - @property - def size_z(self): - if self.level == "no": - return 1 - elif self.level == "quad": - return int(self.d * (self.d + 1) / 2) - else: - raise ValueError(f"Unknown level {self.level}") - - @property - def N(self): - return self.n_positions * self.d - - @property - def M(self): - return self.n_positions * self.size_z - - def __repr__(self): - return f"rangeonlyloc{self.d}d_{self.level}"
- - - -if __name__ == "__main__": - lifter = RangeOnlyLocLifter(n_positions=3, n_landmarks=4, d=2) -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/examples/range_only_slam1.html b/docs/build/_modules/popr/examples/range_only_slam1.html deleted file mode 100644 index 22bbc74..0000000 --- a/docs/build/_modules/popr/examples/range_only_slam1.html +++ /dev/null @@ -1,533 +0,0 @@ - - - - - - - - popr.examples.range_only_slam1 — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.examples.range_only_slam1

-import itertools
-
-import numpy as np
-from poly_matrix.least_squares_problem import LeastSquaresProblem
-from poly_matrix.poly_matrix import PolyMatrix
-
-from popr.lifters import RangeOnlyLifter
-
-
-def vkron(a, b):
-    """
-    if a is 1xN, make it Nx1
-    if b is NxN leave it
-    """
-    if (np.ndim(a) == 2) and (a.shape[0] == 1):
-        a = a.T
-    elif np.ndim(a) == 1:
-        a = a.reshape((-1, 1))
-    if (np.ndim(b) == 2) and (b.shape[0] == 1):
-        b = b.T
-    elif np.ndim(b) == 1:
-        b = b.reshape((-1, 1))
-    return np.kron(a, b)
-
-
-
-[docs] -class RangeOnlySLAM1Lifter(RangeOnlyLifter): - """Range-only SLAM, version 1 - - Uses substitution tau_i=||t_i||^2, alpha_k=||a_k||^2, e_ik = a_k @ t_i - """ - - LEVELS = [ - "no", - "inner", - "outer", - ] - - def __init__( - self, - n_positions, - n_landmarks, - d, - edges=None, - remove_gauge="hard", - resample_landmarks=False, - level="inner", - ): - self.level = level - self.resample_landmarks = resample_landmarks - super().__init__( - n_positions, n_landmarks, d, edges=edges, remove_gauge=remove_gauge - ) - - @property - def base_var_dict(self): - var_dict = {} - var_dict.update({f"x{n}": self.d for n in range(self.n_positions)}) - if self.remove_gauge == "hard": - # for 2d, a1_x, a2_x, a2_y, ... - # for 3d, a1_x, a2_x, a2_y, a3_x, a3_y, a3_z, ... - var_dict.update({f"a{k}": k for k in range(1, self.d)}) - var_dict.update({f"a{k}": self.d for k in range(self.d, self.n_landmarks)}) - else: - var_dict.update({f"a{k}": self.d for k in range(self.n_landmarks)}) - return var_dict - - @property - def sub_var_dict(self): - var_dict = {} - if self.level == "inner": - var_dict.update({f"tau{n}": 1 for n in range(self.n_positions)}) - var_dict.update({f"alpha{k}": 1 for k in range(self.n_landmarks)}) - var_dict.update({f"e{n}{k}": 1 for n, k in self.edges}) - elif self.level == "outer": - dim = self.d**2 - var_dict.update({f"tau{n}": dim for n in range(self.n_positions)}) - var_dict.update({f"alpha{k}": dim for k in range(self.n_landmarks)}) - var_dict.update({f"e{n}{k}": dim for n, k in self.edges}) - return var_dict - - @property - def var_dict(self): - var_dict = {self.HOM: 1} - var_dict.update(self.base_var_dict) - var_dict.update(self.sub_var_dict) - return var_dict - - def get_x(self, theta=None): - if theta is None: - theta = self.theta - positions, landmarks = self.get_positions_and_landmarks(theta) - - x_data = [[1]] - x_data += [list(theta)] - if self.level == "inner": - x_data += [[np.linalg.norm(p) ** 2] for p in positions] - x_data += [[np.linalg.norm(a) ** 2] for a in landmarks] - x_data += [[landmarks[k] @ positions[n]] for n, k in self.edges] - elif self.level == "outer": - x_data += [list(np.kron(p, p)) for p in positions] - x_data += [list(np.kron(a, a)) for a in landmarks] - x_data += [list(np.kron(positions[n], landmarks[k])) for n, k in self.edges] - x = np.concatenate(x_data, axis=0) - return x - - def sample_theta(self): - positions = self.sample_random_positions() - if self.resample_landmarks: - landmarks = self.sample_random_landmarks() - else: - landmarks = self.landmarks - return self.get_theta(positions, landmarks) - - def get_Q_from_y(self, y): - self.ls_problem = LeastSquaresProblem() - if self.level == "outer": - I = np.eye(self.d).flatten().reshape((1, -1)) - for n, k in self.edges: - if self.level == "inner": - # d_nk**2 - ||t_n||**2 + 2t_n@a_k - ||a_k||**2 - # l tau_n e_nk alpha_k - self.ls_problem.add_residual( - {self.HOM: y[n, k], f"tau{n}": -1, f"alpha{k}": -1, f"e{n}{k}": 2} - ) - elif self.level == "outer": - # d_nk**2 - ||t_n||**2 + 2t_n@a_k - ||a_k||**2 - # l -I @ tau_n +2I @ e_nk -I @ alpha_k - self.ls_problem.add_residual( - { - self.HOM: y[n, k], - f"tau{n}": -I, - f"alpha{k}": -I, - f"e{n}{k}": 2 * I, - } - ) - # fix Gauge freedom - if self.remove_gauge == "cost": - I = np.eye(self.d) - for d in range(self.d): - self.ls_problem.add_residual({"a0": I[d].reshape((1, -1))}) - return self.ls_problem.get_Q().get_matrix(self.var_dict) - - def get_A_known(self): - A_list = [] - for n in range(self.n_positions): - if self.level == "inner": - A = PolyMatrix() - A[f"x{n}", f"x{n}"] = np.eye(self.d) - A[self.HOM, f"tau{n}"] = -0.5 - A_list.append(A.get_matrix(self.var_dict)) - else: - for j, k in itertools.product(range(self.d), range(self.d)): - X = np.zeros((self.d, self.d)) - x = np.zeros(self.d**2) - A = PolyMatrix() - X[j, k] += 1.0 - X[k, j] += 1.0 - x[j * self.d + k] += -1.0 - A[f"x{n}", f"x{n}"] += X - A[self.HOM, f"tau{n}"] += x.reshape((1, -1)) - A_list.append(A.get_matrix(self.var_dict)) - - if self.level == "outer": - # TODO(FD) implement the other known matrices - return A_list - - for k in range(self.n_landmarks): - A = PolyMatrix() - if self.remove_gauge == "hard": - if k > 0: - A[f"a{k}", f"a{k}"] = np.eye(self.var_dict[f"a{k}"]) - else: - A[f"a{k}", f"a{k}"] = np.eye(self.d) - A[self.HOM, f"alpha{k}"] = -0.5 - A_list.append(A.get_matrix(self.var_dict)) - for n, k in self.edges: - A = PolyMatrix() - if self.remove_gauge == "hard": - if k > 0: - A[f"x{n}", f"a{k}"] = np.eye(self.d)[:, : self.var_dict[f"a{k}"]] - else: - A[f"x{n}", f"a{k}"] = np.eye(self.d) - A[self.HOM, f"e{n}{k}"] = -1 - A_list.append(A.get_matrix(self.var_dict)) - return A_list - - def get_cost(self, t, y): - # fix Gauge freedom - cost = super().get_cost(t, y) - if self.remove_gauge == "cost": - cost += np.linalg.norm(self.landmarks[0]) ** 2 - return cost - - def fill_depending_on_k(self, row, counter, k, vec): - """Because of Gauge freedom removal, - the first columns of the landmark-based part of J - are incomplete. - """ - if k == 0: - return counter - - # below is equivalent to row[counter, f"a{k}"] = min(k, 3) - # keeping it like this for better readability - elif k == 1: - # i = self.n_positions * self.d - # row[i] = vec[0] - row[counter, f"a{k}"] = vec[0] - return counter + 1 - elif k == 2: - # i = self.n_positions * self.d + 1 - # row[i : i + 2] = vec[:2] - row[counter, f"a{k}"] = vec[:2][None, :] - return counter + 1 - elif k >= 3: - # i = self.n_positions * self.d + 3 + (k - 3) * self.d - # row[i : i + self.d] = vec - row[counter, f"a{k}"] = vec[None, :] - return counter + 1 - - def get_J_lifting(self, t): - if self.level == "inner": - return self.get_J_lifting_inner(t) - elif self.level == "outer": - return self.get_J_lifting_outer(t) - - def get_J_lifting_outer(self, t): - positions, landmarks = self.get_positions_and_landmarks(t) - J_lifting = PolyMatrix(symmetric=False) - for n in range(self.n_positions): - # below is d/d(p_n) (p_n kron p_n) - # fmt:off - J_lifting[f"tau{n}", f"x{n}"] = ( - vkron(positions[n], np.eye(self.d)) - + vkron(np.eye(self.d), positions[n]) - ) - # fmt:on - for k in range(self.n_landmarks): - if self.remove_gauge == "hard": - if k == 0: - continue - # below is d/d(a_k) (a_k kron a_k) - dim = self.var_dict[f"a{k}"] - I = np.zeros((self.d, self.d)) - I[range(dim), range(dim)] = 1.0 - ak = landmarks[k] - # fmt:off - J_lifting[f"alpha{k}", f"a{k}"] = ( - vkron(ak, I) + - vkron(I, ak) - )[:, :dim] - # fmt:on - else: - ak = landmarks[k] - # fmt:off - J_lifting[f"alpha{k}", f"a{k}"] = ( - vkron(ak, np.eye(self.d)) + - vkron(np.eye(self.d), ak) - ) - # fmt:on - for n, k in self.edges: - # d/d(p_n) (p_n kron a_n) - J_lifting[f"e{n}{k}", f"x{n}"] = vkron(np.eye(self.d), self.landmarks[k]) - if self.remove_gauge == "hard": - if k == 0: - continue - dim = self.var_dict[f"a{k}"] - # d/d(a_k) (p_n kron a_k) - # if a_n has k < d elements: - # - # p_n = [x y z] a_k = [a b] - # p_n kron a_k = [xa xb ya yb za zb] - # J = [x - # x - # 0 - # y - # y - # 0 - # z - # z - # 0] - I = np.zeros((self.d, self.d)) - I[range(dim), range(dim)] = 1.0 - J_lifting[f"e{n}{k}", f"a{k}"] = vkron( - self.positions[n], - I, - )[:, :dim] - else: - J_lifting[f"e{n}{k}", f"a{k}"] = vkron( - self.positions[n], - np.eye(self.d), - ) - return J_lifting.get_matrix((self.sub_var_dict, self.base_var_dict)) - - def get_J_lifting_inner(self, t): - positions, landmarks = self.get_positions_and_landmarks(t) - J_lifting = PolyMatrix(symmetric=False) - for n in range(self.n_positions): - J_lifting[f"tau{n}", f"x{n}"] = 2 * positions[n].reshape((1, -1)) - for k in range(self.n_landmarks): - if self.remove_gauge == "hard": - if k == 0: - continue - J_lifting[f"alpha{k}", f"a{k}"] = ( - 2 * landmarks[k, : self.var_dict[f"a{k}"]] - ).reshape((1, -1)) - else: - J_lifting[f"alpha{k}", f"a{k}"] = 2 * landmarks[k].reshape((1, -1)) - for n, k in self.edges: - J_lifting[f"e{n}{k}", f"x{n}"] = landmarks[k].reshape((1, -1)) - if self.remove_gauge == "hard": - if k == 0: - continue - J_lifting[f"e{n}{k}", f"a{k}"] = positions[ - n, : self.var_dict[f"a{k}"] - ].reshape((1, -1)) - else: - J_lifting[f"e{n}{k}", f"a{k}"] = positions[n].reshape((1, -1)) - return J_lifting.get_matrix((self.sub_var_dict, self.base_var_dict)) - - def fill_hessian_depending_on_k(self, hessian, k, fix_i=None, val=2.0): - if k == 0: # and (fix_i is None): - return # no Hessian! - # elif k == 0: - # pass - - elif k == 1: - i = self.n_positions * self.d - if fix_i is None: - hessian[i, i] = val # a1_x - else: - hessian[fix_i[0], i] = val - hessian[i, fix_i[0]] = val - elif k == 2: - i = self.n_positions * self.d + 1 - var_i = range(i, i + 2) - if fix_i is None: - hessian[var_i, var_i] = val # a2_x, a2_y - else: - hessian[fix_i[:2], var_i] = val # a2_x, a2_y - hessian[var_i, fix_i[:2]] = val # a2_x, a2_y - elif k >= 3: - i = self.n_positions * self.d + 3 + (k - 3) * self.d - var_i = range(i, i + self.d) - if fix_i is None: - hessian[var_i, var_i] = val # a3_x, a3_y, a3_z - else: - hessian[fix_i, var_i] = val - hessian[var_i, fix_i] = val - - def get_hess_lifting(self, t): - if self.level == "inner": - return self.get_hess_lifting_inner(t) - else: - raise NotImplementedError(self.level) - - def get_hess_lifting_inner(self, t): - """return list of the hessians of the M lifting functions.""" - hessians = [] - # Hessian of || tau_j ||^2: 2 * I - for n in range(self.n_positions): - hessian = PolyMatrix() - hessian[f"x{n}", f"x{n}"] = 2 * np.eye(self.d) - hessians.append(hessian.get_matrix(self.base_var_dict)) - # Hessian of || alpha_k ||^2: 2 * I - for k in range(self.n_landmarks): - hessian = PolyMatrix() - if k > 0: - hessian[f"a{k}", f"a{k}"] = 2 * np.eye(self.var_dict[f"a{k}"]) - hessians.append(hessian.get_matrix(self.base_var_dict)) - # Hessian of alpha_j@tau_j: tau_j or alpha_j - for n, k in self.edges: - hessian = PolyMatrix() - hessian_old = np.zeros((self.N, self.N)) - if self.remove_gauge == "hard": - # old implementation - i = n * self.d - self.fill_hessian_depending_on_k( - hessian_old, k, fix_i=range(i, i + self.d), val=1.0 - ) - - # new - if k > 0: - hessian[f"x{n}", f"a{k}"] = np.eye(self.d)[ - :, : self.var_dict[f"a{k}"] - ] - else: - # old implementation - i = n * self.d - j = (self.n_positions + k) * self.d - hessian_old[range(i, i + self.d), range(j, j + self.d)] = 1 - hessian_old[range(j, j + self.d), range(i, i + self.d)] = 1 - - # new - hessian[f"x{n}", f"a{k}"] = np.eye(self.d) - - hessian_new = hessian.get_matrix(self.base_var_dict).toarray() - np.testing.assert_allclose(hessian_old, hessian_new) - - hessians.append(hessian.get_matrix(self.base_var_dict)) - assert len(hessians) == self.M - return hessians - - def __repr__(self): - return f"rangeonlyslam1-{self.d}d"
- - - -if __name__ == "__main__": - lifter = RangeOnlySLAM1Lifter( - n_positions=3, n_landmarks=4, d=2, resample_landmarks=True, level="outer" - ) - lifter.run(n_dual=1, noise=0.1, plot=True) - - lifter = RangeOnlySLAM1Lifter( - n_positions=3, n_landmarks=4, d=2, resample_landmarks=False - ) - lifter.run(n_dual=1, noise=0.1, plot=True) - - lifter = RangeOnlySLAM1Lifter( - n_positions=3, n_landmarks=4, d=2, resample_landmarks=True - ) - lifter.run(n_dual=1, noise=0.1, plot=True) - lifter.run(n_dual=1, noise=0.1, plot=True) -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/examples/range_only_slam2.html b/docs/build/_modules/popr/examples/range_only_slam2.html deleted file mode 100644 index c95b1c7..0000000 --- a/docs/build/_modules/popr/examples/range_only_slam2.html +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - - - popr.examples.range_only_slam2 — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.examples.range_only_slam2

-import numpy as np
-from poly_matrix.least_squares_problem import LeastSquaresProblem
-from popr.examples.range_only_slam1 import RangeOnlySLAM1Lifter
-
-
-
-[docs] -class RangeOnlySLAM2Lifter(RangeOnlySLAM1Lifter): - """Range-only SLAM, version 1 - - Uses substitutions e_ik = ||a_k - t_i|| - """ - - def __init__(self, n_positions, n_landmarks, d, edges=None): - super().__init__(n_positions, n_landmarks, d, edges=edges) - - @property - def M(self): - return len(self.edges) - - @property - def sub_var_dict(self): - var_dict = {} - var_dict.update({f"e{n}{k}": 1 for n, k in self.edges}) - return var_dict - - def get_x(self, theta=None): - if theta is None: - theta = self.theta - positions, landmarks = self.get_positions_and_landmarks(theta) - - x_data = [[1]] - x_data += [list(theta)] - x_data += [ - [np.linalg.norm(landmarks[k] - positions[n]) ** 2] for n, k in self.edges - ] - x = np.concatenate(x_data, axis=0) - assert len(x) == self.N + self.M + 1 - return x - - def get_Q_from_y(self, y): - self.ls_problem = LeastSquaresProblem() - for n, k in self.edges: - self.ls_problem.add_residual({self.HOM: y[n, k], f"e{n}{k}": -1}) - # fix Gauge freedom - if self.remove_gauge == "cost": - I = np.eye(self.d) - for d in range(self.d): - self.ls_problem.add_residual({"a0": I[d].reshape((1, -1))}) - return self.ls_problem.get_Q().get_matrix(self.var_dict) - - def get_A_known(self): - from poly_matrix.poly_matrix import PolyMatrix - - A_list = [] - for n, k in self.edges: - A = PolyMatrix() - A[f"x{n}", f"x{n}"] = np.eye(self.d) - if self.remove_gauge == "hard": - if k > 0: - A[f"a{k}", f"a{k}"] = np.eye(self.var_dict[f"a{k}"]) - A[f"x{n}", f"a{k}"] = -np.eye(self.d)[:, : self.var_dict[f"a{k}"]] - else: - A[f"a{k}", f"a{k}"] = np.eye(self.d) - A[f"x{n}", f"a{k}"] = -np.eye(self.d) - A[self.HOM, f"e{n}{k}"] = -0.5 - A_list.append(A.get_matrix(self.var_dict)) - return A_list - - def get_J_lifting(self, t): - positions, landmarks = self.get_positions_and_landmarks(t) - - J_lifting = np.zeros((self.M, self.N)) - for i, (n, k) in enumerate(self.edges): - delta = landmarks[k] - positions[n] - - # grad w.r.t. position - # d/dp_n|| ak - p_n || = -2(ak - p_n) - J_lifting[i, n * self.d : (n + 1) * self.d] = -2 * delta - - if self.remove_gauge == "hard": - # grad w.r.t. landmark d/d_pn ||a_k - p_n || = 2(ak - pn) - self.fill_depending_on_k(J_lifting[i], k, 2 * delta) - else: - start = self.n_positions * self.d + k * self.d - J_lifting[i, start : start + self.d] = 2 * delta - return J_lifting - - def get_hess_lifting(self, t): - """return list of the hessians of the M lifting functions.""" - hessians = [] - for j, (n, k) in enumerate(self.edges): - hessian = np.zeros((self.N, self.N)) - i = n * self.d - - # diagonal for position - hessian[range(i, i + self.d), range(i, i + self.d)] = 2.0 - if self.remove_gauge == "hard": - # diagonal for landmark - self.fill_hessian_depending_on_k(hessian, k, val=2.0) # along i - # diagonal for position - landmark - self.fill_hessian_depending_on_k( - hessian, k, fix_i=range(i, i + self.d), val=-2.0 - ) - else: - j = (self.n_positions + k) * self.d - hessian[range(i, i + self.d), range(i, i + self.d)] = 2 - # diagonal for landmark - hessian[range(j, j + self.d), range(j, j + self.d)] = 2 - # off-diagonal position - landmark - hessian[range(i, i + self.d), range(j, j + self.d)] = -2 - hessian[range(j, j + self.d), range(i, i + self.d)] = -2 - hessians.append(hessian) - return hessians - - def __repr__(self): - return f"rangeonlyslam2-{self.d}d"
- - - -if __name__ == "__main__": - lifter = RangeOnlySLAM2Lifter(n_positions=3, n_landmarks=4, d=2) - lifter.run(n_dual=1) -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/examples/rotation_lifter.html b/docs/build/_modules/popr/examples/rotation_lifter.html deleted file mode 100644 index 5b3d1c7..0000000 --- a/docs/build/_modules/popr/examples/rotation_lifter.html +++ /dev/null @@ -1,330 +0,0 @@ - - - - - - - - popr.examples.rotation_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.examples.rotation_lifter

-import numpy as np
-from poly_matrix.poly_matrix import PolyMatrix
-from scipy.spatial.transform import Rotation as R
-
-from popr.base_lifters import StateLifter
-
-METHOD = "CG"
-SOLVER_KWARGS = dict(
-    min_gradient_norm=1e-7, max_iterations=10000, min_step_size=1e-8, verbosity=1
-)
-
-
-
-[docs] -class RotationLifter(StateLifter): - """Rotation averaging problem.""" - - LEVELS = ["no"] - HOM = "h" - VARIABLE_LIST = [["h", "c"]] - - # whether or not to include the determinant constraints in the known constraints. - ADD_DETERMINANT = False - - NOISE = 1e-3 - - # Add any parameters here that describe the problem (e.g. number of landmarks etc.) - def __init__(self, level="no", param_level="no", d=2, n_meas=2): - self.n_meas = n_meas - self.level = level - super().__init__( - level=level, - param_level=param_level, - d=d, - ) - - @property - def var_dict(self): - return {self.HOM: 1, "c": self.d**2} - - def sample_theta(self): - """Generate a random new feasible point.""" - - if self.d == 2: - angle = np.random.uniform(0, 2 * np.pi) - C = R.from_euler("z", angle).as_matrix()[:2, :2] - elif self.d == 3: - C = R.random().as_matrix() - return C - - def get_x(self, theta=None, parameters=None, var_subset=None) -> np.ndarray: - """Get the lifted vector x given theta and parameters.""" - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - if var_subset is None: - var_subset = self.var_dict.keys() - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - elif key == "c": - x_data += list(theta.flatten("C")) - dim_x = self.get_dim_x(var_subset=var_subset) - assert len(x_data) == dim_x - return np.array(x_data) - - def get_theta(self, x: np.ndarray) -> np.ndarray: - assert np.ndim(x) == 1 - C_flat = x[1 : 1 + self.d**2] - return C_flat.reshape((self.d, self.d)) - - def get_Q(self, noise: float | None = None): - if noise is None: - noise = self.NOISE - if self.y_ is None: - self.y_ = [] - for i in range(self.n_meas): - # noise model: R_i = R.T @ Rnoise - if noise > 0: - # Generate a random small rotation as noise and apply it - noise_rotvec = np.random.normal(scale=noise, size=(self.d,)) - Rnoise = ( - R.from_rotvec(noise_rotvec).as_matrix() - if self.d == 3 - else R.from_euler("z", noise_rotvec[0]).as_matrix()[:2, :2] - ) - Ri = self.theta.T @ Rnoise - else: - Ri = self.theta.T - self.y_.append(Ri) - - return self.get_Q_from_y(self.y_) - - def get_Q_from_y(self, y, output_poly=False): - # f(R) = sum_i || R @ R_i - I ||_F^2 - # argmin f(R) = argmin sum_i || R_i.T @ R_i ||^2 - 2 tr(R.T @ R_i) + ||I||_F^2 - # = argmin sum_i -2 tr(R.T @ R_i) + sum_i d - # = argmin sum_i -2 vec(R).T @ vec(R_i.T) + N * d - # sanity check for zero noise: - # || R @ R.T - I ||_F^2 = 0 - """param y: list of noisy rotation matrices.""" - Q = PolyMatrix() - for Ri in y: - Q[self.HOM, "c"] -= Ri.T.flatten("C")[None, :] - Q[self.HOM, self.HOM] += len(y) * self.d - if output_poly: - return Q - else: - return Q.get_matrix(self.var_dict) - - def local_solver_old( - self, t0, y, verbose=False, method=METHOD, solver_kwargs=SOLVER_KWARGS - ): - import pymanopt - from pymanopt.manifolds import SpecialOrthogonalGroup - - if method == "CG": - from pymanopt.optimizers import ConjugateGradient as Optimizer # fastest - elif method == "SD": - from pymanopt.optimizers import SteepestDescent as Optimizer # slow - elif method == "TR": - from pymanopt.optimizers import TrustRegions as Optimizer # okay - else: - raise ValueError(method) - - if verbose: - solver_kwargs["verbosity"] = 2 - else: - solver_kwargs["verbosity"] = 0 - - manifold = SpecialOrthogonalGroup(self.d, k=1) - - @pymanopt.function.autograd(manifold) - def cost(R): - cost = 0 - for Ri in y: - cost += np.sum((R.T @ Ri - np.eye(self.d)) ** 2) - return cost - - euclidean_gradient = None - problem = pymanopt.Problem( - manifold, cost, euclidean_gradient=euclidean_gradient - ) - optimizer = Optimizer(**solver_kwargs) - - res = optimizer.run(problem, initial_point=t0) - theta_hat = res.point - - success = ("min step_size" in res.stopping_criterion) or ( - "min grad norm" in res.stopping_criterion - ) - info = { - "success": success, - "msg": res.stopping_criterion, - } - if success: - return theta_hat, info, cost - - def test_and_add(self, A_list, Ai, output_poly): - x = self.get_x() - Ai_sparse = Ai.get_matrix(self.var_dict) - err = x.T @ Ai_sparse @ x - assert abs(err) <= 1e-10, err - if output_poly: - A_list.append(Ai) - else: - A_list.append(Ai_sparse) - - def get_A_known(self, var_dict=None, output_poly=False, add_redundant=False): - A_list = [] - if var_dict is None: - var_dict = self.var_dict - - if "c" in var_dict: - # enforce diagonal == 1 - for i in range(self.d): - Ei = np.zeros((self.d, self.d)) - Ei[i, i] = 1.0 - constraint = np.kron(Ei, np.eye(self.d)) - Ai = PolyMatrix(symmetric=True) - Ai["c", "c"] = constraint - Ai[self.HOM, self.HOM] = -1 - self.test_and_add(A_list, Ai, output_poly=output_poly) - - # enforce off-diagonal == 0 - for i in range(self.d): - for j in range(i + 1, self.d): - Ei = np.zeros((self.d, self.d)) - Ei[i, j] = 1.0 - Ei[j, i] = 1.0 - constraint = np.kron(Ei, np.eye(self.d)) - Ai = PolyMatrix(symmetric=True) - Ai["c", "c"] = constraint - self.test_and_add(A_list, Ai, output_poly=output_poly) - - # enforce that determinant is one. - if self.d == 2 and self.ADD_DETERMINANT: - # C = [a b; c d]; ad - bc - 1 = 0 - # a b c d - # a 1 - # b -1 - # c -1 - # d 1 - Ai = PolyMatrix(symmetric=True) - constraint = np.zeros((self.d**2, self.d**2)) - constraint[0, 3] = constraint[3, 0] = 1.0 - constraint[1, 2] = constraint[2, 1] = -1.0 - Ai[self.HOM, self.HOM] = -2 - Ai["c", "c"] = constraint - self.test_and_add(A_list, Ai, output_poly=output_poly) - elif self.d == 3 and self.ADD_DETERMINANT: - # c11 c12 c13 c21 * c32 - c31 * c22 = c13 - # C = [c21, c22, c23]; c1 x c2 = c3: c31 * c12 - c11 * c12 = c23 - # c31 c32 c33 c11 * c22 - c21 * c12 = c33 - print( - "Warning: consider implementing the determinant constraint for RobustPoseLifter, d=3" - ) - return A_list
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/examples/stereo1d_lifter.html b/docs/build/_modules/popr/examples/stereo1d_lifter.html deleted file mode 100644 index a3cdec0..0000000 --- a/docs/build/_modules/popr/examples/stereo1d_lifter.html +++ /dev/null @@ -1,317 +0,0 @@ - - - - - - - - popr.examples.stereo1d_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.examples.stereo1d_lifter

-from typing import Optional
-
-import numpy as np
-from poly_matrix.least_squares_problem import LeastSquaresProblem
-from poly_matrix.poly_matrix import PolyMatrix
-
-from popr.base_lifters import StateLifter
-
-
-
-[docs] -class Stereo1DLifter(StateLifter): - """Toy example for stereo localization in 1D. We minimize the following cost function: - - .. math:: - f(\\theta) = \\sum_{j=0}^{N-1} (u_j - 1 / (\\theta - a_j))^2 - - where :math:`a_j` are the landmarks and :math:`u_j` are the measurements. - - This is the pedagogical running example of `this paper <https://arxiv.org/abs/2308.05783>`_. - and also used in the :ref:`Quick Start Guide`. - """ - - PARAM_LEVELS = ["no", "p", "ppT"] - VARIABLE_LIST = [["h", "x"], ["h", "x", "z_0"], ["h", "x", "z_0", "z_1"]] - - NOISE = 0.1 - - def __init__(self, n_landmarks, param_level="no"): - self.n_landmarks = n_landmarks - self.d = 1 - self.W = 1.0 - - # will be initialized later - self.landmarks_ = None - - super().__init__(param_level=param_level, d=self.d, n_parameters=n_landmarks) - - @property - def landmarks(self): - if self.landmarks_ is None: - self.landmarks_ = np.random.rand(self.n_landmarks, self.d) - return self.landmarks_ - - def sample_parameters(self, theta=None): - if self.parameters_ is None: - return self.sample_parameters_landmarks(self.landmarks) - landmarks = np.random.rand(self.n_landmarks, self.d) - return self.sample_parameters_landmarks(landmarks) - - def sample_theta(self): - x_try = np.random.rand(1) - counter = 0 - while np.min(np.abs(x_try - self.landmarks)) <= 1e-2: - x_try = np.random.rand(1) - counter += 1 - if counter >= 1000: - print("Warning: couldn't find valid setup") - return - return x_try - - def get_x(self, theta=None, parameters=None, var_subset=None): - """ - :param var_subset: list of variables to include in x vector. Set to None for all. - """ - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - - if var_subset is None: - var_subset = self.var_dict.keys() - - if self.param_level == "no": - landmarks = { - f"p_{i}": self.landmarks[i] for i in range(self.landmarks.shape[0]) - } - else: - landmarks = { - f"p_{i}": parameters[f"p_{i}"][: self.d] - for i in range(self.landmarks.shape[0]) - } - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - elif key == "x": - x_data.append(theta[0]) - elif "z" in key: - idx = int(key.split("_")[-1]) - x_data.append(1 / (theta[0] - landmarks[f"p_{idx}"])) - else: - raise ValueError("unknown key in get_x", key) - return np.hstack(x_data) - - @property - def var_dict(self): - vars = [self.HOM, "x"] + [f"z_{j}" for j in range(self.n_landmarks)] - return {v: 1 for v in vars} - - @property - def param_dict(self): - return self.param_dict_landmarks - - def get_Q(self, noise: Optional[float] = None): - if self.landmarks is None: - raise ValueError("self.landmarks must be initialized before calling get_Q.") - if noise is None: - noise = self.NOISE - - y = 1 / (self.theta - self.landmarks.flatten()) + np.random.normal( - scale=noise, loc=0, size=self.n_landmarks - ) - if self.y_ is None: - self.y_ = y - - return self.get_Q_from_y(y) - - def get_Q_from_y(self, y): - ls_problem = LeastSquaresProblem() - for j in range(len(y)): - ls_problem.add_residual({self.HOM: -y[j], f"z_{j}": 1}) - return ls_problem.get_Q().get_matrix(self.var_dict) - - def get_A_known(self, var_dict=None, output_poly=False): - if var_dict is None: - var_dict = self.var_dict - - # if self.add_parameters: - # raise ValueError("can't extract known matrices yet when using parameters.") - - A_known = [] - - # enforce that z_j = 1/(x - a_j) <=> 1 - z_j*x + a_j*z_j = 0 - if not ("x" in var_dict and self.HOM in var_dict): - return [] - - landmark_indices = [ - int(key.split("_")[-1]) for key in var_dict if key.startswith("z_") - ] - for j in landmark_indices: - A = PolyMatrix() - A[self.HOM, f"z_{j}"] = 0.5 * self.landmarks[j] - A["x", f"z_{j}"] = -0.5 - A[self.HOM, self.HOM] = 1.0 - if output_poly: - A_known.append(A) - else: - A_known.append(A.get_matrix(variables=self.var_dict)) - return A_known - - def get_A_known_redundant(self, var_dict=None, output_poly=False): - import itertools - - if var_dict is None: - var_dict = self.var_dict - - assert self.HOM in var_dict, "homogenization variable must be in var_dict" - - landmark_indices = [ - int(key.split("_")[-1]) for key in var_dict if key.startswith("z_") - ] - # add known redundant constraints: - # enforce that z_j - z_i = (a_j - a_i) * z_j * z_i - A_known = [] - for i, j in itertools.combinations(landmark_indices, 2): - A = PolyMatrix() - A[self.HOM, f"z_{j}"] = 1 - A[self.HOM, f"z_{i}"] = -1 - A[f"z_{i}", f"z_{j}"] = self.landmarks[i] - self.landmarks[j] - if output_poly: - A_known.append(A) - else: - A_known.append(A.get_matrix(variables=self.var_dict)) - return A_known - - def get_cost(self, t, y): - return np.sum((y - (1 / (t - self.landmarks.flatten()))) ** 2) - - def local_solver( - self, t_init, y, num_iters=100, eps=1e-5, W=None, verbose=False, **kwargs - ): - info = {} - a = self.landmarks.flatten() - x_op = t_init - for i in range(num_iters): - u = y - (1 / (x_op - a)) - if verbose: - print(f"cost {i}", np.sum(u**2)) - du = 1 / ((x_op - a) ** 2) - if np.linalg.norm(du) > 1e-10: - dx = -np.sum(u * du) / np.sum(du * du) - x_op = x_op + dx - if np.abs(dx) < eps: - msg = f"converged in dx after {i} it" - cost = self.get_cost(x_op, y) - info = {"msg": msg, "cost": cost, "success": True} - return x_op, info, cost - else: - msg = f"converged in du after {i} it" - cost = self.get_cost(x_op, y) - info = {"msg": msg, "cost": self.get_cost(x_op, y), "success": True} - return x_op, info, cost - return None, {"msg": "didn't converge", "cost": None, "success": False}, None - - def __repr__(self): - return f"stereo1d_{self.param_level}"
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/examples/stereo2d_lifter.html b/docs/build/_modules/popr/examples/stereo2d_lifter.html deleted file mode 100644 index 6f707eb..0000000 --- a/docs/build/_modules/popr/examples/stereo2d_lifter.html +++ /dev/null @@ -1,211 +0,0 @@ - - - - - - - - popr.examples.stereo2d_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.examples.stereo2d_lifter

-# import autograd.numpy as np
-import numpy as np
-
-from popr.base_lifters import StereoLifter
-from popr.utils.geometry import convert_phi_to_theta, convert_theta_to_phi
-from popr.utils.stereo2d_problem import _cost, local_solver
-
-
-def change_dimensions(a, y, x):
-    p_w = np.concatenate([a, np.ones((a.shape[0], 1))], axis=1)
-    y_mat = np.c_[[*y]]  # N x 2
-    return p_w[:, :, None], y_mat[:, :, None], x[:, None]
-
-
-GTOL = 1e-6
-
-
-
-[docs] -class Stereo2DLifter(StereoLifter): - """Stereo-camera localization in 2D. - - We minimize the following cost function: - - .. math:: - f(\\theta) = \\sum_{j=0}^{n} (u_j - M q_j / q_j[1])^2 - - where - - - :math:`p_j` are known landmarks (in homogeneous coordinates), - - :math:`u_j` are pixel measurements (2 elements: one pixel in left "image" and one in right "image"), - - :math:`q_j = T(\\theta) p_j` are the (homogeneous) coordinates of landmark j in the (unknown) camera frame, parameterized by :math:`T(\\theta)`, and - - :math:`M` is the stereo camera calibration matrix. Here, it is given by - - .. math:: - - \\begin{bmatrix} - f_u & c_u & \\frac{b f_u}{2} \\\\ - f_v & c_v & -\\frac{b f_v}{2} \\\\ - \\end{bmatrix} - - where :math:`f_u, f_v` are horizontal and vertical focal lengths, :math:`c_u,c_v` are image center points in pixels and :math:`b` is the camera baseline. - - This example is treated in more details in `this paper <https://arxiv.org/abs/2308.05783>`_. - """ - - def __init__(self, n_landmarks, level="no", param_level="no", variable_list=None): - self.W = np.stack([np.eye(2)] * n_landmarks) - - super().__init__( - n_landmarks=n_landmarks, - level=level, - param_level=param_level, - d=2, - variable_list=variable_list, - ) - - @property - def M_matrix(self): - f_u = 484.5 - c_u = 322 - b = 0.24 - return np.array([[f_u, c_u, f_u * b / 2], [f_u, c_u, -f_u * b / 2]]) - - def get_cost(self, t, y, W=None): - - if W is None: - W = self.W - a = self.landmarks - - phi = convert_theta_to_phi(t) - p_w, y, phi = change_dimensions(a, y, phi) - cost = _cost(phi, p_w, y, W, self.M_matrix) - if StereoLifter.NORMALIZE: - return cost / (self.n_landmarks * self.d) - else: - return cost - - def local_solver(self, t_init, y, W=None, verbose=False, **kwargs): - - if W is None: - W = self.W - a = self.landmarks - - init_phi = convert_theta_to_phi(t_init) - p_w, y, __ = change_dimensions(a, y, init_phi) - success, phi_hat, cost = local_solver( - p_w=p_w, y=y, W=W, init_phi=init_phi, log=verbose, gtol=GTOL - ) - if StereoLifter.NORMALIZE: - cost /= self.n_landmarks * self.d - # cost /= self.n_landmarks * self.d - theta_hat = convert_phi_to_theta(phi_hat) - info = {"success": success, "msg": "converged"} - if success: - return theta_hat, info, cost - else: - return None, info, cost
- - - -if __name__ == "__main__": - lifter = Stereo2DLifter(n_landmarks=3) -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/examples/stereo3d_lifter.html b/docs/build/_modules/popr/examples/stereo3d_lifter.html deleted file mode 100644 index cba5256..0000000 --- a/docs/build/_modules/popr/examples/stereo3d_lifter.html +++ /dev/null @@ -1,284 +0,0 @@ - - - - - - - - popr.examples.stereo3d_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.examples.stereo3d_lifter

-import pickle
-
-import numpy as np
-
-from popr.base_lifters import StereoLifter
-from popr.utils.geometry import get_T, get_theta_from_T
-from popr.utils.stereo3d_problem import _cost, local_solver
-
-
-def change_dimensions(a, y):
-    p_w = np.concatenate([a, np.ones((a.shape[0], 1))], axis=1)
-    y_mat = np.c_[[*y]]  # N x 2
-    return p_w[:, :, None], y_mat[:, :, None]
-
-
-GTOL = 1e-6
-
-
-
-[docs] -class Stereo3DLifter(StereoLifter): - """Stereo-camera localization in 3D. - - Analogously to :class:`Stereo2DLifter`, we minimize the following cost function: - - .. math:: - f(\\theta) = \\sum_{j=0}^{n} (u_j - M q_j / q_j[2])^2 - - where - - - :math:`p_j` are known landmarks (in homogeneous coordinates), - - :math:`u_j` are pixel measurements (4 elements: two pixel coordinates in left image and two in right image), - - :math:`q_j = T(\\theta) p_j` are the (homogeneous) coordinates of landmark j in the (unknown) camera frame, parameterized by :math:`T(\\theta)`, and - - :math:`M` is the stereo camera calibration matrix. Here, it is given by - - .. math:: - - \\begin{bmatrix} - f_u & 0 & c_u & \\frac{b f_u}{2} \\\\ - 0 & f_v & c_v & 0 \\\\ - f_u & 0 & c_u & -\\frac{b f_u}{2} \\\\ - 0 & f_v & c_v & 0 \\\\ - \\end{bmatrix} - - where :math:`f_u, f_v` are horizontal and vertical focal lengths, :math:`c_u,c_v` are image center points in pixels and :math:`b` is the camera baseline. - - This example is treated in more details in `this paper <https://arxiv.org/abs/2308.05783>`_. - """ - - def __init__(self, n_landmarks, level="no", param_level="no", variable_list=None): - self.W = np.stack([np.eye(4)] * n_landmarks) - - super().__init__( - n_landmarks=n_landmarks, - level=level, - param_level=param_level, - d=3, - variable_list=variable_list, - ) - - @property - def M_matrix(self): - f_u = 484.5 - f_v = 484.5 - c_u = 322 - c_v = 247 - b = 0.24 - return np.array( - [ - [f_u, 0, c_u, f_u * b / 2], - [0, f_v, c_v, 0], - [f_u, 0, c_u, -f_u * b / 2], - [0, f_v, c_v, 0], - ] - ) - - @staticmethod - def from_file(fname): - with open(fname, "rb") as f: - y_ = pickle.load(f) - landmarks = pickle.load(f) - theta = pickle.load(f) - - level = pickle.load(f) - param_level = pickle.load(f) - variable_list = pickle.load(f) - lifter = Stereo3DLifter( - n_landmarks=landmarks.shape[0], - level=level, - param_level=param_level, - variable_list=variable_list, - ) - lifter.y_ = y_ - lifter.landmarks_ = landmarks - lifter.parameters = np.r_[1, landmarks.flatten()] - lifter.theta = theta - return lifter - - def to_file(self, fname): - with open(fname, "wb") as f: - pickle.dump(self.y_, f) - pickle.dump(self.landmarks, f) - - pickle.dump(self.theta, f) - pickle.dump(self.level, f) - pickle.dump(self.param_level, f) - pickle.dump(self.variable_list, f) - - def get_cost(self, t, y, W=None): - """ - :param t: can be either - - x, y, z, yaw, pitch roll: vector of unknowns, or - - [c1, c2, c3, x, y, z], the theta vector (flattened C and x, y, z) - """ - - if W is None: - W = self.W - a = self.landmarks - - p_w, y = change_dimensions(a, y) - - T = get_T(theta=t, d=3) - - cost = _cost(p_w=p_w, y=y, T=T, M=self.M_matrix, W=W) - if StereoLifter.NORMALIZE: - return cost / (self.n_landmarks * self.d) - else: - return cost - - def local_solver(self, t_init, y, W=None, verbose=False, **kwargs): - """ - :param t_init: same options asfor t in cost. - """ - - if W is None: - W = self.W - - a = self.landmarks - p_w, y = change_dimensions(a, y) - T_init = get_T(theta=t_init, d=3) - - info, T_hat, cost = local_solver( - T_init=T_init, - y=y, - p_w=p_w, - W=W, - M=self.M_matrix, - log=False, - gtol=GTOL, - min_update_norm=-1, # makes this inactive - ) - - if verbose: - print("Stereo3D local solver:", info["msg"]) - - if StereoLifter.NORMALIZE: - - cost /= self.n_landmarks * self.d - - x_hat = get_theta_from_T(T_hat) - x = self.get_x(theta=x_hat) - Q = self.get_Q_from_y(y[:, :, 0]) - cost_Q = x.T @ Q @ x - if abs(cost) > 1e-10: - if not (abs(cost_Q - cost) / cost < 1e-8): - print(f"Warning, cost not equal {cost_Q:.2e} {cost:.2e}") - - if info["success"]: - return x_hat, info, cost - else: - return None, info, cost
- - - -if __name__ == "__main__": - lifter = Stereo3DLifter(n_landmarks=4) -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/examples/wahba_lifter.html b/docs/build/_modules/popr/examples/wahba_lifter.html deleted file mode 100644 index e4fb820..0000000 --- a/docs/build/_modules/popr/examples/wahba_lifter.html +++ /dev/null @@ -1,342 +0,0 @@ - - - - - - - - popr.examples.wahba_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for popr.examples.wahba_lifter

-# import autograd.numpy as np
-import numpy as np
-
-from popr.base_lifters import RobustPoseLifter
-from popr.utils.geometry import get_C_r_from_theta
-from popr.utils.plotting_tools import plot_frame
-
-N_TRYS = 10
-
-# TODO(FD) for some reason this is not required as opposed to what is stated in Heng's paper
-# and it currently breaks tightness (might be a bug in my implementation though)
-USE_INEQ = False
-
-NORMALIZE = False
-
-
-
-[docs] -class WahbaLifter(RobustPoseLifter): - """This example is treated in more details in `this paper <https://arxiv.org/abs/2308.05783>`_, - under the name "PPR" (point-to-point registration). - """ - - NOISE = 1e-2 # inlier noise - NOISE_OUT = 1.0 # outlier noise - - def h_list(self, t): - """ - We want to inforce that - - norm(t) <= 10 (default) - as constraints h_j(t)<=0 - """ - default = super().h_list(t) - return default - - def get_random_position(self): - return np.random.uniform( - -0.5 * self.MAX_DIST ** (1 / self.d), - 0.5 * self.MAX_DIST ** (1 / self.d), - size=self.d, - ) - - def get_B_known(self): - """Get inequality constraints of the form x.T @ B @ x <= 0""" - if not USE_INEQ: - return [] - - default = super().get_B_known() - return default - - def term_in_norm(self, R, t, pi, ui): - return R @ pi + t - ui - - def residual_sq(self, R, t, pi, ui): - # TODO: can easily extend below to matrix-weighted - W = np.eye(self.d) - res_sq = (R @ pi + t - ui).T @ W @ (R @ pi + t - ui) - if NORMALIZE: - return res_sq / (self.n_landmarks * self.d) ** 2 - return res_sq - - def plot_setup(self): - if self.d != 2: - print("Plotting currently only supported for d=2") - return - import matplotlib.pylab as plt - - fig, ax = plt.subplots() - - # R, t = get_C_r_from_theta(self.theta, self.d) - # ax.scatter(*t, color="k", label="pose") - - ax.axis("equal") - t_wc_w, C_cw = plot_frame(ax, self.theta, label="pose", color="gray", d=2) - - if self.y_ is not None: - for i in range(self.y_.shape[0]): - ax.scatter(*self.landmarks[i], color=f"C{i}", label="landmarks") - - # this vector is in camera coordinates - t_cpi_c = self.y_[i] - # t_cpi_w: vector from camera to pi in world coordinates - - ax.plot( - [t_wc_w[0], self.landmarks[i][0]], - [t_wc_w[1], self.landmarks[i][1]], - color=f"C{i}", - ls=":", - ) - if C_cw is not None: - t_cpi_w = C_cw.T @ t_cpi_c - ax.plot( - [t_wc_w[0], t_wc_w[0] + t_cpi_w[0]], - [t_wc_w[1], t_wc_w[1] + t_cpi_w[1]], - color=f"r" if i < self.n_outliers else "g", - ) - - def get_Q( - self, - noise: float | None = None, - output_poly: bool = False, - use_cliques: list = [], - ): - if noise is None: - noise = self.NOISE - - if self.y_ is None: - theta = self.theta[: self.d + self.d**2] - outlier_index = self.get_outlier_index() - - self.y_ = np.empty((self.n_landmarks, self.d)) - R, t = get_C_r_from_theta(theta, self.d) - for i in range(self.n_landmarks): - valid_measurement = False - for _ in range(N_TRYS): - outlier = i in outlier_index - y_i = R @ self.landmarks[i] + t - if outlier: - y_i += np.random.normal( - scale=self.NOISE_OUT, loc=0, size=self.d - ) - else: - y_i += np.random.normal(scale=noise, loc=0, size=self.d) - - residual = self.residual_sq(R, t, self.landmarks[i], y_i) - if not self.robust: - valid_measurement = True - else: - if outlier: - valid_measurement = residual > self.beta - else: - valid_measurement = residual < self.beta - if valid_measurement: - break - if not valid_measurement and self.robust: - self.plot_setup() - raise ValueError("did not find a valid measurement.") - self.y_[i] = y_i - Q = self.get_Q_from_y(self.y_, output_poly=output_poly, use_cliques=use_cliques) - return Q - - def get_Q_from_y(self, y, output_poly: bool = False, use_cliques: list = []): - """ - every cost term can be written as - (1 + wi)/b^2 r^2(x, zi) + (1 - wi) - - residual term: - (Rpi + t - ui).T Wi (Rpi + t - ui) = - [t', vec(R)'] @ [I (pi x I)]' @ Wi @ [I (pi x I)] @ [t ; vec(R)] - ------x'----- -----Pi'----- - - 2 [t', vec(R)'] @ [I (pi x I)]' Wi @ ui - -----x'------ ---------Pi_xl-------- - + ui.T @ Wi @ ui - -----Pi_ll------ - """ - - if len(use_cliques): - js = use_cliques - else: - js = list(range(self.n_landmarks)) - - from poly_matrix import PolyMatrix - - Q = PolyMatrix(symmetric=True) - if NORMALIZE: - norm = (self.n_landmarks * self.d) ** 2 - - Wi = np.eye(self.d) - for i in js: - pi = self.landmarks[i] - ui = y[i] - Pi = np.c_[np.eye(self.d), np.kron(pi, np.eye(self.d))] - - Pi_ll = ui.T @ Wi @ ui - Pi_xl = -(Pi.T @ Wi @ ui)[:, None] - Qi = Pi.T @ Wi @ Pi - if NORMALIZE: - Pi_ll /= norm - Pi_xl /= norm - Qi /= norm - - if self.robust: - Qi /= self.beta**2 - Pi_ll /= self.beta**2 - Pi_xl /= self.beta**2 - # Q["x", "x"] += Qi - Q["t", "t"] += Qi[: self.d, : self.d] - Q["t", "c"] += Qi[: self.d, self.d :] - Q["c", "c"] += Qi[self.d :, self.d :] - - # Q["x", self.HOM] += Pi_xl - Q["t", self.HOM] += Pi_xl[: self.d, :] - Q["c", self.HOM] += Pi_xl[self.d :, :] - Q[self.HOM, self.HOM] += ( - 1 + Pi_ll - ) # 1 from (1 - wi), Pi_ll from first term. - Q[ - self.HOM, f"w_{i}" - ] += -0.5 # from (1 - wi), 0.5 cause on off-diagonal - if self.level == "xwT": - # Q[f"z_{i}", "x"] += 0.5 * Qi - Q[f"z_{i}", "t"] += 0.5 * Qi[:, : self.d] - Q[f"z_{i}", "c"] += 0.5 * Qi[:, self.d :] - - Q[self.HOM, f"w_{i}"] += 0.5 * Pi_ll # from first term - - Q[f"z_{i}", self.HOM] += Pi_xl - elif self.level == "xxT": - Q["z_0", f"w_{i}"] += 0.5 * Qi.flatten()[:, None] - - # Q["x", f"w_{i}"] += Pi_xl - Q["t", f"w_{i}"] += Pi_xl[: self.d, :] - Q["c", f"w_{i}"] += Pi_xl[self.d :, :] - - Q[self.HOM, f"w_{i}"] += 0.5 * Pi_ll - else: - # Q["x", "x"] += Qi - Q["t", "t"] += Qi[: self.d, : self.d] - Q["t", "c"] += Qi[: self.d, self.d :] - Q["c", "c"] += Qi[self.d :, self.d :] - - # Q["x", self.HOM] += Pi_xl - Q["t", self.HOM] += Pi_xl[: self.d, :] - Q["c", self.HOM] += Pi_xl[self.d :, :] - Q[self.HOM, self.HOM] += Pi_ll # on diagonal - if output_poly: - return 0.5 * Q - Q_sparse = 0.5 * Q.get_matrix(variables=self.var_dict) - return Q_sparse - - def __repr__(self): - appendix = "_robust" if self.robust else "" - return f"wahba_{self.d}d_{self.level}_{self.param_level}{appendix}"
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/lifters/range_only_lifters.html b/docs/build/_modules/popr/lifters/range_only_lifters.html deleted file mode 100644 index f7a6783..0000000 --- a/docs/build/_modules/popr/lifters/range_only_lifters.html +++ /dev/null @@ -1,463 +0,0 @@ - - - - - - - - popr.lifters.range_only_lifters — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.lifters.range_only_lifters

-import itertools
-from abc import abstractmethod
-
-import numpy as np
-
-from .state_lifter import StateLifter
-
-# How to deal with Gauge freedom.
-# - None: do not remove it
-# - "hard": remove variables alltogether
-# - "cost": add cost on a0 (attempting to make it 0)
-# recommended is "hard"
-# REMOVE_GAUGE = None
-REMOVE_GAUGE = "hard"
-# REMOVE_GAUGE = "cost"
-
-SOLVER_KWARGS = dict(
-    # method="Nelder-Mead",
-    # method="BFGS"  # the only one that almost always converges
-    method="Powell"
-)
-
-
-
-[docs] -class RangeOnlyLifter(StateLifter): - """Lifters for range-only localization and SLAM problems. - - Some clarifications on names: - - generate_random_landmarks: always fix - - first one at 0, (a0 = [a0_x, a0_y, a0_z] = [0, 0, 0]) - - second one along x, (a1_y = 0, a1_z = 0) - - third along positive y (a2_z = 0, a2_y > 0) } fix the "flip" in 2d, rotation in 3d - - fourth with positive z axis (a3_z > 0) } fix rotation in 3d - --> in 2d, will optimize for landmarks_theta = [a1_x, a2_x, a2_y, ...] - --> in 3d, will optimize for landmarks_theta = [a1_x, a2_x, a2_y, a3_x, a3_y, a3_z, ...] - saved in self.landmarks - - generate_random_positions: - return randomly generated positions (motion model to be implemented) - - - sample_theta: - - for localization problem, this can regenerate both landmarks and positions - - for SLAM problem, regenerate only positions (to be tested) - - theta: vector of unknowns [positions, (landmarks_theta for SLAM, see above)] - x: [1, theta, lifting_functions] - where lifting_functions contains: - - ||t_n||^2 for RangeOnlyLoc - - ||t_n||^2, ||a_k||^2, t_n.T@a_k for RangeOnlySLAM1 - - ||a_k - t_n||^2 for RangeOnlySLAM2 - """ - - def __init__( - self, - n_positions, - n_landmarks, - d, - edges=None, - remove_gauge=REMOVE_GAUGE, - param_level="no", - ): - self.remove_gauge = remove_gauge - self.n_positions = n_positions - self.n_landmarks = n_landmarks - self.landmarks = None - self.d = d - - # TODO(FD) replace edges with W - if edges is None: - self.edges = list( - itertools.product(range(n_positions), range(n_landmarks), repeat=1) - ) - else: - # TODO(FD) add tests - self.edges = edges - super().__init__(param_level=param_level) - - @property - def var_dict(self): - level_dim = self.get_level_dims()[self.level] - if self.var_dict_ is None: - self.var_dict_ = {self.HOM: 1} - self.var_dict.update({"x": self.d**2 + self.d}) - self.var_dict.update( - {f"z_{k}": self.d + level_dim for k in range(self.n_parameters)} - ) - return self.var_dict_ - - @property - def param_dict(self): - return self.param_dict_landmarks - - def sample_parameters(self): - landmarks = np.random.normal(loc=0, scale=1, size=(self.n_landmarks, self.d)) - return self.sample_parameters_landmarks(landmarks) - - def sample_theta(self): - self.positions = np.random.rand(self.n_positions, self.d) - return self.positions - - def get_theta(self, landmarks=None, positions=None): - if landmarks is None: - landmarks = self.sample_random_landmarks() - if positions is None: - positions = self.sample_random_positions() - theta = list(positions.flatten("C")) - if self.remove_gauge == "hard": - theta.append(landmarks[1, 0]) - if self.d == 2: - theta += list(landmarks[2:, :].flatten("C")) - elif self.d == 3: - theta.append(landmarks[2, 0]) - theta.append(landmarks[2, 1]) - theta += list(landmarks[3:, :].flatten("C")) - else: - theta += list(landmarks.flatten("C")) - return np.array(theta) - - def sample_random_landmarks(self): - landmarks = np.random.rand(self.n_landmarks, self.d) - if self.remove_gauge is not None: - landmarks[0, :] = 0.0 - landmarks[1, 1] = 0 # set a1_y = 0 - if self.d == 3: - landmarks[1, 2] = 0 # set a1_z = 0 - landmarks[2, 2] = 0 # set a2_z = 0 - - # TODO(FD) figure out if below is necessary (it shouldn't hurt anyways) - # make sure to also fix the flip - if self.d == 2: - # make sure a2_y > 0. - if landmarks[2, 1] < 0: # - landmarks[:, 1] = -landmarks[:, 1] - elif self.d == 3: - # landmarks 0, 1, 2 now live in the x-y plane. - if landmarks[3, 2] < 0: - landmarks[:, 2] = -landmarks[:, 2] - return landmarks - -
-[docs] - def get_positions_and_landmarks(self, theta): - """ - --> in 2d, will optimize for landmarks_theta = [a1_x, a2_x, a2_y, ...] - --> in 3d, will optimize for landmarks_theta = [a1_x, a2_x, a2_y, a3_x, a3_y, a3_z, ...] - """ - N = self.n_positions * self.d - positions = theta[:N].reshape((-1, self.d)) - - if len(theta) > N: - if self.remove_gauge == "hard": - # range-only SLAM - - # TODO(FD): figure out if it's easier to set to zero or to the actual landmarks. - landmarks = np.zeros((self.n_landmarks, self.d)) - # landmarks = deepcopy(self.landmarks) - - # landmarks[0, :] = 0.0 # self.landmarks[0, :] - landmarks[1, 0] = theta[N] - # landmarks[1, 1] = 0.0 # self.landmarks[1, 1] - if self.d == 2: - landmarks[2:, :] = theta[N + 1 :].reshape( - (self.n_landmarks - 2, self.d) - ) - elif self.d == 3: - # landmarks[1, 2] = 0.0 #self.landmarks[1, 2] - landmarks[2, 0] = theta[N + 1] - landmarks[2, 1] = theta[N + 2] - landmarks[3:, :] = theta[N + 3 :].reshape( - (self.n_landmarks - 3, self.d) - ) - else: - landmarks = theta[N:].reshape((-1, self.d)) - return positions, landmarks - else: - # range-only localization - return positions, self.landmarks
- - -
-[docs] - def get_Q(self, noise: float = 1e-3) -> tuple: - # N x K matrix - y_gt = ( - np.linalg.norm( - self.landmarks[None, :, :] - self.positions[:, None, :], axis=2 - ) - ** 2 - ) - y = y_gt + np.random.normal(loc=0, scale=noise, size=y_gt.shape) - Q = self.get_Q_from_y(y) - - # DEBUGGING - x = self.get_x() - cost1 = x.T @ Q @ x - cost2 = np.sum((y - y_gt) ** 2) - cost3 = self.get_cost(self.theta, y) - assert abs(cost1 - cost2) < 1e-10 - assert abs(cost1 - cost3) < 1e-10 - return Q
- - - def get_J(self, t, y): - import scipy.sparse as sp - - N = self.n_positions * self.n_landmarks - - J = sp.csr_array( - (np.ones(self.N), (range(1, self.N + 1), range(self.N))), - shape=(self.dim_x, self.N), - ) - J[self.N + 1 :, :] = self.get_J_lifting(t) - return J - -
-[docs] - def get_hess(self, t, y): - """get Hessian""" - x = self.get_x(t) - Q = self.get_Q_from_y(y) - J = self.get_J(t, y) - hess = 2 * J.T @ Q @ J - - hessians = self.get_hess_lifting(t) - B = self.ls_problem.get_B_matrix(self.var_dict) - residuals = B @ x - for m, h in enumerate(hessians): - bm_tilde = B[:, -self.M + m] - factor = float(bm_tilde.T @ residuals) - hess += 2 * factor * h - return hess
- - -
-[docs] - def get_grad(self, t, y): - """get gradient""" - J = self.get_J(t, y) - x = self.get_x(t) - Q = self.get_Q_from_y(y) - return 2 * J.T @ Q @ x
- - -
-[docs] - def get_cost(self, t, y): - """ - get cost for given positions, landmarks and noise. - - :param t: (positions, landmarks) tuple - """ - positions, landmarks = self.get_positions_and_landmarks(t) - - y_current = ( - np.linalg.norm(landmarks[None, :, :] - positions[:, None, :], axis=2) ** 2 - ) - cost = 0 - for n, k in self.edges: - cost += (y[n, k] - y_current[n, k]) ** 2 - return cost
- - -
-[docs] - def local_solver( - self, t_init, y, tol=1e-8, verbose=False, solver_kwargs=SOLVER_KWARGS - ): - """ - :param t_init: (positions, landmarks) tuple - """ - from scipy.optimize import minimize - - sol = minimize( - self.get_cost, - x0=t_init, - args=y, - jac=self.get_grad, - # hess=self.get_hess, not used by any solvers. - **solver_kwargs, - tol=tol, - options={"disp": verbose}, # j, "maxfun": 100}, - ) - if sol.success: - that = sol.x - rel_error = self.get_cost(that, y) - self.get_cost(sol.x, y) - assert abs(rel_error) < 1e-10, rel_error - cost = sol.fun - else: - that = cost = None - msg = sol.message + f"(# iterations: {sol.nit})" - return that, msg, cost
- - - @abstractmethod - def sample_theta(self): - pass - - @abstractmethod - def get_Q_from_y(self, y): - pass - - @abstractmethod - def get_J_lifting(self, t): - pass - - @abstractmethod - def get_x(self, theta=None): - return - - def plot_setup(self, title="setup"): - import matplotlib.pylab as plt - - if self.d == 3: - fig = plt.figure() - ax = fig.add_subplot(111, projection="3d") - else: - fig, ax = plt.subplots() - ax.scatter(*self.landmarks.T, label="landmarks", marker="x") - ax.scatter(*self.positions.T, label="positions", marker="x") - ax.legend() - ax.axis("equal") - ax.grid() - ax.set_title(title) - plt.show() - return fig, ax - - def plot_nullvector(self, vec, ax, **kwargs): - j = 0 - for n in range(self.n_positions): - pos = self.positions[n] - line = np.c_[pos, pos + vec[j : j + self.d]] # 2 x self.d - ax.plot(*line, **kwargs) - j += self.d - for n in range(self.n_landmarks): - if n == 1: - pos = self.landmarks[n] - e = np.zeros(self.d) - e[0] = vec[j] - line = np.c_[pos, pos + e] # 2 x self.d - ax.plot(*line, **kwargs) - j += 1 - elif n == 2: - pos = self.landmarks[n] - e = np.zeros(self.d) - e[:2] = vec[j : j + 2] - line = np.c_[pos, pos + e] # 2 x self.d - ax.plot(*line, **kwargs) - j += 2 - elif n > 2: - pos = self.landmarks[n] - e = vec[j : j + self.d] - line = np.c_[pos, pos + e] # 2 x self.d - ax.plot(*line, **kwargs) - j += self.d
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/lifters/robust_pose_lifter.html b/docs/build/_modules/popr/lifters/robust_pose_lifter.html deleted file mode 100644 index e517f5d..0000000 --- a/docs/build/_modules/popr/lifters/robust_pose_lifter.html +++ /dev/null @@ -1,633 +0,0 @@ - - - - - - - - popr.lifters.robust_pose_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for popr.lifters.robust_pose_lifter

-from abc import ABC, abstractmethod
-from copy import deepcopy
-
-import numpy as np
-from poly_matrix.poly_matrix import PolyMatrix
-from scipy.spatial.transform import Rotation as R
-
-from popr.utils.geometry import (
-    get_C_r_from_theta,
-    get_noisy_pose,
-    get_pose_errors_from_theta,
-    get_theta_from_C_r,
-)
-
-from .state_lifter import StateLifter
-
-# import autograd.numpy as np
-
-
-N_TRYS = 10
-
-METHOD = "CG"
-SOLVER_KWARGS = dict(
-    min_gradient_norm=1e-7, max_iterations=10000, min_step_size=1e-8, verbosity=1
-)
-
-# TODO(FD) we need to add a penalty here, otherwise the local solution is not good.
-# However, the penalty results in inequality constraints etc. and that's not easy to deal with.
-PENALTY_RHO = 10
-PENALTY_U = 1e-3
-
-# the cutoff parameter of least squares. If residuals are >= BETA, they are considered outliers.
-BETA = 0.1
-
-
-
-[docs] -class RobustPoseLifter(StateLifter, ABC): - LEVELS = ["no", "xwT", "xxT"] - PARAM_LEVELS = ["no", "p", "ppT"] - LEVEL_NAMES = {"no": "no", "xwT": "x kron w", "xxT": "x kron x"} - MAX_DIST = 10.0 # maximum of norm of t. - - @property - def VARIABLE_LIST(self): - if not self.robust: - return [[self.HOM, "t", "c"]] - else: - base = [self.HOM, "t", "c"] - return [ - base, - base + ["w_0"], - base + ["z_0"], - base + ["w_0", "w_1"], - base + ["w_0", "z_0"], - base + ["z_0", "z_1"], - # base + ["w_0", "w_1", "z_0"], - # base + ["w_0", "w_1", "z_0", "z_1"], - ] - - # Add any parameters here that describe the problem (e.g. number of landmarks etc.) - def __init__( - self, - n_outliers=0, - level="no", - param_level="no", - d=2, - n_landmarks=3, - variable_list=None, - robust=False, - beta=BETA, - ): - """ - :param level: - - xwT: x kron w - - xxT: x kron x - """ - self.beta = beta - self.n_landmarks = n_landmarks - self.landmarks = None - - self.robust = robust - self.level = level - if variable_list == "all": - variable_list = self.get_all_variables() - # elif variable_list is None: - # self.variable_list = self.VARIABLE_LIST - - if not robust: - assert level == "no" - super().__init__( - level=level, - param_level=param_level, - d=d, - variable_list=variable_list, - n_outliers=n_outliers, - robust=robust, - ) - - def penalty(self, t, rho=PENALTY_RHO, u=PENALTY_U): - import autograd.numpy as anp - - try: - return anp.sum( - [rho * u * anp.log10(1 + anp.exp(hi / u)) for hi in self.h_list(t)] - ) - except RuntimeWarning: - PENALTY_U *= 0.1 - u = PENALTY_U - return anp.sum( - [rho * u * anp.log10(1 + anp.exp(hi / u)) for hi in self.h_list(t)] - ) - - @property - def var_dict(self): - """Return key,size pairs of all variables.""" - var_dict = {self.HOM: 1, "t": self.d, "c": self.d**2} - if not self.robust: - return var_dict - - n = self.d**2 + self.d - if self.level == "xwT": - for i in range(self.n_landmarks): - var_dict.update({f"w_{i}": 1, f"z_{i}": n}) - elif self.level == "xxT": - var_dict.update({f"w_{i}": 1 for i in range(self.n_landmarks)}) - var_dict.update({"z_0": n**2}) - return var_dict - - @property - def param_dict(self): - return self.param_dict_landmarks - - def get_all_variables(self): - all_variables = [self.HOM, "t", "c"] - if self.robust: - if self.level == "xxT": - all_variables += [f"w_{i}" for i in range(self.n_landmarks)] - all_variables += ["z_0"] - elif self.level == "xwT": - for i in range(self.n_landmarks): - all_variables += [f"w_{i}", f"z_{i}"] - variable_list = [all_variables] - return variable_list - -
-[docs] - def sample_theta(self): - """Generate a random new feasible point.""" - - # make sure random pose is looking at world centre (where landmarks are) - success = False - i = 0 - while not success: - pc_cw = self.get_random_position() - success = np.all(np.array(self.h_list(pc_cw)) <= 0) - if success: - break - i += 1 - if i >= N_TRYS: - raise ValueError("didn't find valid initialization") - - if self.d == 2: - angle = np.random.uniform(0, 2 * np.pi) - C = R.from_euler("z", angle).as_matrix()[:2, :2] - else: - C = R.random().as_matrix() - theta_x = get_theta_from_C_r(C, pc_cw) - - if self.robust: - outlier_index = np.random.choice( - self.n_landmarks, replace=False, size=self.n_outliers - ) - w = np.ones(self.n_landmarks) - w[outlier_index] = -1 - return np.hstack([theta_x, w]) - else: - return theta_x
- - -
-[docs] - def sample_parameters(self, theta=None): - landmarks = np.random.normal(loc=0, scale=1.0, size=(self.n_landmarks, self.d)) - return self.sample_parameters_landmarks(landmarks)
- - -
-[docs] - def get_x(self, theta=None, parameters=None, var_subset=None) -> np.ndarray: - """Get the lifted vector x given theta and parameters.""" - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - if var_subset is None: - var_subset = self.var_dict.keys() - - if self.robust: - theta_here = theta[: -self.n_landmarks] - else: - theta_here = theta - - # RT below is R_cw. (c=camera, w=world) - RT, t = get_C_r_from_theta(theta_here, self.d) - R = RT.T - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - elif key == "t": - x_data += list(t) - elif key == "c": - x_data += list(R.flatten("C")) - elif "w" in key: - j = int(key.split("_")[-1]) - w_j = theta[-self.n_landmarks + j] - x_data.append(w_j) - elif (self.level == "xxT") and (key == "z_0"): - x_vec = list(get_theta_from_C_r(R, t)) - x_data += list(np.kron(x_vec, x_vec).flatten()) - elif (self.level == "xwT") and ("z" in key): - j = int(key.split("_")[-1]) - w_j = theta[-self.n_landmarks + j] - x_vec = get_theta_from_C_r(R, t) - x_data += list(x_vec * w_j) - dim_x = self.get_dim_x(var_subset=var_subset) - assert len(x_data) == dim_x - return np.array(x_data)
- - - def get_outlier_index(self): - if self.robust: - return np.where(self.theta[-self.n_landmarks :] == -1)[0] - else: - return [] - - def get_error(self, theta_hat): - - theta_hat_pose = theta_hat[: self.d + self.d**2] - theta_gt_pose = self.theta[: self.d + self.d**2] - return get_pose_errors_from_theta(theta_hat_pose, theta_gt_pose, self.d) - -
-[docs] - def get_vec_around_gt(self, delta: float = 0): - """Sample around ground truth. - :param delta: sample from gt + std(delta) (set to 0 to start from gt.) - """ - if self.robust: - theta = deepcopy(self.theta[: self.d + self.d**2]) - C, r = get_C_r_from_theta(theta, self.d) - theta_noisy = get_noisy_pose(C, r, delta=delta) - theta_w = self.theta[self.d + self.d**2 :] - return np.r_[theta_noisy, theta_w] - else: - C, r = get_C_r_from_theta(self.theta, self.d) - theta_noisy = get_noisy_pose(C, r, delta=delta) - return theta_noisy
- - - def get_cost(self, theta, y): - import autograd.numpy as anp - - if self.robust: - x = theta[: -self.n_landmarks] - w = theta[-self.n_landmarks :] - assert np.all(w**2 == 1.0) - else: - x = theta - - R, t = get_C_r_from_theta(x, self.d) - - cost = 0 - for i in range(self.n_landmarks): - res = self.residual_sq(R, t, self.landmarks[i], y[i]) - if self.robust: - cost += (1 + w[i]) / self.beta**2 * res + 1 - w[i] - else: - cost += res - return 0.5 * cost - - def local_solver( - self, t0, y, verbose=False, method=METHOD, solver_kwargs=SOLVER_KWARGS - ): - import pymanopt - from pymanopt.manifolds import Euclidean, Product, SpecialOrthogonalGroup - - if method == "CG": - from pymanopt.optimizers import ConjugateGradient as Optimizer # fastest - elif method == "SD": - from pymanopt.optimizers import SteepestDescent as Optimizer # slow - elif method == "TR": - from pymanopt.optimizers import TrustRegions as Optimizer # okay - else: - raise ValueError(method) - - if verbose: - solver_kwargs["verbosity"] = 2 - else: - solver_kwargs["verbosity"] = 0 - - # We assume that we know w! If we wanted to solve for w too we would need - # IRLS or similar. Since we just care about getting the global solution - # with a local sovler that's not necessary. - if self.robust: - w = self.theta[-self.n_landmarks :] - - manifold = Product((SpecialOrthogonalGroup(self.d, k=1), Euclidean(self.d))) - - @pymanopt.function.autograd(manifold) - def cost(R, t): - cost = 0 - for i in range(self.n_landmarks): - residual = self.residual_sq(R, t, self.landmarks[i], y[i]) - if self.robust: - cost += (1 + w[i]) / self.beta**2 * residual + 1 - w[i] - else: - cost += residual - return 0.5 * cost + self.penalty(t) - - @pymanopt.function.autograd(manifold) - def euclidean_gradient_unused(R, t): - grad_R = np.zeros(R.shape) - grad_t = np.zeros(t.shape) - for i in range(self.n_landmarks): - Wi = np.eye(self.d) - np.outer(y[i], y[i]) - # residual = (R @ pi + t).T @ Wi @ (R @ pi + t) - term = self.term_in_norm(R, t, self.landmarks[i], y[i]) - if self.robust: - grad_R += ( - 2 - * w[i] - / self.beta**2 - * np.outer(Wi.T @ term, self.landmarks[i]) - ) - grad_t += 2 * w[i] / self.beta**2 * Wi.T @ term - else: - grad_R += np.outer(Wi.T @ term, self.landmarks[i]) - grad_t += Wi.T @ term - return grad_R, grad_t - - euclidean_gradient = None - problem = pymanopt.Problem( - manifold, cost, euclidean_gradient=euclidean_gradient - ) - optimizer = Optimizer(**solver_kwargs) - - R_0, t_0 = get_C_r_from_theta(t0[: self.d + self.d**2], self.d) - res = optimizer.run(problem, initial_point=(R_0, t_0)) - R, t = res.point - - if verbose: - print("local solver sanity check:") - print("final penalty:", self.penalty(t)) - w = self.theta[-self.n_landmarks :] - for i in range(self.n_landmarks): - residual = self.residual_sq(R, t, self.landmarks[i], y[i]) - if w[i] == -1: - if verbose: - print(f"outlier residual: {residual:.4e}") - assert ( - residual > self.beta - ), f"outlier residual too small: {residual} <= {self.beta}" - else: - if verbose: - print(f"inlier residual: {residual:.4e}") - assert ( - residual < self.beta - ), f"inlier residual too large: {residual} > {self.beta}" - if verbose: - print("qcqp cost:", res.cost) - - if self.robust: - theta_hat = np.r_[get_theta_from_C_r(R, t), w] - else: - theta_hat = get_theta_from_C_r(R, t) - - cost_penalized = res.cost - if self.robust: - pen = self.penalty(t) - if abs(res.cost) > 1e-10: - assert abs(pen) / res.cost <= 1e-1, (pen, res.cost) - cost_penalized -= pen - - success = ("min step_size" in res.stopping_criterion) or ( - "min grad norm" in res.stopping_criterion - ) - info = { - "success": success, - "msg": res.stopping_criterion, - } - if success: - return theta_hat, info, cost_penalized - else: - return None, info, cost_penalized - - def test_and_add(self, A_list, Ai, output_poly): - x = self.get_x() - Ai_sparse = Ai.get_matrix(self.var_dict) - err = x.T @ Ai_sparse @ x - assert abs(err) <= 1e-10, err - if output_poly: - A_list.append(Ai) - else: - A_list.append(Ai_sparse) - -
-[docs] - def get_A_known(self, var_dict=None, output_poly=False): - A_list = [] - if var_dict is None: - var_dict = self.var_dict - - if "c" in var_dict: - # enforce diagonal == 1 - for i in range(self.d): - Ei = np.zeros((self.d, self.d)) - Ei[i, i] = 1.0 - constraint = np.kron(Ei, np.eye(self.d)) - Ai = PolyMatrix(symmetric=True) - Ai["c", "c"] = constraint - Ai[self.HOM, self.HOM] = -1 - self.test_and_add(A_list, Ai, output_poly=output_poly) - - # enforce off-diagonal == 0 - for i in range(self.d): - for j in range(i + 1, self.d): - Ei = np.zeros((self.d, self.d)) - Ei[i, j] = 1.0 - Ei[j, i] = 1.0 - constraint = np.kron(Ei, np.eye(self.d)) - Ai = PolyMatrix(symmetric=True) - Ai["c", "c"] = constraint - self.test_and_add(A_list, Ai, output_poly=output_poly) - if self.robust: - for key in var_dict: - if "w" in key: - i = key.split("_")[-1] - Ai = PolyMatrix(symmetric=True) - Ai[self.HOM, self.HOM] = -1.0 - Ai[f"w_{i}", f"w_{i}"] = 1.0 - self.test_and_add(A_list, Ai, output_poly=output_poly) - - # below doesn't hold: w_i*w_j = += 1 - # for key_other in [k for k in var_dict if (k.startswith("w") and (k!= key))]: - # Ai = PolyMatrix(symmetric=True) - # Ai[self.HOM, self.HOM] = -1.0 - # Ai[key, key_other] = 0.5 - # self.test_and_add(A_list, Ai, output_poly=output_poly) - - if "z" in key: - if self.level == "xwT": - i = key.split("_")[-1] - """ each z_i equals x * w_i""" - - for j in range(self.d): - Ai = PolyMatrix(symmetric=True) - constraint = np.zeros((self.d + self.d**2)) - constraint[j] = 1.0 - Ai[self.HOM, f"z_{i}"] = constraint[None, :] - constraint = np.zeros((self.d)) - constraint[j] = -1.0 - Ai[f"t", f"w_{i}"] = constraint[:, None] - self.test_and_add(A_list, Ai, output_poly=output_poly) - - for j in range(self.d**2): - Ai = PolyMatrix(symmetric=True) - constraint = np.zeros((self.d + self.d**2)) - constraint[self.d + j] = 1.0 - Ai[self.HOM, f"z_{i}"] = constraint[None, :] - constraint = np.zeros((self.d**2)) - constraint[j] = -1.0 - Ai[f"c", f"w_{i}"] = constraint[:, None] - self.test_and_add(A_list, Ai, output_poly=output_poly) - return A_list
- - -
-[docs] - def get_B_known(self): - """Get inequality constraints of the form x.T @ B @ x <= 0. - By default, we always add ||t|| <= MAX_DIST - """ - B1 = PolyMatrix(symmetric=True) - B1[self.HOM, self.HOM] = -self.MAX_DIST - B1["t", "t"] = np.eye(self.d) - return [B1.get_matrix(self.var_dict)]
- - -
-[docs] - @abstractmethod - def h_list(self, t): - """ - Any inequality constraints to enforce, returned as a list [h_1(t), h_2(t), ...] - We use the convention h_i(t) <= 0. - - By default, we always add |t| <= MAX_DIST - """ - try: - import autograd.numpy as anp - - return [anp.sqrt(anp.sum(t[: self.d] ** 2)) - self.MAX_DIST] - except ModuleNotFoundError: - return [np.sqrt(np.sum(t[: self.d] ** 2)) - self.MAX_DIST]
- - -
-[docs] - @abstractmethod - def get_random_position(self): - """Generate a new random position. Orientation angles will be drawn uniformly from [0, pi].""" - return None
- - - @abstractmethod - def term_in_norm(self, R, t, pi, ui): - return - - @abstractmethod - def residual_sq(self, R, t, pi, ui): - return
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/lifters/state_lifter.html b/docs/build/_modules/popr/lifters/state_lifter.html deleted file mode 100644 index d368ed4..0000000 --- a/docs/build/_modules/popr/lifters/state_lifter.html +++ /dev/null @@ -1,318 +0,0 @@ - - - - - - - - popr.lifters.state_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for popr.lifters.state_lifter

-from abc import abstractmethod
-
-import numpy as np
-
-from ._base_class import BaseClass
-
-
-
-[docs] -class StateLifter(BaseClass): - # sparse hierarchy: define the levels that are implemented - LEVELS = ["no"] - - # used for AutoTemplate - VARIABLE_LIST = ["h"] - TIGHTNESS = "cost" - - # to be overwritten by inheriting class - NOISE = 1e-2 - - def __init__( - self, - level="no", - param_level="no", - d=2, - variable_list=None, - robust=False, - n_outliers=0, - n_parameters=1, - ): - - # variables that get overwritten upon initialization - self.parameters_ = None - self.theta_ = None - self.var_dict_ = None - self.y_ = None - - self.robust = robust - self.n_outliers = n_outliers - - assert level in self.LEVELS - self.level = level - - if variable_list is not None: - self.variable_list = variable_list - else: - self.variable_list = self.VARIABLE_LIST - - if (param_level != "no") and (n_parameters == 1): - print("Warning: make sure to give the correct n_parameters for the level.") - - super().__init__(d, param_level, n_parameters) - - ###### MUST OVERWRITE THESE - - @property - def var_dict(self): - raise ValueError("Inheriting class must implement this!") - -
-[docs] - @abstractmethod - def sample_theta(self) -> np.ndarray: - """Randomly sample a feasible state theta. This function must - implemented by the inheriting class.""" - raise NotImplementedError("need to implement sample_theta")
- - - ###### MUST OVERWRITE THESE FOR TIGHTNESS CHECKS - -
-[docs] - def get_Q(self, output_poly=False, noise=None): - """Construct the cost matrix Q. - - :param noise: set the noise level, if appropriate. - :param output_poly: if True, return the matrix in PolyMatrix format. - - :returns: the cost matrix as a sparse matrix or PolyMatrix. - """ - raise NotImplementedError( - "Need to impelement get_Q in inheriting class if you want to use it." - )
- - - def get_Q_from_y(self, y): - raise NotImplementedError( - "Need to impelement get_Q_from_y in inheriting class if you want to use it." - ) - -
-[docs] - def get_A_known( - self, - add_redundant: bool = False, - var_dict: dict | None = None, - output_poly: bool = False, - ) -> list: - """Construct the matrices defining the known equality constraints.""" - return []
- - -
-[docs] - def get_B_known(self) -> list: - """Construct the matrices defining the known inequality constraints.""" - return []
- - - ###### MUST OVERWRITE THESE FOR ADDING PARAMETERS - -
-[docs] - def sample_parameters(self, theta=None) -> dict: - """Create random set of parameters. By default, there are no parameters - so this function just returns {`self.HOM`: 1.0}.""" - assert ( - self.param_level == "no" - ), "Need to overwrite sample_parameters to use level different than 'no'" - return {self.HOM: 1.0}
- - - @property - def param_dict(self): - assert ( - self.param_level == "no" - ), "Need to overwrite param_dict to use level different than 'no'" - return {self.HOM: 1} - - def get_involved_param_dict(self, var_subset): - keys = [self.HOM] - for v in var_subset: - index = v.split("_") - if len(index) > 1: - index = int(index[-1]) - key = f"p_{index}" - if key not in keys: - keys.append(key) - return {k: self.param_dict[k] for k in keys if k in self.param_dict} - - ###### CAN OPTINALLY OVERWRITE THESE FOR BETTER PERFORMANCE - - def get_grad(self, theta, y=None) -> float: - raise NotImplementedError("must define get_grad if you want to use it.") - - def get_hess(self, theta, y=None) -> float: - raise NotImplementedError("must define get_hess if you want to use it.") - - def get_cost(self, theta, y=None) -> float: - print( - "Warning: using default get_cost, which may be less efficient than a custom one." - ) - x = self.get_x(theta=theta).flatten("C") - if y is not None: - Q = self.get_Q_from_y(y) - else: - Q = self.get_Q() - return float(x.T @ Q @ x) - - def local_solver(self, t0, y=None, *args, **kwargs): - print( - "Warning: using default local_solver, which may be less efficient than a custom one." - ) - print("Ignoring args and kwargs:", args, kwargs) - from cert_tools.sdp_solvers import solve_low_rank_sdp - - if y is not None: - Q = self.get_Q_from_y(y) - else: - Q = self.get_Q() - - Constraints = self.get_A_b_list(A_list=self.get_A_known()) - x0 = self.get_x(theta=t0) - X, info = solve_low_rank_sdp( - Q, Constraints=Constraints, rank=1, verbose=True, x_cand=x0 - ) - # TODO(FD) identify when the solve is not successful. - info["success"] = True - try: - theta = self.get_theta(X[:, 0]) - except: - theta = X[1 : 1 + self.d, 0] - return theta, info, info["cost"] - - @property - def param_dict_landmarks(self): - assert self.n_parameters is not None - - param_dict = {self.HOM: 1} - if self.param_level == "no": - return param_dict - if self.param_level == "p": - param_dict.update({f"p_{i}": self.d for i in range(self.n_parameters)}) - if self.param_level == "ppT": - # Note that ppT is actually - # [p; vech(ppT)] (linear and quadratic terms) - # TODO(FD): rename ppT to quadratic - param_dict.update( - { - f"p_{i}": self.d + int(self.d * (self.d + 1) / 2) - for i in range(self.n_parameters) - } - ) - return param_dict - - def get_theta(self, x): - """Inverse of get_x: given lifted vector x, extract elements corresponding to theta.""" - assert np.ndim(x) == 1 or x.shape[1] == 1 - return x.flatten()[1 : 1 + self.d]
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/lifters/stereo_lifter.html b/docs/build/_modules/popr/lifters/stereo_lifter.html deleted file mode 100644 index 8f57041..0000000 --- a/docs/build/_modules/popr/lifters/stereo_lifter.html +++ /dev/null @@ -1,613 +0,0 @@ - - - - - - - - popr.lifters.stereo_lifter — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for popr.lifters.stereo_lifter

-from abc import ABC, abstractmethod
-
-# import autograd.numpy as np
-import numpy as np
-import scipy.sparse as sp
-from poly_matrix.poly_matrix import PolyMatrix
-
-from popr.utils.geometry import (
-    generate_random_pose,
-    get_C_r_from_theta,
-    get_noisy_pose,
-    get_pose_errors_from_theta,
-    get_T,
-    get_theta_from_C_r,
-)
-
-from .state_lifter import StateLifter
-
-NOISE = 1.0  #
-
-
-SOLVER_KWARGS = dict(
-    min_gradient_norm=1e-6, max_iterations=10000, min_step_size=1e-10, verbosity=1
-)
-
-
-
-[docs] -class StereoLifter(StateLifter, ABC): - """General lifter for stereo localization problem.""" - - NORMALIZE = True - - LEVELS = [ - "no", - "u@u", # ... - "u2", - "u@r", - "uuT", - "urT", - "uxT", - ] - PARAM_LEVELS = ["no", "p", "ppT"] - LEVEL_NAMES = { - "no": "$\\boldsymbol{u}_n$", - "urT": "$\\boldsymbol{u}\\boldsymbol{t}^\\top_n$", - "uxT": "$\\boldsymbol{u}\\boldsymbol{x}^\\top_n$", - } - - def __init__( - self, n_landmarks, d, level="no", param_level="no", variable_list=None - ): - self.y_ = None - self.n_landmarks = n_landmarks - self.landmarks = None - - super().__init__( - d=d, - level=level, - param_level=param_level, - variable_list=variable_list, - n_parameters=n_landmarks, - ) - - @property - @abstractmethod - def M_matrix(self): - raise NotImplementedError("Inheriting class must initialize M_matrix.") - - def get_all_variables(self): - return [[self.HOM, "x"] + [f"z_{i}" for i in range(self.n_landmarks)]] - -
-[docs] - def get_level_dims(self, n=1): - """ - :param n: number of landmarks to consider - """ - return { - "no": 0, - "u@u": n, # ... - "u2": n * self.d, - "u@r": n, - "uuT": n * self.d**2, - "urT": n * self.d**2, - "uxT": n * (self.d * (self.d + self.d**2)), - }
- - - def generate_random_landmarks(self, theta=None): - if theta is not None: - C, r = get_C_r_from_theta(theta, self.d) - if self.d == 3: - # sample left u, v coordinates in left image, and compute landmark coordinates from that. - fu, cu, b = self.M_matrix[0, [0, 2, 3]] - fv, cv = self.M_matrix[1, [1, 2]] - u = np.random.uniform(0, cu * 2, self.n_landmarks) - v = np.random.uniform(0, cv * 2, self.n_landmarks) - z = np.random.uniform(0, 5, self.n_landmarks) - x = 1 / fu * (z * (u - cu) - b) - y = 1 / fv * z * (v - cv) - points_cam = np.c_[x, y, z] # N x 3 - else: - # sample left u in left image, and compute landmark coordinates from that. - fu, cu, b = self.M_matrix[0, :] - u = np.random.uniform(0, cu * 2, self.n_landmarks) - y = np.random.uniform(1, 5, self.n_landmarks) - x = 1 / fu * (y * (u - cu) - b) - points_cam = np.c_[x, y] - # transform points from camera to world - return (C.T @ (points_cam.T - r[:, None])).T - else: - return np.random.rand(self.n_landmarks, self.d) - -
-[docs] - def sample_parameters(self, theta=None): - landmarks = self.generate_random_landmarks(theta=self.theta) - return self.sample_parameters_landmarks(landmarks)
- - - def get_parameters(self, var_subset=None): - return self.get_p(param_subset=var_subset) - - @property - def VARIABLE_LIST(self): - return [ - [self.HOM, "x"], - [self.HOM, "z_0"], - [self.HOM, "x", "z_0"], - [self.HOM, "z_0", "z_1"], # should achieve tightness here - ] - - @property - def param_dict(self): - return self.param_dict_landmarks - - @property - def var_dict(self): - level_dim = self.get_level_dims()[self.level] - if self.var_dict_ is None: - self.var_dict_ = {self.HOM: 1} - self.var_dict.update({"x": self.d**2 + self.d}) - self.var_dict.update( - {f"z_{k}": self.d + level_dim for k in range(self.n_landmarks)} - ) - return self.var_dict_ - -
-[docs] - def get_x(self, theta=None, parameters=None, var_subset=None): - """ - :param var_subset: list of variables to include in x vector. Set to None for all. - """ - if theta is None: - theta = self.theta - if parameters is None: - parameters = self.parameters - if var_subset is None: - var_subset = self.var_dict.keys() - - assert self.landmarks is not None - - # TODO(FD) below is a bit hacky, these two variables should not both be called theta. - # theta is either (x, y, alpha) or (x, y, z, a1, a2, a3) - C, r = get_C_r_from_theta(theta, self.d) - if (self.param_level != "no") and (len(parameters) > 1): - landmarks = parameters - else: - landmarks = { - f"p_{i}": self.landmarks[i, :] for i in range(self.landmarks.shape[0]) - } - - x_data = [] - for key in var_subset: - if key == self.HOM: - x_data.append(1.0) - elif key == "x": - x_data += list(r) + list(C.flatten("C")) # row-wise flatten - elif "z" in key: - j = int(key.split("_")[-1]) - - pj = landmarks[f"p_{j}"][: self.d] # - - zj = C[self.d - 1, :] @ pj + r[self.d - 1] - u = 1 / zj * np.r_[C[: self.d - 1, :] @ pj + r[: self.d - 1], 1] - x_data += list(u) - - if self.level == "no": - continue - elif self.level == "u2": - x_data += list(u**2) - elif self.level == "u@u": - x_data += [u @ u] - elif self.level == "u@r": - x_data += [u @ r] - elif self.level == "uuT": - x_data += list(np.outer(u, u).flatten()) - elif self.level == "urT": - # this works - x_data += list(np.outer(u, r).flatten()) - elif self.level == "uxT": - x = np.r_[r, C.flatten("C")] - x_data += list(np.outer(u, x).flatten()) - dim_x = self.get_dim_x(var_subset=var_subset) - assert len(x_data) == dim_x - return np.array(x_data)
- - -
-[docs] - def get_A_known(self, var_dict=None, output_poly=False): - """ - T = | cx' tx | - | cy' ty | - | cz' tz | - | 0 0 0 1 | - Let pj be the j-th landmark coordinate. - [xj] [cx @ pj + tx] - [yj] = [cy @ pj + ty] - [zj] [cz @ pj + tz] - - Let u be the substitution variable, which has d-1 elements. - Then we want to enforce that: - u_xj = 1/zj * xj -> u_xj * zj = xj -> (cz @ pj + tz) * u_xj - (cx @ pj + tx) = 0 - u_yj = 1/zj * yj -> u_yj * zj = yj -> same as above - u_zj = 1/zj -> u_zj * zj = 1 -> u_zj * (cz @ pj + tz) -1 = 0 - Writing things as homogeneous constraints: - a1) cz @ pj * u_xj + tz*u_xj - cx @ pj - h * tx = 0 - a2) -----1x------- --2x--- -- 3 -- --4--- - a3) cz @ pj * u_zj + tz*u_zj - h*h = 0 - ------1z------- --2z--- - """ - print("not using known stereo templates because they depend on the landmarks.") - return [] - - # x contains: [c1, c2, c3, t] - # z contains: [u_xj, u_yj, u_zj, H.O.T.] - if self.d == 2: - x = self.get_x() - _, tx, tz, cx1, cx2, cz1, cz2, u_xj, u_zj, *_ = x - cz = np.array([cz1, cz2]) - cx = np.array([cx1, cx2]) - pj = self.landmarks[0] - assert abs(cz @ pj * u_xj + tz * u_xj - cx @ pj - tx) < 1e-10 - assert abs(u_zj * cz @ pj + u_zj * tz - 1) < 1e-10 - elif self.d == 3: - x = self.get_x() - # fmt: off - (_, tx, ty, tz, cx1, cx2, cx3, cy1, cy2, cy3, cz1, cz2, cz3, u_x1, u_y1, u_z1, *_) = x - # fmt: on - p1 = self.landmarks[0] - assert ( - abs(u_z1 * (cx1 * p1[0] + cx2 * p1[1] + cx3 * p1[2]) + u_z1 * tx - u_x1) - < 1e-10 - ) - assert ( - abs(u_z1 * (cy1 * p1[0] + cy2 * p1[1] + cy3 * p1[2]) + u_z1 * ty - u_y1) - < 1e-10 - ) - assert ( - abs(u_z1 * (cz1 * p1[0] + cz2 * p1[1] + cz3 * p1[2]) + u_z1 * tz - 1) - < 1e-10 - ) - - if var_dict is None: - var_dict = self.var_dict - - A_known = [] - z_dim = self.get_level_dims()[self.level] - - if "x" not in var_dict or self.HOM not in var_dict: - return A_known - landmarks = [j for j in range(self.n_landmarks) if f"z_{j}" in var_dict] - for j in landmarks: - # one complete constraint has x, z_j and h. - pj = self.landmarks[j] - for i in range(self.d): - A = PolyMatrix() - # -----1i------- --2i--- -- 3 -- --4--- - # a1) cz @ pj * u_xj + tz*u_xj - cx @ pj - h * tx = 0 - # a2) cz @ pj * u_yj + tz*u_yj - cy @ pj - h * ty = 0 - # a3) cz @ pj * u_zj + tz*u_zj - h*h = 0 - # ------1i------- --2i--- - # --- 1i --- - fill_mat = np.zeros((self.d + self.d**2, self.d + z_dim)) - # chooses cz of x, and u_xj, u_yj or u_zj of z - fill_mat[-self.d :, i] = pj - - # --- 2 --- u_zj * tx - # chooses tz of x, and u_ij of z - fill_mat[self.d - 1, i] = 1.0 - A[f"x", f"z_{j}"] = fill_mat - - if i < self.d - 1: # u, (v) - fill_mat = np.zeros((self.d + self.d**2, 1)) - # chooses ci of x - fill_mat[(i + 1) * self.d : (i + 2) * self.d, 0] = -pj - - # chooses ti of x - fill_mat[i, 0] = -1 - A["x", self.HOM] = fill_mat - elif i == self.d - 1: # z - A[self.HOM, self.HOM] = -0 # 2.0 - if output_poly: - A_known.append(A) - else: - A_known.append(A.get_matrix(var_dict)) - self.test_constraints(A_known) - return A_known
- - -
-[docs] - def sample_theta(self): - return generate_random_pose(d=self.d).flatten()
- - - def simulate_y(self, noise: float | None = None): - if noise is None: - noise = NOISE - - assert self.landmarks is not None, "Landmarks must be set before simulating y." - - T = get_T(theta=self.theta, d=self.d) - - y_sim = np.zeros((self.n_landmarks, self.M_matrix.shape[0])) - for j in range(self.n_landmarks): - y_gt = T @ np.r_[self.landmarks[j], 1.0] - - # in 2d: y_gt[1] - # in 3d: y_gt[2] - y_gt /= y_gt[self.d - 1] - y_gt = self.M_matrix @ y_gt - y_sim[j, :] = y_gt + np.random.normal(loc=0, scale=noise, size=len(y_gt)) - return y_sim - -
-[docs] - def get_Q( - self, - noise: float | None = None, - output_poly: bool = False, - use_cliques: list = [], - ) -> PolyMatrix | sp.csr_matrix | sp.csc_matrix: - if self.y_ is None: - if noise is None: - noise = NOISE - self.y_ = self.simulate_y(noise=noise) - - Q = self.get_Q_from_y(self.y_, output_poly=output_poly, use_cliques=use_cliques) - return Q
- - -
-[docs] - def get_Q_from_y( - self, y, output_poly=False, use_cliques=[] - ) -> PolyMatrix | sp.csr_matrix | sp.csc_matrix: - """ - The least squares problem reads - min_T sum_{n=0}^{N-1} || y - Mtilde@z || - where the first d elements of z correspond to u, and Mtilde contains the first d-1 and last element of M - Mtilde is thus of shape d*2 by dim_z, where dim_z=d+dL (the additional Lasserre variables) - y is of length d*2, corresponding to the measured pixel values in left and right image. - """ - from poly_matrix.least_squares_problem import LeastSquaresProblem - - if len(use_cliques): - js = use_cliques - else: - js = range(y.shape[0]) - - # when using lifting (level=urT), then we have - # in 2d: M_tilde is 2 by 6, with first 2 columns: M[:, [0, 2]] - # in 3d: M_tilde is 4 by 12, with first 3 columns: M[:, [0, 1, 3]] - M_tilde = np.zeros((len(y[0]), self.var_dict["z_0"])) - M_tilde[:, : self.d] = self.M_matrix[:, list(range(self.d - 1)) + [self.d]] - - # in 2d: M[:, 1] - # in 3d: M[:, 2] - m = self.M_matrix[:, self.d - 1] - - ls_problem = LeastSquaresProblem() - for j in js: - ls_problem.add_residual({self.HOM: (y[j] - m), f"z_{j}": -M_tilde}) - - if output_poly: - Q = ls_problem.get_Q() - else: - Q = ls_problem.get_Q().get_matrix(self.var_dict) - if self.NORMALIZE: - Q /= self.n_landmarks * self.d - - # sanity check - x = self.get_x() - - # sanity checks. Below is the best conditioned because we don't have to compute B.T @ B, which - # can contain very large values. - B = ls_problem.get_B_matrix(self.var_dict) - errors = B @ x - cost_test = errors.T @ errors - if self.NORMALIZE: - cost_test /= self.n_landmarks * self.d - - if output_poly: - assert isinstance(Q, PolyMatrix) - cost_Q = x.T @ Q.get_matrix(self.var_dict, output_type="csr") @ x - else: - cost_Q = x.T @ Q @ x - assert abs(cost_test - cost_Q) < 1e-6, (cost_test, cost_Q) - if not len(use_cliques): - cost_raw = self.get_cost(self.theta, y) - assert abs(cost_test - cost_raw) < 1e-6, (cost_test, cost_raw) - assert isinstance(Q, (PolyMatrix, sp.csr_matrix, sp.csc_matrix)), type(Q) - return Q
- - -
-[docs] - def get_theta(self, x): - return x[1 : 1 + self.d + self.d**2]
- - -
-[docs] - def get_vec_around_gt(self, delta: float = 0): - if delta == 0: - return self.theta - - C, r = get_C_r_from_theta(self.theta, self.d) - if self.d == 2: - return super().get_vec_around_gt(delta=delta) - else: - return get_noisy_pose(C, r, delta)
- - - def get_C_cw(self, theta=None): - C_cw, __ = get_C_r_from_theta(theta, self.d) - return C_cw - - def get_position(self, theta=None): - C_cw, r_wc_c = get_C_r_from_theta(theta, self.d) - return (-C_cw.T @ r_wc_c)[None, :] - - def get_error(self, theta_hat): - return get_pose_errors_from_theta(theta_hat, self.theta, self.d) - - def local_solver_manopt(self, t0, y, W=None, verbose=False, method="CG", **kwargs): - import pymanopt - from pymanopt.manifolds import Euclidean, Product, SpecialOrthogonalGroup - - assert self.landmarks is not None, "Landmarks must be set before local solver." - - if method == "CG": - from pymanopt.optimizers import ConjugateGradient as Optimizer # fastest - elif method == "SD": - from pymanopt.optimizers import SteepestDescent as Optimizer # slow - elif method == "TR": - from pymanopt.optimizers import TrustRegions as Optimizer # okay - else: - raise ValueError(method) - - solver_kwargs = SOLVER_KWARGS - solver_kwargs.update(kwargs) - - if verbose: - solver_kwargs["verbosity"] = 2 - else: - solver_kwargs["verbosity"] = 1 - - manifold = Product((SpecialOrthogonalGroup(self.d, k=1), Euclidean(self.d))) - - if W is None: - W = np.eye(4) if self.d == 3 else np.eye(2) - - @pymanopt.function.autograd(manifold) - def cost(R, t): - cost = 0 - for i in range(self.n_landmarks): - pi_cam = np.concatenate([R @ self.landmarks[i] + t, [1]], axis=0) # type: ignore - y_gt = self.M_matrix @ (pi_cam / pi_cam[self.d - 1]) - residual = y[i] - y_gt - cost += residual.T @ W @ residual - if self.NORMALIZE: - return cost / (self.n_landmarks * self.d) - return cost - - euclidean_gradient = None # set to None - problem = pymanopt.Problem( - manifold, cost, euclidean_gradient=euclidean_gradient # - ) - optimizer = Optimizer(**solver_kwargs) # type: ignore - - R_0, t_0 = get_C_r_from_theta(t0[: self.d + self.d**2], self.d) - res = optimizer.run(problem, initial_point=(R_0, t_0)) - R, t = res.point - - theta_hat = get_theta_from_C_r(R, t) - return theta_hat, res.stopping_criterion, res.cost - - def __repr__(self): - level_str = str(self.level).replace(".", "-") - return f"stereo{self.d}d_{level_str}_{self.param_level}"
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/utils/common.html b/docs/build/_modules/popr/utils/common.html deleted file mode 100644 index 15e6d5d..0000000 --- a/docs/build/_modules/popr/utils/common.html +++ /dev/null @@ -1,302 +0,0 @@ - - - - - - - - popr.utils.common — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for popr.utils.common

-import itertools
-
-import numpy as np
-import scipy.sparse as sp
-
-
-
-[docs] -def upper_triangular(p): - """Given vector, get the half kronecker product.""" - return np.outer(p, p)[np.triu_indices(len(p))]
- - - -
-[docs] -def diag_indices(n): - """Given the half kronecker product, return diagonal elements""" - z = np.empty((n, n)) - z[np.triu_indices(n)] = range(int(n * (n + 1) / 2)) - return np.diag(z).astype(int)
- - - -def get_aggregate_sparsity(matrix_list_sparse): - agg_ii = [] - agg_jj = [] - for i, A_sparse in enumerate(matrix_list_sparse): - assert isinstance(A_sparse, sp.spmatrix) - ii, jj = A_sparse.nonzero() # type: ignore - agg_ii += list(ii) - agg_jj += list(jj) - return sp.csr_matrix(([1.0] * len(agg_ii), (agg_ii, agg_jj)), A_sparse.shape) - - -
-[docs] -def unravel_multi_index_triu(flat_indices, shape): - """Equivalent of np.multi_index_triu, but using only the upper-triangular part of matrix.""" - i_upper = [] - j_upper = [] - - # for 4 x 4, this would give [4, 7, 9, 11] - cutoffs = np.cumsum(list(range(1, shape[0] + 1))[::-1]) - for idx in flat_indices: - i = np.where(idx < cutoffs)[0][0] - if i == 0: - j = idx - else: - j = idx - cutoffs[i - 1] + i - i_upper.append(i) - j_upper.append(j) - return np.array(i_upper), np.array(j_upper)
- - - -
-[docs] -def ravel_multi_index_triu(index_tuple, shape): - """Equivalent of np.multi_index_triu, but using only the upper-triangular part of matrix.""" - ii, jj = index_tuple - - triu_mask = jj >= ii - i_upper = ii[triu_mask] - j_upper = jj[triu_mask] - flat_indices = [] - for i, j in zip(i_upper, j_upper): - # for i == 0: idx = j - # for i == 1: idx = shape[0] + j - # for i == 2: idx = shape[0] + shape[0]-1 + j - idx = np.sum(range(shape[0] - i, shape[0])) + j - flat_indices.append(idx) - return flat_indices
- - - -
-[docs] -def create_symmetric(vec, eps_sparse, correct=False, sparse=False): - """Create a symmetric matrix from the vectorized elements of the upper half""" - - def get_dim_x(len_vec): - return int(0.5 * (-1 + np.sqrt(1 + 8 * len_vec))) - - try: - # vec is dense - len_vec = len(vec) - dim_x = get_dim_x(len_vec) - triu = np.triu_indices(n=dim_x) - mask = np.abs(vec) > eps_sparse - triu_i_nnz = triu[0][mask] - triu_j_nnz = triu[1][mask] - vec_nnz = vec[mask] - except Exception: - # vec is sparse - len_vec = vec.shape[1] - dim_x = get_dim_x(len_vec) - vec.data[np.abs(vec.data) < eps_sparse] = 0 - vec.eliminate_zeros() - ii, jj = vec.nonzero() # vec is 1 x jj - triu_i_nnz, triu_j_nnz = unravel_multi_index_triu(jj, (dim_x, dim_x)) - vec_nnz = np.array(vec[ii, jj]).flatten() - # assert dim_x == self.get_dim_x(var_dict) - - if sparse: - offdiag = triu_i_nnz != triu_j_nnz - diag = triu_i_nnz == triu_j_nnz - triu_i = triu_i_nnz[offdiag] - triu_j = triu_j_nnz[offdiag] - diag_i = triu_i_nnz[diag] - if correct: - # divide off-diagonal elements by sqrt(2) - vec_nnz_off = vec_nnz[offdiag] / np.sqrt(2) - else: - vec_nnz_off = vec_nnz[offdiag] - vec_nnz_diag = vec_nnz[diag] - Ai = sp.csr_array( - ( - np.r_[vec_nnz_diag, vec_nnz_off, vec_nnz_off], - (np.r_[diag_i, triu_i, triu_j], np.r_[diag_i, triu_j, triu_i]), - ), - (dim_x, dim_x), - dtype=float, - ) - else: - Ai = np.zeros((dim_x, dim_x)) - - if correct: - # divide all elements by sqrt(2) - Ai[triu_i_nnz, triu_j_nnz] = vec_nnz / np.sqrt(2) - Ai[triu_j_nnz, triu_i_nnz] = vec_nnz / np.sqrt(2) - # undo operation for diagonal - Ai[range(dim_x), range(dim_x)] *= np.sqrt(2) - else: - Ai[triu_i_nnz, triu_j_nnz] = vec_nnz - Ai[triu_j_nnz, triu_i_nnz] = vec_nnz - return Ai
- - - -
-[docs] -def get_vec(mat, correct=True, sparse=False) -> np.ndarray | sp.csr_matrix | None: - """Convert NxN Symmetric matrix to (N+1)N/2 vectorized version that preserves inner product. - - :param mat: (spmatrix or ndarray) symmetric matrix - :return: ndarray - """ - from copy import deepcopy - - mat = deepcopy(mat) - if correct: - if isinstance(mat, sp.csc_matrix): - ii, jj = mat.nonzero() - mat[ii, jj] *= np.sqrt(2.0) - diag = ii == jj - mat[ii[diag], jj[diag]] /= np.sqrt(2) # type: ignore - else: - mat *= np.sqrt(2.0) - mat[range(mat.shape[0]), range(mat.shape[0])] /= np.sqrt(2) - if sparse: - assert isinstance(mat, sp.csc_matrix) - ii, jj = mat.nonzero() - if len(ii) == 0: - # got an empty matrix -- this can happen depending on the parameter values. - return None - triu_mask = jj >= ii - - flat_indices = ravel_multi_index_triu([ii[triu_mask], jj[triu_mask]], mat.shape) # type: ignore - data = np.array(mat[ii[triu_mask], jj[triu_mask]]).flatten() # type: ignore - vec_size = int(mat.shape[0] * (mat.shape[0] + 1) / 2) # type: ignore - return sp.csr_matrix( - (data, ([0] * len(flat_indices), flat_indices)), (1, vec_size) - ) - else: - return np.array(mat[np.triu_indices(n=mat.shape[0])]).flatten() # type: ignore
- - - -def get_labels(p, zi, zj, var_dict): - labels = [] - size_i = var_dict[zi] - size_j = var_dict[zj] - if zi == zj: - # only upper diagonal for i == j - key_pairs = itertools.combinations_with_replacement(range(size_i), 2) - else: - key_pairs = itertools.product(range(size_i), range(size_j)) - for i, j in key_pairs: - label = f"{p}-" - label += f"{zi}:{i}." if size_i > 1 else f"{zi}." - label += f"{zj}:{j}" if size_j > 1 else f"{zj}" - labels.append(label) - return labels -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/_modules/popr/utils/constraint.html b/docs/build/_modules/popr/utils/constraint.html deleted file mode 100644 index 413e242..0000000 --- a/docs/build/_modules/popr/utils/constraint.html +++ /dev/null @@ -1,414 +0,0 @@ - - - - - - - - popr.utils.constraint — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for popr.utils.constraint

-import numpy as np
-import scipy.sparse as sp
-from poly_matrix.poly_matrix import PolyMatrix
-
-from popr.utils.common import get_vec
-from popr.utils.plotting_tools import plot_basis
-
-
-def remove_dependent_constraints(constraints, verbose=False):
-    from cert_tools.linalg_tools import find_dependent_columns
-
-    # find which constraints are lin. dep.
-    A_vec = sp.vstack(
-        [constraint.a_full_ for constraint in constraints], format="coo"
-    ).T
-
-    bad_idx = find_dependent_columns(A_vec, verbose=verbose)
-    if len(bad_idx):
-        np.testing.assert_allclose(bad_idx, sorted(bad_idx))
-        # important: by changing the order we
-        for idx in sorted(bad_idx)[::-1]:
-            del constraints[idx]
-
-
-def generate_poly_matrix(constraints, factor_out_parameters=False, lifter=None):
-    plot_rows = []
-    plot_row_labels = []
-    j = -1
-    old_mat_vars = ""
-    for constraint in constraints:
-        mat_vars = constraint.mat_var_dict
-        i = constraint.index
-        if factor_out_parameters:  # use a and not b.
-            if constraint.polyrow_a_ is not None:
-                plot_rows.append(constraint.polyrow_a_)
-            else:
-                if constraint.a_ is not None:
-                    assert (
-                        lifter is not None
-                    ), "Need to provide lifter because a_ is not defined"
-                    polyrow_a = lifter.convert_a_to_polyrow(
-                        constraint.a_, constraint.mat_var_dict
-                    )
-                elif constraint.a_full_ is not None:
-                    assert (
-                        lifter is not None
-                    ), "Need to provide lifter because a_full_ is not defined"
-                    polyrow_a = lifter.convert_a_to_polyrow(
-                        constraint.a_full_, constraint.mat_var_dict
-                    )
-                plot_rows.append(polyrow_a)
-        else:
-            if constraint.polyrow_b_ is not None:
-                plot_rows.append(constraint.polyrow_b_)
-            else:
-                assert (
-                    lifter is not None
-                ), "Need to provide lifter because polyrow_b_ is not defined."
-                plot_rows.append(
-                    lifter.convert_b_to_polyrow(
-                        constraint.b_, mat_vars, constraint.mat_param_dict
-                    )
-                )
-
-        if mat_vars != old_mat_vars:
-            j += 1
-            plot_row_labels.append(f"{j}:b{i}")
-            # plot_row_labels.append(f"{j}{mat_vars}:b{i}")
-            old_mat_vars = mat_vars
-        else:
-            plot_row_labels.append(f"{j}:b{i}")
-
-    templates_poly = PolyMatrix.init_from_row_list(
-        plot_rows, row_labels=plot_row_labels
-    )
-    return templates_poly
-
-
-def plot_poly_matrix(
-    poly_matrix, variables_j=None, variables_i=None, simplify=True, hom="h"
-):
-    if variables_i is None:
-        variables_i = poly_matrix.variable_dict_i
-    if variables_j is None:
-        variables_j = poly_matrix.variable_dict_j
-
-    # plot the templates stored in poly_matrix.
-    fig, ax = plot_basis(
-        poly_matrix,
-        variables_j=variables_j,
-        variables_i=variables_i,
-        discrete=True,
-    )
-    ax.set_yticklabels([])
-    ax.set_yticks([])
-    if simplify:
-        ax.set_xticks([])
-        ax.set_xticklabels([])
-    else:
-        new_xticks = []
-        for lbl in ax.get_xticklabels():
-            lbl = lbl.get_text()
-            if "_" in lbl:  # avoid double subscript
-                new_lbl = f"${lbl.replace(f'{hom}.', '').replace(':', '^')}$"
-            else:
-                new_lbl = f"${lbl.replace(f'{hom}.', '').replace(':', '_')}$"
-            new_xticks.append(new_lbl)
-        ax.set_xticklabels(new_xticks, fontsize=7)
-
-    # plot a red vertical line at each new block of parameters.
-    params = [v.split("-")[0] for v in variables_j]
-    old_param = params[0]
-    for i, p in enumerate(params):
-        if p != old_param:
-            ax.axvline(i - 0.5, color="red", linewidth=1.0)
-            ax.annotate(
-                text=f"${p.replace(':0', '^x').replace(':1', '^y').replace('l.','').replace('.','')}$",
-                xy=(float(i - 0.4), 0.0),
-                fontsize=8,
-                color="red",
-            )
-            old_param = p
-    return fig, ax
-
-
-
-[docs] -class Constraint(object): - """ - This class serves the main purpose of not recomputing representations of constraints more than once. - """ - - def __init__( - self, - index=0, - polyrow_a=None, - polyrow_b=None, - A_poly=None, - A_sparse=None, - b=None, - a=None, - a_full=None, - b_full=None, - mat_var_dict=None, - mat_param_dict=None, - known=False, - template_idx=0, - ): - self.index = index - self.mat_var_dict = mat_var_dict - self.mat_param_dict = mat_param_dict - - self.b_ = b - self.polyrow_b_ = polyrow_b - self.polyrow_a_ = polyrow_a - self.A_poly_ = A_poly - self.A_sparse_ = A_sparse - self.a_ = a - self.b_full_ = b_full - self.a_full_ = a_full - - self.known = known - self.template_idx = template_idx - - # list of applied constraints derived from this constraint. - self.applied_list = [] - - @staticmethod - # @profile - def init_from_b( - index: int, - b: np.ndarray, - mat_var_dict: dict, - lifter=None, - mat_param_dict: dict | None = None, - convert_to_polyrow: bool = True, - known: bool = True, - template_idx: int = 0, - ): - a = None - A_sparse = None - a_full = None - if lifter is not None: - a = lifter.get_reduced_a( - b, var_subset=mat_var_dict, param_subset=mat_param_dict, sparse=True - ) - A_sparse = lifter.get_mat(a, var_dict=mat_var_dict, sparse=True) - a_full = get_vec(A_sparse, sparse=True) - if a_full is None: - return None - if convert_to_polyrow: - assert lifter is not None - A_poly, __ = PolyMatrix.init_from_sparse( - A_sparse, var_dict=lifter.var_dict, unfold=True - ) - polyrow_b = lifter.convert_b_to_polyrow( - b, mat_var_dict, param_subset=mat_param_dict - ) - else: - A_poly = None - polyrow_b = None - return Constraint( - index=index, - a=a, - b=b, - A_sparse=A_sparse, - A_poly=A_poly, - polyrow_b=polyrow_b, - a_full=a_full, - mat_var_dict=mat_var_dict, - mat_param_dict=mat_param_dict, - known=known, - template_idx=template_idx, - ) - - @staticmethod - def init_from_A_poly( - lifter, - A_poly: PolyMatrix, - mat_var_dict: dict, - known: bool = False, - index: int = 0, - template_idx: int = 0, - compute_polyrow_b=False, - ): - Ai_sparse_small = A_poly.get_matrix(variables=mat_var_dict) - ai = get_vec(Ai_sparse_small, correct=True) - bi = lifter.augment_using_zero_padding(ai) - if compute_polyrow_b: - polyrow_b = lifter.convert_b_to_polyrow(bi, mat_var_dict) - else: - polyrow_b = None - polyrow_a = lifter.convert_a_to_polyrow(ai, mat_var_dict) - Ai_sparse = A_poly.get_matrix(variables=lifter.var_dict) - return Constraint( - a=ai, - polyrow_a=polyrow_a, - b=bi, - polyrow_b=polyrow_b, - A_poly=A_poly, - A_sparse=Ai_sparse, - known=known, - index=index, - mat_var_dict=mat_var_dict, - template_idx=template_idx, - ) - - @staticmethod - def init_from_polyrow_b( - polyrow_b: PolyMatrix, - lifter, - index: int = 0, - known: bool = False, - template_idx: int = 0, - mat_var_dict: dict | None = None, - ): - if mat_var_dict is None: - mat_var_dict = lifter.var_dict - A_poly = lifter.convert_polyrow_to_Apoly(polyrow_b) - dict_unroll = lifter.get_var_dict(mat_var_dict, unroll_keys=True) - A_sparse = A_poly.get_matrix(dict_unroll) - a_full = get_vec(A_sparse, sparse=True) - return Constraint( - index=index, - A_poly=A_poly, - polyrow_b=polyrow_b, - A_sparse=A_sparse, - a_full=a_full, - known=known, - template_idx=template_idx, - mat_var_dict=mat_var_dict, - ) - - def scale_to_new_lifter(self, lifter): - if self.known: - assert self.A_poly_ is not None - # known matrices are stored in origin variables, not unrolled form - self.A_sparse_ = self.A_poly_.get_matrix(lifter.var_dict) - self.a_full_ = get_vec(self.A_sparse_, sparse=True) - - else: - assert self.A_poly_ is not None - # known matrices are stored in origin variables, not unrolled form - target_dict_unroll = lifter.get_var_dict(unroll_keys=True) - self.A_sparse_ = self.A_poly_.get_matrix(target_dict_unroll) - self.a_full_ = get_vec(self.A_sparse_, sparse=True) - return self
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/_sources/api.rst.txt b/docs/build/_sources/api.rst.txt deleted file mode 100644 index 0274180..0000000 --- a/docs/build/_sources/api.rst.txt +++ /dev/null @@ -1,10 +0,0 @@ -API and modules -=============== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - api/algorithms - api/lifters - api/utils diff --git a/docs/build/_sources/api/algorithms.rst.txt b/docs/build/_sources/api/algorithms.rst.txt deleted file mode 100644 index 9bb246e..0000000 --- a/docs/build/_sources/api/algorithms.rst.txt +++ /dev/null @@ -1,19 +0,0 @@ -Core Algorithms -=============== - -.. contents:: - :depth: 1 - :local: - - -AutoTight ---------- -.. autoclass:: popcor.AutoTight - :undoc-members: - :members: get_A_learned, get_duality_gap - -AutoTemplate ------------- -.. autoclass:: popcor.AutoTemplate - :undoc-members: - :members: run, apply diff --git a/docs/build/_sources/api/auto_tight.rst.txt b/docs/build/_sources/api/auto_tight.rst.txt deleted file mode 100644 index 13ec899..0000000 --- a/docs/build/_sources/api/auto_tight.rst.txt +++ /dev/null @@ -1,20 +0,0 @@ -Core Algorithms -=============== - -.. contents:: - :depth: 1 - :local: - - -AutoTight ---------- -.. autoclass:: popr.AutoTight - :members: - :show-inheritance: - -AutoTemplate ------------- -.. autoclass:: popr.AutoTemplate - :members: - :show-inheritance: - diff --git a/docs/build/_sources/api/lifters.rst.txt b/docs/build/_sources/api/lifters.rst.txt deleted file mode 100644 index ea0a7d9..0000000 --- a/docs/build/_sources/api/lifters.rst.txt +++ /dev/null @@ -1,59 +0,0 @@ -Base Lifters -============ - -.. contents:: - :depth: 1 - :local: - -Overview --------- - -There are a couple of lifters that serve as a basis for new lifters. -The most basic one is :ref:`StateLifter`, which is generally the best starting point. - -For specific problems, there are a couple of abstract classes that were developed to ease -the development. In particular, we have: - -- :ref:`RobustPoseLifter` -- :ref:`StereoLifter` -- :ref:`PolyLifter` - -Basics ------- - -Below are some general notes about terminology that may be useful in understanding the code and building your own lifters. - -- *theta* is the original (low-dimensional) state variable. -- *x* is the lifted (higher-dimensional) state variable. -- *A* are equality constraints -- *B* are inequality constraints -- *var_dict* refers to the dictionary of variable name - variable size pairs. -- *param_dict* is used to factor out parameters when creating templates. It also comes in name - variable size pairs. - -StateLifter ------------ - -.. autoclass:: popcor.base_lifters.StateLifter - :members: get_x, get_theta, sample_theta, sample_parameters, get_Q, get_A_known, get_B_known, local_solver, get_cost, get_error - :show-inheritance: - -StereoLifter ------------- - -.. autoclass:: popcor.base_lifters.StereoLifter - :show-inheritance: - :undoc-members: - -RobustPoseLifter ------------------- - -.. autoclass:: popcor.base_lifters.RobustPoseLifter - :show-inheritance: - :undoc-members: - -PolyLifter ----------- - -.. autoclass:: popcor.base_lifters.PolyLifter - :show-inheritance: - :undoc-members: \ No newline at end of file diff --git a/docs/build/_sources/api/utils.rst.txt b/docs/build/_sources/api/utils.rst.txt deleted file mode 100644 index 09ef6df..0000000 --- a/docs/build/_sources/api/utils.rst.txt +++ /dev/null @@ -1,29 +0,0 @@ -Utils -===== - - -.. warning:: - This page of the documentation is unfinished, and may significantly change in future versions. - - -Constraints and templates -------------------------- - -.. autoclass:: popcor.utils.constraint.Constraint - :members: - - -Helpers for matrix and vector operations ----------------------------------------- - -.. automodule:: popcor.utils.common - :members: - - -.. :: - - Geometry: lie algebra etc. - -------------------------- - - .. automodule:: popcor.utils.geometry - :members: diff --git a/docs/build/_sources/contributing.rst.txt b/docs/build/_sources/contributing.rst.txt deleted file mode 100644 index 7451363..0000000 --- a/docs/build/_sources/contributing.rst.txt +++ /dev/null @@ -1,7 +0,0 @@ -Contributing -============ - -Below is a rendering of the CONTRIBUTING.md file. - -.. include:: ../../CONTRIBUTING.md - :parser: myst_parser.sphinx_ \ No newline at end of file diff --git a/docs/build/_sources/examples.rst.txt b/docs/build/_sources/examples.rst.txt deleted file mode 100644 index ac5ea96..0000000 --- a/docs/build/_sources/examples.rst.txt +++ /dev/null @@ -1,12 +0,0 @@ -Examples -======== - -.. toctree:: - :numbered: - :maxdepth: 2 - :caption: Contents: - - examples/templates - examples/toy - examples/standard - examples/robust diff --git a/docs/build/_sources/examples/a.rst.txt b/docs/build/_sources/examples/a.rst.txt deleted file mode 100644 index 20902f7..0000000 --- a/docs/build/_sources/examples/a.rst.txt +++ /dev/null @@ -1,48 +0,0 @@ -Toy Examples -============ - -Univariate Polynomials ----------------------- - -.. autoclass:: popr.examples.Poly4Lifter - :undoc-members: - :show-inheritance: - -.. figure:: /figures/poly4_lifter_A.png - :alt: Poly4Lifter Type A - :align: center - :figclass: align-center - - Poly4Lifter Type A - -.. figure:: /figures/poly4_lifter_B.png - :alt: Poly4Lifter Type B - :align: center - :figclass: align-center - - Poly4Lifter Type B - -.. autoclass:: popr.examples.Poly6Lifter - :undoc-members: - :show-inheritance: - -.. figure:: /figures/poly6_lifter_A.png - :alt: Poly6Lifter Type A - :align: center - :figclass: align-center - - Poly6Lifter Type A - -.. figure:: /figures/poly6_lifter_B.png - :alt: Poly6Lifter Type B - :align: center - :figclass: align-center - - Poly6Lifter Type B - -Other Toy Examples ------------------- - -.. autoclass:: popr.examples.Stereo1DLifter - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/build/_sources/examples/b.rst.txt b/docs/build/_sources/examples/b.rst.txt deleted file mode 100644 index 383559e..0000000 --- a/docs/build/_sources/examples/b.rst.txt +++ /dev/null @@ -1,26 +0,0 @@ -Standard Estimation Problems -============================ - -Range-Only Localization ------------------------ - -.. autoclass:: popr.examples.RangeOnlyLocLifter - - -Stereo-Camera Localization --------------------------- - -.. autoclass:: popr.examples.Stereo2DLifter - :undoc-members: - :show-inheritance: - -.. autoclass:: popr.examples.Stereo3DLifter - :undoc-members: - :show-inheritance: - -Rotation Averaging ------------------- - -.. autoclass:: popr.examples.RotationLifter - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/build/_sources/examples/c.rst.txt b/docs/build/_sources/examples/c.rst.txt deleted file mode 100644 index 16d052d..0000000 --- a/docs/build/_sources/examples/c.rst.txt +++ /dev/null @@ -1,13 +0,0 @@ -Robust Estimation Problems -========================== - -Robust Registration Problems ----------------------------- - -.. autoclass:: popr.examples.MonoLifter - :undoc-members: - :show-inheritance: - -.. autoclass:: popr.examples.WahbaLifter - :undoc-members: - :show-inheritance: diff --git a/docs/build/_sources/examples/d.rst.txt b/docs/build/_sources/examples/d.rst.txt deleted file mode 100644 index 607382c..0000000 --- a/docs/build/_sources/examples/d.rst.txt +++ /dev/null @@ -1,16 +0,0 @@ -Templates -========= - -Example for AutoTight ------------------------------- - -.. autoclass:: popr.examples.ExampleLifter - :undoc-members: - :show-inheritance: - - -Example for AutoTemplate ---------------------------------- - -.. note:: - Coming soon: barebone template for AutoTemplate \ No newline at end of file diff --git a/docs/build/_sources/examples/robust.rst.txt b/docs/build/_sources/examples/robust.rst.txt deleted file mode 100644 index b275afa..0000000 --- a/docs/build/_sources/examples/robust.rst.txt +++ /dev/null @@ -1,13 +0,0 @@ -Robust Estimation Problems -========================== - -Robust Registration Problems ----------------------------- - -.. autoclass:: popcor.examples.MonoLifter - :undoc-members: - :show-inheritance: - -.. autoclass:: popcor.examples.WahbaLifter - :undoc-members: - :show-inheritance: diff --git a/docs/build/_sources/examples/standard.rst.txt b/docs/build/_sources/examples/standard.rst.txt deleted file mode 100644 index 3936ee7..0000000 --- a/docs/build/_sources/examples/standard.rst.txt +++ /dev/null @@ -1,26 +0,0 @@ -Standard Estimation Problems -============================ - -Range-Only Localization ------------------------ - -.. autoclass:: popcor.examples.RangeOnlyLocLifter - - -Stereo-Camera Localization --------------------------- - -.. autoclass:: popcor.examples.Stereo2DLifter - :undoc-members: - :show-inheritance: - -.. autoclass:: popcor.examples.Stereo3DLifter - :undoc-members: - :show-inheritance: - -Rotation Averaging ------------------- - -.. autoclass:: popcor.examples.RotationLifter - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/build/_sources/examples/templates.rst.txt b/docs/build/_sources/examples/templates.rst.txt deleted file mode 100644 index edd6404..0000000 --- a/docs/build/_sources/examples/templates.rst.txt +++ /dev/null @@ -1,16 +0,0 @@ -Templates -========= - -Example for AutoTight ------------------------------- - -.. autoclass:: popcor.examples.ExampleLifter - :undoc-members: - :show-inheritance: - - -Example for AutoTemplate ---------------------------------- - -.. note:: - Coming soon: barebone template for AutoTemplate diff --git a/docs/build/_sources/examples/toy.rst.txt b/docs/build/_sources/examples/toy.rst.txt deleted file mode 100644 index 69666d1..0000000 --- a/docs/build/_sources/examples/toy.rst.txt +++ /dev/null @@ -1,48 +0,0 @@ -Toy Examples -============ - -Univariate Polynomials ----------------------- - -.. autoclass:: popcor.examples.Poly4Lifter - :undoc-members: - :show-inheritance: - -.. figure:: /_static/poly4_lifter_A.png - :alt: Poly4Lifter Type A - :align: center - :figclass: align-center - - Poly4Lifter Type A - -.. figure:: /_static/poly4_lifter_B.png - :alt: Poly4Lifter Type B - :align: center - :figclass: align-center - - Poly4Lifter Type B - -.. autoclass:: popcor.examples.Poly6Lifter - :undoc-members: - :show-inheritance: - -.. figure:: /_static/poly6_lifter_A.png - :alt: Poly6Lifter Type A - :align: center - :figclass: align-center - - Poly6Lifter Type A - -.. figure:: /_static/poly6_lifter_B.png - :alt: Poly6Lifter Type B - :align: center - :figclass: align-center - - Poly6Lifter Type B - -Other Toy Examples ------------------- - -.. autoclass:: popcor.examples.Stereo1DLifter - :undoc-members: - :show-inheritance: diff --git a/docs/build/_sources/index.rst.txt b/docs/build/_sources/index.rst.txt deleted file mode 100644 index 90d9a91..0000000 --- a/docs/build/_sources/index.rst.txt +++ /dev/null @@ -1,38 +0,0 @@ - -.. warning:: - - This package is currently in pre-release state. You are welcome to try it out, - and please get in touch via github or e-mail if you have any questions, suggestions or issues. - -Welcome to POPCOR! -================== - -.. figure:: _static/overview.png - :align: center - :width: 500px - - -.. include:: ../../README.md - :start-after: .. start-doc - :end-before: .. end-doc - :parser: myst_parser.sphinx_ - - -.. toctree:: - :maxdepth: 2 - :hidden: - - self - quickstart - api - examples - whatsnew - contributing - - - -.. - currently commented out cause it does not seem useful: - Indices - ------- - * :ref:`genindex` diff --git a/docs/build/_sources/quickstart.rst.txt b/docs/build/_sources/quickstart.rst.txt deleted file mode 100644 index e78a249..0000000 --- a/docs/build/_sources/quickstart.rst.txt +++ /dev/null @@ -1,148 +0,0 @@ -Quick Start Guide -================= - -Installation ------------- - -POPR can be installed by running from a terminal: - -.. code-block:: bash - - git clone --recurse-submodules git@github.com:duembgen/popcor - cd popcor - conda env create -f environment.yml - - - -Problem Formulation -------------------- - -We start with polynomial optimization problems (POPs) of the form: - -.. math:: - - \begin{align} q^\star =&\min_{\theta} f(\theta) \\ - \text{s.t. } &g(\theta) = 0 \\ - &h(\theta) \geq 0 - \end{align} - -where :math:`f,g,\text{ and } h` are polynomial functions, and both :math:`g` and :math:`h` can be vector-valued. Many maximum-a-posteriori or maximum-likelihood estimation problems can be formulated as such, for example `range-only localization `_ and `range-aided SLAM `_, (`matrix-weighted `_) `SLAM `_, and `outlier-robust estimation `_. The same is true for many control and planning problems, for example the `inverted pendulum `_ and other classical dynamical systems, and even contact-rich problems such as `slider-pusher planning problems `_. - -Any POP can be equivalently written in the following QCQP form: - -.. math:: - - \begin{align} q^\star =&\min_{x} x^\top Q x \\ - \text{s.t. } &(\forall i): x^\top A_i x = b_i \\ - &(\forall j): x^\top B_j x \geq 0 - \end{align} - -with cost matrix :math:`Q`, known constraint matrices :math:`A_i,B_j`. -Note that - -- We always include the so-called homogenization variable, which enables to write linear and constant terms as quadratics. By convention, we set the first element of :math:`x` to one, and we use :math:`b_0=1, A_0` to encorce this constraint. -- All inequality and some equality constraints correspond to the constraints from the original POP. -- Some additional equality constraints correspond to new substitution variables that need to be added to formulate the problem as a quadratic. - -.. warning:: - Note that while inequality constraints can be added to the problem formulation, there is no implementation yet to add find and add redundant inequality constraints to the relaxation. - -For the standard usage, the user first needs to define a custom **Lifter** class which essentially contains all elements related to the QCQP problem formulation. -This class should inherit from :ref:`StateLifter`. A basic skeleton of such a -Lifter class is provided in :ref:`Example for AutoTight`. The main purpose of this class is -that it provides all basic operations related to the problem formulation, such as: - -- to sample feasible states (:meth:`popcor.base_lifters.StateLifter.sample_theta`), -- to get the lifted vector (:meth:`popcor.base_lifters.StateLifter.get_x`), - -For a bit more advanced functionality (for example for the :ref:`SDP Relaxation` in the next section), you also need to define functions such as - -- get the cost matrix (:py:meth:`popcor.base_lifters.StateLifter.get_Q`), -- get known constraint matrices (:meth:`popcor.base_lifters.StateLifter.get_A_known`, :meth:`popcor.base_lifters.StateLifter.get_B_known`). - -Many example lifters are provided, you can find them under :ref:`Examples`. - -**Example: instantiating and using lifter** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following code snippet shows some basic operations (and useful sanity checks) for the example -lifter class :class:`popcor.examples.Poly4Lifter`. Note that this and all following examples can be found -in the file :file:`../../tests/test_quickstart.py`. - -.. literalinclude:: ../../tests/test_quickstart.py - :language: python - :lines: 9-23 - :dedent: 4 - - -SDP Relaxation --------------- - -It is straightforward to derive a convex relaxation of the original QCQP, using the reformulation :math:`x^\top Qx=\langle x, Qx\rangle = \langle Q, xx^\top \rangle`, where :math:`\langle \cdot, \cdot \rangle` denotes the trace inner product. Then introducing :math:`X:=xx^\top` and relaxing its rank, we obtain the following convex relaxation, in the form of an SDP: - -.. math:: - \begin{align} p^\star = &\min_{X \succeq 0} \langle Q, X \rangle \\ - \text{s.t. } &(\forall i): \langle A_i, X \rangle = b_i \\ - &(\forall j): \langle B_j, X \rangle \geq 0 - \end{align} - - -**Example: solving the QCQP using rank relaxation** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following code snippet shows how you can use the simple lifter from earlier to find the global -optimum of the nonconvex polynomial problem, by solving an SDP. - -.. literalinclude:: ../../tests/test_quickstart.py - :language: python - :lines: 28-55 - :dedent: 4 - - - -AutoTight Method ----------------- - -**AutoTight** is used to find all possible constraints matrices :math:`A_r`, which are also automatically satisfied by solutions of the QCQP. They are also called **redundant constraints** because they do not change the feasible set of the original problem, but when adding those constraints to the SDP (rank-)relaxation, they often improve tightness. Denoting by :math:`A_r` the redundant constraints, we can solve the following SDP: - -.. math:: - \begin{align} p_r^\star = &\min_{X \succeq 0} \langle Q, X \rangle \\ - \text{s.t. } &(\forall i): \langle A_i, X \rangle = b_i \\ - &(\forall r): \langle A_r, X \rangle = 0 \\ - &(\forall j): \langle B_j, X \rangle \geq 0 - \end{align} - -We use the term **cost-tight** to say that strong duality holds (:math:`p_r^\star = q^\star`) while by rank-tight we denote the fact that the SDP solver returns a rank-one solution. -If successful, the output is a set of constraints that leads to a tight SDP relaxation of the original problem, which can be used to solve the problem to global optimality (if we have rank tightness) or certify given solutions (if we have cost tightness). - -More information on how to use AutoTight can be found :ref:`here ` and a simple example is given next. - -**Example: tightening the SDP relaxation using AutoTight** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. literalinclude:: ../../tests/test_quickstart.py - :language: python - :lines: 60-101 - :dedent: 4 - - -AutoTemplate Method -------------------- - -*AutoTemplate* follows the same principle as *AutoTight*, but its output are templates rather than constraint matrices. These templates can be seen as "parametrized" versions of the constraint matrices, and can be applied to new problem instances of any size without having to learn the constraints again from scratch. - -More information on how to use AutoTemplate can be found :ref:`here ` and a simple example is given next. - -**Example: tightening a different problem using AutoTemplate** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. literalinclude:: ../../tests/test_quickstart.py - :language: python - :lines: 106-135 - :dedent: 4 - - -References ----------- - -`[1] F. Dümbgen, C. Holmes, B. Agro and T. Barfoot, "Toward Globally Optimal State Estimation Using Automatically Tightened Semidefinite Relaxations," in IEEE Transactions on Robotics, vol. 40, pp. 4338-4358, 2024, doi: 10.1109/TRO.2024.3454570. `_ diff --git a/docs/build/_sources/whatsnew.rst.txt b/docs/build/_sources/whatsnew.rst.txt deleted file mode 100644 index 89a6f49..0000000 --- a/docs/build/_sources/whatsnew.rst.txt +++ /dev/null @@ -1,7 +0,0 @@ -What's new -========== - -Below is a rendering of the CHANGELOG.md file. - -.. include:: ../../CHANGELOG.md - :parser: myst_parser.sphinx_ diff --git a/docs/build/_sources/whatsnew/0.0.1.rst.txt b/docs/build/_sources/whatsnew/0.0.1.rst.txt deleted file mode 100644 index e69de29..0000000 diff --git a/docs/build/_sources/whatsnew/CHANGELOG.md.txt b/docs/build/_sources/whatsnew/CHANGELOG.md.txt deleted file mode 100644 index 0a61887..0000000 --- a/docs/build/_sources/whatsnew/CHANGELOG.md.txt +++ /dev/null @@ -1,4 +0,0 @@ - -- This is the initial release of the toolbox. It is based on and now included there as a submodule. It is based on the publication [1]. - -`[1] F. Dümbgen, C. Holmes, B. Agro and T. Barfoot, "Toward Globally Optimal State Estimation Using Automatically Tightened Semidefinite Relaxations," in IEEE Transactions on Robotics, vol. 40, pp. 4338-4358, 2024, doi: 10.1109/TRO.2024.3454570. `_ \ No newline at end of file diff --git a/docs/build/_static/_sphinx_javascript_frameworks_compat.js b/docs/build/_static/_sphinx_javascript_frameworks_compat.js deleted file mode 100644 index 8141580..0000000 --- a/docs/build/_static/_sphinx_javascript_frameworks_compat.js +++ /dev/null @@ -1,123 +0,0 @@ -/* Compatability shim for jQuery and underscores.js. - * - * Copyright Sphinx contributors - * Released under the two clause BSD licence - */ - -/** - * small helper function to urldecode strings - * - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL - */ -jQuery.urldecode = function(x) { - if (!x) { - return x - } - return decodeURIComponent(x.replace(/\+/g, ' ')); -}; - -/** - * small helper function to urlencode strings - */ -jQuery.urlencode = encodeURIComponent; - -/** - * This function returns the parsed url parameters of the - * current request. Multiple values per key are supported, - * it will always return arrays of strings for the value parts. - */ -jQuery.getQueryParameters = function(s) { - if (typeof s === 'undefined') - s = document.location.search; - var parts = s.substr(s.indexOf('?') + 1).split('&'); - var result = {}; - for (var i = 0; i < parts.length; i++) { - var tmp = parts[i].split('=', 2); - var key = jQuery.urldecode(tmp[0]); - var value = jQuery.urldecode(tmp[1]); - if (key in result) - result[key].push(value); - else - result[key] = [value]; - } - return result; -}; - -/** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ -jQuery.fn.highlightText = function(text, className) { - function highlight(node, addItems) { - if (node.nodeType === 3) { - var val = node.nodeValue; - var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && - !jQuery(node.parentNode).hasClass(className) && - !jQuery(node.parentNode).hasClass("nohighlight")) { - var span; - var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.className = className; - } - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - if (isInSVG) { - var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - var bbox = node.parentElement.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute('class', className); - addItems.push({ - "parent": node.parentNode, - "target": rect}); - } - } - } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this, addItems); - }); - } - } - var addItems = []; - var result = this.each(function() { - highlight(this, addItems); - }); - for (var i = 0; i < addItems.length; ++i) { - jQuery(addItems[i].parent).before(addItems[i].target); - } - return result; -}; - -/* - * backward compatibility for jQuery.browser - * This will be supported until firefox bug is fixed. - */ -if (!jQuery.browser) { - jQuery.uaMatch = function(ua) { - ua = ua.toLowerCase(); - - var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec(ua) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || - []; - - return { - browser: match[ 1 ] || "", - version: match[ 2 ] || "0" - }; - }; - jQuery.browser = {}; - jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; -} diff --git a/docs/build/_static/api.rst b/docs/build/_static/api.rst deleted file mode 100644 index 0274180..0000000 --- a/docs/build/_static/api.rst +++ /dev/null @@ -1,10 +0,0 @@ -API and modules -=============== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - api/algorithms - api/lifters - api/utils diff --git a/docs/build/_static/api/algorithms.rst b/docs/build/_static/api/algorithms.rst deleted file mode 100644 index 13ec899..0000000 --- a/docs/build/_static/api/algorithms.rst +++ /dev/null @@ -1,20 +0,0 @@ -Core Algorithms -=============== - -.. contents:: - :depth: 1 - :local: - - -AutoTight ---------- -.. autoclass:: popr.AutoTight - :members: - :show-inheritance: - -AutoTemplate ------------- -.. autoclass:: popr.AutoTemplate - :members: - :show-inheritance: - diff --git a/docs/build/_static/api/lifters.rst b/docs/build/_static/api/lifters.rst deleted file mode 100644 index 677454e..0000000 --- a/docs/build/_static/api/lifters.rst +++ /dev/null @@ -1,59 +0,0 @@ -Base Lifters -============ - -.. contents:: - :depth: 1 - :local: - -Overview --------- - -There are a couple of lifters that serve as a basis for new lifters. -The most basic one is :ref:`StateLifter`, which is generally the best starting point. - -For specific problems, there are a couple of abstract classes that were developed to ease -the development. In particular, we have: - -- :ref:`RobustPoseLifter` for point-to-point registration (Wahba) (:ref:`WahbaLifter`) and point-to-line registration (:ref:`MonoLifter`), which both try to regress an unknown pose. Robust cost functions are supported. -- :ref:`StereoLifter` for stereo localization in 2D (:ref:`Stereo2DLifter`) and 3D (:ref:`Stereo3DLifter`). -- :ref:`PolyLifter` for univariate polynomials of any order. - -Basics ------- - -Below are some general notes about terminology that may be useful in understanding the code and building your own lifters. - -- *theta* is the original (low-dimensional) state variable. -- *x* is the lifted (higher-dimensional) state variable. -- *A* are equality constraints -- *B* are inequality constraints -- *var_dict* refers to the dictionary of variable name - variable size pairs. -- *param_dict* is used to factor out parameters when creating templates. It also comes in name - variable size pairs. - -StateLifter ------------ - -.. autoclass:: popr.base_lifters.StateLifter - :members: get_x, get_theta, sample_theta, sample_parameters, get_Q, get_A_known, get_B_known, local_solver, get_cost, get_error - :show-inheritance: - -StereoLifter ------------- - -.. autoclass:: popr.base_lifters.StereoLifter - :member: LEVELS - :show-inheritance: - -RobustPoseLifter ------------------- - -.. autoclass:: popr.base_lifters.RobustPoseLifter - :members: local_solver_manopt - :show-inheritance: - -PolyLifter ----------- - -.. autoclass:: popr.base_lifters.PolyLifter - :members: - :show-inheritance: diff --git a/docs/build/_static/api/utils.rst b/docs/build/_static/api/utils.rst deleted file mode 100644 index 86b050e..0000000 --- a/docs/build/_static/api/utils.rst +++ /dev/null @@ -1,30 +0,0 @@ -Utils -===== - -.. contents:: - :depth: 2 - :local: - - -Constraints and Templates -------------------------- - -.. autoclass:: popr.utils.constraint.Constraint - :members: - :show-inheritance: - - -Matrix and vector operations ----------------------------- - -.. automodule:: popr.utils.common - :members: - - -Geometry: lie algebra etc. --------------------------- - -.. automodule:: popr.utils.geometry - :members: - - diff --git a/docs/build/_static/basic.css b/docs/build/_static/basic.css deleted file mode 100644 index 7ebbd6d..0000000 --- a/docs/build/_static/basic.css +++ /dev/null @@ -1,914 +0,0 @@ -/* - * Sphinx stylesheet -- basic theme. - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -div.section::after { - display: block; - content: ''; - clear: left; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; - word-wrap: break-word; - overflow-wrap : break-word; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar #searchbox form.search { - overflow: hidden; -} - -div.sphinxsidebar #searchbox input[type="text"] { - float: left; - width: 80%; - padding: 0.25em; - box-sizing: border-box; -} - -div.sphinxsidebar #searchbox input[type="submit"] { - float: left; - width: 20%; - border-left: none; - padding: 0.25em; - box-sizing: border-box; -} - - -img { - border: 0; - max-width: 100%; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin-top: 10px; -} - -ul.search li { - padding: 5px 0; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li p.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; - margin-left: auto; - margin-right: auto; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable { - width: 100%; -} - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable ul { - margin-top: 0; - margin-bottom: 0; - list-style-type: none; -} - -table.indextable > tbody > tr > td > ul { - padding-left: 0em; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -div.genindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -/* -- domain module index --------------------------------------------------- */ - -table.modindextable td { - padding: 2px; - border-collapse: collapse; -} - -/* -- general body styles --------------------------------------------------- */ - -div.body { - min-width: 360px; - max-width: 800px; -} - -div.body p, div.body dd, div.body li, div.body blockquote { - -moz-hyphens: auto; - -ms-hyphens: auto; - -webkit-hyphens: auto; - hyphens: auto; -} - -a.headerlink { - visibility: hidden; -} - -a:visited { - color: #551A8B; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink, -caption:hover > a.headerlink, -p.caption:hover > a.headerlink, -div.code-block-caption:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.first { - margin-top: 0 !important; -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -img.align-left, figure.align-left, .figure.align-left, object.align-left { - clear: left; - float: left; - margin-right: 1em; -} - -img.align-right, figure.align-right, .figure.align-right, object.align-right { - clear: right; - float: right; - margin-left: 1em; -} - -img.align-center, figure.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -img.align-default, figure.align-default, .figure.align-default { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left; -} - -.align-center { - text-align: center; -} - -.align-default { - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar, -aside.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px; - background-color: #ffe; - width: 40%; - float: right; - clear: right; - overflow-x: auto; -} - -p.sidebar-title { - font-weight: bold; -} - -nav.contents, -aside.topic, -div.admonition, div.topic, blockquote { - clear: left; -} - -/* -- topics ---------------------------------------------------------------- */ - -nav.contents, -aside.topic, -div.topic { - border: 1px solid #ccc; - padding: 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- content of sidebars/topics/admonitions -------------------------------- */ - -div.sidebar > :last-child, -aside.sidebar > :last-child, -nav.contents > :last-child, -aside.topic > :last-child, -div.topic > :last-child, -div.admonition > :last-child { - margin-bottom: 0; -} - -div.sidebar::after, -aside.sidebar::after, -nav.contents::after, -aside.topic::after, -div.topic::after, -div.admonition::after, -blockquote::after { - display: block; - content: ''; - clear: both; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - margin-top: 10px; - margin-bottom: 10px; - border: 0; - border-collapse: collapse; -} - -table.align-center { - margin-left: auto; - margin-right: auto; -} - -table.align-default { - margin-left: auto; - margin-right: auto; -} - -table caption span.caption-number { - font-style: italic; -} - -table caption span.caption-text { -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 5px; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -th { - text-align: left; - padding-right: 5px; -} - -table.citation { - border-left: solid 1px gray; - margin-left: 1px; -} - -table.citation td { - border-bottom: none; -} - -th > :first-child, -td > :first-child { - margin-top: 0px; -} - -th > :last-child, -td > :last-child { - margin-bottom: 0px; -} - -/* -- figures --------------------------------------------------------------- */ - -div.figure, figure { - margin: 0.5em; - padding: 0.5em; -} - -div.figure p.caption, figcaption { - padding: 0.3em; -} - -div.figure p.caption span.caption-number, -figcaption span.caption-number { - font-style: italic; -} - -div.figure p.caption span.caption-text, -figcaption span.caption-text { -} - -/* -- field list styles ----------------------------------------------------- */ - -table.field-list td, table.field-list th { - border: 0 !important; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.field-name { - -moz-hyphens: manual; - -ms-hyphens: manual; - -webkit-hyphens: manual; - hyphens: manual; -} - -/* -- hlist styles ---------------------------------------------------------- */ - -table.hlist { - margin: 1em 0; -} - -table.hlist td { - vertical-align: top; -} - -/* -- object description styles --------------------------------------------- */ - -.sig { - font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; -} - -.sig-name, code.descname { - background-color: transparent; - font-weight: bold; -} - -.sig-name { - font-size: 1.1em; -} - -code.descname { - font-size: 1.2em; -} - -.sig-prename, code.descclassname { - background-color: transparent; -} - -.optional { - font-size: 1.3em; -} - -.sig-paren { - font-size: larger; -} - -.sig-param.n { - font-style: italic; -} - -/* C++ specific styling */ - -.sig-inline.c-texpr, -.sig-inline.cpp-texpr { - font-family: unset; -} - -.sig.c .k, .sig.c .kt, -.sig.cpp .k, .sig.cpp .kt { - color: #0033B3; -} - -.sig.c .m, -.sig.cpp .m { - color: #1750EB; -} - -.sig.c .s, .sig.c .sc, -.sig.cpp .s, .sig.cpp .sc { - color: #067D17; -} - - -/* -- other body styles ----------------------------------------------------- */ - -ol.arabic { - list-style: decimal; -} - -ol.loweralpha { - list-style: lower-alpha; -} - -ol.upperalpha { - list-style: upper-alpha; -} - -ol.lowerroman { - list-style: lower-roman; -} - -ol.upperroman { - list-style: upper-roman; -} - -:not(li) > ol > li:first-child > :first-child, -:not(li) > ul > li:first-child > :first-child { - margin-top: 0px; -} - -:not(li) > ol > li:last-child > :last-child, -:not(li) > ul > li:last-child > :last-child { - margin-bottom: 0px; -} - -ol.simple ol p, -ol.simple ul p, -ul.simple ol p, -ul.simple ul p { - margin-top: 0; -} - -ol.simple > li:not(:first-child) > p, -ul.simple > li:not(:first-child) > p { - margin-top: 0; -} - -ol.simple p, -ul.simple p { - margin-bottom: 0; -} - -aside.footnote > span, -div.citation > span { - float: left; -} -aside.footnote > span:last-of-type, -div.citation > span:last-of-type { - padding-right: 0.5em; -} -aside.footnote > p { - margin-left: 2em; -} -div.citation > p { - margin-left: 4em; -} -aside.footnote > p:last-of-type, -div.citation > p:last-of-type { - margin-bottom: 0em; -} -aside.footnote > p:last-of-type:after, -div.citation > p:last-of-type:after { - content: ""; - clear: both; -} - -dl.field-list { - display: grid; - grid-template-columns: fit-content(30%) auto; -} - -dl.field-list > dt { - font-weight: bold; - word-break: break-word; - padding-left: 0.5em; - padding-right: 5px; -} - -dl.field-list > dd { - padding-left: 0.5em; - margin-top: 0em; - margin-left: 0em; - margin-bottom: 0em; -} - -dl { - margin-bottom: 15px; -} - -dd > :first-child { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -.sig dd { - margin-top: 0px; - margin-bottom: 0px; -} - -.sig dl { - margin-top: 0px; - margin-bottom: 0px; -} - -dl > dd:last-child, -dl > dd:last-child > :last-child { - margin-bottom: 0; -} - -dt:target, span.highlighted { - background-color: #fbe54e; -} - -rect.highlighted { - fill: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.guilabel, .menuselection { - font-family: sans-serif; -} - -.accelerator { - text-decoration: underline; -} - -.classifier { - font-style: oblique; -} - -.classifier:before { - font-style: normal; - margin: 0 0.5em; - content: ":"; - display: inline-block; -} - -abbr, acronym { - border-bottom: dotted 1px; - cursor: help; -} - -.translated { - background-color: rgba(207, 255, 207, 0.2) -} - -.untranslated { - background-color: rgba(255, 207, 207, 0.2) -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; - overflow-y: hidden; /* fixes display issues on Chrome browsers */ -} - -pre, div[class*="highlight-"] { - clear: both; -} - -span.pre { - -moz-hyphens: none; - -ms-hyphens: none; - -webkit-hyphens: none; - hyphens: none; - white-space: nowrap; -} - -div[class*="highlight-"] { - margin: 1em 0; -} - -td.linenos pre { - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - display: block; -} - -table.highlighttable tbody { - display: block; -} - -table.highlighttable tr { - display: flex; -} - -table.highlighttable td { - margin: 0; - padding: 0; -} - -table.highlighttable td.linenos { - padding-right: 0.5em; -} - -table.highlighttable td.code { - flex: 1; - overflow: hidden; -} - -.highlight .hll { - display: block; -} - -div.highlight pre, -table.highlighttable pre { - margin: 0; -} - -div.code-block-caption + div { - margin-top: 0; -} - -div.code-block-caption { - margin-top: 1em; - padding: 2px 5px; - font-size: small; -} - -div.code-block-caption code { - background-color: transparent; -} - -table.highlighttable td.linenos, -span.linenos, -div.highlight span.gp { /* gp: Generic.Prompt */ - user-select: none; - -webkit-user-select: text; /* Safari fallback only */ - -webkit-user-select: none; /* Chrome/Safari */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* IE10+ */ -} - -div.code-block-caption span.caption-number { - padding: 0.1em 0.3em; - font-style: italic; -} - -div.code-block-caption span.caption-text { -} - -div.literal-block-wrapper { - margin: 1em 0; -} - -code.xref, a code { - background-color: transparent; - font-weight: bold; -} - -h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { - background-color: transparent; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: sans-serif; -} - -div.viewcode-block:target { - margin: -1px -10px; - padding: 0 10px; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -span.eqno a.headerlink { - position: absolute; - z-index: 1; -} - -div.math:hover a.headerlink { - visibility: visible; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } -} \ No newline at end of file diff --git a/docs/build/_static/check-solid.svg b/docs/build/_static/check-solid.svg deleted file mode 100644 index 92fad4b..0000000 --- a/docs/build/_static/check-solid.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/build/_static/clipboard.min.js b/docs/build/_static/clipboard.min.js deleted file mode 100644 index 54b3c46..0000000 --- a/docs/build/_static/clipboard.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * clipboard.js v2.0.8 - * https://clipboardjs.com/ - * - * Licensed MIT © Zeno Rocha - */ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return o}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),c=n.n(e);function a(t){try{return document.execCommand(t)}catch(t){return}}var f=function(t){t=c()(t);return a("cut"),t};var l=function(t){var e,n,o,r=1 - - - - diff --git a/docs/build/_static/copybutton.css b/docs/build/_static/copybutton.css deleted file mode 100644 index f1916ec..0000000 --- a/docs/build/_static/copybutton.css +++ /dev/null @@ -1,94 +0,0 @@ -/* Copy buttons */ -button.copybtn { - position: absolute; - display: flex; - top: .3em; - right: .3em; - width: 1.7em; - height: 1.7em; - opacity: 0; - transition: opacity 0.3s, border .3s, background-color .3s; - user-select: none; - padding: 0; - border: none; - outline: none; - border-radius: 0.4em; - /* The colors that GitHub uses */ - border: #1b1f2426 1px solid; - background-color: #f6f8fa; - color: #57606a; -} - -button.copybtn.success { - border-color: #22863a; - color: #22863a; -} - -button.copybtn svg { - stroke: currentColor; - width: 1.5em; - height: 1.5em; - padding: 0.1em; -} - -div.highlight { - position: relative; -} - -/* Show the copybutton */ -.highlight:hover button.copybtn, button.copybtn.success { - opacity: 1; -} - -.highlight button.copybtn:hover { - background-color: rgb(235, 235, 235); -} - -.highlight button.copybtn:active { - background-color: rgb(187, 187, 187); -} - -/** - * A minimal CSS-only tooltip copied from: - * https://codepen.io/mildrenben/pen/rVBrpK - * - * To use, write HTML like the following: - * - *

Short

- */ - .o-tooltip--left { - position: relative; - } - - .o-tooltip--left:after { - opacity: 0; - visibility: hidden; - position: absolute; - content: attr(data-tooltip); - padding: .2em; - font-size: .8em; - left: -.2em; - background: grey; - color: white; - white-space: nowrap; - z-index: 2; - border-radius: 2px; - transform: translateX(-102%) translateY(0); - transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); -} - -.o-tooltip--left:hover:after { - display: block; - opacity: 1; - visibility: visible; - transform: translateX(-100%) translateY(0); - transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); - transition-delay: .5s; -} - -/* By default the copy button shouldn't show up when printing a page */ -@media print { - button.copybtn { - display: none; - } -} diff --git a/docs/build/_static/copybutton.js b/docs/build/_static/copybutton.js deleted file mode 100644 index 2ea7ff3..0000000 --- a/docs/build/_static/copybutton.js +++ /dev/null @@ -1,248 +0,0 @@ -// Localization support -const messages = { - 'en': { - 'copy': 'Copy', - 'copy_to_clipboard': 'Copy to clipboard', - 'copy_success': 'Copied!', - 'copy_failure': 'Failed to copy', - }, - 'es' : { - 'copy': 'Copiar', - 'copy_to_clipboard': 'Copiar al portapapeles', - 'copy_success': '¡Copiado!', - 'copy_failure': 'Error al copiar', - }, - 'de' : { - 'copy': 'Kopieren', - 'copy_to_clipboard': 'In die Zwischenablage kopieren', - 'copy_success': 'Kopiert!', - 'copy_failure': 'Fehler beim Kopieren', - }, - 'fr' : { - 'copy': 'Copier', - 'copy_to_clipboard': 'Copier dans le presse-papier', - 'copy_success': 'Copié !', - 'copy_failure': 'Échec de la copie', - }, - 'ru': { - 'copy': 'Скопировать', - 'copy_to_clipboard': 'Скопировать в буфер', - 'copy_success': 'Скопировано!', - 'copy_failure': 'Не удалось скопировать', - }, - 'zh-CN': { - 'copy': '复制', - 'copy_to_clipboard': '复制到剪贴板', - 'copy_success': '复制成功!', - 'copy_failure': '复制失败', - }, - 'it' : { - 'copy': 'Copiare', - 'copy_to_clipboard': 'Copiato negli appunti', - 'copy_success': 'Copiato!', - 'copy_failure': 'Errore durante la copia', - } -} - -let locale = 'en' -if( document.documentElement.lang !== undefined - && messages[document.documentElement.lang] !== undefined ) { - locale = document.documentElement.lang -} - -let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; -if (doc_url_root == '#') { - doc_url_root = ''; -} - -/** - * SVG files for our copy buttons - */ -let iconCheck = ` - ${messages[locale]['copy_success']} - - -` - -// If the user specified their own SVG use that, otherwise use the default -let iconCopy = ``; -if (!iconCopy) { - iconCopy = ` - ${messages[locale]['copy_to_clipboard']} - - - -` -} - -/** - * Set up copy/paste for code blocks - */ - -const runWhenDOMLoaded = cb => { - if (document.readyState != 'loading') { - cb() - } else if (document.addEventListener) { - document.addEventListener('DOMContentLoaded', cb) - } else { - document.attachEvent('onreadystatechange', function() { - if (document.readyState == 'complete') cb() - }) - } -} - -const codeCellId = index => `codecell${index}` - -// Clears selected text since ClipboardJS will select the text when copying -const clearSelection = () => { - if (window.getSelection) { - window.getSelection().removeAllRanges() - } else if (document.selection) { - document.selection.empty() - } -} - -// Changes tooltip text for a moment, then changes it back -// We want the timeout of our `success` class to be a bit shorter than the -// tooltip and icon change, so that we can hide the icon before changing back. -var timeoutIcon = 2000; -var timeoutSuccessClass = 1500; - -const temporarilyChangeTooltip = (el, oldText, newText) => { - el.setAttribute('data-tooltip', newText) - el.classList.add('success') - // Remove success a little bit sooner than we change the tooltip - // So that we can use CSS to hide the copybutton first - setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) - setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) -} - -// Changes the copy button icon for two seconds, then changes it back -const temporarilyChangeIcon = (el) => { - el.innerHTML = iconCheck; - setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) -} - -const addCopyButtonToCodeCells = () => { - // If ClipboardJS hasn't loaded, wait a bit and try again. This - // happens because we load ClipboardJS asynchronously. - if (window.ClipboardJS === undefined) { - setTimeout(addCopyButtonToCodeCells, 250) - return - } - - // Add copybuttons to all of our code cells - const COPYBUTTON_SELECTOR = 'div.highlight pre'; - const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) - codeCells.forEach((codeCell, index) => { - const id = codeCellId(index) - codeCell.setAttribute('id', id) - - const clipboardButton = id => - `` - codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) - }) - -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} - -/** - * Removes excluded text from a Node. - * - * @param {Node} target Node to filter. - * @param {string} exclude CSS selector of nodes to exclude. - * @returns {DOMString} Text from `target` with text removed. - */ -function filterText(target, exclude) { - const clone = target.cloneNode(true); // clone as to not modify the live DOM - if (exclude) { - // remove excluded nodes - clone.querySelectorAll(exclude).forEach(node => node.remove()); - } - return clone.innerText; -} - -// Callback when a copy button is clicked. Will be passed the node that was clicked -// should then grab the text and replace pieces of text that shouldn't be used in output -function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { - var regexp; - var match; - - // Do we check for line continuation characters and "HERE-documents"? - var useLineCont = !!lineContinuationChar - var useHereDoc = !!hereDocDelim - - // create regexp to capture prompt and remaining line - if (isRegexp) { - regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') - } else { - regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') - } - - const outputLines = []; - var promptFound = false; - var gotLineCont = false; - var gotHereDoc = false; - const lineGotPrompt = []; - for (const line of textContent.split('\n')) { - match = line.match(regexp) - if (match || gotLineCont || gotHereDoc) { - promptFound = regexp.test(line) - lineGotPrompt.push(promptFound) - if (removePrompts && promptFound) { - outputLines.push(match[2]) - } else { - outputLines.push(line) - } - gotLineCont = line.endsWith(lineContinuationChar) & useLineCont - if (line.includes(hereDocDelim) & useHereDoc) - gotHereDoc = !gotHereDoc - } else if (!onlyCopyPromptLines) { - outputLines.push(line) - } else if (copyEmptyLines && line.trim() === '') { - outputLines.push(line) - } - } - - // If no lines with the prompt were found then just use original lines - if (lineGotPrompt.some(v => v === true)) { - textContent = outputLines.join('\n'); - } - - // Remove a trailing newline to avoid auto-running when pasting - if (textContent.endsWith("\n")) { - textContent = textContent.slice(0, -1) - } - return textContent -} - - -var copyTargetText = (trigger) => { - var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); - - // get filtered text - let exclude = '.linenos'; - - let text = filterText(target, exclude); - return formatCopyText(text, '', false, true, true, true, '', '') -} - - // Initialize with a callback so we can modify the text before copy - const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) - - // Update UI with error/success messages - clipboard.on('success', event => { - clearSelection() - temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) - temporarilyChangeIcon(event.trigger) - }) - - clipboard.on('error', event => { - temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) - }) -} - -runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/docs/build/_static/copybutton_funcs.js b/docs/build/_static/copybutton_funcs.js deleted file mode 100644 index dbe1aaa..0000000 --- a/docs/build/_static/copybutton_funcs.js +++ /dev/null @@ -1,73 +0,0 @@ -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} - -/** - * Removes excluded text from a Node. - * - * @param {Node} target Node to filter. - * @param {string} exclude CSS selector of nodes to exclude. - * @returns {DOMString} Text from `target` with text removed. - */ -export function filterText(target, exclude) { - const clone = target.cloneNode(true); // clone as to not modify the live DOM - if (exclude) { - // remove excluded nodes - clone.querySelectorAll(exclude).forEach(node => node.remove()); - } - return clone.innerText; -} - -// Callback when a copy button is clicked. Will be passed the node that was clicked -// should then grab the text and replace pieces of text that shouldn't be used in output -export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { - var regexp; - var match; - - // Do we check for line continuation characters and "HERE-documents"? - var useLineCont = !!lineContinuationChar - var useHereDoc = !!hereDocDelim - - // create regexp to capture prompt and remaining line - if (isRegexp) { - regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') - } else { - regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') - } - - const outputLines = []; - var promptFound = false; - var gotLineCont = false; - var gotHereDoc = false; - const lineGotPrompt = []; - for (const line of textContent.split('\n')) { - match = line.match(regexp) - if (match || gotLineCont || gotHereDoc) { - promptFound = regexp.test(line) - lineGotPrompt.push(promptFound) - if (removePrompts && promptFound) { - outputLines.push(match[2]) - } else { - outputLines.push(line) - } - gotLineCont = line.endsWith(lineContinuationChar) & useLineCont - if (line.includes(hereDocDelim) & useHereDoc) - gotHereDoc = !gotHereDoc - } else if (!onlyCopyPromptLines) { - outputLines.push(line) - } else if (copyEmptyLines && line.trim() === '') { - outputLines.push(line) - } - } - - // If no lines with the prompt were found then just use original lines - if (lineGotPrompt.some(v => v === true)) { - textContent = outputLines.join('\n'); - } - - // Remove a trailing newline to avoid auto-running when pasting - if (textContent.endsWith("\n")) { - textContent = textContent.slice(0, -1) - } - return textContent -} diff --git a/docs/build/_static/css/badge_only.css b/docs/build/_static/css/badge_only.css deleted file mode 100644 index 88ba55b..0000000 --- a/docs/build/_static/css/badge_only.css +++ /dev/null @@ -1 +0,0 @@ -.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px} \ No newline at end of file diff --git a/docs/build/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/build/_static/css/fonts/Roboto-Slab-Bold.woff deleted file mode 100644 index 6cb6000..0000000 Binary files a/docs/build/_static/css/fonts/Roboto-Slab-Bold.woff and /dev/null differ diff --git a/docs/build/_static/css/fonts/Roboto-Slab-Bold.woff2 b/docs/build/_static/css/fonts/Roboto-Slab-Bold.woff2 deleted file mode 100644 index 7059e23..0000000 Binary files a/docs/build/_static/css/fonts/Roboto-Slab-Bold.woff2 and /dev/null differ diff --git a/docs/build/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/build/_static/css/fonts/Roboto-Slab-Regular.woff deleted file mode 100644 index f815f63..0000000 Binary files a/docs/build/_static/css/fonts/Roboto-Slab-Regular.woff and /dev/null differ diff --git a/docs/build/_static/css/fonts/Roboto-Slab-Regular.woff2 b/docs/build/_static/css/fonts/Roboto-Slab-Regular.woff2 deleted file mode 100644 index f2c76e5..0000000 Binary files a/docs/build/_static/css/fonts/Roboto-Slab-Regular.woff2 and /dev/null differ diff --git a/docs/build/_static/css/fonts/fontawesome-webfont.eot b/docs/build/_static/css/fonts/fontawesome-webfont.eot deleted file mode 100644 index e9f60ca..0000000 Binary files a/docs/build/_static/css/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/docs/build/_static/css/fonts/fontawesome-webfont.svg b/docs/build/_static/css/fonts/fontawesome-webfont.svg deleted file mode 100644 index 855c845..0000000 --- a/docs/build/_static/css/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,2671 +0,0 @@ - - - - -Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 - By ,,, -Copyright Dave Gandy 2016. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/build/_static/css/fonts/fontawesome-webfont.ttf b/docs/build/_static/css/fonts/fontawesome-webfont.ttf deleted file mode 100644 index 35acda2..0000000 Binary files a/docs/build/_static/css/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/docs/build/_static/css/fonts/fontawesome-webfont.woff b/docs/build/_static/css/fonts/fontawesome-webfont.woff deleted file mode 100644 index 400014a..0000000 Binary files a/docs/build/_static/css/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/docs/build/_static/css/fonts/fontawesome-webfont.woff2 b/docs/build/_static/css/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 4d13fc6..0000000 Binary files a/docs/build/_static/css/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/docs/build/_static/css/fonts/lato-bold-italic.woff b/docs/build/_static/css/fonts/lato-bold-italic.woff deleted file mode 100644 index 88ad05b..0000000 Binary files a/docs/build/_static/css/fonts/lato-bold-italic.woff and /dev/null differ diff --git a/docs/build/_static/css/fonts/lato-bold-italic.woff2 b/docs/build/_static/css/fonts/lato-bold-italic.woff2 deleted file mode 100644 index c4e3d80..0000000 Binary files a/docs/build/_static/css/fonts/lato-bold-italic.woff2 and /dev/null differ diff --git a/docs/build/_static/css/fonts/lato-bold.woff b/docs/build/_static/css/fonts/lato-bold.woff deleted file mode 100644 index c6dff51..0000000 Binary files a/docs/build/_static/css/fonts/lato-bold.woff and /dev/null differ diff --git a/docs/build/_static/css/fonts/lato-bold.woff2 b/docs/build/_static/css/fonts/lato-bold.woff2 deleted file mode 100644 index bb19504..0000000 Binary files a/docs/build/_static/css/fonts/lato-bold.woff2 and /dev/null differ diff --git a/docs/build/_static/css/fonts/lato-normal-italic.woff b/docs/build/_static/css/fonts/lato-normal-italic.woff deleted file mode 100644 index 76114bc..0000000 Binary files a/docs/build/_static/css/fonts/lato-normal-italic.woff and /dev/null differ diff --git a/docs/build/_static/css/fonts/lato-normal-italic.woff2 b/docs/build/_static/css/fonts/lato-normal-italic.woff2 deleted file mode 100644 index 3404f37..0000000 Binary files a/docs/build/_static/css/fonts/lato-normal-italic.woff2 and /dev/null differ diff --git a/docs/build/_static/css/fonts/lato-normal.woff b/docs/build/_static/css/fonts/lato-normal.woff deleted file mode 100644 index ae1307f..0000000 Binary files a/docs/build/_static/css/fonts/lato-normal.woff and /dev/null differ diff --git a/docs/build/_static/css/fonts/lato-normal.woff2 b/docs/build/_static/css/fonts/lato-normal.woff2 deleted file mode 100644 index 3bf9843..0000000 Binary files a/docs/build/_static/css/fonts/lato-normal.woff2 and /dev/null differ diff --git a/docs/build/_static/css/theme.css b/docs/build/_static/css/theme.css deleted file mode 100644 index 0f14f10..0000000 --- a/docs/build/_static/css/theme.css +++ /dev/null @@ -1,4 +0,0 @@ -html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search .wy-dropdown>aactive,.wy-side-nav-search .wy-dropdown>afocus,.wy-side-nav-search>a:hover,.wy-side-nav-search>aactive,.wy-side-nav-search>afocus{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon,.wy-side-nav-search>a.icon{display:block}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.switch-menus{position:relative;display:block;margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-side-nav-search>div.switch-menus>div.language-switch,.wy-side-nav-search>div.switch-menus>div.version-switch{display:inline-block;padding:.2em}.wy-side-nav-search>div.switch-menus>div.language-switch select,.wy-side-nav-search>div.switch-menus>div.version-switch select{display:inline-block;margin-right:-2rem;padding-right:2rem;max-width:240px;text-align-last:center;background:none;border:none;border-radius:0;box-shadow:none;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-size:1em;font-weight:400;color:hsla(0,0%,100%,.3);cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none}.wy-side-nav-search>div.switch-menus>div.language-switch select:active,.wy-side-nav-search>div.switch-menus>div.language-switch select:focus,.wy-side-nav-search>div.switch-menus>div.language-switch select:hover,.wy-side-nav-search>div.switch-menus>div.version-switch select:active,.wy-side-nav-search>div.switch-menus>div.version-switch select:focus,.wy-side-nav-search>div.switch-menus>div.version-switch select:hover{background:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.wy-side-nav-search>div.switch-menus>div.language-switch select option,.wy-side-nav-search>div.switch-menus>div.version-switch select option{color:#000}.wy-side-nav-search>div.switch-menus>div.language-switch:has(>select):after,.wy-side-nav-search>div.switch-menus>div.version-switch:has(>select):after{display:inline-block;width:1.5em;height:100%;padding:.1em;content:"\f0d7";font-size:1em;line-height:1.2em;font-family:FontAwesome;text-align:center;pointer-events:none;box-sizing:border-box}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/build/_static/custom.css b/docs/build/_static/custom.css deleted file mode 100644 index f5e37af..0000000 --- a/docs/build/_static/custom.css +++ /dev/null @@ -1,8 +0,0 @@ -/* Hide the breadcrumb navigation at the top of each page */ -.wy-nav-content .wy-breadcrumbs { - display: none !important; -} -.wy-nav-content hr { - display: none; -} - diff --git a/docs/build/_static/doctools.js b/docs/build/_static/doctools.js deleted file mode 100644 index 0398ebb..0000000 --- a/docs/build/_static/doctools.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Base JavaScript utilities for all Sphinx HTML documentation. - */ -"use strict"; - -const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ - "TEXTAREA", - "INPUT", - "SELECT", - "BUTTON", -]); - -const _ready = (callback) => { - if (document.readyState !== "loading") { - callback(); - } else { - document.addEventListener("DOMContentLoaded", callback); - } -}; - -/** - * Small JavaScript module for the documentation. - */ -const Documentation = { - init: () => { - Documentation.initDomainIndexTable(); - Documentation.initOnKeyListeners(); - }, - - /** - * i18n support - */ - TRANSLATIONS: {}, - PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), - LOCALE: "unknown", - - // gettext and ngettext don't access this so that the functions - // can safely bound to a different name (_ = Documentation.gettext) - gettext: (string) => { - const translated = Documentation.TRANSLATIONS[string]; - switch (typeof translated) { - case "undefined": - return string; // no translation - case "string": - return translated; // translation exists - default: - return translated[0]; // (singular, plural) translation tuple exists - } - }, - - ngettext: (singular, plural, n) => { - const translated = Documentation.TRANSLATIONS[singular]; - if (typeof translated !== "undefined") - return translated[Documentation.PLURAL_EXPR(n)]; - return n === 1 ? singular : plural; - }, - - addTranslations: (catalog) => { - Object.assign(Documentation.TRANSLATIONS, catalog.messages); - Documentation.PLURAL_EXPR = new Function( - "n", - `return (${catalog.plural_expr})` - ); - Documentation.LOCALE = catalog.locale; - }, - - /** - * helper function to focus on search bar - */ - focusSearchBar: () => { - document.querySelectorAll("input[name=q]")[0]?.focus(); - }, - - /** - * Initialise the domain index toggle buttons - */ - initDomainIndexTable: () => { - const toggler = (el) => { - const idNumber = el.id.substr(7); - const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); - if (el.src.substr(-9) === "minus.png") { - el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; - toggledRows.forEach((el) => (el.style.display = "none")); - } else { - el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; - toggledRows.forEach((el) => (el.style.display = "")); - } - }; - - const togglerElements = document.querySelectorAll("img.toggler"); - togglerElements.forEach((el) => - el.addEventListener("click", (event) => toggler(event.currentTarget)) - ); - togglerElements.forEach((el) => (el.style.display = "")); - if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); - }, - - initOnKeyListeners: () => { - // only install a listener if it is really needed - if ( - !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && - !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS - ) - return; - - document.addEventListener("keydown", (event) => { - // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; - // bail with special keys - if (event.altKey || event.ctrlKey || event.metaKey) return; - - if (!event.shiftKey) { - switch (event.key) { - case "ArrowLeft": - if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; - - const prevLink = document.querySelector('link[rel="prev"]'); - if (prevLink && prevLink.href) { - window.location.href = prevLink.href; - event.preventDefault(); - } - break; - case "ArrowRight": - if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; - - const nextLink = document.querySelector('link[rel="next"]'); - if (nextLink && nextLink.href) { - window.location.href = nextLink.href; - event.preventDefault(); - } - break; - } - } - - // some keyboard layouts may need Shift to get / - switch (event.key) { - case "/": - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; - Documentation.focusSearchBar(); - event.preventDefault(); - } - }); - }, -}; - -// quick alias for translations -const _ = Documentation.gettext; - -_ready(Documentation.init); diff --git a/docs/build/_static/documentation_options.js b/docs/build/_static/documentation_options.js deleted file mode 100644 index d1f2291..0000000 --- a/docs/build/_static/documentation_options.js +++ /dev/null @@ -1,13 +0,0 @@ -const DOCUMENTATION_OPTIONS = { - VERSION: '0.0.1', - LANGUAGE: 'en', - COLLAPSE_INDEX: false, - BUILDER: 'html', - FILE_SUFFIX: '.html', - LINK_SUFFIX: '.html', - HAS_SOURCE: true, - SOURCELINK_SUFFIX: '.txt', - NAVIGATION_WITH_KEYS: false, - SHOW_SEARCH_SUMMARY: true, - ENABLE_SEARCH_SHORTCUTS: true, -}; \ No newline at end of file diff --git a/docs/build/_static/examples.rst b/docs/build/_static/examples.rst deleted file mode 100644 index 5957b65..0000000 --- a/docs/build/_static/examples.rst +++ /dev/null @@ -1,12 +0,0 @@ -Examples -======== - -.. toctree:: - :numbered: - :maxdepth: 2 - :caption: Contents: - - examples/d - examples/a - examples/b - examples/c diff --git a/docs/build/_static/examples/a.rst b/docs/build/_static/examples/a.rst deleted file mode 100644 index e80a824..0000000 --- a/docs/build/_static/examples/a.rst +++ /dev/null @@ -1,12 +0,0 @@ -Toy Examples -============ - -.. contents:: - :depth: 1 - :local: - -Simple Polynomials ------------------- -.. autoclass:: popr.examples.Poly4Lifter - -.. autoclass:: popr.examples.Poly6Lifter diff --git a/docs/build/_static/examples/b.rst b/docs/build/_static/examples/b.rst deleted file mode 100644 index 2ae33a0..0000000 --- a/docs/build/_static/examples/b.rst +++ /dev/null @@ -1,21 +0,0 @@ -Standard Estimation Problems -============================ - -.. contents:: - :depth: 1 - :local: - -Range-Only Localization ------------------------ - -.. autoclass:: popr.examples.RangeOnlyLocLifter - - -Stereo-Camera Localization --------------------------- - -.. autoclass:: popr.examples.Stereo1DLifter - -.. autoclass:: popr.examples.Stereo2DLifter - -.. autoclass:: popr.examples.Stereo3DLifter \ No newline at end of file diff --git a/docs/build/_static/examples/c.rst b/docs/build/_static/examples/c.rst deleted file mode 100644 index e1ea21c..0000000 --- a/docs/build/_static/examples/c.rst +++ /dev/null @@ -1,13 +0,0 @@ -Robust Estimation Problems -============================ - -.. contents:: - :depth: 1 - :local: - -Robust Registration Problems ----------------------------- - -.. autoclass:: popr.examples.MonoLifter - -.. autoclass:: popr.examples.WahbaLifter diff --git a/docs/build/_static/examples/d.rst b/docs/build/_static/examples/d.rst deleted file mode 100644 index a6dfeac..0000000 --- a/docs/build/_static/examples/d.rst +++ /dev/null @@ -1,4 +0,0 @@ -ExampleLifter -============= - -.. autoclass:: popr.examples.ExampleLifter diff --git a/docs/build/_static/file.png b/docs/build/_static/file.png deleted file mode 100644 index a858a41..0000000 Binary files a/docs/build/_static/file.png and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-bold.eot b/docs/build/_static/fonts/Lato/lato-bold.eot deleted file mode 100644 index 3361183..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-bold.eot and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-bold.ttf b/docs/build/_static/fonts/Lato/lato-bold.ttf deleted file mode 100644 index 29f691d..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-bold.ttf and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-bold.woff b/docs/build/_static/fonts/Lato/lato-bold.woff deleted file mode 100644 index c6dff51..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-bold.woff and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-bold.woff2 b/docs/build/_static/fonts/Lato/lato-bold.woff2 deleted file mode 100644 index bb19504..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-bold.woff2 and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-bolditalic.eot b/docs/build/_static/fonts/Lato/lato-bolditalic.eot deleted file mode 100644 index 3d41549..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-bolditalic.eot and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-bolditalic.ttf b/docs/build/_static/fonts/Lato/lato-bolditalic.ttf deleted file mode 100644 index f402040..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-bolditalic.ttf and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-bolditalic.woff b/docs/build/_static/fonts/Lato/lato-bolditalic.woff deleted file mode 100644 index 88ad05b..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-bolditalic.woff and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-bolditalic.woff2 b/docs/build/_static/fonts/Lato/lato-bolditalic.woff2 deleted file mode 100644 index c4e3d80..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-bolditalic.woff2 and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-italic.eot b/docs/build/_static/fonts/Lato/lato-italic.eot deleted file mode 100644 index 3f82642..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-italic.eot and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-italic.ttf b/docs/build/_static/fonts/Lato/lato-italic.ttf deleted file mode 100644 index b4bfc9b..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-italic.ttf and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-italic.woff b/docs/build/_static/fonts/Lato/lato-italic.woff deleted file mode 100644 index 76114bc..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-italic.woff and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-italic.woff2 b/docs/build/_static/fonts/Lato/lato-italic.woff2 deleted file mode 100644 index 3404f37..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-italic.woff2 and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-regular.eot b/docs/build/_static/fonts/Lato/lato-regular.eot deleted file mode 100644 index 11e3f2a..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-regular.eot and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-regular.ttf b/docs/build/_static/fonts/Lato/lato-regular.ttf deleted file mode 100644 index 74decd9..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-regular.ttf and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-regular.woff b/docs/build/_static/fonts/Lato/lato-regular.woff deleted file mode 100644 index ae1307f..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-regular.woff and /dev/null differ diff --git a/docs/build/_static/fonts/Lato/lato-regular.woff2 b/docs/build/_static/fonts/Lato/lato-regular.woff2 deleted file mode 100644 index 3bf9843..0000000 Binary files a/docs/build/_static/fonts/Lato/lato-regular.woff2 and /dev/null differ diff --git a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot deleted file mode 100644 index 79dc8ef..0000000 Binary files a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot and /dev/null differ diff --git a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf deleted file mode 100644 index df5d1df..0000000 Binary files a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf and /dev/null differ diff --git a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff deleted file mode 100644 index 6cb6000..0000000 Binary files a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff and /dev/null differ diff --git a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 deleted file mode 100644 index 7059e23..0000000 Binary files a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 and /dev/null differ diff --git a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot deleted file mode 100644 index 2f7ca78..0000000 Binary files a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot and /dev/null differ diff --git a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf deleted file mode 100644 index eb52a79..0000000 Binary files a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf and /dev/null differ diff --git a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff deleted file mode 100644 index f815f63..0000000 Binary files a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff and /dev/null differ diff --git a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 deleted file mode 100644 index f2c76e5..0000000 Binary files a/docs/build/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 and /dev/null differ diff --git a/docs/build/_static/index.rst b/docs/build/_static/index.rst deleted file mode 100644 index f1dc4a6..0000000 --- a/docs/build/_static/index.rst +++ /dev/null @@ -1,24 +0,0 @@ -Welcome to POPR's documentation! -================================= - -.. toctree:: - :maxdepth: 2 - - self - quickstart - api - examples - whatsnew - contributing - -.. include:: ../../README.md - :parser: myst_parser.sphinx_ - - -.. - currently commented out cause it does not seem useful: - Indices - ------- - * :ref:`genindex` - - diff --git a/docs/build/_static/jquery.js b/docs/build/_static/jquery.js deleted file mode 100644 index c4c6022..0000000 --- a/docs/build/_static/jquery.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t a.language.name.localeCompare(b.language.name)); - - const languagesHTML = ` -
-
Languages
- ${languages - .map( - (translation) => ` -
- ${translation.language.code} -
- `, - ) - .join("\n")} -
- `; - return languagesHTML; - } - - function renderVersions(config) { - if (!config.versions.active.length) { - return ""; - } - const versionsHTML = ` -
-
Versions
- ${config.versions.active - .map( - (version) => ` -
- ${version.slug} -
- `, - ) - .join("\n")} -
- `; - return versionsHTML; - } - - function renderDownloads(config) { - if (!Object.keys(config.versions.current.downloads).length) { - return ""; - } - const downloadsNameDisplay = { - pdf: "PDF", - epub: "Epub", - htmlzip: "HTML", - }; - - const downloadsHTML = ` -
-
Downloads
- ${Object.entries(config.versions.current.downloads) - .map( - ([name, url]) => ` -
- ${downloadsNameDisplay[name]} -
- `, - ) - .join("\n")} -
- `; - return downloadsHTML; - } - - document.addEventListener("readthedocs-addons-data-ready", function (event) { - const config = event.detail.data(); - - const flyout = ` -
- - Read the Docs - v: ${config.versions.current.slug} - - -
-
- ${renderLanguages(config)} - ${renderVersions(config)} - ${renderDownloads(config)} -
-
On Read the Docs
-
- Project Home -
-
- Builds -
-
- Downloads -
-
-
-
Search
-
-
- -
-
-
-
- - Hosted by Read the Docs - -
-
- `; - - // Inject the generated flyout into the body HTML element. - document.body.insertAdjacentHTML("beforeend", flyout); - - // Trigger the Read the Docs Addons Search modal when clicking on the "Search docs" input from inside the flyout. - document - .querySelector("#flyout-search-form") - .addEventListener("focusin", () => { - const event = new CustomEvent("readthedocs-search-show"); - document.dispatchEvent(event); - }); - }) -} - -if (themeLanguageSelector || themeVersionSelector) { - function onSelectorSwitch(event) { - const option = event.target.selectedIndex; - const item = event.target.options[option]; - window.location.href = item.dataset.url; - } - - document.addEventListener("readthedocs-addons-data-ready", function (event) { - const config = event.detail.data(); - - const versionSwitch = document.querySelector( - "div.switch-menus > div.version-switch", - ); - if (themeVersionSelector) { - let versions = config.versions.active; - if (config.versions.current.hidden || config.versions.current.type === "external") { - versions.unshift(config.versions.current); - } - const versionSelect = ` - - `; - - versionSwitch.innerHTML = versionSelect; - versionSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); - } - - const languageSwitch = document.querySelector( - "div.switch-menus > div.language-switch", - ); - - if (themeLanguageSelector) { - if (config.projects.translations.length) { - // Add the current language to the options on the selector - let languages = config.projects.translations.concat( - config.projects.current, - ); - languages = languages.sort((a, b) => - a.language.name.localeCompare(b.language.name), - ); - - const languageSelect = ` - - `; - - languageSwitch.innerHTML = languageSelect; - languageSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); - } - else { - languageSwitch.remove(); - } - } - }); -} - -document.addEventListener("readthedocs-addons-data-ready", function (event) { - // Trigger the Read the Docs Addons Search modal when clicking on "Search docs" input from the topnav. - document - .querySelector("[role='search'] input") - .addEventListener("focusin", () => { - const event = new CustomEvent("readthedocs-search-show"); - document.dispatchEvent(event); - }); -}); \ No newline at end of file diff --git a/docs/build/_static/language_data.js b/docs/build/_static/language_data.js deleted file mode 100644 index c7fe6c6..0000000 --- a/docs/build/_static/language_data.js +++ /dev/null @@ -1,192 +0,0 @@ -/* - * This script contains the language-specific data used by searchtools.js, - * namely the list of stopwords, stemmer, scorer and splitter. - */ - -var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; - - -/* Non-minified version is copied as a separate JS file, if available */ - -/** - * Porter Stemmer - */ -var Stemmer = function() { - - var step2list = { - ational: 'ate', - tional: 'tion', - enci: 'ence', - anci: 'ance', - izer: 'ize', - bli: 'ble', - alli: 'al', - entli: 'ent', - eli: 'e', - ousli: 'ous', - ization: 'ize', - ation: 'ate', - ator: 'ate', - alism: 'al', - iveness: 'ive', - fulness: 'ful', - ousness: 'ous', - aliti: 'al', - iviti: 'ive', - biliti: 'ble', - logi: 'log' - }; - - var step3list = { - icate: 'ic', - ative: '', - alize: 'al', - iciti: 'ic', - ical: 'ic', - ful: '', - ness: '' - }; - - var c = "[^aeiou]"; // consonant - var v = "[aeiouy]"; // vowel - var C = c + "[^aeiouy]*"; // consonant sequence - var V = v + "[aeiou]*"; // vowel sequence - - var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 - var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 - var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 - var s_v = "^(" + C + ")?" + v; // vowel in stem - - this.stemWord = function (w) { - var stem; - var suffix; - var firstch; - var origword = w; - - if (w.length < 3) - return w; - - var re; - var re2; - var re3; - var re4; - - firstch = w.substr(0,1); - if (firstch == "y") - w = firstch.toUpperCase() + w.substr(1); - - // Step 1a - re = /^(.+?)(ss|i)es$/; - re2 = /^(.+?)([^s])s$/; - - if (re.test(w)) - w = w.replace(re,"$1$2"); - else if (re2.test(w)) - w = w.replace(re2,"$1$2"); - - // Step 1b - re = /^(.+?)eed$/; - re2 = /^(.+?)(ed|ing)$/; - if (re.test(w)) { - var fp = re.exec(w); - re = new RegExp(mgr0); - if (re.test(fp[1])) { - re = /.$/; - w = w.replace(re,""); - } - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1]; - re2 = new RegExp(s_v); - if (re2.test(stem)) { - w = stem; - re2 = /(at|bl|iz)$/; - re3 = new RegExp("([^aeiouylsz])\\1$"); - re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re2.test(w)) - w = w + "e"; - else if (re3.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - else if (re4.test(w)) - w = w + "e"; - } - } - - // Step 1c - re = /^(.+?)y$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(s_v); - if (re.test(stem)) - w = stem + "i"; - } - - // Step 2 - re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step2list[suffix]; - } - - // Step 3 - re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step3list[suffix]; - } - - // Step 4 - re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; - re2 = /^(.+?)(s|t)(ion)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - if (re.test(stem)) - w = stem; - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1] + fp[2]; - re2 = new RegExp(mgr1); - if (re2.test(stem)) - w = stem; - } - - // Step 5 - re = /^(.+?)e$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - re2 = new RegExp(meq1); - re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) - w = stem; - } - re = /ll$/; - re2 = new RegExp(mgr1); - if (re.test(w) && re2.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - - // and turn initial Y back to y - if (firstch == "y") - w = firstch.toLowerCase() + w.substr(1); - return w; - } -} - diff --git a/docs/build/_static/logo.png b/docs/build/_static/logo.png deleted file mode 100644 index d72353f..0000000 Binary files a/docs/build/_static/logo.png and /dev/null differ diff --git a/docs/build/_static/logo.svg b/docs/build/_static/logo.svg deleted file mode 100644 index c6484f0..0000000 --- a/docs/build/_static/logo.svg +++ /dev/null @@ -1,468 +0,0 @@ - - - -Polynomial Optimization for Certifiably Optimal Robotics diff --git a/docs/build/_static/logo1.png b/docs/build/_static/logo1.png deleted file mode 100644 index a2d6ef3..0000000 Binary files a/docs/build/_static/logo1.png and /dev/null differ diff --git a/docs/build/_static/logo2.png b/docs/build/_static/logo2.png deleted file mode 100644 index 98b2e57..0000000 Binary files a/docs/build/_static/logo2.png and /dev/null differ diff --git a/docs/build/_static/minus.png b/docs/build/_static/minus.png deleted file mode 100644 index d96755f..0000000 Binary files a/docs/build/_static/minus.png and /dev/null differ diff --git a/docs/build/_static/overview.png b/docs/build/_static/overview.png deleted file mode 100644 index 8bf338e..0000000 Binary files a/docs/build/_static/overview.png and /dev/null differ diff --git a/docs/build/_static/overview.svg b/docs/build/_static/overview.svg deleted file mode 100644 index 453213c..0000000 --- a/docs/build/_static/overview.svg +++ /dev/null @@ -1,16503 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - non-tight convex relaxationtight convex relaxationnon-convex problem diff --git a/docs/build/_static/overview.webp b/docs/build/_static/overview.webp deleted file mode 100644 index d4cc81b..0000000 Binary files a/docs/build/_static/overview.webp and /dev/null differ diff --git a/docs/build/_static/plus.png b/docs/build/_static/plus.png deleted file mode 100644 index 7107cec..0000000 Binary files a/docs/build/_static/plus.png and /dev/null differ diff --git a/docs/build/_static/poly4_lifter_A.png b/docs/build/_static/poly4_lifter_A.png deleted file mode 100644 index 35ce631..0000000 Binary files a/docs/build/_static/poly4_lifter_A.png and /dev/null differ diff --git a/docs/build/_static/poly4_lifter_B.png b/docs/build/_static/poly4_lifter_B.png deleted file mode 100644 index 4c8ac96..0000000 Binary files a/docs/build/_static/poly4_lifter_B.png and /dev/null differ diff --git a/docs/build/_static/poly6_lifter_A.png b/docs/build/_static/poly6_lifter_A.png deleted file mode 100644 index b6f900c..0000000 Binary files a/docs/build/_static/poly6_lifter_A.png and /dev/null differ diff --git a/docs/build/_static/poly6_lifter_B.png b/docs/build/_static/poly6_lifter_B.png deleted file mode 100644 index 3fb47cc..0000000 Binary files a/docs/build/_static/poly6_lifter_B.png and /dev/null differ diff --git a/docs/build/_static/pygments.css b/docs/build/_static/pygments.css deleted file mode 100644 index 6f8b210..0000000 --- a/docs/build/_static/pygments.css +++ /dev/null @@ -1,75 +0,0 @@ -pre { line-height: 125%; } -td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -.highlight .hll { background-color: #ffffcc } -.highlight { background: #f8f8f8; } -.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #F00 } /* Error */ -.highlight .k { color: #008000; font-weight: bold } /* Keyword */ -.highlight .o { color: #666 } /* Operator */ -.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #9C6500 } /* Comment.Preproc */ -.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ -.highlight .gr { color: #E40000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #008400 } /* Generic.Inserted */ -.highlight .go { color: #717171 } /* Generic.Output */ -.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #04D } /* Generic.Traceback */ -.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #008000 } /* Keyword.Pseudo */ -.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #B00040 } /* Keyword.Type */ -.highlight .m { color: #666 } /* Literal.Number */ -.highlight .s { color: #BA2121 } /* Literal.String */ -.highlight .na { color: #687822 } /* Name.Attribute */ -.highlight .nb { color: #008000 } /* Name.Builtin */ -.highlight .nc { color: #00F; font-weight: bold } /* Name.Class */ -.highlight .no { color: #800 } /* Name.Constant */ -.highlight .nd { color: #A2F } /* Name.Decorator */ -.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #00F } /* Name.Function */ -.highlight .nl { color: #767600 } /* Name.Label */ -.highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #19177C } /* Name.Variable */ -.highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #BBB } /* Text.Whitespace */ -.highlight .mb { color: #666 } /* Literal.Number.Bin */ -.highlight .mf { color: #666 } /* Literal.Number.Float */ -.highlight .mh { color: #666 } /* Literal.Number.Hex */ -.highlight .mi { color: #666 } /* Literal.Number.Integer */ -.highlight .mo { color: #666 } /* Literal.Number.Oct */ -.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ -.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ -.highlight .sc { color: #BA2121 } /* Literal.String.Char */ -.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ -.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ -.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ -.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ -.highlight .sx { color: #008000 } /* Literal.String.Other */ -.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ -.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ -.highlight .ss { color: #19177C } /* Literal.String.Symbol */ -.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #00F } /* Name.Function.Magic */ -.highlight .vc { color: #19177C } /* Name.Variable.Class */ -.highlight .vg { color: #19177C } /* Name.Variable.Global */ -.highlight .vi { color: #19177C } /* Name.Variable.Instance */ -.highlight .vm { color: #19177C } /* Name.Variable.Magic */ -.highlight .il { color: #666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/build/_static/quickstart.rst b/docs/build/_static/quickstart.rst deleted file mode 100644 index b9002b6..0000000 --- a/docs/build/_static/quickstart.rst +++ /dev/null @@ -1,145 +0,0 @@ -Quick Start Guide -================= - -Installation ------------- - -The tool can be installed by running from a terminal: - -.. code-block:: bash - - git clone --recurse-submodules git@github.com:duembgen/popr - cd popr - conda env create -f environment.yml - - - -Problem Formulation -------------------- - -We start with polynomial optimization problems (POPs) of the form: - -.. math:: - - \begin{align} q^\star =&\min_{\theta} f(\theta) \\ - \text{s.t. } &g(\theta) = 0 \\ - &h(\theta) \geq 0 - \end{align} - -where :math:`f,g,\text{ and } h` are polynomial functions, and both :math:`g` and :math:`h` can be vector-valued. Many maximum-a-posteriori or maximum-likelihood estimation problems can be formulated as such, for example `range-only localization `_ and `range-aided SLAM `_, (`matrix-weighted `_) `SLAM `_, and `outlier-robust estimation `_. The same is true for many control and planning problems, for example the `inverted pendulum `_ and other classical dynamical systems, and even contact-rich problems such as `slider-pusher planning problems `_. - -Any POP can be equivalently written in the following QCQP form: - -.. math:: - - \begin{align} q^\star =&\min_{x} x^\top Q x \\ - \text{s.t. } &(\forall i): x^\top A_i x = b_i \\ - &(\forall j): x^\top B_j x \geq 0 - \end{align} - -with cost matrix :math:`Q`, known constraint matrices :math:`A_i,B_j`. -Note that - -- We always include the so-called homogenization variable, which enables to write linear and constant terms as quadratics. By convention, we set the first element of :math:`x` to one, and we use :math:`b_0=1, A_0` to encorce this constraint. -- All inequality and some equality constraints correspond to the constraints from the original POP. -- Some additional equality constraints correspond to new substitution variables that need to be added to formulate the problem as a quadratic. - -.. warning:: - Note that while inequality constraints can be added to the problem formulation, there is no implementation yet to add find and add redundant inequality constraints to the relaxation. - -For the standard usage, the user first needs to define a custom **Lifter** class which essentially contains all elements related to the QCQP problem formulation. -This class should inherit from :ref:`StateLifter`. A basic skeleton of such a -Lifter class is provided in :ref:`ExampleLifter`. The main purpose of this class is -that it provides all basic operations related to the problem formulation, such as: - -- to sample feasible states (:meth:`popr.lifters.StateLifter.sample_theta`), -- to get the lifted vector (:meth:`popr.lifters.StateLifter.get_x`), - -For a bit more advanced functionality (for example for the :ref:`SDP Relaxation` in the next section), you also need to define functions such as - -- get the cost matrix (:py:meth:`popr.lifters.StateLifter.get_Q`), -- get known constraint matrices (:meth:`popr.lifters.StateLifter.get_A_known`, :meth:`popr.lifters.StateLifter.get_B_known`). - -Many example lifters are provided, you can find them under :ref:`Examples`. - -**Example: instantiating and using lifter** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following code snippet shows some basic operations (and useful sanity checks) for the example -lifter class :class:`popr.examples.Poly4Lifter`. Note that this and all following examples can be found -in the file :file:`../../tests/test_quickstart.py`. - -.. literalinclude:: ../../tests/test_quickstart.py - :language: python - :lines: 9-23 - :dedent: 4 - - -SDP Relaxation --------------- - -It is straightforward to derive a convex relaxation of the original QCQP, using the reformulation :math:`x^\top Qx=\langle x, Qx\rangle = \langle Q, xx^\top \rangle`, where :math:`\langle \cdot, \cdot \rangle` denotes the trace inner product. Then introducing :math:`X:=xx^\top` and relaxing its rank, we obtain the following convex relaxation, in the form of an SDP: - -.. math:: - \begin{align} p^\star = &\min_{X \succeq 0} \langle Q, X \rangle \\ - \text{s.t. } &(\forall i): \langle A_i, X \rangle = b_i \\ - &(\forall j): \langle B_j, X \rangle \geq 0 - \end{align} - - -**Example: solving the QCQP using rank relaxation** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following code snippet shows how you can use the simple lifter from earlier to find the global -optimum of the nonconvex polynomial problem, by solving an SDP. - -.. literalinclude:: ../../tests/test_quickstart.py - :language: python - :lines: 28-55 - :dedent: 4 - - - -AutoTight Method ----------------- - -**AutoTight** is used to find all possible constraints matrices :math:`A_r`, which are also automatically satisfied by solutions of the QCQP. They are also called **redundant constraints** because they do not change the feasible set of the original problem, but when adding those constraints to the SDP (rank-)relaxation, they often improve tightness. Denoting by :math:`A_r` the redundant constraints, we can solve the following SDP: - -.. math:: - \begin{align} p_r^\star = &\min_{X \succeq 0} \langle Q, X \rangle \\ - \text{s.t. } &(\forall i): \langle A_i, X \rangle = b_i \\ - &(\forall r): \langle A_r, X \rangle = 0 \\ - &(\forall j): \langle B_j, X \rangle \geq 0 - \end{align} - -We use the term **cost-tight** to say that strong duality holds (:math:`p_r^\star = q^\star`) while by rank-tight we denote the fact that the SDP solver returns a rank-one solution. -If successful, the output is a set of constraints that leads to a tight SDP relaxation of the original problem, which can be used to solve the problem to global optimality (if we have rank tightness) or certify given solutions (if we have cost tightness). - -More information on how to use AutoTight can be found :ref:`here ` and a simple example is given next. - -**Example: tightening the SDP relaxation using AutoTight.** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. literalinclude:: ../../tests/test_quickstart.py - :language: python - :lines: 60-100 - :dedent: 4 - - -AutoTemplate Method -------------------- - -*AutoTemplate* follows the same principle as *AutoTight*, but its output are templates rather than constraint matrices. These templates can be seen as "parametrized" versions of the constraint matrices, and can be applied to new problem instances of any size without having to learn the constraints again from scratch. - -More information on how to use AutoTemplate can be found :ref:`here ` and a simple example is given next. - -.. literalinclude:: ../../tests/test_quickstart.py - :language: python - :lines: 105-135 - :dedent: 4 - - -References ----------- - -`[1] F. Dümbgen, C. Holmes, B. Agro and T. Barfoot, "Toward Globally Optimal State Estimation Using Automatically Tightened Semidefinite Relaxations," in IEEE Transactions on Robotics, vol. 40, pp. 4338-4358, 2024, doi: 10.1109/TRO.2024.3454570. `_ diff --git a/docs/build/_static/searchtools.js b/docs/build/_static/searchtools.js deleted file mode 100644 index 2c774d1..0000000 --- a/docs/build/_static/searchtools.js +++ /dev/null @@ -1,632 +0,0 @@ -/* - * Sphinx JavaScript utilities for the full-text search. - */ -"use strict"; - -/** - * Simple result scoring code. - */ -if (typeof Scorer === "undefined") { - var Scorer = { - // Implement the following function to further tweak the score for each result - // The function takes a result array [docname, title, anchor, descr, score, filename] - // and returns the new score. - /* - score: result => { - const [docname, title, anchor, descr, score, filename, kind] = result - return score - }, - */ - - // query matches the full name of an object - objNameMatch: 11, - // or matches in the last dotted part of the object name - objPartialMatch: 6, - // Additive scores depending on the priority of the object - objPrio: { - 0: 15, // used to be importantResults - 1: 5, // used to be objectResults - 2: -5, // used to be unimportantResults - }, - // Used when the priority is not in the mapping. - objPrioDefault: 0, - - // query found in title - title: 15, - partialTitle: 7, - // query found in terms - term: 5, - partialTerm: 2, - }; -} - -// Global search result kind enum, used by themes to style search results. -class SearchResultKind { - static get index() { return "index"; } - static get object() { return "object"; } - static get text() { return "text"; } - static get title() { return "title"; } -} - -const _removeChildren = (element) => { - while (element && element.lastChild) element.removeChild(element.lastChild); -}; - -/** - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping - */ -const _escapeRegExp = (string) => - string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string - -const _displayItem = (item, searchTerms, highlightTerms) => { - const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; - const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; - const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; - const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; - const contentRoot = document.documentElement.dataset.content_root; - - const [docName, title, anchor, descr, score, _filename, kind] = item; - - let listItem = document.createElement("li"); - // Add a class representing the item's type: - // can be used by a theme's CSS selector for styling - // See SearchResultKind for the class names. - listItem.classList.add(`kind-${kind}`); - let requestUrl; - let linkUrl; - if (docBuilder === "dirhtml") { - // dirhtml builder - let dirname = docName + "/"; - if (dirname.match(/\/index\/$/)) - dirname = dirname.substring(0, dirname.length - 6); - else if (dirname === "index/") dirname = ""; - requestUrl = contentRoot + dirname; - linkUrl = requestUrl; - } else { - // normal html builders - requestUrl = contentRoot + docName + docFileSuffix; - linkUrl = docName + docLinkSuffix; - } - let linkEl = listItem.appendChild(document.createElement("a")); - linkEl.href = linkUrl + anchor; - linkEl.dataset.score = score; - linkEl.innerHTML = title; - if (descr) { - listItem.appendChild(document.createElement("span")).innerHTML = - " (" + descr + ")"; - // highlight search terms in the description - if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js - highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); - } - else if (showSearchSummary) - fetch(requestUrl) - .then((responseData) => responseData.text()) - .then((data) => { - if (data) - listItem.appendChild( - Search.makeSearchSummary(data, searchTerms, anchor) - ); - // highlight search terms in the summary - if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js - highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); - }); - Search.output.appendChild(listItem); -}; -const _finishSearch = (resultCount) => { - Search.stopPulse(); - Search.title.innerText = _("Search Results"); - if (!resultCount) - Search.status.innerText = Documentation.gettext( - "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." - ); - else - Search.status.innerText = Documentation.ngettext( - "Search finished, found one page matching the search query.", - "Search finished, found ${resultCount} pages matching the search query.", - resultCount, - ).replace('${resultCount}', resultCount); -}; -const _displayNextItem = ( - results, - resultCount, - searchTerms, - highlightTerms, -) => { - // results left, load the summary and display it - // this is intended to be dynamic (don't sub resultsCount) - if (results.length) { - _displayItem(results.pop(), searchTerms, highlightTerms); - setTimeout( - () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), - 5 - ); - } - // search finished, update title and status message - else _finishSearch(resultCount); -}; -// Helper function used by query() to order search results. -// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. -// Order the results by score (in opposite order of appearance, since the -// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. -const _orderResultsByScoreThenName = (a, b) => { - const leftScore = a[4]; - const rightScore = b[4]; - if (leftScore === rightScore) { - // same score: sort alphabetically - const leftTitle = a[1].toLowerCase(); - const rightTitle = b[1].toLowerCase(); - if (leftTitle === rightTitle) return 0; - return leftTitle > rightTitle ? -1 : 1; // inverted is intentional - } - return leftScore > rightScore ? 1 : -1; -}; - -/** - * Default splitQuery function. Can be overridden in ``sphinx.search`` with a - * custom function per language. - * - * The regular expression works by splitting the string on consecutive characters - * that are not Unicode letters, numbers, underscores, or emoji characters. - * This is the same as ``\W+`` in Python, preserving the surrogate pair area. - */ -if (typeof splitQuery === "undefined") { - var splitQuery = (query) => query - .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) - .filter(term => term) // remove remaining empty strings -} - -/** - * Search Module - */ -const Search = { - _index: null, - _queued_query: null, - _pulse_status: -1, - - htmlToText: (htmlString, anchor) => { - const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); - for (const removalQuery of [".headerlink", "script", "style"]) { - htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); - } - if (anchor) { - const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); - if (anchorContent) return anchorContent.textContent; - - console.warn( - `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` - ); - } - - // if anchor not specified or not found, fall back to main content - const docContent = htmlElement.querySelector('[role="main"]'); - if (docContent) return docContent.textContent; - - console.warn( - "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." - ); - return ""; - }, - - init: () => { - const query = new URLSearchParams(window.location.search).get("q"); - document - .querySelectorAll('input[name="q"]') - .forEach((el) => (el.value = query)); - if (query) Search.performSearch(query); - }, - - loadIndex: (url) => - (document.body.appendChild(document.createElement("script")).src = url), - - setIndex: (index) => { - Search._index = index; - if (Search._queued_query !== null) { - const query = Search._queued_query; - Search._queued_query = null; - Search.query(query); - } - }, - - hasIndex: () => Search._index !== null, - - deferQuery: (query) => (Search._queued_query = query), - - stopPulse: () => (Search._pulse_status = -1), - - startPulse: () => { - if (Search._pulse_status >= 0) return; - - const pulse = () => { - Search._pulse_status = (Search._pulse_status + 1) % 4; - Search.dots.innerText = ".".repeat(Search._pulse_status); - if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); - }; - pulse(); - }, - - /** - * perform a search for something (or wait until index is loaded) - */ - performSearch: (query) => { - // create the required interface elements - const searchText = document.createElement("h2"); - searchText.textContent = _("Searching"); - const searchSummary = document.createElement("p"); - searchSummary.classList.add("search-summary"); - searchSummary.innerText = ""; - const searchList = document.createElement("ul"); - searchList.setAttribute("role", "list"); - searchList.classList.add("search"); - - const out = document.getElementById("search-results"); - Search.title = out.appendChild(searchText); - Search.dots = Search.title.appendChild(document.createElement("span")); - Search.status = out.appendChild(searchSummary); - Search.output = out.appendChild(searchList); - - const searchProgress = document.getElementById("search-progress"); - // Some themes don't use the search progress node - if (searchProgress) { - searchProgress.innerText = _("Preparing search..."); - } - Search.startPulse(); - - // index already loaded, the browser was quick! - if (Search.hasIndex()) Search.query(query); - else Search.deferQuery(query); - }, - - _parseQuery: (query) => { - // stem the search terms and add them to the correct list - const stemmer = new Stemmer(); - const searchTerms = new Set(); - const excludedTerms = new Set(); - const highlightTerms = new Set(); - const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); - splitQuery(query.trim()).forEach((queryTerm) => { - const queryTermLower = queryTerm.toLowerCase(); - - // maybe skip this "word" - // stopwords array is from language_data.js - if ( - stopwords.indexOf(queryTermLower) !== -1 || - queryTerm.match(/^\d+$/) - ) - return; - - // stem the word - let word = stemmer.stemWord(queryTermLower); - // select the correct list - if (word[0] === "-") excludedTerms.add(word.substr(1)); - else { - searchTerms.add(word); - highlightTerms.add(queryTermLower); - } - }); - - if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js - localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) - } - - // console.debug("SEARCH: searching for:"); - // console.info("required: ", [...searchTerms]); - // console.info("excluded: ", [...excludedTerms]); - - return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; - }, - - /** - * execute search (requires search index to be loaded) - */ - _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const titles = Search._index.titles; - const allTitles = Search._index.alltitles; - const indexEntries = Search._index.indexentries; - - // Collect multiple result groups to be sorted separately and then ordered. - // Each is an array of [docname, title, anchor, descr, score, filename, kind]. - const normalResults = []; - const nonMainIndexResults = []; - - _removeChildren(document.getElementById("search-progress")); - - const queryLower = query.toLowerCase().trim(); - for (const [title, foundTitles] of Object.entries(allTitles)) { - if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { - for (const [file, id] of foundTitles) { - const score = Math.round(Scorer.title * queryLower.length / title.length); - const boost = titles[file] === title ? 1 : 0; // add a boost for document titles - normalResults.push([ - docNames[file], - titles[file] !== title ? `${titles[file]} > ${title}` : title, - id !== null ? "#" + id : "", - null, - score + boost, - filenames[file], - SearchResultKind.title, - ]); - } - } - } - - // search for explicit entries in index directives - for (const [entry, foundEntries] of Object.entries(indexEntries)) { - if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { - for (const [file, id, isMain] of foundEntries) { - const score = Math.round(100 * queryLower.length / entry.length); - const result = [ - docNames[file], - titles[file], - id ? "#" + id : "", - null, - score, - filenames[file], - SearchResultKind.index, - ]; - if (isMain) { - normalResults.push(result); - } else { - nonMainIndexResults.push(result); - } - } - } - } - - // lookup as object - objectTerms.forEach((term) => - normalResults.push(...Search.performObjectSearch(term, objectTerms)) - ); - - // lookup as search terms in fulltext - normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); - - // let the scorer override scores with a custom scoring function - if (Scorer.score) { - normalResults.forEach((item) => (item[4] = Scorer.score(item))); - nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); - } - - // Sort each group of results by score and then alphabetically by name. - normalResults.sort(_orderResultsByScoreThenName); - nonMainIndexResults.sort(_orderResultsByScoreThenName); - - // Combine the result groups in (reverse) order. - // Non-main index entries are typically arbitrary cross-references, - // so display them after other results. - let results = [...nonMainIndexResults, ...normalResults]; - - // remove duplicate search results - // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept - let seen = new Set(); - results = results.reverse().reduce((acc, result) => { - let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); - if (!seen.has(resultStr)) { - acc.push(result); - seen.add(resultStr); - } - return acc; - }, []); - - return results.reverse(); - }, - - query: (query) => { - const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); - const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); - - // for debugging - //Search.lastresults = results.slice(); // a copy - // console.info("search results:", Search.lastresults); - - // print the results - _displayNextItem(results, results.length, searchTerms, highlightTerms); - }, - - /** - * search for object names - */ - performObjectSearch: (object, objectTerms) => { - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const objects = Search._index.objects; - const objNames = Search._index.objnames; - const titles = Search._index.titles; - - const results = []; - - const objectSearchCallback = (prefix, match) => { - const name = match[4] - const fullname = (prefix ? prefix + "." : "") + name; - const fullnameLower = fullname.toLowerCase(); - if (fullnameLower.indexOf(object) < 0) return; - - let score = 0; - const parts = fullnameLower.split("."); - - // check for different match types: exact matches of full name or - // "last name" (i.e. last dotted part) - if (fullnameLower === object || parts.slice(-1)[0] === object) - score += Scorer.objNameMatch; - else if (parts.slice(-1)[0].indexOf(object) > -1) - score += Scorer.objPartialMatch; // matches in last name - - const objName = objNames[match[1]][2]; - const title = titles[match[0]]; - - // If more than one term searched for, we require other words to be - // found in the name/title/description - const otherTerms = new Set(objectTerms); - otherTerms.delete(object); - if (otherTerms.size > 0) { - const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); - if ( - [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) - ) - return; - } - - let anchor = match[3]; - if (anchor === "") anchor = fullname; - else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; - - const descr = objName + _(", in ") + title; - - // add custom score for some objects according to scorer - if (Scorer.objPrio.hasOwnProperty(match[2])) - score += Scorer.objPrio[match[2]]; - else score += Scorer.objPrioDefault; - - results.push([ - docNames[match[0]], - fullname, - "#" + anchor, - descr, - score, - filenames[match[0]], - SearchResultKind.object, - ]); - }; - Object.keys(objects).forEach((prefix) => - objects[prefix].forEach((array) => - objectSearchCallback(prefix, array) - ) - ); - return results; - }, - - /** - * search for full-text terms in the index - */ - performTermsSearch: (searchTerms, excludedTerms) => { - // prepare search - const terms = Search._index.terms; - const titleTerms = Search._index.titleterms; - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const titles = Search._index.titles; - - const scoreMap = new Map(); - const fileMap = new Map(); - - // perform the search on the required terms - searchTerms.forEach((word) => { - const files = []; - const arr = [ - { files: terms[word], score: Scorer.term }, - { files: titleTerms[word], score: Scorer.title }, - ]; - // add support for partial matches - if (word.length > 2) { - const escapedWord = _escapeRegExp(word); - if (!terms.hasOwnProperty(word)) { - Object.keys(terms).forEach((term) => { - if (term.match(escapedWord)) - arr.push({ files: terms[term], score: Scorer.partialTerm }); - }); - } - if (!titleTerms.hasOwnProperty(word)) { - Object.keys(titleTerms).forEach((term) => { - if (term.match(escapedWord)) - arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); - }); - } - } - - // no match but word was a required one - if (arr.every((record) => record.files === undefined)) return; - - // found search word in contents - arr.forEach((record) => { - if (record.files === undefined) return; - - let recordFiles = record.files; - if (recordFiles.length === undefined) recordFiles = [recordFiles]; - files.push(...recordFiles); - - // set score for the word in each file - recordFiles.forEach((file) => { - if (!scoreMap.has(file)) scoreMap.set(file, {}); - scoreMap.get(file)[word] = record.score; - }); - }); - - // create the mapping - files.forEach((file) => { - if (!fileMap.has(file)) fileMap.set(file, [word]); - else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); - }); - }); - - // now check if the files don't contain excluded terms - const results = []; - for (const [file, wordList] of fileMap) { - // check if all requirements are matched - - // as search terms with length < 3 are discarded - const filteredTermCount = [...searchTerms].filter( - (term) => term.length > 2 - ).length; - if ( - wordList.length !== searchTerms.size && - wordList.length !== filteredTermCount - ) - continue; - - // ensure that none of the excluded terms is in the search result - if ( - [...excludedTerms].some( - (term) => - terms[term] === file || - titleTerms[term] === file || - (terms[term] || []).includes(file) || - (titleTerms[term] || []).includes(file) - ) - ) - break; - - // select one (max) score for the file. - const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); - // add result to the result list - results.push([ - docNames[file], - titles[file], - "", - null, - score, - filenames[file], - SearchResultKind.text, - ]); - } - return results; - }, - - /** - * helper function to return a node containing the - * search summary for a given text. keywords is a list - * of stemmed words. - */ - makeSearchSummary: (htmlText, keywords, anchor) => { - const text = Search.htmlToText(htmlText, anchor); - if (text === "") return null; - - const textLower = text.toLowerCase(); - const actualStartPosition = [...keywords] - .map((k) => textLower.indexOf(k.toLowerCase())) - .filter((i) => i > -1) - .slice(-1)[0]; - const startWithContext = Math.max(actualStartPosition - 120, 0); - - const top = startWithContext === 0 ? "" : "..."; - const tail = startWithContext + 240 < text.length ? "..." : ""; - - let summary = document.createElement("p"); - summary.classList.add("context"); - summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; - - return summary; - }, -}; - -_ready(Search.init); diff --git a/docs/build/_static/sphinx_highlight.js b/docs/build/_static/sphinx_highlight.js deleted file mode 100644 index 8a96c69..0000000 --- a/docs/build/_static/sphinx_highlight.js +++ /dev/null @@ -1,154 +0,0 @@ -/* Highlighting utilities for Sphinx HTML documentation. */ -"use strict"; - -const SPHINX_HIGHLIGHT_ENABLED = true - -/** - * highlight a given string on a node by wrapping it in - * span elements with the given class name. - */ -const _highlight = (node, addItems, text, className) => { - if (node.nodeType === Node.TEXT_NODE) { - const val = node.nodeValue; - const parent = node.parentNode; - const pos = val.toLowerCase().indexOf(text); - if ( - pos >= 0 && - !parent.classList.contains(className) && - !parent.classList.contains("nohighlight") - ) { - let span; - - const closestNode = parent.closest("body, svg, foreignObject"); - const isInSVG = closestNode && closestNode.matches("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.classList.add(className); - } - - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - const rest = document.createTextNode(val.substr(pos + text.length)); - parent.insertBefore( - span, - parent.insertBefore( - rest, - node.nextSibling - ) - ); - node.nodeValue = val.substr(0, pos); - /* There may be more occurrences of search term in this node. So call this - * function recursively on the remaining fragment. - */ - _highlight(rest, addItems, text, className); - - if (isInSVG) { - const rect = document.createElementNS( - "http://www.w3.org/2000/svg", - "rect" - ); - const bbox = parent.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute("class", className); - addItems.push({ parent: parent, target: rect }); - } - } - } else if (node.matches && !node.matches("button, select, textarea")) { - node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); - } -}; -const _highlightText = (thisNode, text, className) => { - let addItems = []; - _highlight(thisNode, addItems, text, className); - addItems.forEach((obj) => - obj.parent.insertAdjacentElement("beforebegin", obj.target) - ); -}; - -/** - * Small JavaScript module for the documentation. - */ -const SphinxHighlight = { - - /** - * highlight the search words provided in localstorage in the text - */ - highlightSearchWords: () => { - if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight - - // get and clear terms from localstorage - const url = new URL(window.location); - const highlight = - localStorage.getItem("sphinx_highlight_terms") - || url.searchParams.get("highlight") - || ""; - localStorage.removeItem("sphinx_highlight_terms") - url.searchParams.delete("highlight"); - window.history.replaceState({}, "", url); - - // get individual terms from highlight string - const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); - if (terms.length === 0) return; // nothing to do - - // There should never be more than one element matching "div.body" - const divBody = document.querySelectorAll("div.body"); - const body = divBody.length ? divBody[0] : document.querySelector("body"); - window.setTimeout(() => { - terms.forEach((term) => _highlightText(body, term, "highlighted")); - }, 10); - - const searchBox = document.getElementById("searchbox"); - if (searchBox === null) return; - searchBox.appendChild( - document - .createRange() - .createContextualFragment( - '" - ) - ); - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords: () => { - document - .querySelectorAll("#searchbox .highlight-link") - .forEach((el) => el.remove()); - document - .querySelectorAll("span.highlighted") - .forEach((el) => el.classList.remove("highlighted")); - localStorage.removeItem("sphinx_highlight_terms") - }, - - initEscapeListener: () => { - // only install a listener if it is really needed - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; - - document.addEventListener("keydown", (event) => { - // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; - // bail with special keys - if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; - if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { - SphinxHighlight.hideSearchWords(); - event.preventDefault(); - } - }); - }, -}; - -_ready(() => { - /* Do not call highlightSearchWords() when we are on the search page. - * It will highlight words from the *previous* search query. - */ - if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); - SphinxHighlight.initEscapeListener(); -}); diff --git a/docs/build/_static/whatsnew.rst b/docs/build/_static/whatsnew.rst deleted file mode 100644 index 8583e81..0000000 --- a/docs/build/_static/whatsnew.rst +++ /dev/null @@ -1,5 +0,0 @@ -CHANGELOG -========= - -.. include:: ../../CHANGELOG.md - :parser: myst_parser.sphinx_ \ No newline at end of file diff --git a/docs/build/api.html b/docs/build/api.html deleted file mode 100644 index 0eb4568..0000000 --- a/docs/build/api.html +++ /dev/null @@ -1,300 +0,0 @@ - - - - - - - - - API and modules — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
- - -
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/api/algorithms.html b/docs/build/api/algorithms.html deleted file mode 100644 index 83d9cf4..0000000 --- a/docs/build/api/algorithms.html +++ /dev/null @@ -1,345 +0,0 @@ - - - - - - - - - Core Algorithms — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- -
-

Core Algorithms

- -
-

AutoTight

-
-
-class popcor.AutoTight[source]
-

Class for automatic constraint generation.

-
-
-static get_A_learned(lifter, A_known=[], var_dict=None, method='qrp', verbose=False) list[source]
-

Generate list of learned constraints by sampling the lifter.

-
-
Parameters:
-
    -
  • lifter – StateLifter object

  • -
  • A_known – list of known constraints, if given, will generate basis that is orthogonal to these given constraints.

  • -
  • var_dict – variable dictionary, if None, will use all variables

  • -
  • method – method to use for basis generation, can be ‘qr’, ‘qrp’, or ‘svd’. ‘qrp’ is recommended.

  • -
  • verbose – if True, will print timing information

  • -
-
-
Returns:
-

list of learned constraints.

-
-
-
- -
-
-static get_duality_gap(cost_local, cost_sdp)[source]
-
- -
- -
-
-

AutoTemplate

-
-
-class popcor.AutoTemplate(lifter: StateLifter)[source]
-

Class to incrementally learn and augment constraint templates until we reach tightness.

-
-
-apply(lifter: StateLifter, use_known: bool = False) list[source]
-

Apply the learned templates to a new lifter.

-
- -
-
-run(use_known: bool = True, use_incremental: bool = False, variable_list: list[list[str]] | None = None, verbose: bool = False, plot: bool = False)[source]
-

Run the template learning algorithm until we reach tightness, or run out of variables to add.

-
-
Parameters:
-
    -
  • use_known – whether to use the known constraints of the lfiter (must have get_A_known).

  • -
  • use_incremental – whether to keep adding the learned tempaltes to the set of known constraints, to enforce we find orthogonal ones.

  • -
  • variable_list – list of lists of variables to consider. If not given, will use the VARIABLE_LIST parameter of the lifter class.

  • -
-
-
-
- -
- -
-
- - -
-
- -
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/api/auto_tight.html b/docs/build/api/auto_tight.html deleted file mode 100644 index ae6b824..0000000 --- a/docs/build/api/auto_tight.html +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - - - - Core Algorithms — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Core Algorithms

- -
-

AutoTight

-
-
-class popr.AutoTight[source]
-

Bases: object

-
-
-static generate_matrices(lifter, basis, normalize=False, sparse=True, trunc_tol=1e-10, var_dict=None)[source]
-

Generate constraint matrices from the rows of the nullspace basis matrix.

-
- -
-
-static generate_matrices_simple(lifter, basis, normalize=False, sparse=True, trunc_tol=1e-10, var_dict=None)[source]
-

Generate constraint matrices from the rows of the nullspace basis matrix.

-
- -
-
-static get_basis(lifter, Y, A_known: list = [], basis_known: ndarray | None = None, method='qrp', eps_svd=None)[source]
-

Generate basis from lifted state matrix Y.

-
-
Parameters:
-

A_known – if given, will generate basis that is orthogonal to these given constraints.

-
-
Returns:
-

basis, S

-
-
-
- -
- -
-
-

AutoTemplate

-
-
-class popr.AutoTemplate(lifter: StateLifter, variable_list: list | None = None)[source]
-

Bases: object

-

Class to incrementally learn and augment constraint templates until we reach tightness.

-
-
-clean_constraints(constraints, remove_dependent=True, remove_imprecise=True)[source]
-

This function is used in two different ways.

-

First use case: Given the new templates, in b-PolyRow form, we determine which of the templates are actually -independent to a_current. We only want to augment the independent ones, otherwise we waste computing effort.

-

Second use case: After applying the templates to as many variable pairs as we wish, we call this function again, -to make sure all the matrices going into the SDP are in fact linearly independent.

-
- -
-
-extract_known_templates()[source]
-

Find which of the known constraints are relevant for the current variables.

-
- -
-
-get_sufficient_templates(new_order, new_lifter)[source]
-

Use the templates in learner to populate the own templates and constraints.

-
- -
- -
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/api/lifters.html b/docs/build/api/lifters.html deleted file mode 100644 index 37225eb..0000000 --- a/docs/build/api/lifters.html +++ /dev/null @@ -1,449 +0,0 @@ - - - - - - - - - Base Lifters — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- -
-

Base Lifters

- -
-

Overview

-

There are a couple of lifters that serve as a basis for new lifters. -The most basic one is StateLifter, which is generally the best starting point.

-

For specific problems, there are a couple of abstract classes that were developed to ease -the development. In particular, we have:

- -
-
-

Basics

-

Below are some general notes about terminology that may be useful in understanding the code and building your own lifters.

-
    -
  • theta is the original (low-dimensional) state variable.

  • -
  • x is the lifted (higher-dimensional) state variable.

  • -
  • A are equality constraints

  • -
  • B are inequality constraints

  • -
  • var_dict refers to the dictionary of variable name - variable size pairs.

  • -
  • param_dict is used to factor out parameters when creating templates. It also comes in name - variable size pairs.

  • -
-
-
-

StateLifter

-
-
-class popcor.base_lifters.StateLifter(level='no', param_level='no', d=2, variable_list=None, robust=False, n_outliers=0, n_parameters=1)[source]
-

Bases: BaseClass

-
-
-get_A_known(add_redundant: bool = False, var_dict: dict | None = None, output_poly: bool = False) list[source]
-

Construct the matrices defining the known equality constraints.

-
-
Parameters:
-
    -
  • add_redundant – if True, add redundant constraints.

  • -
  • var_dict – if provided, return only the matrices that involve these variables.

  • -
  • output_poly – if True, return the matrices in PolyMatrix format.

  • -
-
-
-
- -
-
-get_B_known() list[source]
-

Construct the matrices defining the known inequality constraints.

-
- -
-
-get_Q(output_poly=False, noise=None)[source]
-

Construct the cost matrix Q.

-
-
Parameters:
-
    -
  • noise – set the noise level, if appropriate.

  • -
  • output_poly – if True, return the matrix in PolyMatrix format.

  • -
-
-
Returns:
-

the cost matrix as a sparse matrix or PolyMatrix.

-
-
-
- -
-
-get_cost(theta, y=None) float[source]
-

Compute the cost of the given state theta. This uses the simple form -x.T @ Q @ x. Consider overwriting this for more efficient computations.

-
- -
-
-get_theta(x)[source]
-

Inverse of get_x: given lifted vector x, extract elements corresponding to theta.

-
- -
-
-get_x(theta: ndarray | None = None, var_subset: Iterable | None = None, **kwargs) ndarray
-

Get the lifted state vector x.

-
-
Parameters:
-
    -
  • theta – if given, use this theta instead of the ground truth one.

  • -
  • var_subset – list of parameter keys to use. If None, use all.

  • -
-
-
Returns:
-

lifted vector x

-
-
-
- -
-
-local_solver(t0, y=None, *args, **kwargs)[source]
-

Default local solver that uses IPOPT to solve the QCQP problem defined by Q and the constraints matrices. -Consider overwriting this for more efficient solvers.

-
- -
-
-sample_parameters(theta=None) dict[source]
-

Create random set of parameters. By default, there are no parameters -so this function just returns {self.HOM: 1.0}.

-
- -
-
-abstract sample_theta() ndarray[source]
-

Randomly sample a feasible state theta. This function must be -implemented by the inheriting class.

-
- -
- -
-
-

StereoLifter

-
-
-class popcor.base_lifters.StereoLifter(n_landmarks, d, level='no', param_level='no', variable_list=None)[source]
-

Bases: StateLifter, ABC

-

StereoLifter is a general lifter class for the stereo localization problem, supporting both 2D and 3D cases.

-

See Stereo2DLifter for 2D and Stereo3DLifter for 3D.

-
- -
-
-

RobustPoseLifter

-
-
-class popcor.base_lifters.RobustPoseLifter(n_outliers=0, level='no', param_level='no', d=2, n_landmarks=3, variable_list=None, robust=False, beta=0.1)[source]
-

Bases: StateLifter, ABC

-

RobustPoseLifter is a general class for point-to-point, point-to-line, and point-to-plane registration problems, -with starndard or robust loss functions.

-

The goal is to regress an unknown pose based on extrinsic measurements.

-

See WahbaLifter for point-to-point registration and MonoLifter) for point-to-line registration.

-

Implemented lifting functions are:

-
-
    -
  • xwT: \(x \otimes w\)

  • -
  • xxT: \(x \otimes x\)

  • -
-
-
- -
-
-

PolyLifter

-
-
-class popcor.base_lifters.PolyLifter(degree, param_level='no')[source]
-

Bases: StateLifter

-

Simple univariate polynomial lifter, mostly for testing and pedagogical purposes.

-
- -
-
- - -
-
- -
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/api/utils.html b/docs/build/api/utils.html deleted file mode 100644 index 79766cb..0000000 --- a/docs/build/api/utils.html +++ /dev/null @@ -1,335 +0,0 @@ - - - - - - - - - Utils — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- -
-

Utils

-
-

Warning

-

This page of the documentation is unfinished, and may significantly change in future versions.

-
-
-

Constraints and templates

-
-
-class popcor.utils.constraint.Constraint(index=0, polyrow_a=None, polyrow_b=None, A_poly=None, A_sparse=None, b=None, a=None, a_full=None, b_full=None, mat_var_dict=None, mat_param_dict=None, known=False, template_idx=0)[source]
-

This class serves the main purpose of not recomputing representations of constraints more than once.

-
- -
-
-

Helpers for matrix and vector operations

-
-
-popcor.utils.common.create_symmetric(vec, eps_sparse, correct=False, sparse=False)[source]
-

Create a symmetric matrix from the vectorized elements of the upper half

-
- -
-
-popcor.utils.common.diag_indices(n)[source]
-

Given the half kronecker product, return diagonal elements

-
- -
-
-popcor.utils.common.get_vec(mat, correct=True, sparse=False) ndarray | csr_matrix | None[source]
-

Convert NxN Symmetric matrix to (N+1)N/2 vectorized version that preserves inner product.

-
-
Parameters:
-

mat – (spmatrix or ndarray) symmetric matrix

-
-
Returns:
-

ndarray

-
-
-
- -
-
-popcor.utils.common.ravel_multi_index_triu(index_tuple, shape)[source]
-

Equivalent of np.multi_index_triu, but using only the upper-triangular part of matrix.

-
- -
-
-popcor.utils.common.unravel_multi_index_triu(flat_indices, shape)[source]
-

Equivalent of np.multi_index_triu, but using only the upper-triangular part of matrix.

-
- -
-
-popcor.utils.common.upper_triangular(p)[source]
-

Given vector, get the half kronecker product.

-
- -
-
- - -
-
- -
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/contributing.html b/docs/build/contributing.html deleted file mode 100644 index 9d66829..0000000 --- a/docs/build/contributing.html +++ /dev/null @@ -1,354 +0,0 @@ - - - - - - - - - Contributing — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- -
-

Contributing

-

Below is a rendering of the CONTRIBUTING.md file.

-
-

How to contribute to POPR

-

We welcome any contributions to POPR! Contributions are done through pull requests.

-
-

General guidelines

-

Please try to the best of your abilities to:

-
    -
  • use black and isort for formatting your code

  • -
  • provide at least minimal documentation

  • -
  • tests for core functionalities.

  • -
  • add your information to the CHANGELOG.

  • -
-
-
-

Adding a new lifter class

-

You can start with the ExampleLifter skeleton, and feel free to add more functionalities depending on the nature of the problem. You can also consider adding a new base class similar to RobustPoseLifter or StereoLifter if you want to create multiple new lifters that all share similar functionalities.

-
-
-

Adding new functionalities

-

We welcome new functionalities / solvers / tools that facilitate problem formulation as well. If you add something general please make sure it is tested on all examples, as done for example in tests/test_autotight.py.

-
-
-

Resources

-

Below are a couple of guidelines and useful resources. This is a living document aimed to contain a list of useful resources for developers. Feel free to extend it.

-
-

Testing

-

Added functionality should include passing unit tests. Please follow the available examples in the tests folder.

-

It is also encouraged that added functionality is added as testable code to the documentation. To do that, create .. doctest: blocks and run

-
make doctest
-
-
-

to make sure there are no errors. There is also the possibility to just use literal includes from test files inside the documentation. See docs/source/quickstart.rst for an example.

-
-
-

Setting up mosek license on server

-

This has already been done – just keeping track of the process here to make it is easy to redo this in the future. TAken from here, page 11.

-
    -
  1. Go to Settings -> Security -> Secrets and variables -> Actions (direct link)

  2. -
  3. Create secret called MSK_LICENSE with content

  4. -
-
START_LICENSE\n
-FEATURE PTS ....
-...
-... here copy the text of the "FEATURE" sections in the license ...
-...
-... ... 5FE1 5DBC"
-END_LICENSE\n
-
-
-
    -
  1. Add the following to the workflow file:

  2. -
-
- name: Setup MOSEK & Run Tests
-    env:
-      MOSEKLM_LICENSE_FILE: ${{ secrets.MSK_LICENSE }}
-    run: |
-      pytest -sv
-
-
-
-
-

Testing Github actions locally

-

It can be super useful to run github actions locally for debugging! -I did this by using this tool. Basically, it came down to:

-
    -
  • Installing docker

  • -
  • From this repo, running:

  • -
-
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
-
-
-
    -
  • Also from this repo, running:

  • -
-
sudo ./bin/act
-
-
-
-
-
-
- - -
-
- -
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/examples.html b/docs/build/examples.html deleted file mode 100644 index 09ea360..0000000 --- a/docs/build/examples.html +++ /dev/null @@ -1,301 +0,0 @@ - - - - - - - - - Examples — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - -
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/examples/a.html b/docs/build/examples/a.html deleted file mode 100644 index f152112..0000000 --- a/docs/build/examples/a.html +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - - - - 2. Toy Examples — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

2. Toy Examples

-
-

2.1. Univariate Polynomials

-
-
-class popr.examples.Poly4Lifter(poly_type='A')[source]
-

Bases: PolyLifter

-

Fourth-degree polynomial examples.

-

Two types are provided:

-
    -
  • poly_type=”A”: one global minimum, one local minimum

  • -
  • poly_type=”B”: two global minima

  • -
-
- -
-Poly4Lifter Type A -
-

Poly4Lifter Type A

-
-
-
-Poly4Lifter Type B -
-

Poly4Lifter Type B

-
-
-
-
-class popr.examples.Poly6Lifter(poly_type='A')[source]
-

Bases: PolyLifter

-

Sixth-degree polynomial examples.

-

Two types are provided:

-
    -
  • poly_type=”A”: one global minimum, two local minima, 2 local maxima

  • -
  • poly_type=”B”: one global minimum, one local minimum, one local maximum

  • -
-
- -
-Poly6Lifter Type A -
-

Poly6Lifter Type A

-
-
-
-Poly6Lifter Type B -
-

Poly6Lifter Type B

-
-
-
-
-

2.2. Other Toy Examples

-
-
-class popr.examples.Stereo1DLifter(n_landmarks, param_level='no')[source]
-

Bases: StateLifter

-

Toy example for stereo localization in 1D. We minimize the following cost function:

-
-\[f(\theta) = \sum_{j=0}^{n} (u_j - 1 / (\theta - a_j))^2\]
-

where \(a_j\) are the landmarks and \(u_j\) are the measurements.

-

This is the running example of this paper.

-
- -
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/examples/b.html b/docs/build/examples/b.html deleted file mode 100644 index c32a7c6..0000000 --- a/docs/build/examples/b.html +++ /dev/null @@ -1,224 +0,0 @@ - - - - - - - - - 1. Standard Estimation Problems — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

1. Standard Estimation Problems

-
-

1.1. Range-Only Localization

-
-
-class popr.examples.RangeOnlyLocLifter(n_positions, n_landmarks, d, W=None, level='no', variable_list=None, param_level='no')[source]
-

Range-only localization in 2D or 3D.

-

We minimize the cost function

-
-\[f(\theta) = \sum_{n=0}^{N-1} \sum_{k=0}^{K-1} w_{nk} (d_{nk}^2 - ||p_n - a_k||^2)^2\]
-

where

-
    -
  • \(w_{nk}\) is the weight for the nth point and kth landmark (currently assumed binary to mark missing edges).

  • -
  • \(\theta\) is the flattened vector of positions \(p_n\).

  • -
  • \(d_{nk}\) is the distance measurement from point n to landmark k.

  • -
  • \(a_k\) is the kth landmark.

  • -
-

Note that in the current implementation, there is no regularization term so the problem could be split into individual points.

-

We experiment with two different substitutions to turn the cost function into aquadratic form:

-
    -
  • level “no” uses substitution \(z_i=||p_i||^2=x_i^2 + y_i^2\) (or equivalent 3D version).

  • -
  • level “quad” uses substitution \(y_i=[x_i^2, x_iy_i, y_i^2]\) (or equivalent 3D version).

  • -
-

This example is treated in more details in this paper.

-
- -
-
-

1.2. Stereo-Camera Localization

-
-
-class popr.examples.Stereo2DLifter(n_landmarks, level='no', param_level='no', variable_list=None)[source]
-

Bases: StereoLifter

-

Stereo-camera localization in 2D.

-

We minimize the following cost function:

-
-\[f(\theta) = \sum_{j=0}^{n} (u_j - M q_j / q_j[1])^2\]
-

where

-
    -
  • \(p_j\) are known landmarks (in homogeneous coordinates),

  • -
  • \(u_j\) are pixel measurements (2 elements: one pixel in left “image” and one in right “image”),

  • -
  • \(q_j = T(\theta) p_j\) are the (homogeneous) coordinates of landmark j in the (unknown) camera frame, parameterized by \(T(\theta)\), and

  • -
  • \(M\) is the stereo camera calibration matrix. Here, it is given by

  • -
-
-\[\begin{split}\begin{bmatrix} - f_u & c_u & \frac{b f_u}{2} \\ - f_v & c_v & -\frac{b f_v}{2} \\ -\end{bmatrix}\end{split}\]
-

where \(f_u, f_v\) are horizontal and vertical focal lengths, \(c_u,c_v\) are image center points in pixels and \(b\) is the camera baseline.

-

This example is treated in more details in this paper.

-
- -
-
-class popr.examples.Stereo3DLifter(n_landmarks, level='no', param_level='no', variable_list=None)[source]
-

Bases: StereoLifter

-

Stereo-camera localization in 3D.

-

Analogously to Stereo2DLifter, we minimize the following cost function:

-
-\[f(\theta) = \sum_{j=0}^{n} (u_j - M q_j / q_j[2])^2\]
-

where

-
    -
  • \(p_j\) are known landmarks (in homogeneous coordinates),

  • -
  • \(u_j\) are pixel measurements (4 elements: two pixel coordinates in left image and two in right image),

  • -
  • \(q_j = T(\theta) p_j\) are the (homogeneous) coordinates of landmark j in the (unknown) camera frame, parameterized by \(T(\theta)\), and

  • -
  • \(M\) is the stereo camera calibration matrix. Here, it is given by

  • -
-
-\[\begin{split}\begin{bmatrix} - f_u & 0 & c_u & \frac{b f_u}{2} \\ - 0 & f_v & c_v & 0 \\ - f_u & 0 & c_u & -\frac{b f_u}{2} \\ - 0 & f_v & c_v & 0 \\ -\end{bmatrix}\end{split}\]
-

where \(f_u, f_v\) are horizontal and vertical focal lengths, \(c_u,c_v\) are image center points in pixels and \(b\) is the camera baseline.

-

This example is treated in more details in this paper.

-
- -
-
-

1.3. Rotation Averaging

-
-
-class popr.examples.RotationLifter(level='no', param_level='no', d=2, n_meas=2)[source]
-

Bases: StateLifter

-

Rotation averaging problem.

-
- -
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/examples/c.html b/docs/build/examples/c.html deleted file mode 100644 index 3c4803b..0000000 --- a/docs/build/examples/c.html +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - - - - 2. Robust Estimation Problems — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

2. Robust Estimation Problems

-
-

2.1. Robust Registration Problems

-
-
-class popr.examples.MonoLifter(n_outliers=0, level='no', param_level='no', d=2, n_landmarks=3, variable_list=None, robust=False, beta=0.1)[source]
-

Bases: RobustPoseLifter

-

This example is treated in more details in this paper, -under the name “PLR” (point-to-line registration).

-

We experiment with -:param level:

-
-
    -
  • xwT: \(x \otimes w\)

  • -
  • xxT: \(x \otimes x\)

  • -
-
-
- -
-
-class popr.examples.WahbaLifter(n_outliers=0, level='no', param_level='no', d=2, n_landmarks=3, variable_list=None, robust=False, beta=0.1)[source]
-

Bases: RobustPoseLifter

-

This example is treated in more details in this paper, -under the name “PPR” (point-to-point registration).

-

We experiment with -:param level:

-
-
    -
  • xwT: \(x \otimes w\)

  • -
  • xxT: \(x \otimes x\)

  • -
-
-
- -
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/examples/d.html b/docs/build/examples/d.html deleted file mode 100644 index 6e6c760..0000000 --- a/docs/build/examples/d.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - - - 1. Templates — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

1. Templates

-
-

1.1. Example for AutoTight

-
-
-class popr.examples.ExampleLifter(param_level='no')[source]
-

Bases: StateLifter

-

Example Lifter class.

-

This class implements the bare minimum to use AutoTight.

-

To create a new Lifter for your problem formulation, create -a copy of this file and fill in the missing parts.

-

You can take a look at the Examples for inspiration.

-
- -
-
-

1.2. Example for AutoTemplate

-
-

Note

-

Coming soon: barebone template for AutoTemplate

-
-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/examples/robust.html b/docs/build/examples/robust.html deleted file mode 100644 index c912b79..0000000 --- a/docs/build/examples/robust.html +++ /dev/null @@ -1,317 +0,0 @@ - - - - - - - - - 4. Robust Estimation Problems — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- -
-

4. Robust Estimation Problems

-
-

4.1. Robust Registration Problems

-
-
-class popcor.examples.MonoLifter(n_outliers=0, level='no', param_level='no', d=2, n_landmarks=3, variable_list=None, robust=False, beta=0.1)[source]
-

Bases: RobustPoseLifter

-

This example is treated in more details in this paper, -under the name “PLR” (point-to-line registration).

-

RobustPoseLifter is a general class for point-to-point, point-to-line, and point-to-plane registration problems, -with starndard or robust loss functions.

-

The goal is to regress an unknown pose based on extrinsic measurements.

-

See WahbaLifter for point-to-point registration and MonoLifter) for point-to-line registration.

-

Implemented lifting functions are:

-
-
    -
  • xwT: \(x \otimes w\)

  • -
  • xxT: \(x \otimes x\)

  • -
-
-
- -
-
-class popcor.examples.WahbaLifter(n_outliers=0, level='no', param_level='no', d=2, n_landmarks=3, variable_list=None, robust=False, beta=0.1)[source]
-

Bases: RobustPoseLifter

-

This example is treated in more details in this paper, -under the name “PPR” (point-to-point registration).

-

RobustPoseLifter is a general class for point-to-point, point-to-line, and point-to-plane registration problems, -with starndard or robust loss functions.

-

The goal is to regress an unknown pose based on extrinsic measurements.

-

See WahbaLifter for point-to-point registration and MonoLifter) for point-to-line registration.

-

Implemented lifting functions are:

-
-
    -
  • xwT: \(x \otimes w\)

  • -
  • xxT: \(x \otimes x\)

  • -
-
-
- -
-
- - -
-
- -
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/examples/standard.html b/docs/build/examples/standard.html deleted file mode 100644 index dd14817..0000000 --- a/docs/build/examples/standard.html +++ /dev/null @@ -1,365 +0,0 @@ - - - - - - - - - 3. Standard Estimation Problems — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- -
-

3. Standard Estimation Problems

-
-

3.1. Range-Only Localization

-
-
-class popcor.examples.RangeOnlyLocLifter(n_positions, n_landmarks, d, W=None, level='no', variable_list=None, param_level='no')[source]
-

Range-only localization in 2D or 3D.

-

We minimize the cost function

-
-\[f(\theta) = \sum_{n=0}^{N-1} \sum_{k=0}^{K-1} w_{nk} (d_{nk}^2 - ||p_n - a_k||^2)^2\]
-

where

-
    -
  • \(w_{nk}\) is the weight for the nth point and kth landmark (currently assumed binary to mark missing edges).

  • -
  • \(\theta\) is the flattened vector of positions \(p_n\).

  • -
  • \(d_{nk}\) is the distance measurement from point n to landmark k.

  • -
  • \(a_k\) is the kth landmark.

  • -
-

Note that in the current implementation, there is no regularization term so the problem could be split into individual points.

-

We experiment with two different substitutions to turn the cost function into aquadratic form:

-
    -
  • level “no” uses substitution \(z_i=||p_i||^2=x_i^2 + y_i^2\) (or equivalent 3D version).

  • -
  • level “quad” uses substitution \(y_i=[x_i^2, x_iy_i, y_i^2]\) (or equivalent 3D version).

  • -
-

This example is treated in more details in this paper.

-
- -
-
-

3.2. Stereo-Camera Localization

-
-
-class popcor.examples.Stereo2DLifter(n_landmarks, level='no', param_level='no', variable_list=None)[source]
-

Bases: StereoLifter

-

Stereo-camera localization in 2D.

-

We minimize the following cost function:

-
-\[f(\theta) = \sum_{j=0}^{n} (u_j - M q_j / q_j[1])^2\]
-

where

-
    -
  • \(p_j\) are known landmarks (in homogeneous coordinates),

  • -
  • \(u_j\) are pixel measurements (2 elements: one pixel in left “image” and one in right “image”),

  • -
  • \(q_j = T(\theta) p_j\) are the (homogeneous) coordinates of landmark j in the (unknown) camera frame, parameterized by \(T(\theta)\), and

  • -
  • \(M\) is the stereo camera calibration matrix. Here, it is given by

  • -
-
-\[\begin{split}\begin{bmatrix} - f_u & c_u & \frac{b f_u}{2} \\ - f_v & c_v & -\frac{b f_v}{2} \\ -\end{bmatrix}\end{split}\]
-

where \(f_u, f_v\) are horizontal and vertical focal lengths, \(c_u,c_v\) are image center points in pixels and \(b\) is the camera baseline.

-

This example is treated in more details in this paper.

-
- -
-
-class popcor.examples.Stereo3DLifter(n_landmarks, level='no', param_level='no', variable_list=None)[source]
-

Bases: StereoLifter

-

Stereo-camera localization in 3D.

-

Analogously to Stereo2DLifter, we minimize the following cost function:

-
-\[f(\theta) = \sum_{j=0}^{n} (u_j - M q_j / q_j[2])^2\]
-

where

-
    -
  • \(p_j\) are known landmarks (in homogeneous coordinates),

  • -
  • \(u_j\) are pixel measurements (4 elements: two pixel coordinates in left image and two in right image),

  • -
  • \(q_j = T(\theta) p_j\) are the (homogeneous) coordinates of landmark j in the (unknown) camera frame, parameterized by \(T(\theta)\), and

  • -
  • \(M\) is the stereo camera calibration matrix. Here, it is given by

  • -
-
-\[\begin{split}\begin{bmatrix} - f_u & 0 & c_u & \frac{b f_u}{2} \\ - 0 & f_v & c_v & 0 \\ - f_u & 0 & c_u & -\frac{b f_u}{2} \\ - 0 & f_v & c_v & 0 \\ -\end{bmatrix}\end{split}\]
-

where \(f_u, f_v\) are horizontal and vertical focal lengths, \(c_u,c_v\) are image center points in pixels and \(b\) is the camera baseline.

-

This example is treated in more details in this paper.

-
- -
-
-

3.3. Rotation Averaging

-
-
-class popcor.examples.RotationLifter(level='no', param_level='no', d=2, n_meas=2)[source]
-

Bases: StateLifter

-

Rotation averaging problem.

-
- -
-
- - -
-
- -
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/examples/templates.html b/docs/build/examples/templates.html deleted file mode 100644 index 04f0099..0000000 --- a/docs/build/examples/templates.html +++ /dev/null @@ -1,296 +0,0 @@ - - - - - - - - - 1. Templates — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- -
-

1. Templates

-
-

1.1. Example for AutoTight

-
-
-class popcor.examples.ExampleLifter(param_level='no')[source]
-

Bases: StateLifter

-

Example Lifter class.

-

This class implements the bare minimum to use AutoTight.

-

To create a new Lifter for your problem formulation, create -a copy of this file and fill in the missing parts.

-

You can take a look at the Examples for inspiration.

-
- -
-
-

1.2. Example for AutoTemplate

-
-

Note

-

Coming soon: barebone template for AutoTemplate

-
-
-
- - -
-
- -
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/examples/toy.html b/docs/build/examples/toy.html deleted file mode 100644 index 718e147..0000000 --- a/docs/build/examples/toy.html +++ /dev/null @@ -1,344 +0,0 @@ - - - - - - - - - 2. Toy Examples — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- -
-

2. Toy Examples

-
-

2.1. Univariate Polynomials

-
-
-class popcor.examples.Poly4Lifter(poly_type='A')[source]
-

Bases: PolyLifter

-

Fourth-degree polynomial examples.

-

Two types are provided:

-
    -
  • poly_type=”A”: one global minimum, one local minimum

  • -
  • poly_type=”B”: two global minima

  • -
-

Simple univariate polynomial lifter, mostly for testing and pedagogical purposes.

-
- -
-Poly4Lifter Type A -
-

Poly4Lifter Type A

-
-
-
-Poly4Lifter Type B -
-

Poly4Lifter Type B

-
-
-
-
-class popcor.examples.Poly6Lifter(poly_type='A')[source]
-

Bases: PolyLifter

-

Sixth-degree polynomial examples.

-

Two types are provided:

-
    -
  • poly_type=”A”: one global minimum, two local minima, 2 local maxima

  • -
  • poly_type=”B”: one global minimum, one local minimum, one local maximum

  • -
-

Simple univariate polynomial lifter, mostly for testing and pedagogical purposes.

-
- -
-Poly6Lifter Type A -
-

Poly6Lifter Type A

-
-
-
-Poly6Lifter Type B -
-

Poly6Lifter Type B

-
-
-
-
-

2.2. Other Toy Examples

-
-
-class popcor.examples.Stereo1DLifter(n_landmarks, param_level='no')[source]
-

Bases: StateLifter

-

Toy example for stereo localization in 1D. We minimize the following cost function:

-
-\[f(\theta) = \sum_{j=0}^{N-1} (u_j - 1 / (\theta - a_j))^2\]
-

where \(a_j\) are the landmarks and \(u_j\) are the measurements.

-

This is the pedagogical running example of this paper. -and also used in the Quick Start Guide.

-
- -
-
- - -
-
- -
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/genindex.html b/docs/build/genindex.html deleted file mode 100644 index ba1059b..0000000 --- a/docs/build/genindex.html +++ /dev/null @@ -1,458 +0,0 @@ - - - - - - - - Index — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- - -

Index

- -
- A - | C - | D - | E - | G - | L - | M - | P - | R - | S - | U - | W - -
-

A

- - - -
- -

C

- - - -
- -

D

- - -
- -

E

- - -
- -

G

- - - -
- -

L

- - -
- -

M

- - - -
- -

P

- - - -
- -

R

- - - -
- -

S

- - - -
- -

U

- - - -
- -

W

- - -
- - - -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/index.html b/docs/build/index.html deleted file mode 100644 index 305df6e..0000000 --- a/docs/build/index.html +++ /dev/null @@ -1,321 +0,0 @@ - - - - - - - - - Welcome to POPCOR! — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- -
-

Warning

-

This package is currently in pre-release state. You are welcome to try it out, -and please get in touch via github or e-mail if you have any questions, suggestions or issues.

-
-
-

Welcome to POPCOR!

-
-_images/overview.png - -
-
-

POPCOR: Polynomial Optimization for Certifiably Optimal Robotics

-

A toolbox for setting up and solving polynomial optimization problems in robotics with certifiable optimality.

-
-

Purpose

-

This toolbox includes methods to formulate and solve polynomial optimization problems in robotics. -The focus of this toolbox is on creating tight semidefinite relaxations, which means that we can -replace difficult (often NP-hard) optimization problems by easier-to-solve convex problems. In doing so, we can identify globally optimal solution of the original problem.

-

The toolbox allows to run the AutoTight and AutoTemplate algorithms on problems of your choice. -These algorithms are described in detail in this paper. For a high-level overview of how this works, please refer to the Quick start guide.

-

If you use this toolbox, please cite our corresponding paper:

-
@article{autotight2024,
-  author={Dümbgen, Frederike and Holmes, Connor and Agro, Ben and Barfoot, Timothy D.},
-  title={{Toward Globally Optimal State Estimation Using Automatically Tightened Semidefinite Relaxations}},
-  journal={IEEE Transactions on Robotics},
-  volume={40},
-  pages={4338-4358},
-  year={2024},
-  doi={10.1109/TRO.2024.3454570}
-}
-
-
-
-
-

Who this tool is for

-

This tool is created for engineers (in particular, roboticists) who want to test if an optimization problem they need to solve could be amenable to certifiable optimization. For many problems, it has been shown that random initialization can lead to convergence to poor minima, while the same problems admit a so-called convex relaxation which allows to solve it to global optimality (or certify local solutions). This tool is meant to help you try quickly if your current problem also belongs to this family of problems that can be solved to certifiable global optimality.

-
-
-

Dependencies

-

Besides the automatically installed dependencies when using the above instructions, you need to also have a valid MOSEK license in order to use this repository. If you are an academic, you can get a license for free here.

-
-
-

Credits

-

The design of this toolbox, in particular the documentation, is inspired by the nice work of the creators of PEPit.

-
-
-
-
-
- - -
-
- -
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/objects.inv b/docs/build/objects.inv deleted file mode 100644 index 9136dbf..0000000 Binary files a/docs/build/objects.inv and /dev/null differ diff --git a/docs/build/py-modindex.html b/docs/build/py-modindex.html deleted file mode 100644 index 9d1b2e4..0000000 --- a/docs/build/py-modindex.html +++ /dev/null @@ -1,292 +0,0 @@ - - - - - - - - Python Module Index — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- - -

Python Module Index

- -
- p -
- - - - - - - - - - -
 
- p
- popcor -
    - popcor.utils.common -
- - -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/quickstart.html b/docs/build/quickstart.html deleted file mode 100644 index fd870ae..0000000 --- a/docs/build/quickstart.html +++ /dev/null @@ -1,497 +0,0 @@ - - - - - - - - - Quick Start Guide — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- -
-

Quick Start Guide

-
-

Installation

-

POPR can be installed by running from a terminal:

-
git clone --recurse-submodules git@github.com:duembgen/popcor
-cd popcor
-conda env create -f environment.yml
-
-
-
-
-

Problem Formulation

-

We start with polynomial optimization problems (POPs) of the form:

-
-\[\begin{split}\begin{align} q^\star =&\min_{\theta} f(\theta) \\ - \text{s.t. } &g(\theta) = 0 \\ - &h(\theta) \geq 0 -\end{align}\end{split}\]
-

where \(f,g,\text{ and } h\) are polynomial functions, and both \(g\) and \(h\) can be vector-valued. Many maximum-a-posteriori or maximum-likelihood estimation problems can be formulated as such, for example range-only localization and range-aided SLAM, (matrix-weighted) SLAM, and outlier-robust estimation. The same is true for many control and planning problems, for example the inverted pendulum and other classical dynamical systems, and even contact-rich problems such as slider-pusher planning problems.

-

Any POP can be equivalently written in the following QCQP form:

-
-\[\begin{split}\begin{align} q^\star =&\min_{x} x^\top Q x \\ - \text{s.t. } &(\forall i): x^\top A_i x = b_i \\ - &(\forall j): x^\top B_j x \geq 0 -\end{align}\end{split}\]
-

with cost matrix \(Q\), known constraint matrices \(A_i,B_j\). -Note that

-
    -
  • We always include the so-called homogenization variable, which enables to write linear and constant terms as quadratics. By convention, we set the first element of \(x\) to one, and we use \(b_0=1, A_0\) to encorce this constraint.

  • -
  • All inequality and some equality constraints correspond to the constraints from the original POP.

  • -
  • Some additional equality constraints correspond to new substitution variables that need to be added to formulate the problem as a quadratic.

  • -
-
-

Warning

-

Note that while inequality constraints can be added to the problem formulation, there is no implementation yet to add find and add redundant inequality constraints to the relaxation.

-
-

For the standard usage, the user first needs to define a custom Lifter class which essentially contains all elements related to the QCQP problem formulation. -This class should inherit from StateLifter. A basic skeleton of such a -Lifter class is provided in Example for AutoTight. The main purpose of this class is -that it provides all basic operations related to the problem formulation, such as:

- -

For a bit more advanced functionality (for example for the SDP Relaxation in the next section), you also need to define functions such as

- -

Many example lifters are provided, you can find them under Examples.

-
-

Example: instantiating and using lifter

-

The following code snippet shows some basic operations (and useful sanity checks) for the example -lifter class popcor.examples.Poly4Lifter. Note that this and all following examples can be found -in the file ../../tests/test_quickstart.py.

-
from popcor.examples import Poly4Lifter
-
-lifter = Poly4Lifter()
-
-Q = lifter.get_Q()
-
-# theta corresponds to the ground truth; in this case, the global minimum.
-x = lifter.get_x(lifter.theta).flatten()
-cost_optimum = float(x.T @ Q @ x)
-
-# the cost at any other randomly sampled point has to be larger.
-for i in range(10):
-    theta_random = lifter.sample_theta()
-    x_random = lifter.get_x(theta_random).flatten()
-    assert float(x_random.T @ Q @ x_random) > cost_optimum
-
-
-
-
-
-

SDP Relaxation

-

It is straightforward to derive a convex relaxation of the original QCQP, using the reformulation \(x^\top Qx=\langle x, Qx\rangle = \langle Q, xx^\top \rangle\), where \(\langle \cdot, \cdot \rangle\) denotes the trace inner product. Then introducing \(X:=xx^\top\) and relaxing its rank, we obtain the following convex relaxation, in the form of an SDP:

-
-\[\begin{split}\begin{align} p^\star = &\min_{X \succeq 0} \langle Q, X \rangle \\ - \text{s.t. } &(\forall i): \langle A_i, X \rangle = b_i \\ - &(\forall j): \langle B_j, X \rangle \geq 0 -\end{align}\end{split}\]
-
-

Example: solving the QCQP using rank relaxation

-

The following code snippet shows how you can use the simple lifter from earlier to find the global -optimum of the nonconvex polynomial problem, by solving an SDP.

-
from cert_tools.linalg_tools import rank_project
-from cert_tools.sdp_solvers import solve_sdp
-
-from popcor.examples import Poly4Lifter
-
-lifter = Poly4Lifter()
-
-# the cost matrix
-Q = lifter.get_Q()
-
-# the equality constraints
-A_known = lifter.get_A_known()
-
-# the homogenization constraint
-A_0 = lifter.get_A0()
-
-X, info = solve_sdp(Q, [(A_0, 1.0)] + [(A_i, 0.0) for A_i in A_known])
-assert X is not None
-
-# if X is rank one, the global optimum can be found in element X_10 of the matrix.
-theta_pick = X[1, 0]
-assert abs(theta_pick - lifter.theta) < 1e-5
-
-# We can also first extract the rank-1 estimate (X=xx') and then extract theta.
-x, info_rank = rank_project(X, p=1)
-theta_round = x[1]
-
-assert abs(theta_round - lifter.theta) < 1e-5
-
-
-
-
-
-

AutoTight Method

-

AutoTight is used to find all possible constraints matrices \(A_r\), which are also automatically satisfied by solutions of the QCQP. They are also called redundant constraints because they do not change the feasible set of the original problem, but when adding those constraints to the SDP (rank-)relaxation, they often improve tightness. Denoting by \(A_r\) the redundant constraints, we can solve the following SDP:

-
-\[\begin{split}\begin{align} p_r^\star = &\min_{X \succeq 0} \langle Q, X \rangle \\ - \text{s.t. } &(\forall i): \langle A_i, X \rangle = b_i \\ - &(\forall r): \langle A_r, X \rangle = 0 \\ - &(\forall j): \langle B_j, X \rangle \geq 0 -\end{align}\end{split}\]
-

We use the term cost-tight to say that strong duality holds (\(p_r^\star = q^\star\)) while by rank-tight we denote the fact that the SDP solver returns a rank-one solution. -If successful, the output is a set of constraints that leads to a tight SDP relaxation of the original problem, which can be used to solve the problem to global optimality (if we have rank tightness) or certify given solutions (if we have cost tightness).

-

More information on how to use AutoTight can be found here and a simple example is given next.

-
-

Example: tightening the SDP relaxation using AutoTight

-
from cert_tools.sdp_solvers import solve_sdp
-
-from popcor.auto_tight import AutoTight
-from popcor.examples.stereo1d_lifter import Stereo1DLifter
-
-lifter = Stereo1DLifter(n_landmarks=5)
-
-auto_tight = AutoTight()
-
-# solve the SDP -- it is not cost tight!
-Q = lifter.get_Q(noise=1e-1)
-A_known = lifter.get_A_known()
-A_0 = lifter.get_A0()
-
-# solve locally starting at ground truth
-x, info_local, cost = lifter.local_solver(lifter.theta, y=lifter.y_)
-assert x is not None
-assert info_local["success"]
-
-constraints = lifter.get_A_b_list(A_list=A_known)
-X, info_sdp = solve_sdp(Q, constraints)
-
-gap = auto_tight.get_duality_gap(info_local["cost"], info_sdp["cost"])
-assert gap > 0.1
-
-# learn matrices and solve again
-A_learned = auto_tight.get_A_learned(lifter)
-constraints = lifter.get_A_b_list(A_list=A_learned)
-X, info_sdp_learned = solve_sdp(Q, constraints)
-gap = auto_tight.get_duality_gap(info_local["cost"], info_sdp_learned["cost"])
-
-# note that the gap can be slightly negative because of mismatch in convergence tolerances etc.
-assert abs(gap) < 1e-2
-
-# problem: if we change landmarks, the constraints do not generalize!
-new_lifter = Stereo1DLifter(n_landmarks=5)
-assert not np.all(new_lifter.landmarks == lifter.landmarks)
-
-# assert that the following line raises an error
-# this is exactly why we need to use auto_template here!
-with pytest.raises(Exception):
-    new_lifter.test_constraints(A_learned, errors="raise", n_seeds=1)
-
-
-
-
-
-

AutoTemplate Method

-

AutoTemplate follows the same principle as AutoTight, but its output are templates rather than constraint matrices. These templates can be seen as “parametrized” versions of the constraint matrices, and can be applied to new problem instances of any size without having to learn the constraints again from scratch.

-

More information on how to use AutoTemplate can be found here and a simple example is given next.

-
-

Example: tightening a different problem using AutoTemplate

-
from cert_tools.sdp_solvers import solve_sdp
-
-from popcor.auto_template import AutoTemplate
-from popcor.auto_tight import AutoTight
-from popcor.examples.stereo1d_lifter import Stereo1DLifter
-
-# important: we need to use param_level="p", otherwise the parameters
-# are not factored out and the constraints are not generalizable.
-lifter = Stereo1DLifter(n_landmarks=3, param_level="p")
-
-# learn the template matrices
-auto_template = AutoTemplate(lifter)
-data, success = auto_template.run(use_known=False, plot=True)
-assert success
-
-# apply the templates to a different lifter
-new_lifter = Stereo1DLifter(n_landmarks=5, param_level="p")
-A_learned = auto_template.apply(new_lifter, use_known=True)
-
-Q = new_lifter.get_Q(noise=1e-1)
-
-# adds homogenization constraint and all b_i terms
-constraints = new_lifter.get_A_b_list(A_list=A_learned)
-X, info_sdp = solve_sdp(Q, constraints)
-
-# evaluate duality gap
-x, info_local, cost = new_lifter.local_solver(new_lifter.theta, y=new_lifter.y_)
-gap = AutoTight.get_duality_gap(info_local["cost"], info_sdp["cost"])
-# note that the gap can be slightly negative because of mismatch in convergence tolerances etc.
-assert abs(gap) < 1e-2
-
-
-
-
-
-

References

-

[1] F. Dümbgen, C. Holmes, B. Agro and T. Barfoot, “Toward Globally Optimal State Estimation Using Automatically Tightened Semidefinite Relaxations,” in IEEE Transactions on Robotics, vol. 40, pp. 4338-4358, 2024, doi: 10.1109/TRO.2024.3454570.

-
-
- - -
-
- -
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/search.html b/docs/build/search.html deleted file mode 100644 index 75f1ed0..0000000 --- a/docs/build/search.html +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - Search — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- - - - -
- -
- -
-
-
- -
- -
-

© Copyright 2025, POPCOR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - - - GitHub logo - - - - - - \ No newline at end of file diff --git a/docs/build/searchindex.js b/docs/build/searchindex.js deleted file mode 100644 index 3c20c3c..0000000 --- a/docs/build/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({"alltitles": {"API and modules": [[0, null]], "Added": [[12, "added"]], "Adding a new lifter class": [[4, "adding-a-new-lifter-class"]], "Adding new functionalities": [[4, "adding-new-functionalities"]], "AutoTemplate": [[1, "autotemplate"]], "AutoTemplate Method": [[11, "autotemplate-method"]], "AutoTight": [[1, "autotight"]], "AutoTight Method": [[11, "autotight-method"]], "Base Lifters": [[2, null]], "Basics": [[2, "basics"]], "Change Log": [[12, "change-log"]], "Changed": [[12, "changed"]], "Constraints and templates": [[3, "constraints-and-templates"]], "Contents:": [[0, null], [5, null]], "Contributing": [[4, null]], "Core Algorithms": [[1, null]], "Credits": [[10, "credits"]], "Dependencies": [[10, "dependencies"]], "Example for AutoTemplate": [[8, "example-for-autotemplate"]], "Example for AutoTight": [[8, "example-for-autotight"]], "Example: instantiating and using lifter": [[11, "example-instantiating-and-using-lifter"]], "Example: solving the QCQP using rank relaxation": [[11, "example-solving-the-qcqp-using-rank-relaxation"]], "Example: tightening a different problem using AutoTemplate": [[11, "example-tightening-a-different-problem-using-autotemplate"]], "Example: tightening the SDP relaxation using AutoTight": [[11, "example-tightening-the-sdp-relaxation-using-autotight"]], "Examples": [[5, null]], "Fixed": [[12, "fixed"]], "General guidelines": [[4, "general-guidelines"]], "Helpers for matrix and vector operations": [[3, "module-popcor.utils.common"]], "How to contribute to POPR": [[4, "how-to-contribute-to-popr"]], "Installation": [[11, "installation"]], "Other Toy Examples": [[9, "other-toy-examples"]], "Overview": [[2, "overview"]], "POPCOR: Polynomial Optimization for Certifiably Optimal Robotics": [[10, "popcor-polynomial-optimization-for-certifiably-optimal-robotics"]], "PolyLifter": [[2, "polylifter"]], "Problem Formulation": [[11, "problem-formulation"]], "Purpose": [[10, "purpose"]], "Quick Start Guide": [[11, null]], "Range-Only Localization": [[7, "range-only-localization"]], "References": [[11, "references"]], "Resources": [[4, "resources"]], "Robust Estimation Problems": [[6, null]], "Robust Registration Problems": [[6, "robust-registration-problems"]], "RobustPoseLifter": [[2, "robustposelifter"]], "Rotation Averaging": [[7, "rotation-averaging"]], "SDP Relaxation": [[11, "sdp-relaxation"]], "Setting up mosek license on server": [[4, "setting-up-mosek-license-on-server"]], "Standard Estimation Problems": [[7, null]], "StateLifter": [[2, "statelifter"]], "Stereo-Camera Localization": [[7, "stereo-camera-localization"]], "StereoLifter": [[2, "stereolifter"]], "Templates": [[8, null]], "Testing": [[4, "testing"]], "Testing Github actions locally": [[4, "testing-github-actions-locally"]], "Toy Examples": [[9, null]], "Univariate Polynomials": [[9, "univariate-polynomials"]], "Utils": [[3, null]], "Welcome to POPCOR!": [[10, null]], "What\u2019s new": [[12, null]], "Who this tool is for": [[10, "who-this-tool-is-for"]], "[0.0.1] - 2025-05-25": [[12, "id1"]], "[Unreleased] \u2013 YYYY-MM-DD": [[12, "unreleased-yyyy-mm-dd"]]}, "docnames": ["api", "api/algorithms", "api/lifters", "api/utils", "contributing", "examples", "examples/robust", "examples/standard", "examples/templates", "examples/toy", "index", "quickstart", "whatsnew"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.intersphinx": 1, "sphinx.ext.todo": 2, "sphinx.ext.viewcode": 1}, "filenames": ["api.rst", "api/algorithms.rst", "api/lifters.rst", "api/utils.rst", "contributing.rst", "examples.rst", "examples/robust.rst", "examples/standard.rst", "examples/templates.rst", "examples/toy.rst", "index.rst", "quickstart.rst", "whatsnew.rst"], "indexentries": {"apply() (popcor.autotemplate method)": [[1, "popcor.AutoTemplate.apply", false]], "autotemplate (class in popcor)": [[1, "popcor.AutoTemplate", false]], "autotight (class in popcor)": [[1, "popcor.AutoTight", false]], "constraint (class in popcor.utils.constraint)": [[3, "popcor.utils.constraint.Constraint", false]], "create_symmetric() (in module popcor.utils.common)": [[3, "popcor.utils.common.create_symmetric", false]], "diag_indices() (in module popcor.utils.common)": [[3, "popcor.utils.common.diag_indices", false]], "examplelifter (class in popcor.examples)": [[8, "popcor.examples.ExampleLifter", false]], "get_a_known() (popcor.base_lifters.statelifter method)": [[2, "popcor.base_lifters.StateLifter.get_A_known", false]], "get_a_learned() (popcor.autotight static method)": [[1, "popcor.AutoTight.get_A_learned", false]], "get_b_known() (popcor.base_lifters.statelifter method)": [[2, "popcor.base_lifters.StateLifter.get_B_known", false]], "get_cost() (popcor.base_lifters.statelifter method)": [[2, "popcor.base_lifters.StateLifter.get_cost", false]], "get_duality_gap() (popcor.autotight static method)": [[1, "popcor.AutoTight.get_duality_gap", false]], "get_q() (popcor.base_lifters.statelifter method)": [[2, "popcor.base_lifters.StateLifter.get_Q", false]], "get_theta() (popcor.base_lifters.statelifter method)": [[2, "popcor.base_lifters.StateLifter.get_theta", false]], "get_vec() (in module popcor.utils.common)": [[3, "popcor.utils.common.get_vec", false]], "get_x() (popcor.base_lifters.statelifter method)": [[2, "popcor.base_lifters.StateLifter.get_x", false]], "local_solver() (popcor.base_lifters.statelifter method)": [[2, "popcor.base_lifters.StateLifter.local_solver", false]], "module": [[3, "module-popcor.utils.common", false]], "monolifter (class in popcor.examples)": [[6, "popcor.examples.MonoLifter", false]], "poly4lifter (class in popcor.examples)": [[9, "popcor.examples.Poly4Lifter", false]], "poly6lifter (class in popcor.examples)": [[9, "popcor.examples.Poly6Lifter", false]], "polylifter (class in popcor.base_lifters)": [[2, "popcor.base_lifters.PolyLifter", false]], "popcor.utils.common": [[3, "module-popcor.utils.common", false]], "rangeonlyloclifter (class in popcor.examples)": [[7, "popcor.examples.RangeOnlyLocLifter", false]], "ravel_multi_index_triu() (in module popcor.utils.common)": [[3, "popcor.utils.common.ravel_multi_index_triu", false]], "robustposelifter (class in popcor.base_lifters)": [[2, "popcor.base_lifters.RobustPoseLifter", false]], "rotationlifter (class in popcor.examples)": [[7, "popcor.examples.RotationLifter", false]], "run() (popcor.autotemplate method)": [[1, "popcor.AutoTemplate.run", false]], "sample_parameters() (popcor.base_lifters.statelifter method)": [[2, "popcor.base_lifters.StateLifter.sample_parameters", false]], "sample_theta() (popcor.base_lifters.statelifter method)": [[2, "popcor.base_lifters.StateLifter.sample_theta", false]], "statelifter (class in popcor.base_lifters)": [[2, "popcor.base_lifters.StateLifter", false]], "stereo1dlifter (class in popcor.examples)": [[9, "popcor.examples.Stereo1DLifter", false]], "stereo2dlifter (class in popcor.examples)": [[7, "popcor.examples.Stereo2DLifter", false]], "stereo3dlifter (class in popcor.examples)": [[7, "popcor.examples.Stereo3DLifter", false]], "stereolifter (class in popcor.base_lifters)": [[2, "popcor.base_lifters.StereoLifter", false]], "unravel_multi_index_triu() (in module popcor.utils.common)": [[3, "popcor.utils.common.unravel_multi_index_triu", false]], "upper_triangular() (in module popcor.utils.common)": [[3, "popcor.utils.common.upper_triangular", false]], "wahbalifter (class in popcor.examples)": [[6, "popcor.examples.WahbaLifter", false]]}, "objects": {"popcor": [[1, 0, 1, "", "AutoTemplate"], [1, 0, 1, "", "AutoTight"]], "popcor.AutoTemplate": [[1, 1, 1, "", "apply"], [1, 1, 1, "", "run"]], "popcor.AutoTight": [[1, 1, 1, "", "get_A_learned"], [1, 1, 1, "", "get_duality_gap"]], "popcor.base_lifters": [[2, 0, 1, "", "PolyLifter"], [2, 0, 1, "", "RobustPoseLifter"], [2, 0, 1, "", "StateLifter"], [2, 0, 1, "", "StereoLifter"]], "popcor.base_lifters.StateLifter": [[2, 1, 1, "", "get_A_known"], [2, 1, 1, "", "get_B_known"], [2, 1, 1, "", "get_Q"], [2, 1, 1, "", "get_cost"], [2, 1, 1, "", "get_theta"], [2, 1, 1, "", "get_x"], [2, 1, 1, "", "local_solver"], [2, 1, 1, "", "sample_parameters"], [2, 1, 1, "", "sample_theta"]], "popcor.examples": [[8, 0, 1, "", "ExampleLifter"], [6, 0, 1, "", "MonoLifter"], [9, 0, 1, "", "Poly4Lifter"], [9, 0, 1, "", "Poly6Lifter"], [7, 0, 1, "", "RangeOnlyLocLifter"], [7, 0, 1, "", "RotationLifter"], [9, 0, 1, "", "Stereo1DLifter"], [7, 0, 1, "", "Stereo2DLifter"], [7, 0, 1, "", "Stereo3DLifter"], [6, 0, 1, "", "WahbaLifter"]], "popcor.utils": [[3, 2, 0, "-", "common"]], "popcor.utils.common": [[3, 3, 1, "", "create_symmetric"], [3, 3, 1, "", "diag_indices"], [3, 3, 1, "", "get_vec"], [3, 3, 1, "", "ravel_multi_index_triu"], [3, 3, 1, "", "unravel_multi_index_triu"], [3, 3, 1, "", "upper_triangular"]], "popcor.utils.constraint": [[3, 0, 1, "", "Constraint"]]}, "objnames": {"0": ["py", "class", "Python class"], "1": ["py", "method", "Python method"], "2": ["py", "module", "Python module"], "3": ["py", "function", "Python function"]}, "objtypes": {"0": "py:class", "1": "py:method", "2": "py:module", "3": "py:function"}, "terms": {"": 11, "0": [2, 3, 6, 7, 9, 11], "001": [], "05": [], "05783": [], "07": [], "08": [], "1": [2, 3, 6, 7, 9, 11], "10": [10, 11], "100": [], "10000": [], "11": 4, "1109": [10, 11], "1d": 9, "1e": 11, "1x": [], "1z": [], "2": [2, 3, 4, 6, 7, 9, 11], "2024": [10, 11], "2308": [], "2d": [2, 7], "2x": [], "2z": [], "3": [2, 6, 11], "3454570": [10, 11], "3d": [2, 7], "4": 7, "40": [10, 11], "4338": [10, 11], "4358": [10, 11], "5": 11, "500px": [], "5dbc": 4, "5fe1": 4, "A": [2, 9, 10, 11], "By": [2, 11], "For": [2, 10, 11], "If": [1, 2, 4, 10, 11], "In": [2, 10], "It": [2, 4, 11, 12], "One": [], "Such": [], "The": [2, 6, 10, 11, 12], "Then": 11, "There": [2, 4], "These": [10, 11], "To": [4, 8], "_": [], "a0": [], "a0_i": [], "a0_x": [], "a0_z": [], "a1": [], "a1_i": [], "a1_x": [], "a1_z": [], "a2": [], "a2_i": [], "a2_x": [], "a2_z": [], "a3": [], "a3_i": [], "a3_x": [], "a3_z": [], "a_0": 11, "a_curr": [], "a_ful": 3, "a_i": 11, "a_j": 9, "a_k": 7, "a_known": [1, 11], "a_learn": 11, "a_list": 11, "a_poli": 3, "a_r": 11, "a_spars": 3, "a_sparse_": [], "ab": 11, "abc": 2, "abil": 4, "about": 2, "abov": 10, "abstract": 2, "academ": 10, "act": 4, "actual": [], "ad": [1, 11], "add": [1, 2, 4, 11], "add_redund": 2, "addit": 11, "additon": [], "adher": 12, "admit": 10, "advanc": 11, "after": [], "again": 11, "agro": [10, 11], "aid": 11, "aim": 4, "algebra": [], "algorithm": [0, 10], "align": 11, "all": [1, 2, 4, 11, 12], "allow": 10, "along": [], "alpha_k": [], "alreadi": 4, "also": [2, 4, 9, 10, 11], "altern": [], "alwai": 11, "amen": 10, "aml": [], "an": [2, 4, 6, 10, 11], "analog": 7, "angl": [], "ani": [4, 10, 11], "api": [], "appli": [1, 11], "apply_templ": [], "appropri": 2, "aquadrat": 7, "ar": [2, 4, 6, 7, 9, 10, 11], "arg": 2, "argument": [], "around": [], "articl": 10, "arxiv": [], "assert": 11, "assum": 7, "augment": 1, "author": 10, "auto_templ": 11, "auto_tight": 11, "automat": [1, 10, 11], "autotempl": [0, 5, 10], "autotight": [0, 5, 10], "autotight2024": 10, "avail": 4, "averag": 5, "axi": [], "b": [2, 3, 7, 9, 11], "b_": [], "b_0": 11, "b_full": 3, "b_i": 11, "b_j": 11, "bare": 8, "barebon": 8, "barfoot": [10, 11], "base": [0, 4, 6, 7, 8, 9, 12], "base_lift": [2, 11], "baseclass": 2, "baselin": 7, "bash": 4, "basi": [1, 2], "basic": [0, 4, 11], "basis_known": [], "becaus": 11, "been": [4, 10], "begin": [7, 11], "belong": 10, "below": [2, 4, 12], "ben": 10, "besid": 10, "best": [2, 4], "beta": [2, 6], "between": [], "bin": 4, "binari": 7, "bit": 11, "black": 4, "block": 4, "bmatrix": 7, "bool": [1, 2], "both": [2, 11], "bridg": [], "build": 2, "built": [], "c": 11, "c_u": 7, "c_v": 7, "calibr": 7, "call": [4, 10, 11], "came": 4, "camera": 5, "can": [1, 4, 8, 10, 11], "case": [2, 11], "cd": 11, "cdot": 11, "center": 7, "cert_tool": 11, "certifi": 11, "cg": [], "challeng": [], "chang": [3, 11], "changelog": [4, 12], "check": 11, "choic": 10, "cite": 10, "clarif": [], "class": [1, 2, 3, 6, 7, 8, 9, 11], "classic": 11, "clean_constraint": [], "clone": 11, "code": [2, 4, 11], "com": [4, 11, 12], "come": [2, 8], "common": 3, "comput": 2, "conda": 11, "condit": [], "connor": 10, "consid": [1, 2, 4], "constant": 11, "constraint": [0, 1, 2, 11], "constraint_learn": 12, "construct": 2, "constructor": [], "contact": 11, "contain": [4, 11], "content": 4, "context": [], "contribut": [], "control": 11, "convent": 11, "converg": [10, 11], "convert": 3, "convex": [10, 11], "coordin": 7, "copi": [4, 8], "core": [0, 4], "correct": 3, "correspond": [2, 10, 11], "correspondng": [], "cost": [2, 7, 9, 11], "cost_loc": 1, "cost_optimum": 11, "cost_sdp": 1, "could": [7, 10], "coupl": [2, 4], "creat": [2, 3, 4, 8, 10, 11], "create_symmetr": 3, "creator": 10, "csc_matrix": [], "csr_matrix": 3, "curl": 4, "current": [7, 10], "custom": 11, "cvxpy": [], "cx": [], "cy": [], "cz": [], "d": [2, 6, 7, 10], "d_": 7, "data": 11, "debug": 4, "def": [], "default": 2, "defin": [2, 11], "degre": [2, 9], "delta": [], "denot": 11, "depend": 4, "depth": [], "deriv": 11, "describ": 10, "design": 10, "detail": [6, 7, 10], "determin": [], "develop": [2, 4], "diag_indic": 3, "diagon": 3, "dict": 2, "dictionari": [1, 2], "did": 4, "differ": 7, "difficult": 10, "dim_z": [], "dimension": 2, "direct": 4, "directli": [], "discuss": [], "distanc": 7, "dl": [], "do": [4, 10, 11], "doc": 4, "docker": 4, "doctest": 4, "document": [3, 4, 10, 12], "doi": [10, 11], "done": 4, "down": 4, "drawn": [], "driven": [], "drop": [], "dual": [], "dualiti": 11, "duembgen": 11, "dynam": 11, "d\u00fcmbgen": [10, 11], "e": 10, "e_ik": [], "earlier": 11, "eas": 2, "easi": 4, "easier": 10, "edg": 7, "effici": 2, "effort": [], "element": [2, 3, 7, 11], "empti": [], "enabl": 11, "encorc": 11, "encourag": 4, "end": [7, 11], "end_licens": 4, "enforc": 1, "engin": 10, "env": [4, 11], "environ": 11, "ep": [], "eps_spars": 3, "eps_svd": [], "equal": [2, 11], "equival": [3, 7, 11], "error": [4, 11], "essenti": 11, "estim": [5, 10, 11], "etc": 11, "evalu": 11, "even": 11, "exactli": 11, "exampl": [4, 6, 7], "examplelift": [4, 8], "except": 11, "experi": 7, "extend": 4, "extens": [], "extract": [2, 11], "extract_known_templ": [], "extrins": [2, 6], "f": [7, 9, 11], "f_u": 7, "f_v": 7, "facilit": 4, "fact": 11, "factor": [2, 11], "fals": [1, 2, 3, 6, 11], "famili": 10, "feasibl": [2, 11], "featur": 4, "feel": 4, "figur": [], "file": [4, 8, 11, 12], "fill": 8, "find": [1, 11], "first": 11, "fix": [], "flat_indic": 3, "flatten": [7, 11], "flip": [], "float": [2, 11], "focal": 7, "focu": 10, "focus": [], "folder": 4, "follow": [4, 7, 9, 11], "foral": 11, "form": [2, 7, 11], "format": [2, 4, 12], "formul": [4, 8, 10], "found": 11, "fourth": 9, "frac": 7, "frame": 7, "frederik": 10, "free": [4, 10], "from": [3, 4, 7, 11], "function": [2, 6, 7, 9, 11], "futur": [3, 4], "g": 11, "gap": 11, "gener": [1, 2, 6, 11], "generaliz": 11, "generate_matric": [], "generate_matrices_simpl": [], "generate_random_landmark": [], "generate_random_posit": [], "geometri": [], "geq": 11, "get": [2, 3, 10, 11], "get_a0": 11, "get_a_b_list": 11, "get_a_known": [1, 2, 11], "get_a_known_redund": [], "get_a_learn": [1, 11], "get_b_known": [2, 11], "get_basi": [], "get_cost": 2, "get_duality_gap": [1, 11], "get_grad": [], "get_hess": [], "get_level_dim": [], "get_positions_and_landmark": [], "get_q": [2, 11], "get_q_from_i": [], "get_random_posit": [], "get_sufficient_templ": [], "get_theta": 2, "get_vec": 3, "get_vec_around_gt": [], "get_x": [2, 11], "git": 11, "github": [10, 11, 12], "githubusercont": 4, "given": [1, 2, 3, 7, 11], "global": [9, 10, 11], "go": 4, "goal": [2, 6], "good": [], "gradient": [], "ground": [2, 11], "gt": [], "guid": [9, 10], "guidelin": [], "h": 11, "h_1": [], "h_2": [], "h_i": [], "h_list": [], "ha": [4, 10, 11], "half": 3, "hard": 10, "have": [1, 2, 10, 11], "help": 10, "helper": 0, "here": [4, 7, 10, 11], "hessian": [], "high": 10, "higher": 2, "hold": 11, "holm": [10, 11], "hom": 2, "homogen": [7, 11], "horizont": 7, "how": [10, 11], "howev": [], "http": [4, 12], "i": [1, 2, 3, 4, 6, 7, 9, 11, 12], "identifi": 10, "ieee": [10, 11], "imag": 7, "implement": [2, 6, 7, 8, 11], "import": 11, "improv": 11, "includ": [4, 10, 11, 12], "incorpor": [], "increment": 1, "inde": [], "independ": [], "index": 3, "index_tupl": 3, "individu": 7, "inequ": [2, 11], "info": 11, "info_loc": 11, "info_rank": 11, "info_sdp": 11, "info_sdp_learn": 11, "inform": [1, 4, 11], "inherit": [2, 11], "initi": [10, 12], "inner": [3, 11], "input": [], "insid": 4, "inspir": [8, 10], "instal": [4, 10], "instanc": 11, "instead": 2, "instruct": 10, "interest": [], "interpret": [], "introduc": 11, "invers": 2, "invert": 11, "involv": 2, "ipopt": 2, "isort": 4, "issu": 10, "iter": 2, "its": 11, "itself": [], "j": [7, 9, 11], "journal": 10, "just": [2, 4], "k": 7, "keep": [1, 4, 12], "kei": 2, "known": [1, 2, 3, 7, 11], "kron": [], "kroneck": 3, "kth": 7, "kwarg": 2, "l_k": [], "lagrangian": [], "landmark": [7, 9, 11], "landmarks_theta": [], "langl": 11, "languag": [], "larger": 11, "lasserr": [], "last": [], "lead": [10, 11], "learn": [1, 11], "learner": [], "least": 4, "left": 7, "len": [], "length": 7, "let": [], "level": [2, 6, 7, 10], "lfiter": 1, "licens": 10, "lie": [], "lift": [2, 6, 11], "lifter": [0, 1, 8, 9], "lifting_funct": [], "likelihood": 11, "linalg_tool": 11, "line": [2, 6, 11], "linear": 11, "linearli": [], "link": 4, "list": [1, 2, 4], "liter": 4, "live": 4, "local": [2, 5, 9, 10, 11], "local_solv": [2, 11], "local_solver_manopt": [], "log": [], "look": 8, "loss": [2, 6], "low": 2, "m": 7, "mai": [2, 3], "mail": 10, "main": [3, 11], "make": 4, "mani": [10, 11], "mark": 7, "master": 4, "mat": 3, "mat_param_dict": 3, "mat_var_dict": 3, "math": [], "mathemat": [], "matric": [2, 11], "matrix": [0, 2, 7, 11], "max_dist": [], "max_iter": [], "maxima": 9, "maximum": [9, 11], "md": [4, 12], "mean": 10, "meant": 10, "measur": [2, 6, 7, 9], "method": [1, 10], "might": [], "min_": 11, "min_gradient_norm": [], "min_step_s": [], "min_t": [], "minim": [4, 7, 9], "minima": [9, 10], "minimum": [8, 9, 11], "mismatch": 11, "miss": [7, 8], "model": [], "modul": [], "monocular": [], "monolift": [2, 6], "more": [2, 3, 4, 6, 7, 11], "mosek": 10, "moseklm_license_fil": 4, "most": 2, "mostli": [2, 9], "motion": [], "msk_licens": 4, "mtild": [], "much": [], "multi_index_triu": 3, "multipl": 4, "must": [1, 2], "mutabl": [], "n": [3, 4, 7, 9], "n_landmark": [2, 6, 7, 9, 11], "n_mea": 7, "n_outlier": [2, 6], "n_paramet": 2, "n_posit": 7, "n_seed": 11, "name": [2, 4, 6], "natur": 4, "ndarrai": [2, 3], "need": [10, 11], "neg": 11, "nekto": 4, "new": [1, 2, 8, 11], "new_lift": 11, "new_ord": [], "next": 11, "nice": 10, "nk": 7, "nois": [2, 11], "non": [], "nonconvex": 11, "none": [1, 2, 3, 6, 7, 11], "normal": [], "notabl": 12, "note": [2, 7, 11], "now": 12, "np": [3, 10, 11], "nth": 7, "nullspac": [], "num_it": [], "number": [], "numer": [], "nxn": 3, "object": 1, "obtain": 11, "often": [10, 11], "onc": 3, "one": [2, 7, 9, 11], "ones": 1, "ongo": [], "onli": [2, 3, 5, 11], "onto": [], "oper": [0, 11], "optim": 11, "optimum": 11, "order": 10, "org": [], "orient": [], "origin": [2, 10, 11], "orthogon": 1, "other": [5, 11], "otherwis": 11, "otim": [2, 6], "our": 10, "out": [1, 2, 10, 11], "outlier": 11, "output": 11, "output_poli": 2, "overview": [0, 10], "overwrit": 2, "own": 2, "p": [3, 11], "p_i": 7, "p_j": 7, "p_n": 7, "p_r": 11, "p_w": [], "packag": 10, "page": [3, 4, 10], "pair": 2, "paper": [6, 7, 9, 10], "param": [], "param_dict": 2, "param_level": [2, 6, 7, 8, 9, 11], "paramet": [1, 2, 3, 11], "parameter": 7, "parametr": 11, "part": [3, 8], "particular": [2, 10], "pass": 4, "pedagog": [2, 9], "pendulum": 11, "peopl": [], "pepit": 10, "pi": [], "pixel": 7, "pj": [], "plan": 11, "plane": [2, 6], "pleas": [4, 10], "plot": [1, 11], "plr": 6, "point": [2, 6, 7, 11], "poly4lift": [9, 11], "poly6lift": 9, "poly_typ": 9, "polylift": [0, 9], "polymatrix": 2, "polynomi": [2, 5, 11], "polyrow": [], "polyrow_a": 3, "polyrow_b": 3, "poor": 10, "pop": 11, "popcor": [1, 2, 3, 6, 7, 8, 9, 11], "popr": 11, "popul": [], "pose": [2, 6], "posit": 7, "possibl": [4, 11], "posteriori": 11, "powel": [], "power": [], "pp": 11, "ppr": 6, "ppt": [], "pre": 10, "preserv": 3, "principl": 11, "print": 1, "problem": [2, 4, 5, 8, 10], "process": 4, "product": [3, 11], "progress": [], "project": 12, "properti": [], "proto": 4, "provid": [2, 4, 9, 11], "pt": 4, "public": [], "pull": 4, "purpos": [2, 3, 9, 11], "pusher": 11, "py": [4, 11], "pymanopt": [], "pytest": [4, 11], "q": [2, 11], "q_j": 7, "qcqp": 2, "qr": 1, "qrp": 1, "quad": 7, "quadrat": 11, "question": 10, "quick": [9, 10], "quickli": 10, "quickstart": 4, "quit": [], "qx": 11, "r": 11, "rais": 11, "random": [2, 10], "randomli": [2, 11], "rang": [5, 11], "rangeonlylift": [], "rangeonlyloc": [], "rangeonlyloclift": 7, "rangeonlyslam1": [], "rangeonlyslam1lift": [], "rangeonlyslam2": [], "rangeonlyslam2lift": [], "rangl": 11, "rank": [], "rank_project": 11, "rather": 11, "ravel_multi_index_triu": 3, "raw": 4, "reach": 1, "read": [], "recommend": 1, "recomput": 3, "recurs": 11, "redo": 4, "redund": [2, 11], "ref": [], "refer": [2, 10], "reformul": 11, "regener": [], "registr": [2, 5], "regress": [2, 6], "regular": 7, "relat": 11, "relax": 10, "releas": [10, 12], "relev": [], "remove_depend": [], "remove_gaug": [], "remove_imprecis": [], "render": [4, 12], "replac": 10, "repo": 4, "repositori": 10, "represent": 3, "request": 4, "resample_landmark": [], "research": [], "return": [1, 2, 3, 11], "rich": 11, "right": 7, "robot": 11, "roboticist": 10, "robust": [2, 5, 11], "robustposelift": [0, 4, 6], "rotat": 5, "rotationlift": 7, "row": [], "rst": 4, "run": [1, 4, 9, 10, 11], "sai": 11, "same": [10, 11], "sampl": [1, 2, 11], "sample_paramet": 2, "sample_theta": [2, 11], "saniti": 11, "satisfi": 11, "save": [], "sceleton": [], "scratch": 11, "sdp": [], "sdp_solver": 11, "second": [], "secret": 4, "section": [4, 11], "secur": 4, "see": [2, 4, 6], "seen": 11, "self": 2, "semant": 12, "semidefinit": [10, 11], "sequenc": [], "serv": [2, 3], "set": [1, 2, 10, 11], "setup": 4, "sh": 4, "shape": 3, "share": 4, "ship": [], "should": [4, 11], "show": 11, "shown": 10, "significantli": 3, "similar": 4, "simpl": [2, 9, 11], "sixth": 9, "size": [2, 11], "skeleton": [4, 11], "slam": 11, "slider": 11, "slightli": 11, "snippet": 11, "so": [2, 7, 10, 11], "solut": [10, 11], "solv": [2, 10], "solve_sdp": 11, "solver": [2, 4, 11], "solver_kwarg": [], "some": [2, 11], "someth": 4, "sometim": [], "soon": 8, "sourc": [1, 2, 3, 4, 6, 7, 8, 9], "spars": [2, 3], "specif": 2, "specifi": [], "split": 7, "spmatrix": 3, "squar": [], "ssf": 4, "standard": [5, 11], "star": 11, "starndard": [2, 6], "start": [2, 4, 9, 10], "start_licens": 4, "state": [2, 10, 11], "statelift": [0, 1, 7, 8, 9, 11], "static": 1, "std": [], "stereo": [2, 5, 9], "stereo1d_lift": 11, "stereo1dlift": [9, 11], "stereo2dlift": [2, 7], "stereo3dlift": [2, 7], "stereolift": [0, 4, 7], "still": [], "str": 1, "straightforward": 11, "strong": 11, "submodul": [11, 12], "substitut": [7, 11], "succeq": 11, "success": 11, "sudo": 4, "suggest": 10, "sum_": [7, 9], "super": 4, "support": 2, "sure": 4, "sv": 4, "svd": 1, "symmetr": 3, "system": 11, "t": [2, 7, 11], "t0": 2, "t_i": [], "t_init": [], "t_n": [], "take": 8, "taken": 4, "tau_i": [], "tempalt": 1, "templat": [0, 1, 2, 5, 11], "template_idx": 3, "term": [7, 11], "termin": 11, "terminologi": 2, "test": [2, 9, 10, 11], "test_autotight": 4, "test_constraint": 11, "test_doc": [], "test_quickstart": 11, "test_setup_problem": [], "testabl": 4, "text": [4, 11], "textsc": [], "th": [], "than": [3, 11], "thei": [10, 11], "them": 11, "theta": [2, 7, 9, 11], "theta_pick": 11, "theta_random": 11, "theta_round": 11, "thi": [2, 3, 4, 6, 7, 8, 9, 11, 12], "thing": [], "third": [], "those": 11, "through": 4, "thu": [], "tight": [1, 10, 11], "tighten": 10, "time": 1, "timothi": 10, "titl": 10, "tlsv1": 4, "todo": [], "toi": 5, "tol": [], "toler": 11, "tool": 4, "toolbox": [10, 12], "top": 11, "topic": [], "topqx": [], "topqxrangl": [], "touch": 10, "toward": [10, 11], "trace": 11, "track": 4, "transact": [10, 11], "transform": [], "translat": [], "treat": [6, 7], "triangular": 3, "tro": [10, 11], "true": [1, 2, 3, 11], "trunc_tol": [], "truth": [2, 11], "try": [4, 10], "tupl": [], "turn": 7, "two": [7, 9], "tx": [], "ty": [], "type": 9, "typic": [], "tz": [], "u": [], "u_j": [7, 9], "u_xj": [], "u_yj": [], "u_zj": [], "under": [6, 11], "understand": 2, "unfinish": 3, "uniformli": [], "unit": 4, "univari": [2, 5], "unknown": [2, 6, 7], "unravel_multi_index_triu": 3, "unreleas": [], "until": 1, "up": 10, "upper": 3, "upper_triangular": 3, "us": [1, 2, 3, 4, 7, 8, 9, 10], "usag": 11, "use_cliqu": [], "use_increment": 1, "use_kei": [], "use_known": [1, 11], "user": 11, "utiasasrl": 12, "util": 0, "valid": 10, "valu": 11, "var_dict": [1, 2], "var_kei": [], "var_subset": 2, "variabl": [1, 2, 4, 11], "variable_list": [1, 2, 6, 7], "vec": 3, "vector": [0, 2, 7, 11], "vectorzi": [], "verbos": 1, "version": [3, 7, 11, 12], "vertic": 7, "via": 10, "vol": 11, "volum": 10, "w": [2, 6, 7], "w_": 7, "wa": [], "wahba": [], "wahbalift": [2, 6], "wai": [], "want": [4, 10], "wast": [], "we": [1, 2, 4, 7, 9, 10, 11], "webp": [], "weight": [7, 11], "welcom": 4, "well": 4, "were": 2, "when": [2, 10, 11], "where": [7, 9, 11], "whether": 1, "which": [2, 10, 11], "while": [10, 11], "why": 11, "width": [], "wish": [], "without": 11, "work": 10, "workflow": 4, "world": [], "would": [], "write": 11, "written": 11, "x": [2, 6, 11], "x_10": 11, "x_i": 7, "x_iy_i": 7, "x_random": 11, "xj": [], "xwt": [2, 6], "xx": 11, "xxt": [2, 6], "y": [2, 11], "y_": 11, "y_i": 7, "year": 10, "yet": 11, "yj": [], "yml": 11, "you": [4, 8, 10, 11], "your": [2, 4, 8, 10], "z": [], "z_0": [], "z_1": [], "z_i": 7, "zj": []}, "titles": ["API and modules", "Core Algorithms", "Base Lifters", "Utils", "Contributing", "Examples", "4. Robust Estimation Problems", "3. Standard Estimation Problems", "1. Templates", "2. Toy Examples", "Welcome to POPCOR!", "Quick Start Guide", "What\u2019s new"], "titleterms": {"": 12, "0": 12, "05": 12, "1": 12, "2025": 12, "25": 12, "action": 4, "ad": [4, 12], "algebra": [], "algorithm": 1, "api": 0, "autotempl": [1, 8, 11], "autotight": [1, 8, 11], "averag": 7, "background": [], "base": 2, "basic": 2, "camera": 7, "can": [], "cannot": [], "certifi": 10, "chang": 12, "changelog": [], "class": 4, "constraint": 3, "content": [0, 5], "contribut": 4, "core": 1, "credit": 10, "dd": 12, "depend": 10, "differ": 11, "do": [], "document": [], "estim": [6, 7], "etc": [], "exampl": [5, 8, 9, 11], "examplelift": [], "find": [], "fix": 12, "formul": 11, "function": 4, "gener": 4, "geometri": [], "github": 4, "guid": 11, "guidelin": 4, "helper": 3, "how": 4, "i": 10, "instal": 11, "instanti": 11, "licens": 4, "lie": [], "lifter": [2, 4, 11], "local": [4, 7], "log": 12, "matrix": 3, "method": 11, "mm": 12, "modul": 0, "mosek": 4, "new": [4, 12], "onli": 7, "oper": 3, "optim": 10, "other": 9, "overview": 2, "polylift": 2, "polynomi": [9, 10], "popcor": 10, "popr": 4, "problem": [6, 7, 11], "progress": [], "purpos": 10, "qcqp": 11, "quick": 11, "rang": 7, "rangeonlylift": [], "rank": 11, "redund": [], "refer": 11, "registr": 6, "relax": 11, "resourc": 4, "robot": 10, "robust": 6, "robustposelift": 2, "rotat": 7, "sdp": 11, "server": 4, "set": 4, "simpl": [], "solv": 11, "standard": 7, "start": 11, "statelift": 2, "stereo": 7, "stereolift": 2, "templat": [3, 8], "test": 4, "thi": 10, "tighten": 11, "toi": 9, "tool": 10, "univari": 9, "unreleas": 12, "up": 4, "us": 11, "usag": [], "util": 3, "vector": 3, "welcom": 10, "what": 12, "who": 10, "work": [], "yyyi": 12}}) \ No newline at end of file diff --git a/docs/build/whatsnew.html b/docs/build/whatsnew.html deleted file mode 100644 index 63c8b07..0000000 --- a/docs/build/whatsnew.html +++ /dev/null @@ -1,298 +0,0 @@ - - - - - - - - - What’s new — POPCOR 0.0.1 documentation - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
- - - GitHub logo - Go to GitHub source code - -
-
-
-
- -
-

What’s new

-

Below is a rendering of the CHANGELOG.md file.

-
-

Change Log

-

All notable changes to this project will be documented in this file.

-

The format is based on Keep a Changelog -and this project adheres to Semantic Versioning.

-
-

[Unreleased] – YYYY-MM-DD

-
-

Added

-
-
-

Changed

-
-
-

Fixed

-
-
-
-

[0.0.1] - 2025-05-25

-

This is the initial release of the toolbox. It is based on https://github.com/utiasASRL/constraint_learning and now included there as a submodule.

-
-
-
- - -
-
- -
-
-
-
- - - - GitHub logo - - - - - \ No newline at end of file diff --git a/docs/build/whatsnew/0.0.1.html b/docs/build/whatsnew/0.0.1.html deleted file mode 100644 index a71b981..0000000 --- a/docs/build/whatsnew/0.0.1.html +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - <no title> — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- - - -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/whatsnew/CHANGELOG.html b/docs/build/whatsnew/CHANGELOG.html deleted file mode 100644 index ac29f44..0000000 --- a/docs/build/whatsnew/CHANGELOG.html +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - <no title> — POPR 0.0.1 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- - -

[1] F. Dümbgen, C. Holmes, B. Agro and T. Barfoot, "Toward Globally Optimal State Estimation Using Automatically Tightened Semidefinite Relaxations," in IEEE Transactions on Robotics, vol. 40, pp. 4338-4358, 2024, doi: 10.1109/TRO.2024.3454570. <https://arxiv.org/abs/2308.05783>_

- - -
-
-
- -
- -
-

© Copyright 2025, POPR Contributors.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/source/api/lifters.rst b/docs/source/api/lifters.rst index ea0a7d9..7d68b43 100644 --- a/docs/source/api/lifters.rst +++ b/docs/source/api/lifters.rst @@ -51,9 +51,16 @@ RobustPoseLifter :show-inheritance: :undoc-members: +RangeOnlyLifter +--------------- + +.. autoclass:: popcor.base_lifters.RangeOnlyLifter + :show-inheritance: + :undoc-members: + PolyLifter ---------- .. autoclass:: popcor.base_lifters.PolyLifter :show-inheritance: - :undoc-members: \ No newline at end of file + :undoc-members: diff --git a/docs/source/conf.py b/docs/source/conf.py index a0cad1f..e06c07c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -34,6 +34,7 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "myst_parser", "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.intersphinx", @@ -45,7 +46,6 @@ "sphinx.ext.napoleon", "sphinx.ext.autosummary", "sphinx.ext.autosectionlabel", - "myst_parser", "sphinx_copybutton", ] diff --git a/docs/source/examples/standard.rst b/docs/source/examples/standard.rst index 3936ee7..e2bf32c 100644 --- a/docs/source/examples/standard.rst +++ b/docs/source/examples/standard.rst @@ -4,7 +4,13 @@ Standard Estimation Problems Range-Only Localization ----------------------- -.. autoclass:: popcor.examples.RangeOnlyLocLifter +.. autoclass:: popcor.examples.RangeOnlySqLifter + :undoc-members: + :show-inheritance: + +.. autoclass:: popcor.examples.RangeOnlyNsqLifter + :undoc-members: + :show-inheritance: Stereo-Camera Localization @@ -23,4 +29,4 @@ Rotation Averaging .. autoclass:: popcor.examples.RotationLifter :undoc-members: - :show-inheritance: \ No newline at end of file + :show-inheritance: diff --git a/popcor/__init__.py b/popcor/__init__.py index 07aa66c..79b07d0 100644 --- a/popcor/__init__.py +++ b/popcor/__init__.py @@ -1,4 +1,4 @@ from .auto_template import AutoTemplate from .auto_tight import AutoTight -__version__ = "0.0.1" +__version__ = "0.0.2" diff --git a/popcor/base_lifters/__init__.py b/popcor/base_lifters/__init__.py index 2facb8c..20351d1 100644 --- a/popcor/base_lifters/__init__.py +++ b/popcor/base_lifters/__init__.py @@ -1,4 +1,5 @@ -from .poly_lifters import PolyLifter +from .poly_lifter import PolyLifter +from .range_only_lifter import RangeOnlyLifter from .robust_pose_lifter import RobustPoseLifter from .state_lifter import StateLifter from .stereo_lifter import StereoLifter diff --git a/popcor/base_lifters/poly_lifters.py b/popcor/base_lifters/poly_lifter.py similarity index 100% rename from popcor/base_lifters/poly_lifters.py rename to popcor/base_lifters/poly_lifter.py diff --git a/popcor/base_lifters/range_only_lifter.py b/popcor/base_lifters/range_only_lifter.py new file mode 100644 index 0000000..e8634ca --- /dev/null +++ b/popcor/base_lifters/range_only_lifter.py @@ -0,0 +1,378 @@ +from abc import ABC, abstractmethod + +import autograd.numpy as anp +import matplotlib.pylab as plt +import numpy as np +import scipy.sparse as sp +from scipy.optimize import minimize + +from .state_lifter import StateLifter + +NOISE = 1e-2 # std deviation of distance noise + +METHOD = "BFGS" +NORMALIZE = True + +# TODO(FD): parameters below are not all equivalent. +SOLVER_KWARGS = { + "BFGS": dict(gtol=1e-6, xrtol=1e-10), # relative step size + "Nelder-Mead": dict(xatol=1e-10), # absolute step size + "Powell": dict(ftol=1e-6, xtol=1e-10), + "TNC": dict(gtol=1e-6, xtol=1e-10), +} + +# size of the region of intereist: [0, SCALE]^d +SCALE = 2.0 + + +class RangeOnlyLifter(StateLifter, ABC): + """Range-only localization base class, in 2D or 3D. + + This is base class for different flavors of the range-only localization problem, + where the goal is to estimate positions from distance measurements to fixed landmarks. + + See :class:`~popcor.examples.RangeOnlyNsqLifter` and :class:`~popcor.examples.RangeOnlySqLifter` + for concrete implementations. + """ + + def __init__( + self, + n_positions, + n_landmarks, + d, + W=None, + level="no", + variable_list=None, + param_level="no", + ): + self.n_positions = n_positions + self.n_landmarks = n_landmarks + self.landmarks_ = None # will be set later + + if W is not None: + assert W.shape == (n_landmarks, n_positions) + self.W = W + else: + self.W = np.ones((n_positions, n_landmarks)) + self.y_ = None + + if variable_list == "all": + variable_list = self.get_all_variables() + + super().__init__( + level=level, d=d, variable_list=variable_list, param_level=param_level + ) + + @staticmethod + def create_bad_fixed(n_positions, n_landmarks, d=2): + assert n_positions == 1 + landmarks = np.random.rand(n_landmarks, d) + landmarks[:, 1] *= 0.1 # align landmarks along X. + theta = np.array([[0.2, 0.3]]).reshape((1, -1)) + return landmarks, theta + + @staticmethod + def create_bad(n_positions, n_landmarks, d=2): + # create landmarks that are roughly in a subspace of dimension d-1 + landmarks = np.hstack( # type: ignore + [ + np.random.rand(n_landmarks, d - 1) * SCALE, + np.random.rand(n_landmarks, 1) * 0.3 + SCALE / 2.0, + ] + ) + theta = np.hstack( + [ + np.random.rand(n_positions, d - 1) * SCALE, + np.max(landmarks[:, -1]) + np.random.rand(n_positions, 1), + ] + ) + return landmarks, theta + + @staticmethod + def create_good(n_positions, n_landmarks, d=2): + landmarks = np.random.rand(n_landmarks, d) + landmarks = (landmarks - np.min(landmarks, axis=0)) / ( + np.max(landmarks, axis=0) - np.min(landmarks, axis=0) + ) + # remove landmarks a bit from border for plotting reasons + landmarks = (landmarks + SCALE * 0.05) * SCALE * 0.9 + theta = np.random.uniform( + [np.min(landmarks, axis=0)], [np.max(landmarks, axis=0)] + ) + return landmarks, theta + + @property + def landmarks(self): + landmarks = np.random.rand(self.n_landmarks, self.d) + if self.landmarks_ is None: + self.landmarks_ = landmarks + return self.landmarks_ + + @landmarks.setter + def landmarks(self, landmarks): + assert landmarks.shape == (self.n_landmarks, self.d) + self.landmarks_ = landmarks + + @property + def VARIABLE_LIST(self): + return [ + [self.HOM, "x_0"], + [self.HOM, "x_0", "z_0"], + [self.HOM, "x_0", "z_0", "z_1"], + [self.HOM, "x_0", "x_1", "z_0", "z_1"], + ] + + @abstractmethod + def get_all_variables(self) -> list: + return [] + + @abstractmethod + def get_cost(self, theta, y, sub_idx=None, ad=False) -> float: + return 0.0 + + def get_vec_around_gt(self, delta: float = 0): + """Sample around ground truth. + :param delta: sample from gt + std(delta) (set to 0 to start from gt.) + """ + assert self.landmarks is not None, "landmarks must be set before sampling" + + if delta == 0: + return self.theta + else: + bbox_max = np.max(self.landmarks, axis=0) * 2 + bbox_min = np.min(self.landmarks, axis=0) * 2 + pos = ( + np.random.rand(self.n_positions, self.d) + * (bbox_max - bbox_min)[None, :] + + bbox_min[None, :] + ) + return pos.flatten() + + def sample_parameters(self, theta=None): + landmarks = np.random.rand(self.n_landmarks, self.d) + return self.sample_parameters_landmarks(landmarks) + + def overwrite_theta(self, theta): + """To bypass "theta can only be set once" check.""" + assert theta.shape == (self.n_positions, self.d) + self.theta_ = theta + + def sample_theta(self): + return np.random.rand(self.n_positions, self.d).flatten() + + def get_residuals(self, t, y, squared=True, ad=False): + positions = t.reshape((-1, self.d)) + sum = anp.sum if ad else np.sum # type: ignore + norm = anp.linalg.norm if ad else np.linalg.norm # type: ignore + + if squared: + y_current = sum( + (self.landmarks[None, :, :] - positions[:, None, :]) ** 2, axis=2 + ) + return self.W * (y**2 - y_current) + else: + y_current = norm( + (self.landmarks[None, :, :] - positions[:, None, :]), axis=2 + ) + return self.W * (y - y_current) + + def get_cost_from_res(self, residuals, sub_idx, ad=False): + """ + Get cost for given positions, landmarks and noise. + + :param t: flattened positions of length Nd + :param y: N x K distance measurements + """ + if ad: + if sub_idx is None: + cost = anp.sum(residuals**2) # type: ignore + else: + cost = anp.sum(residuals[sub_idx] ** 2) # type: ignore + if NORMALIZE: + cost /= anp.sum(self.W > 0) # type: ignore + else: + if sub_idx is None: + cost = np.sum(residuals**2) + else: + cost = np.sum(residuals[sub_idx] ** 2) + if NORMALIZE: + cost /= np.sum(self.W > 0) + return cost + + def simulate_y(self, noise: float | None = None, squared: bool = True): + assert self.landmarks is not None + # N x K matrix + if noise is None: + noise = NOISE + positions = self.theta.reshape(self.n_positions, -1) + y_gt = np.linalg.norm( + self.landmarks[None, :, :] - positions[:, None, :], axis=2 + ) + if squared: + return y_gt**2 + np.random.normal(loc=0, scale=noise, size=y_gt.shape) + else: + return y_gt + np.random.normal(loc=0, scale=noise, size=y_gt.shape) + + def get_Q(self, noise: float | None = None, output_poly: bool = False) -> tuple: + if self.y_ is None: + self.y_ = self.simulate_y(noise=noise) + Q = self.get_Q_from_y(self.y_, output_poly=output_poly) + + # DEBUGGING + x = self.get_x() + cost1 = x.T @ Q @ x + cost3 = self.get_cost(self.theta, self.y_) + assert abs(cost1 - cost3) < 1e-10 + return Q + + def get_sub_idx_x(self, sub_idx, add_z=True): + sub_idx_x = [0] + for idx in sub_idx: + sub_idx_x += [1 + idx * self.d + d for d in range(self.d)] + if not add_z: + return sub_idx_x + for idx in sub_idx: + sub_idx_x += [ + 1 + self.n_positions * self.d + idx * self.size_z + d + for d in range(self.size_z) + ] + return sub_idx_x + + def get_theta(self, x): + assert abs(x[0] - 1.0) > 1e-10 + # below is if we have order x_1, z_1, x_2, z_2, ... + # x.reshape((self.n_positions, -1))[:, : self.d].flatten("C") + # below is if we have order x_1, x_2, ..., z_1, z_2, ... + return x[: self.n_positions * self.d] + + def get_error(self, theta_hat, error_type="rmse", *args, **kwargs): + if error_type == "rmse": + return np.sqrt(np.mean((self.theta - theta_hat) ** 2)) + elif error_type == "mse": + return np.mean((self.theta - theta_hat) ** 2) + else: + raise ValueError(f"Unkwnon error_type {error_type}") + + def local_solver( + self, + t0, + y, + verbose=False, + method="BFGS", + solver_kwargs=SOLVER_KWARGS, + ): + """ + :param t_init: (positions, landmarks) tuple + """ + + use_autograd = False + if self.get_grad(t0, y) is None: + use_autograd = True + + if use_autograd: + from autograd import grad # , hessian + + def fun(x): + return self.get_cost(theta=x, y=y, ad=True) + + # TODO(FD): split problem into individual points. + options = solver_kwargs[method] + options["disp"] = verbose + sol = minimize( + fun, + x0=t0, + args=y, + jac=grad(fun), # type: ignore + # hess=hessian(fun), not used by any solvers + method=method, + options=options, + ) + else: + # TODO(FD): split problem into individual points. + options = solver_kwargs[method] + options["disp"] = verbose + sol = minimize( + self.get_cost, + x0=t0, + args=y, + jac=self.get_grad, + # hess=self.get_hess, not used by any solvers. + method=method, + options=options, + ) + + info = {} + info["success"] = sol.success + info["msg"] = sol.message + f"(# iterations: {sol.nit})" + if sol.success: + that = sol.x + rel_error = self.get_cost(that, y) - self.get_cost(sol.x, y) + assert abs(rel_error) < 1e-10, rel_error + residuals = self.get_residuals(that, y) + cost = sol.fun + info["max res"] = np.max(np.abs(residuals)) + hess = self.get_hess(that, y) + assert isinstance(hess, sp.csc_matrix) + eigs = np.linalg.eigvalsh(hess.toarray()) + info["cond Hess"] = eigs[-1] / eigs[0] + else: + that = cost = None + info["max res"] = None + info["cond Hess"] = None + info["cost"] = cost + return that, info, cost + + def plot(self, y=None, xlims=[0, 2], ylims=[0, 2], ax=None): + if ax is None: + fig, ax = plt.subplots() + fig.set_size_inches(5, 5) + else: + fig = plt.gcf() + + ax.scatter(*self.landmarks[:, :2].T, color="k", marker="+", label="landmarks") + ax.scatter(*self.theta[:, :2].T, color="C0", marker="o") + + im = None + if y is not None: + xs = np.linspace(xlims[0], xlims[1], 100) + ys = np.linspace(ylims[0], ylims[1], 100) + xx, yy = np.meshgrid(xs, ys) + zz = [ + self.get_cost(theta=np.array([xi, yi])[None, :], y=y) + for xi, yi in zip(xx.flatten(), yy.flatten()) + ] + im = ax.pcolormesh( + xx, + yy, + np.reshape(zz, xx.shape), + norm="log", + alpha=0.5, + vmin=1e-5, + vmax=1, + ) + ax.set_aspect("equal") + return fig, ax, im + + def get_valid_samples(self, n_samples): + samples = [self.sample_theta() for _ in range(n_samples)] + return np.vstack(samples) + + @property + @abstractmethod + def size_z(self) -> int: + return 1 + + @property + def param_dict(self): + return self.param_dict_landmarks + + @property + def N(self): + return self.n_positions * self.d + + @property + def M(self): + return self.n_positions * self.size_z + + def __repr__(self): + return f"rangeonlyloc{self.d}d_{self.level}" diff --git a/popcor/base_lifters/robust_pose_lifter.py b/popcor/base_lifters/robust_pose_lifter.py index b71e45d..dbce8b3 100644 --- a/popcor/base_lifters/robust_pose_lifter.py +++ b/popcor/base_lifters/robust_pose_lifter.py @@ -70,7 +70,7 @@ def __init__( The goal is to regress an unknown pose based on extrinsic measurements. - See :class:`~popcor.examples.WahbaLifter` for point-to-point registration and :class:`~popcor.examples.MonoLifter`) for point-to-line registration. + See class:`~popcor.examples.WahbaLifter` for point-to-point registration and :class:`~popcor.examples.MonoLifter`) for point-to-line registration. Implemented lifting functions are: diff --git a/popcor/base_lifters/state_lifter.py b/popcor/base_lifters/state_lifter.py index c5995a5..6360ab6 100644 --- a/popcor/base_lifters/state_lifter.py +++ b/popcor/base_lifters/state_lifter.py @@ -141,10 +141,10 @@ def get_involved_param_dict(self, var_subset): # CAN OPTINALLY OVERWRITE THESE FOR BETTER PERFORMANCE - def get_grad(self, theta, y=None) -> float: + def get_grad(self, theta, y=None) -> np.ndarray: raise NotImplementedError("must define get_grad if you want to use it.") - def get_hess(self, theta, y=None) -> float: + def get_hess(self, theta, y=None) -> np.ndarray: raise NotImplementedError("must define get_hess if you want to use it.") def get_cost(self, theta, y: np.ndarray | None = None) -> float: diff --git a/popcor/examples/__init__.py b/popcor/examples/__init__.py index 495ae9b..6694b44 100644 --- a/popcor/examples/__init__.py +++ b/popcor/examples/__init__.py @@ -2,7 +2,8 @@ from .mono_lifter import MonoLifter from .poly4_lifter import Poly4Lifter from .poly6_lifter import Poly6Lifter -from .range_only_lifters import RangeOnlyLocLifter +from .ro_nsq_lifter import RangeOnlyNsqLifter +from .ro_sq_lifter import RangeOnlySqLifter from .rotation_lifter import RotationLifter from .stereo1d_lifter import Stereo1DLifter from .stereo2d_lifter import Stereo2DLifter @@ -14,7 +15,8 @@ MonoLifter, Poly4Lifter, Poly6Lifter, - RangeOnlyLocLifter, + RangeOnlyNsqLifter, + RangeOnlySqLifter, RotationLifter, Stereo1DLifter, Stereo2DLifter, diff --git a/popcor/examples/mono_lifter.py b/popcor/examples/mono_lifter.py index f6bbad3..e35b672 100644 --- a/popcor/examples/mono_lifter.py +++ b/popcor/examples/mono_lifter.py @@ -20,13 +20,17 @@ class MonoLifter(RobustPoseLifter): - """This example is treated in more details in `this paper `_, - under the name "PLR" (point-to-line registration). - """ NOISE = 1e-3 # inlier noise NOISE_OUT = 0.1 # outlier noise + def __init__(self, *args, **kwargs): + """This example is treated in more details in `this paper `_, + under the name "PLR" (point-to-line registration). + """ + # only introduced this for the documentation -- otherwise the RobustPoseLifter __init__ is shown. + return super().__init__(*args, **kwargs) + @property def TIGHTNESS(self): return "cost" if self.robust else "rank" diff --git a/popcor/examples/ro_nsq_lifter.py b/popcor/examples/ro_nsq_lifter.py new file mode 100644 index 0000000..65f44fe --- /dev/null +++ b/popcor/examples/ro_nsq_lifter.py @@ -0,0 +1,282 @@ +import numpy as np +import scipy.sparse as sp +from poly_matrix import PolyMatrix + +from popcor.base_lifters import RangeOnlyLifter + +NOISE = 1e-2 # std deviation of distance noise + +NORMALIZE = True + + +class RangeOnlyNsqLifter(RangeOnlyLifter): + """Range-only localization in 2D or 3D. + + Almost same as RangeOnlySqLifter, but we do not square the distances. We minimize + + .. math:: f(\\theta) = \\sum_{n=0}^{N-1} \\sum_{k=0}^{K-1} w_{nk} (d_{nk} - ||p_n - a_k||)^2 + + where + + - :math:`w_{nk}` is the weight for the nth point and kth landmark (currently assumed binary to mark missing edges). + - :math:`\\theta` is the flattened vector of positions :math:`p_n`. + - :math:`d_{nk}` is the distance measurement from point n to landmark k. + - :math:`a_k` is the kth landmark. + + Note that in the current implementation, there is no regularization term so the problem could be split into individual points. + + We experiment with two different substitutions to turn the cost function into a quadratic form: + + - level "normals" uses a reformulation that introduce normal vectors, as proposed by Halstedt et al (see below). + - level "simple" uses substitution :math:`z_i=||p_n - a_k||` (or equivalent 3D version). + + .. math:: f(\\theta) = \\sum_{n=0}^{N-1} \\sum_{k=0}^{K-1} w_{nk} || z_{nk} d_{nk} - (p_n - a_k) ||^2 + + where all are as above, except: + + - :math:`\\theta` is now the flattened vector of positions :math:`p_n` and also normal vectors :math:`z_{nk}`. + """ + + TIGHTNESS = "rank" + LEVELS = ["normals", "simple"] + LEVEL_NAMES = { + "normals": "$\\boldymbol{y}_n$", + "simple": "$z_n$", + } + MONOMIAL_DEGREE = 1 + + @staticmethod + def create_good(n_positions, n_landmarks, d=2): + landmarks, theta = RangeOnlyLifter.create_good(n_positions, n_landmarks, d) + lifter = RangeOnlyNsqLifter(n_positions, n_landmarks, d) + lifter.overwrite_theta(theta) + lifter.landmarks = landmarks + return lifter + + @staticmethod + def create_bad(n_positions, n_landmarks, d=2): + landmarks, theta = RangeOnlyLifter.create_bad(n_positions, n_landmarks, d) + lifter = RangeOnlyNsqLifter(n_positions, n_landmarks, d) + lifter.overwrite_theta(theta) + lifter.landmarks = landmarks + return lifter + + @staticmethod + def create_bad_fixed(n_positions, n_landmarks, d=2): + landmarks, theta = RangeOnlyLifter.create_bad_fixed(n_positions, n_landmarks, d) + lifter = RangeOnlyNsqLifter(n_positions, n_landmarks, d) + lifter.overwrite_theta(theta) + lifter.landmarks = landmarks + return lifter + + def __init__( + self, + n_positions, + n_landmarks, + d, + W=None, + level="normals", + variable_list=None, + param_level="no", + ): + if level == "simple": + raise NotImplementedError("simple is not implemented yet.") + super().__init__( + n_positions=n_positions, + n_landmarks=n_landmarks, + d=d, + W=W, + level=level, + variable_list=variable_list, + param_level=param_level, + ) + + @property + def VARIABLE_LIST(self): + return [ + [self.HOM, "x_0"], + [self.HOM, "x_0"] + [f"z_0_{i}" for i in range(self.n_landmarks)], + ] + + def get_all_variables(self): + vars = [self.HOM] + vars += [f"x_{i}" for i in range(self.n_positions)] + vars += [ + f"z_{i}_{k}" + for i in range(self.n_positions) + for k in range(self.n_landmarks) + ] + return [vars] + + def get_A_known(self, var_dict=None, output_poly=False, add_redundant=False): + if var_dict is None: + var_dict = self.var_dict + + A_list = [] + if self.level == "normals": + for i in range(self.n_positions): + for k in range(self.n_landmarks): + # enforce the normal vectors are indeed unit-norm + A = PolyMatrix(symmetric=True) + A[f"z_{i}_{k}", f"z_{i}_{k}"] = np.eye(self.d) + A[self.HOM, self.HOM] = -1.0 + if output_poly: + A_list.append(A) + else: + A_list.append(A.get_matrix(self.var_dict)) + + elif self.level == "simple": + # enfore that y_{nk}^2 = ||p_n - a_k||^2 = ||p_n||^2 - 2a_k^T p_n + ||a_k||^2 + raise NotImplementedError( + "get_A_known not implemented yet for simple level" + ) + return A_list + + def get_residuals(self, t, y, ad=False): + return super().get_residuals(t, y, ad=ad, squared=False) + + def get_cost(self, theta, y, sub_idx=None, ad=False): + residuals = self.get_residuals(theta, y, ad=ad) + return self.get_cost_from_res(residuals, sub_idx, ad=ad) + + def get_normals(self, theta=None): + if theta is None: + theta = self.theta + + if np.ndim(theta) < 2: + theta = theta[None, :] + # N x M x d normals. + return (self.landmarks[None, :, :] - theta[:, None, :]) / np.linalg.norm( + self.landmarks[None, :, :] - theta, axis=2 + )[:, :, None] + + def get_x(self, theta=None, parameters=None, var_subset=None): + if var_subset is None: + var_subset = self.var_dict + if theta is None: + theta = self.theta + if parameters is None: + parameters = self.parameters + + positions = theta.reshape(self.n_positions, -1) + normals = self.get_normals(positions) + + x_data = [] + for val in var_subset: + if val == "h": + x_data += [1.0] + elif val.startswith("x_"): + assert theta is not None + i = int(val.split("x_")[-1]) + x_data += list(theta[i, :]) + elif val.startswith("z_"): # z_i_k + i = int(val.split("_")[-2]) + k = int(val.split("_")[-1]) + z = normals[i, k, :] + x_data += list(z) + assert len(x_data) == self.get_dim_x(var_subset) + return np.array(x_data) + + def get_J_lifting(self, t): + pos = t.reshape((-1, self.d)) + ii = [] + jj = [] + data = [] + + for n in range(self.n_positions): + if self.level == "normals": + pass + elif self.level == "simple": + pass + J_lifting = sp.csr_array( + (data, (ii, jj)), + shape=(self.M, self.N), + ) + return J_lifting + + def get_Q_from_y(self, y, output_poly: bool = False): + Q = PolyMatrix() + for k in range(self.n_landmarks): + for i in range(self.n_positions): + d_ik = y[i, k] + # || n_ik * d_ik - (m_k - x_i) || ^2 + # = d_ik^2 - 2*d_ik*n_ik' (m_k - x_i) + ||m_k - x_i||^2 + # = d_ik^2 + ||m_k||^2 - 2m_k' x_i - 2 * d_ik * m_k' n_ik + 2 * d_ik * n_ik' x_i + ||x_i||^2 + m_k = self.landmarks[k] + b_k = d_ik**2 + np.sum(m_k**2) + + Q["h", "h"] += b_k # d_ik^2 + ||m_k||^2 + Q["h", f"x_{i}"] += -m_k[None, :] # -2m_k' x_i + Q["h", f"z_{i}_{k}"] += -d_ik * m_k[None, :] # -2 d_ik * m_k' n_ik + Q[f"x_{i}", f"z_{i}_{k}"] += d_ik * np.eye(self.d) # 2 d_ik * n_ik' x_i + Q[f"x_{i}", f"x_{i}"] += np.eye(self.d) # + ||x_i||^2 + + if output_poly: + return Q + else: + return Q.get_matrix(self.var_dict) + + def simulate_y(self, noise: float | None = None, squared: bool = True): + return super().simulate_y(noise=noise, squared=False) + + @property + def var_dict(self): + var_dict = {self.HOM: 1} + var_dict.update({f"x_{n}": self.d for n in range(self.n_positions)}) + var_dict.update( + { + f"z_{n}_{k}": self.size_z + for n in range(self.n_positions) + for k in range(self.n_landmarks) + } + ) + return var_dict + + @property + def size_z(self) -> int: + if self.level == "normals": + return self.d + elif self.level == "simple": + return 1 + else: + raise ValueError(f"Unknown level {self.level}") + + def get_valid_samples( + self, + n_samples, + max_trials=3, + min_dist=1e-2, + radius=1.0, + center=None, + vectorized=True, + ): + # quick and dirty implementation to make sure we don't sample too close + # from a landmark (otherwise length is zero and the normal vector will have nans) + if center is None: + center = np.ones(self.landmarks.shape[1]) + samples = [] + for i in range(n_samples): + for j in range(max_trials): + sample = ( + (np.random.rand(self.landmarks.shape[1]) - 0.5) * 2 * radius + ) + center # between 0 and 1 + if np.all( + np.linalg.norm(sample[None, :] - self.landmarks, axis=1) > min_dist + ): + samples.append(sample) + break + if j == max_trials - 1: + print(f"Warning: did not find valid sample in {max_trials} trials") + + samples = np.vstack(samples) + + normals = self.landmarks[None, :, :] - samples[:, None, :] + normals /= np.linalg.norm(normals, axis=2)[:, :, None] + return np.hstack([samples, normals.reshape(normals.shape[0], -1)]) + + def __repr__(self): + return f"rangeonlyloc{self.d}d_{self.level}" + + +if __name__ == "__main__": + lifter = RangeOnlyNsqLifter(n_positions=3, n_landmarks=4, d=2) diff --git a/popcor/examples/range_only_lifters.py b/popcor/examples/ro_sq_lifter.py similarity index 59% rename from popcor/examples/range_only_lifters.py rename to popcor/examples/ro_sq_lifter.py index b9ba4bd..e5e4697 100644 --- a/popcor/examples/range_only_lifters.py +++ b/popcor/examples/ro_sq_lifter.py @@ -1,29 +1,18 @@ -import matplotlib.pylab as plt +import itertools + import numpy as np import scipy.sparse as sp from poly_matrix.least_squares_problem import LeastSquaresProblem -from scipy.optimize import minimize -from popcor.base_lifters import StateLifter +from popcor.base_lifters import RangeOnlyLifter from popcor.utils.common import diag_indices, upper_triangular -plt.ion() - NOISE = 1e-2 # std deviation of distance noise -METHOD = "BFGS" NORMALIZE = True -# TODO(FD): parameters below are not all equivalent. -SOLVER_KWARGS = { - "BFGS": dict(gtol=1e-6, xrtol=1e-10), # relative step size - "Nelder-Mead": dict(xatol=1e-10), # absolute step size - "Powell": dict(ftol=1e-6, xtol=1e-10), - "TNC": dict(gtol=1e-6, xtol=1e-10), -} - -class RangeOnlyLocLifter(StateLifter): +class RangeOnlySqLifter(RangeOnlyLifter): """Range-only localization in 2D or 3D. We minimize the cost function @@ -53,24 +42,31 @@ class RangeOnlyLocLifter(StateLifter): "no": "$z_n$", "quad": "$\\boldsymbol{y}_n$", } - - def get_vec_around_gt(self, delta: float = 0): - """Sample around ground truth. - :param delta: sample from gt + std(delta) (set to 0 to start from gt.) - """ - assert self.landmarks is not None, "landmarks must be set before sampling" - - if delta == 0: - return self.theta - else: - bbox_max = np.max(self.landmarks, axis=0) * 2 - bbox_min = np.min(self.landmarks, axis=0) * 2 - pos = ( - np.random.rand(self.n_positions, self.d) - * (bbox_max - bbox_min)[None, :] - + bbox_min[None, :] - ) - return pos.flatten() + MONOMIAL_DEGREE = 2 + + @staticmethod + def create_good(n_positions, n_landmarks, d=2): + landmarks, theta = RangeOnlyLifter.create_good(n_positions, n_landmarks, d) + lifter = RangeOnlySqLifter(n_positions, n_landmarks, d) + lifter.overwrite_theta(theta) + lifter.landmarks = landmarks + return lifter + + @staticmethod + def create_bad(n_positions, n_landmarks, d=2): + landmarks, theta = RangeOnlyLifter.create_bad(n_positions, n_landmarks, d) + lifter = RangeOnlySqLifter(n_positions, n_landmarks, d) + lifter.overwrite_theta(theta) + lifter.landmarks = landmarks + return lifter + + @staticmethod + def create_bad_fixed(n_positions, n_landmarks, d=2): + landmarks, theta = RangeOnlyLifter.create_bad_fixed(n_positions, n_landmarks, d) + lifter = RangeOnlySqLifter(n_positions, n_landmarks, d) + lifter.overwrite_theta(theta) + lifter.landmarks = landmarks + return lifter def __init__( self, @@ -82,42 +78,21 @@ def __init__( variable_list=None, param_level="no", ): - self.n_positions = n_positions - self.n_landmarks = n_landmarks - self.landmarks_ = None # will be set later - - if W is not None: - assert W.shape == (n_landmarks, n_positions) - self.W = W - else: - self.W = np.ones((n_positions, n_landmarks)) - self.y_ = None - - if variable_list == "all": - variable_list = self.get_all_variables() super().__init__( - level=level, d=d, variable_list=variable_list, param_level=param_level + n_positions=n_positions, + n_landmarks=n_landmarks, + d=d, + W=W, + level=level, + variable_list=variable_list, + param_level=param_level, ) - @property - def landmarks(self): - landmarks = np.random.rand(self.n_landmarks, self.d) - if self.landmarks_ is None: - self.landmarks_ = landmarks - return self.landmarks_ - - @landmarks.setter - def landmarks(self, landmarks): - assert landmarks.shape == (self.n_landmarks, self.d) - self.landmarks_ = landmarks - @property def VARIABLE_LIST(self): return [ [self.HOM, "x_0"], [self.HOM, "x_0", "z_0"], - [self.HOM, "x_0", "z_0", "z_1"], - [self.HOM, "x_0", "x_1", "z_0", "z_1"], ] def get_all_variables(self): @@ -126,18 +101,6 @@ def get_all_variables(self): vars += [f"z_{i}" for i in range(self.n_positions)] return [vars] - def sample_parameters(self, theta=None): - landmarks = np.random.rand(self.n_landmarks, self.d) - return self.sample_parameters_landmarks(landmarks) - - def overwrite_theta(self, theta): - """To bypass "theta can only be set once" check.""" - assert theta.shape == (self.n_positions, self.d) - self.theta_ = theta - - def sample_theta(self): - return np.random.rand(self.n_positions, self.d).flatten() - def get_A_known(self, var_dict=None, output_poly=False, add_redundant=False): from poly_matrix.poly_matrix import PolyMatrix @@ -178,6 +141,13 @@ def get_A_known(self, var_dict=None, output_poly=False, add_redundant=False): A_list.append(A.get_matrix(self.var_dict)) return A_list + def get_residuals(self, t, y, ad=False): + return super().get_residuals(t, y, ad=ad, squared=True) + + def get_cost(self, theta, y, sub_idx=None, ad=False): + residuals = self.get_residuals(theta, y, ad=ad) + return self.get_cost_from_res(residuals, sub_idx, ad=ad) + def get_x(self, theta=None, parameters=None, var_subset=None): if var_subset is None: var_subset = self.var_dict @@ -276,69 +246,10 @@ def fixed_hessian_list(self): else: raise ValueError(f"Unsupported dimension {self.d} for fixed hessians.") - def get_residuals(self, t, y): - positions = t.reshape((-1, self.d)) - y_current = ( - np.linalg.norm(self.landmarks[None, :, :] - positions[:, None, :], axis=2) - ** 2 - ) - return self.W * (y - y_current) - - def get_cost(self, theta, y, sub_idx=None): + def get_Q_from_y(self, y, output_poly: bool = False): """ - get cost for given positions, landmarks and noise. - - :param t: flattened positions of length Nd - :param y: N x K distance measurements + :param y: the distance measurements, shape (n_positions, n_landmarks). IMPORTANT: these are not squared! """ - residuals = self.get_residuals(theta, y) - if sub_idx is None: - cost = np.sum(residuals**2) - else: - cost = np.sum(residuals[sub_idx] ** 2) - if NORMALIZE: - return cost / np.sum(self.W > 0) - return cost - - def get_grad(self, t, y, sub_idx=None): - """get gradient""" - J = self.get_J(t, y) - x = self.get_x(t) - Q = self.get_Q_from_y(y) - if sub_idx is None: - return 2 * J.T @ Q @ x - else: - sub_idx_x = self.get_sub_idx_x(sub_idx) - return 2 * J.T[:, sub_idx_x] @ Q[sub_idx_x, :][:, sub_idx_x] @ x[sub_idx_x] - - def get_J(self, t, y): - J = sp.csr_array( - (np.ones(self.N), (range(1, self.N + 1), range(self.N))), - shape=(self.N + 1, self.N), - ) - J_lift = self.get_J_lifting(t) - J = sp.vstack([J, J_lift]) - return J - - def get_hess(self, t, y): - """get Hessian""" - x = self.get_x(t) - Q = self.get_Q_from_y(y) - J = self.get_J(t, y) - hess = 2 * J.T @ Q @ J - - hessians = self.get_hess_lifting(t) - B = self.ls_problem.get_B_matrix(self.var_dict) - residuals = B @ x - for m, h in enumerate(hessians): - bm_tilde = B[:, -self.M + m] - factor = float(bm_tilde.T @ residuals) - hess += 2 * factor * h - return hess - - def get_Q_from_y(self, y, output_poly: bool = False): - import itertools - self.ls_problem = LeastSquaresProblem() if self.level == "quad": @@ -350,7 +261,7 @@ def get_Q_from_y(self, y, output_poly: bool = False): if self.level == "no": self.ls_problem.add_residual( { - self.HOM: y[n, k] - np.linalg.norm(ak) ** 2, + self.HOM: y[n, k] ** 2 - np.linalg.norm(ak) ** 2, f"x_{n}": 2 * ak.reshape((1, -1)), f"z_{n}": -1, } @@ -359,7 +270,7 @@ def get_Q_from_y(self, y, output_poly: bool = False): mat = np.zeros((1, self.size_z)) mat[0, diag_idx] = -1 res_dict = { - self.HOM: y[n, k] - np.linalg.norm(ak) ** 2, + self.HOM: y[n, k] ** 2 - np.linalg.norm(ak) ** 2, f"x_{n}": 2 * ak.reshape((1, -1)), f"z_{n}": mat, } @@ -373,118 +284,9 @@ def get_Q_from_y(self, y, output_poly: bool = False): return Q def simulate_y(self, noise: float | None = None, squared: bool = True): - assert self.landmarks is not None - # N x K matrix - if noise is None: - noise = NOISE - positions = self.theta.reshape(self.n_positions, -1) - y_gt = np.linalg.norm( - self.landmarks[None, :, :] - positions[:, None, :], axis=2 - ) - if squared: - return y_gt**2 + np.random.normal(loc=0, scale=noise, size=y_gt.shape) - else: - return y_gt + np.random.normal(loc=0, scale=noise, size=y_gt.shape) - - def get_Q(self, noise: float | None = None, output_poly: bool = False) -> tuple: - if self.y_ is None: - self.y_ = self.simulate_y(noise=noise) - Q = self.get_Q_from_y(self.y_, output_poly=output_poly) - - # DEBUGGING - x = self.get_x() - cost1 = x.T @ Q @ x - cost3 = self.get_cost(self.theta, self.y_) - assert abs(cost1 - cost3) < 1e-10 - return Q - - def get_D(self, that): - D = np.eye(1 + self.n_positions * self.d + self.size_z) - x = self.get_x(theta=that) - J = self.get_J_lifting(t=that) - - D = sp.lil_array((len(x), len(x))) - D[range(len(x)), range(len(x))] = 1.0 - D[:, 0] = x - D[-J.shape[0] :, 1 : 1 + J.shape[1]] = J # type: ignore - return D.tocsc() - - def get_sub_idx_x(self, sub_idx, add_z=True): - sub_idx_x = [0] - for idx in sub_idx: - sub_idx_x += [1 + idx * self.d + d for d in range(self.d)] - if not add_z: - return sub_idx_x - for idx in sub_idx: - sub_idx_x += [ - 1 + self.n_positions * self.d + idx * self.size_z + d - for d in range(self.size_z) - ] - return sub_idx_x - - def get_theta(self, x): - assert abs(x[0] - 1.0) > 1e-10 - # below is if we have order x_1, z_1, x_2, z_2, ... - # x.reshape((self.n_positions, -1))[:, : self.d].flatten("C") - # below is if we have order x_1, x_2, ..., z_1, z_2, ... - return x[: self.n_positions * self.d] - - def get_position(self, theta=None): - if theta is not None: - return theta.reshape(self.n_positions, self.d) - - def get_error(self, theta_hat, error_type="rmse", *args, **kwargs): - if error_type == "rmse": - return np.sqrt(np.mean((self.theta - theta_hat) ** 2)) - elif error_type == "mse": - return np.mean((self.theta - theta_hat) ** 2) - else: - raise ValueError(f"Unkwnon error_type {error_type}") - - def local_solver( - self, - t0, - y, - verbose=False, - method="BFGS", - solver_kwargs=SOLVER_KWARGS, - ): - """ - :param t_init: (positions, landmarks) tuple - """ - - # TODO(FD): split problem into individual points. - options = solver_kwargs[method] - options["disp"] = verbose - sol = minimize( - self.get_cost, - x0=t0, - args=y, - jac=self.get_grad, - # hess=self.get_hess, not used by any solvers. - method=method, - options=options, - ) - - info = {} - info["success"] = sol.success - info["msg"] = sol.message + f"(# iterations: {sol.nit})" - if sol.success: - that = sol.x - rel_error = self.get_cost(that, y) - self.get_cost(sol.x, y) - assert abs(rel_error) < 1e-10, rel_error - residuals = self.get_residuals(that, y) - cost = sol.fun - info["max res"] = np.max(np.abs(residuals)) - hess = self.get_hess(that, y) - eigs = np.linalg.eigvalsh(hess.toarray()) - info["cond Hess"] = eigs[-1] / eigs[0] - else: - that = cost = None - info["max res"] = None - info["cond Hess"] = None - info["cost"] = cost - return that, info, cost + # purposefully not using squared=True here: + # the noise should always be added to the non-squared distances. + return super().simulate_y(noise=noise, squared=False) @property def var_dict(self): @@ -494,11 +296,7 @@ def var_dict(self): return var_dict @property - def param_dict(self): - return self.param_dict_landmarks - - @property - def size_z(self): + def size_z(self) -> int: if self.level == "no": return 1 elif self.level == "quad": @@ -506,17 +304,56 @@ def size_z(self): else: raise ValueError(f"Unknown level {self.level}") - @property - def N(self): - return self.n_positions * self.d - - @property - def M(self): - return self.n_positions * self.size_z - def __repr__(self): return f"rangeonlyloc{self.d}d_{self.level}" + # ============ below are currently not used anymore, but it is an elegant way to compute the ============= + # gradient and hessian when there are no constraints + def get_grad(self, t, y, sub_idx=None): + J = self.get_J(t, y) + x = self.get_x(t) + Q = self.get_Q_from_y(y) + if sub_idx is None: + return 2 * J.T @ Q @ x + else: + sub_idx_x = self.get_sub_idx_x(sub_idx) + return 2 * J.T[:, sub_idx_x] @ Q[sub_idx_x, :][:, sub_idx_x] @ x[sub_idx_x] # type: ignore + + def get_J(self, t, y): + J = sp.csr_array( + (np.ones(self.N), (range(1, self.N + 1), range(self.N))), + shape=(self.N + 1, self.N), + ) + J_lift = self.get_J_lifting(t) + J = sp.vstack([J, J_lift]) + return J + + def get_hess(self, t, y): + x = self.get_x(t) + Q = self.get_Q_from_y(y) + J = self.get_J(t, y) + hess = 2 * J.T @ Q @ J + + hessians = self.get_hess_lifting(t) + B = self.ls_problem.get_B_matrix(self.var_dict) + residuals = B @ x + for m, h in enumerate(hessians): + bm_tilde = B[:, -self.M + m] + factor = float(bm_tilde.T @ residuals) + hess += 2 * factor * h + return hess + + def get_D(self, that): + D = np.eye(1 + self.n_positions * self.d + self.size_z) + x = self.get_x(theta=that) + J = self.get_J_lifting(t=that) + + D = sp.lil_array((len(x), len(x))) + D[range(len(x)), range(len(x))] = 1.0 + D[:, 0] = x + D[-J.shape[0] :, 1 : 1 + J.shape[1]] = J # type: ignore + return D.tocsc() + if __name__ == "__main__": - lifter = RangeOnlyLocLifter(n_positions=3, n_landmarks=4, d=2) + lifter = RangeOnlySqLifter(n_positions=3, n_landmarks=4, d=2) diff --git a/popcor/examples/stereo1d_lifter.py b/popcor/examples/stereo1d_lifter.py index 3cc84ee..366c48d 100644 --- a/popcor/examples/stereo1d_lifter.py +++ b/popcor/examples/stereo1d_lifter.py @@ -1,5 +1,3 @@ -from typing import Optional - import numpy as np from poly_matrix.least_squares_problem import LeastSquaresProblem from poly_matrix.poly_matrix import PolyMatrix diff --git a/popcor/examples/wahba_lifter.py b/popcor/examples/wahba_lifter.py index 590920f..4d55155 100644 --- a/popcor/examples/wahba_lifter.py +++ b/popcor/examples/wahba_lifter.py @@ -15,13 +15,17 @@ class WahbaLifter(RobustPoseLifter): - """This example is treated in more details in `this paper `_, - under the name "PPR" (point-to-point registration). - """ NOISE = 1e-2 # inlier noise NOISE_OUT = 1.0 # outlier noise + def __init__(self, *args, **kwargs): + """This example is treated in more details in `this paper `_, + under the name "PPR" (point-to-point registration). + """ + # only introduced this for the documentation -- otherwise the RobustPoseLifter __init__ is shown. + return super().__init__(*args, **kwargs) + def h_list(self, t): """ We want to inforce that diff --git a/popcor/solvers/common.py b/popcor/solvers/common.py index 2d992eb..b5da3f8 100644 --- a/popcor/solvers/common.py +++ b/popcor/solvers/common.py @@ -4,7 +4,7 @@ def find_local_minimum(lifter, y, delta=1.0, verbose=False, n_inits=10, plot=False): from popcor.base_lifters import StateLifter - from popcor.examples import RangeOnlyLocLifter + from popcor.examples import RangeOnlySqLifter assert isinstance(lifter, StateLifter) local_solutions = [] @@ -101,7 +101,7 @@ def find_local_minimum(lifter, y, delta=1.0, verbose=False, n_inits=10, plot=Fal ) # plot all solutions that converged to those (for RO only, for stereo it's too crowded) - if isinstance(lifter, RangeOnlyLocLifter): + if isinstance(lifter, RangeOnlySqLifter): for i in global_inds[1:]: # first one corresponds to ground truth plot_frame(ax, theta=inits[i], color="g", marker=".") for i in local_inds: diff --git a/popcor/utils/test_tools.py b/popcor/utils/test_tools.py index a61a4fd..c2ddfe5 100644 --- a/popcor/utils/test_tools.py +++ b/popcor/utils/test_tools.py @@ -4,7 +4,7 @@ MonoLifter, Poly4Lifter, Poly6Lifter, - RangeOnlyLocLifter, + RangeOnlySqLifter, RotationLifter, Stereo1DLifter, Stereo2DLifter, @@ -17,31 +17,53 @@ n_landmarks = 3 n_poses = 4 n_positions = 3 -# fmt: off Lifters = [ - (Poly4Lifter, dict()), #ok - (Poly6Lifter, dict()), #ok - (WahbaLifter, dict(n_landmarks=3, d=2, robust=False, level="no", n_outliers=0)), #ok - (MonoLifter, dict(n_landmarks=5, d=2, robust=False, level="no", n_outliers=0)), # not tight - (WahbaLifter, dict(n_landmarks=5, d=2, robust=True, level="xwT", n_outliers=1)), # not tight - (MonoLifter, dict(n_landmarks=6, d=2, robust=True, level="xwT", n_outliers=1)), # not tight - (RangeOnlyLocLifter, dict(n_positions=n_positions, n_landmarks=n_landmarks, d=d, level="no")), # ok - (RangeOnlyLocLifter, dict(n_positions=n_positions, n_landmarks=n_landmarks, d=d, level="quad")), # ok - (Stereo1DLifter, dict(n_landmarks=n_landmarks)), # not tight - #(Stereo1DLifter, dict(n_landmarks=n_landmarks, param_level="p")), # skip - #(Stereo2DLifter, dict(n_landmarks=n_landmarks)), - #(Stereo3DLifter, dict(n_landmarks=n_landmarks)), - (RotationLifter, dict(d=2)), # ok - (RotationLifter, dict(d=3)), # ok + ( + Poly4Lifter, + dict(), # ok + ), + (Poly6Lifter, dict()), # ok + ( + WahbaLifter, + dict(n_landmarks=3, d=2, robust=False, level="no", n_outliers=0), + ), # ok + ( + MonoLifter, + dict(n_landmarks=5, d=2, robust=False, level="no", n_outliers=0), + ), # not tight + ( + WahbaLifter, + dict(n_landmarks=5, d=2, robust=True, level="xwT", n_outliers=1), + ), # not tight + ( + MonoLifter, + dict(n_landmarks=6, d=2, robust=True, level="xwT", n_outliers=1), + ), # not tight + ( + RangeOnlySqLifter, + dict(n_positions=n_positions, n_landmarks=n_landmarks, d=d, level="no"), + ), # ok + ( + RangeOnlySqLifter, + dict(n_positions=n_positions, n_landmarks=n_landmarks, d=d, level="quad"), + ), # ok + (Stereo1DLifter, dict(n_landmarks=n_landmarks)), # not tight + (Stereo1DLifter, dict(n_landmarks=n_landmarks, param_level="p")), # skip + (Stereo2DLifter, dict(n_landmarks=n_landmarks)), + (Stereo3DLifter, dict(n_landmarks=n_landmarks)), + (RotationLifter, dict(d=2)), # ok + (RotationLifter, dict(d=3)), # ok ] ExampleLifters = [ (WahbaLifter, dict(n_landmarks=5, d=2, robust=False, level="no", n_outliers=1)), - (RangeOnlyLocLifter, dict(n_positions=n_poses, n_landmarks=n_landmarks, d=d, level="quad")), + ( + RangeOnlySqLifter, + dict(n_positions=n_poses, n_landmarks=n_landmarks, d=d, level="quad"), + ), (Stereo1DLifter, dict(n_landmarks=n_landmarks)), (Stereo2DLifter, dict(n_landmarks=n_landmarks)), ] -# fmt: on def constraints_test_with_tol(lifter, A_list, tol): diff --git a/setup.cfg b/setup.cfg index 9cd1405..e185b01 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,4 +33,4 @@ install_requires = [flake8] ignore = W292, W391, F541, F841, E203, E501, W503, E741 exclude = _notebooks/*, *.ipynb_checkpoints*, venv/ -max-line-length = 99 \ No newline at end of file +max-line-length = 99 diff --git a/tests/test_autotemplate.py b/tests/test_autotemplate.py index ef6d847..383454c 100644 --- a/tests/test_autotemplate.py +++ b/tests/test_autotemplate.py @@ -1,7 +1,7 @@ import numpy as np from popcor import AutoTemplate -from popcor.examples import RangeOnlyLocLifter, Stereo1DLifter +from popcor.examples import RangeOnlySqLifter, Stereo1DLifter # random seed, for reproducibility SEED = 3 @@ -27,7 +27,7 @@ def test_stereo_1d(): def test_range_only(): np.random.seed(SEED) for level in ["no", "quad"]: - lifter = RangeOnlyLocLifter(n_positions=4, n_landmarks=5, d=2, level=level) + lifter = RangeOnlySqLifter(n_positions=4, n_landmarks=5, d=2, level=level) learner = AutoTemplate( lifter=lifter, ) diff --git a/tests/test_autotight.py b/tests/test_autotight.py index be4b080..70253da 100644 --- a/tests/test_autotight.py +++ b/tests/test_autotight.py @@ -70,8 +70,7 @@ def test_constraints_stereo(): if __name__ == "__main__": - import pytest - + # import pytest # pytest.main([__file__, "-s"]) # print("all tests passed")