Skip to content

Commit

Permalink
Mathematical program additional bindings (RobotLocomotion#21754)
Browse files Browse the repository at this point in the history
Adds some missing python bindings to mathematical program.
  • Loading branch information
AlexandreAmice authored Aug 1, 2024
1 parent bb415eb commit b9de456
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 19 deletions.
9 changes: 9 additions & 0 deletions bindings/pydrake/solvers/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ drake_pybind_library(
"solvers_py_scs.cc",
"solvers_py_sdpa_free_format.cc",
"solvers_py_semidefinite_relaxation.cc",
"solvers_py_program_attribute.cc",
"solvers_py_snopt.cc",
"solvers_py_unrevised_lemke.cc",
],
Expand Down Expand Up @@ -291,4 +292,12 @@ drake_py_unittest(
],
)

drake_py_unittest(
name = "program_attribute_test",
deps = [
":solvers",
"//bindings/pydrake/common/test_utilities",
],
)

add_lint_tests_pydrake()
1 change: 1 addition & 0 deletions bindings/pydrake/solvers/solvers_py.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ top-level documentation for :py:mod:`pydrake.math`.

// The order of these calls matters. Some modules rely on prior definitions.
internal::DefineSolversEvaluators(m);
internal::DefineProgramAttribute(m);
internal::DefineSolversMathematicalProgram(m);
internal::DefineSolversAugmentedLagrangian(m);
internal::DefineSolversBranchAndBound(m);
Expand Down
4 changes: 4 additions & 0 deletions bindings/pydrake/solvers/solvers_py.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ void DefineSolversGurobi(py::module m);
/* Defines the IPOPT bindings. See solvers_py_ipopt.cc. */
void DefineSolversIpopt(py::module m);

/* Defines the ProgramAttributes bindings. See solvers_py_program_attributes.cc.
*/
void DefineProgramAttribute(py::module m);

/* Defines the cost, constraint, mathematical program, etc. bindings.
See solvers_py_mathematicalprogram.cc.
TODO(jwnimmer-tri) Split this into smaller pieces.
Expand Down
6 changes: 5 additions & 1 deletion bindings/pydrake/solvers/solvers_py_evaluators.cc
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ auto RegisterBinding(py::handle* scope) {
.def("variables", &B::variables, cls_doc.variables.doc)
.def(
"ToLatex", &B::ToLatex, py::arg("precision") = 3, cls_doc.ToLatex.doc)
.def("__str__", &B::to_string, cls_doc.to_string.doc);
.def("__str__", &B::to_string, cls_doc.to_string.doc)
.def("__hash__", [](const B& self) { return std::hash<B>{}(self); })
.def(
"__eq__", [](const B& self, const B& other) { return self == other; },
py::is_operator());
if (!std::is_same_v<C, EvaluatorBase>) {
// This is required for implicit argument conversion. See below for
// `EvaluatorBase`'s generic constructor for attempting downcasting.
Expand Down
27 changes: 9 additions & 18 deletions bindings/pydrake/solvers/solvers_py_mathematicalprogram.cc
Original file line number Diff line number Diff line change
Expand Up @@ -348,24 +348,6 @@ void BindSolverInterfaceAndFlags(py::module m) {
},
py::is_operator());

py::enum_<ProgramType>(m, "ProgramType", doc.ProgramType.doc)
.value("kLP", ProgramType::kLP, doc.ProgramType.kLP.doc)
.value("kQP", ProgramType::kQP, doc.ProgramType.kQP.doc)
.value("kSOCP", ProgramType::kSOCP, doc.ProgramType.kSOCP.doc)
.value("kSDP", ProgramType::kSDP, doc.ProgramType.kSDP.doc)
.value("kGP", ProgramType::kGP, doc.ProgramType.kGP.doc)
.value("kCGP", ProgramType::kCGP, doc.ProgramType.kCGP.doc)
.value("kMILP", ProgramType::kMILP, doc.ProgramType.kMILP.doc)
.value("kMIQP", ProgramType::kMIQP, doc.ProgramType.kMIQP.doc)
.value("kMISOCP", ProgramType::kMISOCP, doc.ProgramType.kMISOCP.doc)
.value("kMISDP", ProgramType::kMISDP, doc.ProgramType.kMISDP.doc)
.value("kQuadraticCostConicConstraint",
ProgramType::kQuadraticCostConicConstraint,
doc.ProgramType.kQuadraticCostConicConstraint.doc)
.value("kNLP", ProgramType::kNLP, doc.ProgramType.kNLP.doc)
.value("kLCP", ProgramType::kLCP, doc.ProgramType.kLCP.doc)
.value("kUnknown", ProgramType::kUnknown, doc.ProgramType.kUnknown.doc);

py::enum_<SolverType> solver_type(m, "SolverType", doc.SolverType.doc);
solver_type // BR
.value("kClp", SolverType::kClp, doc.SolverType.kClp.doc)
Expand Down Expand Up @@ -893,6 +875,13 @@ void BindMathematicalProgram(py::module m) {
},
py::arg("formulas"),
doc.MathematicalProgram.AddConstraint.doc_1args_constEigenDenseBase)
.def(
"AddConstraint",
[](MathematicalProgram* self, const Binding<Constraint>& binding) {
return self->AddConstraint(binding);
},
py::arg("binding"),
doc.MathematicalProgram.AddConstraint.doc_1args_binding)
.def("AddLinearConstraint",
static_cast<Binding<LinearConstraint> (MathematicalProgram::*)(
const Eigen::Ref<const Eigen::MatrixXd>&,
Expand Down Expand Up @@ -1588,6 +1577,8 @@ for every column of ``prog_var_vals``. )""")
py_rvp::copy, doc.MathematicalProgram.indeterminates.doc)
.def("indeterminate", &MathematicalProgram::indeterminate, py::arg("i"),
doc.MathematicalProgram.indeterminate.doc)
.def("required_capabilities", &MathematicalProgram::required_capabilities,
doc.MathematicalProgram.required_capabilities.doc)
.def("indeterminates_index", &MathematicalProgram::indeterminates_index,
doc.MathematicalProgram.indeterminates_index.doc)
.def("decision_variables", &MathematicalProgram::decision_variables,
Expand Down
71 changes: 71 additions & 0 deletions bindings/pydrake/solvers/solvers_py_program_attribute.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#include "drake/bindings/pydrake/common/eigen_pybind.h"
#include "drake/bindings/pydrake/documentation_pybind.h"
#include "drake/bindings/pydrake/pydrake_pybind.h"
#include "drake/bindings/pydrake/solvers/solvers_py.h"
#include "drake/solvers/mathematical_program.h"

namespace drake {
namespace pydrake {
namespace internal {
void DefineProgramAttribute(py::module m) {
// NOLINTNEXTLINE(build/namespaces): Emulate placement in namespace.
using namespace drake::solvers;
constexpr auto& doc = pydrake_doc.drake.solvers;
py::enum_<ProgramAttribute>(m, "ProgramAttribute", doc.ProgramAttribute.doc)
.value("kGenericCost", ProgramAttribute::kGenericCost,
doc.ProgramAttribute.kGenericCost.doc)
.value("kGenericConstraint", ProgramAttribute::kGenericConstraint,
doc.ProgramAttribute.kGenericConstraint.doc)
.value("kQuadraticCost", ProgramAttribute::kQuadraticCost,
doc.ProgramAttribute.kQuadraticCost.doc)
.value("kQuadraticConstraint", ProgramAttribute::kQuadraticConstraint,
doc.ProgramAttribute.kQuadraticConstraint.doc)
.value("kLinearCost", ProgramAttribute::kLinearCost,
doc.ProgramAttribute.kLinearCost.doc)
.value("kLinearConstraint", ProgramAttribute::kLinearConstraint,
doc.ProgramAttribute.kLinearConstraint.doc)
.value("kLinearEqualityConstraint",
ProgramAttribute::kLinearEqualityConstraint,
doc.ProgramAttribute.kLinearEqualityConstraint.doc)
.value("kLinearComplementarityConstraint",
ProgramAttribute::kLinearComplementarityConstraint,
doc.ProgramAttribute.kLinearComplementarityConstraint.doc)
.value("kLorentzConeConstraint", ProgramAttribute::kLorentzConeConstraint,
doc.ProgramAttribute.kLorentzConeConstraint.doc)
.value("kRotatedLorentzConeConstraint",
ProgramAttribute::kRotatedLorentzConeConstraint,
doc.ProgramAttribute.kRotatedLorentzConeConstraint.doc)
.value("kPositiveSemidefiniteConstraint",
ProgramAttribute::kPositiveSemidefiniteConstraint,
doc.ProgramAttribute.kPositiveSemidefiniteConstraint.doc)
.value("kExponentialConeConstraint",
ProgramAttribute::kExponentialConeConstraint,
doc.ProgramAttribute.kExponentialConeConstraint.doc)
.value("kL2NormCost", ProgramAttribute::kL2NormCost,
doc.ProgramAttribute.kL2NormCost.doc)
.value("kBinaryVariable", ProgramAttribute::kBinaryVariable,
doc.ProgramAttribute.kBinaryVariable.doc)
.value("kCallback", ProgramAttribute::kCallback,
doc.ProgramAttribute.kCallback.doc);

py::enum_<ProgramType>(m, "ProgramType", doc.ProgramType.doc)
.value("kLP", ProgramType::kLP, doc.ProgramType.kLP.doc)
.value("kQP", ProgramType::kQP, doc.ProgramType.kQP.doc)
.value("kSOCP", ProgramType::kSOCP, doc.ProgramType.kSOCP.doc)
.value("kSDP", ProgramType::kSDP, doc.ProgramType.kSDP.doc)
.value("kGP", ProgramType::kGP, doc.ProgramType.kGP.doc)
.value("kCGP", ProgramType::kCGP, doc.ProgramType.kCGP.doc)
.value("kMILP", ProgramType::kMILP, doc.ProgramType.kMILP.doc)
.value("kMIQP", ProgramType::kMIQP, doc.ProgramType.kMIQP.doc)
.value("kMISOCP", ProgramType::kMISOCP, doc.ProgramType.kMISOCP.doc)
.value("kMISDP", ProgramType::kMISDP, doc.ProgramType.kMISDP.doc)
.value("kQuadraticCostConicConstraint",
ProgramType::kQuadraticCostConicConstraint,
doc.ProgramType.kQuadraticCostConicConstraint.doc)
.value("kNLP", ProgramType::kNLP, doc.ProgramType.kNLP.doc)
.value("kLCP", ProgramType::kLCP, doc.ProgramType.kLCP.doc)
.value("kUnknown", ProgramType::kUnknown, doc.ProgramType.kUnknown.doc);
}
} // namespace internal
} // namespace pydrake
} // namespace drake
108 changes: 108 additions & 0 deletions bindings/pydrake/solvers/test/evaluators_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import numpy as np
import scipy.sparse
import copy

import pydrake.solvers as mp
import pydrake.symbolic as sym
Expand Down Expand Up @@ -91,6 +92,49 @@ def test_to_latex(self):
binding = mp.Binding[mp.ExpressionCost](cost, cost.vars())
self.assertEqual(binding.ToLatex(precision=1), "(y + \\sin{x})")

def test_binding_eq(self):
x = sym.Variable("x")
y = sym.Variable("y")
z = sym.Variable("z")
e1 = np.sin(x) + y
e2 = np.cos(x) + y
e3 = np.sin(z) + y

cost1 = mp.ExpressionCost(e=e1)
cost1_binding1 = mp.Binding[mp.ExpressionCost](cost1, cost1.vars())
cost1_binding2 = mp.Binding[mp.ExpressionCost](cost1, cost1.vars())

cost2 = mp.ExpressionCost(e=e2)
cost2_binding = mp.Binding[mp.ExpressionCost](cost2, cost2.vars())

cost3 = mp.ExpressionCost(e=e3)
cost3_binding = mp.Binding[mp.ExpressionCost](cost3, cost3.vars())

self.assertTrue(cost1_binding1 == cost1_binding1)
self.assertTrue(cost1_binding1 == cost1_binding2)
self.assertEqual(cost1_binding1, cost1_binding2)

# The bindings have the same variables but different expressions.
self.assertNotEqual(cost1_binding1, cost2_binding)
# The bindings have the same expression but different variables.
self.assertNotEqual(cost1_binding1, cost3_binding)

def test_binding_hash(self):
x = sym.Variable("x")
y = sym.Variable("y")
e = np.log(2*x) + y**2
e2 = y

cost1 = mp.ExpressionCost(e=e)
cost1_binding1 = mp.Binding[mp.ExpressionCost](cost1, cost1.vars())
cost1_binding2 = mp.Binding[mp.ExpressionCost](cost1, cost1.vars())

cost2 = mp.ExpressionCost(e=e2)
cost2_binding = mp.Binding[mp.ExpressionCost](cost2, cost2.vars())

self.assertEqual(hash(cost1_binding1), hash(cost1_binding2))
self.assertNotEqual(hash(cost1_binding1), hash(cost2_binding))


class TestConstraints(unittest.TestCase):
def test_bounding_box_constraint(self):
Expand Down Expand Up @@ -231,6 +275,70 @@ def test_binding_instantiations(self):
for cls in cls_list:
mp.Binding[cls]

def test_binding_eq(self):
x = sym.Variable("x")
y = sym.Variable("y")
z = sym.Variable("z")
e1 = np.sin(x) + y
e2 = np.cos(x) + y
e3 = np.sin(z) + y
constraint1 = mp.ExpressionConstraint(v=np.array([e1]),
lb=np.array([1.0]),
ub=np.array([2.0]))
constraint1_binding1 = mp.Binding[mp.ExpressionConstraint](
constraint1, constraint1.vars()
)
constraint1_binding2 = mp.Binding[mp.ExpressionConstraint](
constraint1, constraint1.vars()
)

constraint2 = mp.ExpressionConstraint(v=np.array([e2]),
lb=np.array([1.0]),
ub=np.array([2.0]))
constraint2_binding = mp.Binding[mp.ExpressionConstraint](
constraint2, constraint2.vars()
)

constraint3 = mp.ExpressionConstraint(v=np.array([e3]),
lb=np.array([1.0]),
ub=np.array([2.0]))
constraint3_binding = mp.Binding[mp.ExpressionConstraint](
constraint3, constraint3.vars()
)

self.assertTrue(constraint1_binding1 == constraint1_binding1)
self.assertTrue(constraint1_binding1 == constraint1_binding2)
self.assertEqual(constraint1_binding1, constraint1_binding2)

# The bindings have the same variables but different expressions.
self.assertNotEqual(constraint1_binding1, constraint2_binding)
# The bindings have the same expression but different variables.
self.assertNotEqual(constraint1_binding1, constraint3_binding)

def test_binding_hash(self):
x = sym.Variable("x")
y = sym.Variable("y")
e = np.log(2*x) + y**2
e2 = y
constraint1 = mp.ExpressionConstraint(v=np.array([e]),
lb=np.array([1.0]),
ub=np.array([2.0]))
constraint1_binding1 = mp.Binding[mp.ExpressionConstraint](
constraint1, constraint1.vars()
)
constraint1_binding2 = mp.Binding[mp.ExpressionConstraint](
constraint1, constraint1.vars()
)
constraint2 = mp.ExpressionConstraint(v=np.array([e2]),
lb=np.array([1.0]),
ub=np.array([2.0]))
constraint2_binding = mp.Binding[mp.ExpressionConstraint](
constraint2, constraint2.vars()
)
self.assertEqual(hash(constraint1_binding1),
hash(constraint1_binding2))
self.assertNotEqual(hash(constraint1_binding1), hash(constraint2))


# A dummy value function for MinimumValue{Lower,Upper}BoundConstraint.
def value_function(x: np.ndarray, v_influence: float) -> np.ndarray:
Expand Down
29 changes: 29 additions & 0 deletions bindings/pydrake/solvers/test/mathematicalprogram_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,16 @@ def test_addconstraint_matrix(self):
self.assertTrue(result.GetSolution(x)[0] <= 2)
self.assertTrue(result.GetSolution(x)[0] >= -2)

def test_addconstraint_binding(self):
prog = mp.MathematicalProgram()
x = prog.NewContinuousVariables(1, 'x')
prog.AddConstraint(x[0] <= 2)
# This ensures that constraint is of type Binding<Constraint> and not
# a more specific type.
constraint = prog.GetAllConstraints()[0]
constraint2 = prog.AddConstraint(constraint)
self.assertEqual(constraint, constraint2)

def test_initial_guess(self):
prog = mp.MathematicalProgram()
count = 6
Expand Down Expand Up @@ -1358,6 +1368,25 @@ def test_add_indeterminates_and_decision_variables(self):
numpy_compare.assert_equal(prog.indeterminates()[0], x0)
numpy_compare.assert_equal(prog.indeterminate(1), x1)

def test_required_capabilities(self):

prog = mp.MathematicalProgram()
X = prog.NewSymmetricContinuousVariables(3, "X")
prog.AddPositiveSemidefiniteConstraint(X)

prog.AddLinearConstraint(X[0, 0] >= 0)
prog.AddLinearEqualityConstraint(X[1, 0] == 1)
prog.AddLinearCost(X[0, 0])
expected_attributes = [
mp.ProgramAttribute.kLinearCost,
mp.ProgramAttribute.kLinearEqualityConstraint,
mp.ProgramAttribute.kLinearConstraint,
mp.ProgramAttribute.kPositiveSemidefiniteConstraint]
for attribute in expected_attributes:
self.assertIn(attribute, prog.required_capabilities())
for attribute in prog.required_capabilities():
self.assertIn(attribute, expected_attributes)

def test_make_first_available_solver(self):
gurobi_solver = GurobiSolver()
scs_solver = ScsSolver()
Expand Down
48 changes: 48 additions & 0 deletions bindings/pydrake/solvers/test/program_attribute_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import unittest
from pydrake.solvers import (
ProgramAttribute,
ProgramType
)


class TestProgramAttribute(unittest.TestCase):
def test_program_attribute_enum(self):
# This list checks that all the enums exist to ensure that none are
# deleted by accident.
enum_expected = [ProgramAttribute.kGenericCost,
ProgramAttribute.kGenericConstraint,
ProgramAttribute.kQuadraticCost,
ProgramAttribute.kQuadraticConstraint,
ProgramAttribute.kLinearCost,
ProgramAttribute.kLinearConstraint,
ProgramAttribute.kLinearEqualityConstraint,
ProgramAttribute.kLinearComplementarityConstraint,
ProgramAttribute.kLorentzConeConstraint,
ProgramAttribute.kRotatedLorentzConeConstraint,
ProgramAttribute.kPositiveSemidefiniteConstraint,
ProgramAttribute.kExponentialConeConstraint,
ProgramAttribute.kL2NormCost,
ProgramAttribute.kBinaryVariable,
ProgramAttribute.kCallback,
]


class TestProgramType(unittest.TestCase):
def test_program_attribute_enum(self):
# This list checks that all the enums exist to ensure that none are
# deleted by accident.
enum_expected = [ProgramType.kLP,
ProgramType.kQP,
ProgramType.kSOCP,
ProgramType.kSDP,
ProgramType.kGP,
ProgramType.kCGP,
ProgramType.kMILP,
ProgramType.kMIQP,
ProgramType.kMISOCP,
ProgramType.kMISDP,
ProgramType.kQuadraticCostConicConstraint,
ProgramType.kNLP,
ProgramType.kLCP,
ProgramType.kUnknown
]

0 comments on commit b9de456

Please sign in to comment.