Skip to content

Commit a76176b

Browse files
committed
Merge remote-tracking branch 'origin/main' into centralize-backend-specific-code
2 parents e89cad0 + 165b097 commit a76176b

8 files changed

Lines changed: 233 additions & 75 deletions

File tree

symforce/codegen/codegen.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,8 +421,8 @@ def generate_function(
421421
output_dir=output_dir,
422422
lcm_type_dir=Path(types_codegen_data["lcm_type_dir"]),
423423
function_dir=out_function_dir,
424-
python_types_dir=Path(lcm_data["python_types_dir"]),
425-
cpp_types_dir=Path(lcm_data["cpp_types_dir"]),
424+
python_types_dir=lcm_data["python_types_dir"],
425+
cpp_types_dir=lcm_data["cpp_types_dir"],
426426
generated_files=[Path(v.output_path) for v in templates.items],
427427
)
428428

symforce/codegen/codegen_util.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import importlib.abc
1414
import importlib.util
1515
import itertools
16-
import os
1716
from pathlib import Path
1817
import sympy
1918
import sys
@@ -610,23 +609,28 @@ def get_base_instance(obj: T.Sequence[T.Any]) -> T.Any:
610609

611610
def generate_lcm_types(
612611
lcm_type_dir: T.Openable, lcm_files: T.Sequence[str], lcm_output_dir: T.Openable = None
613-
) -> T.Dict[str, str]:
612+
) -> T.Dict[str, Path]:
614613
"""
615614
Generates the language-specific type files for all symforce generated ".lcm" files.
616615
617616
Args:
618617
lcm_type_dir: Directory containing symforce-generated .lcm files
619618
lcm_files: List of .lcm files to process
620619
"""
620+
lcm_type_dir = Path(lcm_type_dir)
621+
621622
if lcm_output_dir is None:
622-
lcm_output_dir = os.path.join(lcm_type_dir, "..")
623+
lcm_output_dir = lcm_type_dir / ".."
624+
else:
625+
lcm_output_dir = Path(lcm_output_dir)
623626

624-
python_types_dir = os.path.join(lcm_output_dir, "python")
625-
cpp_types_dir = os.path.join(lcm_output_dir, "cpp", "lcmtypes")
626-
lcm_include_dir = os.path.join("lcmtypes")
627+
python_types_dir = lcm_output_dir / "python"
628+
cpp_types_dir = lcm_output_dir / "cpp" / "lcmtypes"
629+
lcm_include_dir = "lcmtypes"
627630

628631
result = {"python_types_dir": python_types_dir, "cpp_types_dir": cpp_types_dir}
629632

633+
# TODO(brad, aaron): Do something reasonable with lcm_files other than returning early
630634
# If no LCM files provided, do nothing
631635
if not lcm_files:
632636
return result
@@ -638,23 +642,23 @@ def generate_lcm_types(
638642
skymarshal.main(
639643
[SkymarshalPython, SkymarshalCpp],
640644
args=[
641-
lcm_type_dir,
645+
str(lcm_type_dir),
642646
"--python",
643647
"--python-path",
644-
os.path.join(python_types_dir, "lcmtypes"),
648+
str(python_types_dir / "lcmtypes"),
645649
"--python-namespace-packages",
646650
"--python-package-prefix",
647651
"lcmtypes",
648652
"--cpp",
649653
"--cpp-hpath",
650-
cpp_types_dir,
654+
str(cpp_types_dir),
651655
"--cpp-include",
652656
lcm_include_dir,
653657
],
654658
)
655659

656660
# Autoformat generated python files
657-
format_util.format_py_dir(python_types_dir)
661+
format_util.format_py_dir(str(python_types_dir))
658662

659663
return result
660664

symforce/codegen/types_package_codegen.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def generate_types(
160160
if not using_external_templates:
161161
templates.render()
162162
lcm_data = codegen_util.generate_lcm_types(lcm_type_dir, lcm_files, lcm_bindings_output_dir)
163-
codegen_data.update(lcm_data)
163+
codegen_data.update({key: str(val) for key, val in lcm_data.items()})
164164

165165
return codegen_data
166166

symforce/examples/robot_3d_localization/plotting.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def plot_solution(
101101
if show_iteration_text:
102102
text = ax.text(8, 7, 9, "-", color="black")
103103

104-
def update_plot(slider_value: np.float64, show_iteration_text: bool) -> None:
104+
def update_plot(slider_value: np.float64) -> None:
105105
"""
106106
Update the plot using the given iteration.
107107
"""

symforce/opt/internal/factor_utils.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,13 @@ Factor<Scalar> JacobianFixed(Functor func, const std::vector<Key>& keys_to_func,
174174
constexpr int M = JacobianMat::RowsAtCompileTime;
175175
constexpr int N = JacobianMat::ColsAtCompileTime;
176176

177+
// NOTE(harrison): accoring to c++ spec you shouldn't have to specify a default lambda capture
178+
// and should only have to specify a capture for func. However, this is not handled correctly in
179+
// older versions of clang, which is why we have one here.
177180
return Factor<Scalar>(
178-
[func](const Values<Scalar>& values, const std::vector<index_entry_t>& keys_to_func,
179-
VectorX<Scalar>* residual, MatrixX<Scalar>* jacobian, MatrixX<Scalar>* hessian,
180-
VectorX<Scalar>* rhs) {
181+
[=](const Values<Scalar>& values, const std::vector<index_entry_t>& keys_to_func,
182+
VectorX<Scalar>* residual, MatrixX<Scalar>* jacobian, MatrixX<Scalar>* hessian,
183+
VectorX<Scalar>* rhs) {
181184
SYM_ASSERT(residual != nullptr);
182185
Eigen::Matrix<Scalar, M, 1> residual_fixed;
183186

symforce/pybind/cc_factor.cc

Lines changed: 77 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include "./cc_factor.h"
77

8+
#include <cstring>
89
#include <functional>
910

1011
#include <Eigen/Dense>
@@ -35,67 +36,89 @@ namespace {
3536
using PyHessianFunc =
3637
std::function<py::tuple(const sym::Valuesd&, const std::vector<index_entry_t>&)>;
3738

39+
/**
40+
* If Matrix is Eigen::SparseMatrix<double> and matrix is not a scipy.sparse.csc_matrix, or
41+
* if Matrix is any other type and matrix is a scipy.sparse.csc_matrix, throws a value error.
42+
*/
43+
template <typename Matrix>
44+
void ThrowIfSparsityMismatch(const py::object& matrix) {
45+
if (std::strcmp(Py_TYPE(matrix.ptr())->tp_name, "csc_matrix") == 0) {
46+
throw py::value_error("Non-sparse matrix expected, scipy.sparse.csc_matrix found instead.");
47+
}
48+
}
49+
template <>
50+
void ThrowIfSparsityMismatch<Eigen::SparseMatrix<double>>(const py::object& matrix) {
51+
if (!py::isinstance(matrix, py::module_::import("scipy.sparse").attr("csc_matrix"))) {
52+
throw py::value_error(
53+
fmt::format("scipy.sparse.csc_matrix expected, found {} instead.", py::type::of(matrix)));
54+
}
55+
}
56+
57+
template <typename Matrix>
3858
auto WrapPyHessianFunc(PyHessianFunc&& hessian_func) {
3959
using Vec = Eigen::VectorXd;
40-
using Mat = Eigen::MatrixXd;
4160
return [hessian_func = std::move(hessian_func)](
4261
const sym::Valuesd& values, const std::vector<index_entry_t>& keys,
43-
Vec* const residual, Mat* const jacobian, Mat* const hessian, Vec* const rhs) {
62+
Vec* const residual, Matrix* const jacobian, Matrix* const hessian, Vec* const rhs) {
4463
const py::tuple out_tuple = hessian_func(values, keys);
4564
if (residual != nullptr) {
4665
*residual = py::cast<Vec>(out_tuple[0]);
4766
}
4867
if (jacobian != nullptr) {
49-
*jacobian = py::cast<Mat>(out_tuple[1]);
68+
ThrowIfSparsityMismatch<Matrix>(out_tuple[1]);
69+
*jacobian = py::cast<Matrix>(out_tuple[1]);
5070
}
5171
if (hessian != nullptr) {
52-
*hessian = py::cast<Mat>(out_tuple[2]);
72+
ThrowIfSparsityMismatch<Matrix>(out_tuple[2]);
73+
*hessian = py::cast<Matrix>(out_tuple[2]);
5374
}
5475
if (rhs != nullptr) {
5576
*rhs = py::cast<Vec>(out_tuple[3]);
5677
}
5778
};
5879
}
5980

60-
sym::Factord MakeHessianFactorSeparateKeys(PyHessianFunc hessian_func,
61-
const std::vector<sym::Key>& keys_to_func,
62-
const std::vector<sym::Key>& keys_to_optimize) {
63-
return sym::Factord(WrapPyHessianFunc(std::move(hessian_func)), keys_to_func, keys_to_optimize);
64-
}
65-
66-
sym::Factord MakeHessianFactorCommonKeys(PyHessianFunc hessian_func,
67-
const std::vector<sym::Key>& keys) {
68-
return sym::Factord(WrapPyHessianFunc(std::move(hessian_func)), keys);
81+
template <typename... Keys>
82+
sym::Factord MakeHessianFactor(PyHessianFunc hessian_func, const std::vector<Keys>&... keys,
83+
bool sparse) {
84+
if (sparse) {
85+
return sym::Factord(WrapPyHessianFunc<Eigen::SparseMatrix<double>>(std::move(hessian_func)),
86+
keys...);
87+
} else {
88+
return sym::Factord(WrapPyHessianFunc<Eigen::MatrixXd>(std::move(hessian_func)), keys...);
89+
}
6990
}
7091

7192
using PyJacobianFunc =
7293
std::function<py::tuple(const sym::Valuesd&, const std::vector<index_entry_t>&)>;
7394

74-
sym::Factord::DenseJacobianFunc WrapPyJacobianFunc(PyJacobianFunc&& jacobian_func) {
75-
return sym::Factord::DenseJacobianFunc(
95+
template <typename Matrix>
96+
sym::Factord::JacobianFunc<Matrix> WrapPyJacobianFunc(PyJacobianFunc&& jacobian_func) {
97+
return sym::Factord::JacobianFunc<Matrix>(
7698
[jacobian_func = std::move(jacobian_func)](
7799
const sym::Valuesd& values, const std::vector<index_entry_t>& keys,
78-
Eigen::VectorXd* const residual, Eigen::MatrixXd* const jacobian) {
100+
Eigen::VectorXd* const residual, Matrix* const jacobian) {
79101
const py::tuple out_tuple = jacobian_func(values, keys);
80102
if (residual != nullptr) {
81103
*residual = py::cast<Eigen::VectorXd>(out_tuple[0]);
82104
}
83105
if (jacobian != nullptr) {
84-
*jacobian = py::cast<Eigen::MatrixXd>(out_tuple[1]);
106+
ThrowIfSparsityMismatch<Matrix>(out_tuple[1]);
107+
*jacobian = py::cast<Matrix>(out_tuple[1]);
85108
}
86109
});
87110
}
88111

89-
sym::Factord MakeJacobianFactorSeparateKeys(PyJacobianFunc jacobian_func,
90-
const std::vector<sym::Key>& keys_to_func,
91-
const std::vector<sym::Key>& keys_to_optimize) {
92-
return sym::Factord::Jacobian(WrapPyJacobianFunc(std::move(jacobian_func)), keys_to_func,
93-
keys_to_optimize);
94-
}
95-
96-
sym::Factord MakeJacobianFactorCommonKeys(PyJacobianFunc jacobian_func,
97-
const std::vector<sym::Key>& keys) {
98-
return sym::Factord::Jacobian(WrapPyJacobianFunc(std::move(jacobian_func)), keys);
112+
template <typename... Keys>
113+
sym::Factord MakeJacobianFactor(PyJacobianFunc jacobian_func, const std::vector<Keys>&... keys,
114+
bool sparse) {
115+
if (sparse) {
116+
return sym::Factord::Jacobian(
117+
WrapPyJacobianFunc<Eigen::SparseMatrix<double>>(std::move(jacobian_func)), keys...);
118+
} else {
119+
return sym::Factord::Jacobian(WrapPyJacobianFunc<Eigen::MatrixXd>(std::move(jacobian_func)),
120+
keys...);
121+
}
99122
}
100123

101124
} // namespace
@@ -112,35 +135,49 @@ void AddFactorWrapper(pybind11::module_ module) {
112135
point, generates a linear approximation to the residual function.
113136
)")
114137
// TODO(brad): Add wrapper of the constructor from SparseHessianFunc
115-
.def(py::init(&MakeHessianFactorCommonKeys), py::arg("hessian_func"), py::arg("keys"), R"(
116-
Create directly from a (dense) hessian functor. This is the lowest-level constructor.
138+
.def(py::init(&MakeHessianFactor<sym::Key>), py::arg("hessian_func"), py::arg("keys"),
139+
py::arg("sparse") = false, R"(
140+
Create directly from a hessian functor. This is the lowest-level constructor.
117141
118142
Args:
119143
keys: The set of input arguments, in order, accepted by func.
144+
sparse: Create a sparse factor if True, dense factor if false. Defaults to dense.
145+
146+
Precondition:
147+
The jacobian and hessian returned by hessian_func have type scipy.sparse.csc_matrix if and only if sparse = True.
120148
)")
121-
.def(py::init(&MakeHessianFactorSeparateKeys), py::arg("hessian_func"),
122-
py::arg("keys_to_func"), py::arg("keys_to_optimize"),
149+
.def(py::init(&MakeHessianFactor<sym::Key, sym::Key>), py::arg("hessian_func"),
150+
py::arg("keys_to_func"), py::arg("keys_to_optimize"), py::arg("sparse") = false,
123151
R"(
124-
Create directly from a (sparse) hessian functor. This is the lowest-level constructor.
152+
Create directly from a hessian functor. This is the lowest-level constructor.
125153
126154
Args:
127155
keys_to_func: The set of input arguments, in order, accepted by func.
128156
keys_to_optimize: The set of input arguments that correspond to the derivative in func. Must be a subset of keys_to_func.
157+
sparse: Create a sparse factor if True, dense factor if false. Defaults to dense.
158+
159+
Precondition:
160+
The jacobian and hessian returned by hessian_func have type scipy.sparse.csc_matrix if and only if sparse = True.
129161
)")
130162
.def("is_sparse", &sym::Factord::IsSparse,
131163
"Does this factor use a sparse jacobian/hessian matrix?")
132-
.def_static("jacobian", &MakeJacobianFactorCommonKeys, py::arg("jacobian_func"),
133-
py::arg("keys"), R"(
164+
.def_static("jacobian", &MakeJacobianFactor<sym::Key>, py::arg("jacobian_func"),
165+
py::arg("keys"), py::arg("sparse") = false, R"(
134166
Create from a function that computes the jacobian. The hessian will be computed using the
135167
Gauss Newton approximation:
136168
H = J.T * J
137169
rhs = J.T * b
138170
139171
Args:
140172
keys: The set of input arguments, in order, accepted by func.
173+
sparse: Create a sparse factor if True, dense factor if false. Defaults to dense.
174+
175+
Precondition:
176+
The jacobian returned by jacobian_func has type scipy.sparse.csc_matrix if and only if sparse = True.
141177
)")
142-
.def_static("jacobian", &MakeJacobianFactorSeparateKeys, py::arg("jacobian_func"),
143-
py::arg("keys_to_func"), py::arg("keys_to_optimize"), R"(
178+
.def_static("jacobian", &MakeJacobianFactor<sym::Key, sym::Key>, py::arg("jacobian_func"),
179+
py::arg("keys_to_func"), py::arg("keys_to_optimize"), py::arg("sparse") = false,
180+
R"(
144181
Create from a function that computes the jacobian. The hessian will be computed using the
145182
Gauss Newton approximation:
146183
H = J.T * J
@@ -149,6 +186,10 @@ void AddFactorWrapper(pybind11::module_ module) {
149186
Args:
150187
keys_to_func: The set of input arguments, in order, accepted by func.
151188
keys_to_optimize: The set of input arguments that correspond to the derivative in func. Must be a subset of keys_to_func.
189+
sparse: Create a sparse factor if True, dense factor if false. Defaults to dense.
190+
191+
Precondition:
192+
The jacobian returned by jacobian_func has type scipy.sparse.csc_matrix if and only if sparse = True.
152193
)")
153194
.def(
154195
"linearize",

symforce/pybind/cc_sym.pyi

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,27 +57,37 @@ class Factor:
5757
self,
5858
hessian_func: typing.Callable[[Values, typing.List[index_entry_t]], tuple],
5959
keys: typing.List[Key],
60+
sparse: bool = False,
6061
) -> None:
6162
"""
62-
Create directly from a (dense) hessian functor. This is the lowest-level constructor.
63+
Create directly from a hessian functor. This is the lowest-level constructor.
6364
6465
Args:
6566
keys: The set of input arguments, in order, accepted by func.
67+
sparse: Create a sparse factor if True, dense factor if false. Defaults to dense.
6668
69+
Precondition:
70+
The jacobian and hessian returned by hessian_func have type scipy.sparse.csc_matrix if and only if sparse = True.
6771
6872
69-
Create directly from a (sparse) hessian functor. This is the lowest-level constructor.
73+
74+
Create directly from a hessian functor. This is the lowest-level constructor.
7075
7176
Args:
7277
keys_to_func: The set of input arguments, in order, accepted by func.
7378
keys_to_optimize: The set of input arguments that correspond to the derivative in func. Must be a subset of keys_to_func.
79+
sparse: Create a sparse factor if True, dense factor if false. Defaults to dense.
80+
81+
Precondition:
82+
The jacobian and hessian returned by hessian_func have type scipy.sparse.csc_matrix if and only if sparse = True.
7483
"""
7584
@typing.overload
7685
def __init__(
7786
self,
7887
hessian_func: typing.Callable[[Values, typing.List[index_entry_t]], tuple],
7988
keys_to_func: typing.List[Key],
8089
keys_to_optimize: typing.List[Key],
90+
sparse: bool = False,
8191
) -> None: ...
8292
def __repr__(self) -> str: ...
8393
def all_keys(self) -> typing.List[Key]:
@@ -93,6 +103,7 @@ class Factor:
93103
def jacobian(
94104
jacobian_func: typing.Callable[[Values, typing.List[index_entry_t]], tuple],
95105
keys: typing.List[Key],
106+
sparse: bool = False,
96107
) -> Factor:
97108
"""
98109
Create from a function that computes the jacobian. The hessian will be computed using the
@@ -102,6 +113,10 @@ class Factor:
102113
103114
Args:
104115
keys: The set of input arguments, in order, accepted by func.
116+
sparse: Create a sparse factor if True, dense factor if false. Defaults to dense.
117+
118+
Precondition:
119+
The jacobian returned by jacobian_func has type scipy.sparse.csc_matrix if and only if sparse = True.
105120
106121
107122
@@ -113,13 +128,18 @@ class Factor:
113128
Args:
114129
keys_to_func: The set of input arguments, in order, accepted by func.
115130
keys_to_optimize: The set of input arguments that correspond to the derivative in func. Must be a subset of keys_to_func.
131+
sparse: Create a sparse factor if True, dense factor if false. Defaults to dense.
132+
133+
Precondition:
134+
The jacobian returned by jacobian_func has type scipy.sparse.csc_matrix if and only if sparse = True.
116135
"""
117136
@staticmethod
118137
@typing.overload
119138
def jacobian(
120139
jacobian_func: typing.Callable[[Values, typing.List[index_entry_t]], tuple],
121140
keys_to_func: typing.List[Key],
122141
keys_to_optimize: typing.List[Key],
142+
sparse: bool = False,
123143
) -> Factor: ...
124144
def linearize(self, arg0: Values) -> tuple:
125145
"""

0 commit comments

Comments
 (0)