Skip to content

Commit 76b7b12

Browse files
authored
Growing IRIS-ZO Regions Along a Parametrized Subspace (#22558)
1 parent 6d0d71c commit 76b7b12

File tree

3 files changed

+334
-49
lines changed

3 files changed

+334
-49
lines changed

planning/iris/iris_zo.cc

+88-43
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ HPolyhedron IrisZo(const planning::CollisionChecker& checker,
8484
const HPolyhedron& domain, const IrisZoOptions& options) {
8585
auto start = std::chrono::high_resolution_clock::now();
8686
const int num_threads_to_use =
87-
checker.SupportsParallelChecking()
87+
checker.SupportsParallelChecking() &&
88+
options.get_parameterization_is_threadsafe()
8889
? std::min(options.parallelism.num_threads(),
8990
checker.num_allocated_contexts())
9091
: 1;
@@ -100,14 +101,30 @@ HPolyhedron IrisZo(const planning::CollisionChecker& checker,
100101
// Prevent directly terminating if the ellipsoid is too large.
101102
double previous_volume = 0;
102103

103-
const int dim = starting_ellipsoid.ambient_dimension();
104-
int current_num_faces = domain.A().rows();
104+
const int ambient_dimension = checker.plant().num_positions();
105+
const int parameterized_dimension =
106+
options.get_parameterization_dimension().value_or(ambient_dimension);
105107

106108
DRAKE_THROW_UNLESS(num_threads_to_use > 0);
107-
DRAKE_THROW_UNLESS(domain.ambient_dimension() == dim);
109+
DRAKE_THROW_UNLESS(starting_ellipsoid.ambient_dimension() ==
110+
parameterized_dimension);
111+
DRAKE_THROW_UNLESS(domain.ambient_dimension() == parameterized_dimension);
108112
DRAKE_THROW_UNLESS(domain.IsBounded());
109113
DRAKE_THROW_UNLESS(domain.PointInSet(current_ellipsoid_center));
110114

115+
const int computed_ambient_dimension =
116+
options.get_parameterization()(starting_ellipsoid_center).size();
117+
if (computed_ambient_dimension != ambient_dimension) {
118+
throw std::runtime_error(fmt::format(
119+
"The plant has {} positions, but the given parameterization "
120+
"returned a point with the wrong dimension (its size was "
121+
"{}) when called on {}.",
122+
ambient_dimension, computed_ambient_dimension,
123+
fmt_eigen(starting_ellipsoid_center.transpose())));
124+
}
125+
126+
int current_num_faces = domain.A().rows();
127+
111128
if (options.max_iterations_separating_planes <= 0) {
112129
throw std::runtime_error(
113130
"The maximum number of iterations for separating planes "
@@ -116,7 +133,7 @@ HPolyhedron IrisZo(const planning::CollisionChecker& checker,
116133
VPolytope cvxh_vpoly;
117134
if (options.containment_points.has_value()) {
118135
cvxh_vpoly = VPolytope(options.containment_points.value());
119-
DRAKE_THROW_UNLESS(domain.ambient_dimension() ==
136+
DRAKE_THROW_UNLESS(parameterized_dimension ==
120137
options.containment_points->rows());
121138

122139
constexpr float kPointInSetTol = 1e-5;
@@ -133,7 +150,8 @@ HPolyhedron IrisZo(const planning::CollisionChecker& checker,
133150

134151
for (int col = 0; col < options.containment_points->cols(); ++col) {
135152
Eigen::VectorXd conf = options.containment_points->col(col);
136-
cont_vec.emplace_back(conf);
153+
cont_vec.emplace_back(options.get_parameterization()(conf));
154+
DRAKE_ASSERT(cont_vec.back().size() == ambient_dimension);
137155
}
138156

139157
std::vector<uint8_t> containment_point_col_free =
@@ -147,11 +165,14 @@ HPolyhedron IrisZo(const planning::CollisionChecker& checker,
147165
}
148166
// For debugging visualization.
149167
Eigen::Vector3d point_to_draw = Eigen::Vector3d::Zero();
150-
if (options.meshcat && dim <= 3) {
168+
if (options.meshcat && ambient_dimension <= 3) {
151169
std::string path = "seedpoint";
152170
options.meshcat->SetObject(path, Sphere(0.06),
153171
geometry::Rgba(0.1, 1, 1, 1.0));
154-
point_to_draw.head(dim) = current_ellipsoid_center;
172+
Eigen::VectorXd conf_ambient =
173+
options.get_parameterization()(current_ellipsoid_center);
174+
DRAKE_ASSERT(conf_ambient.size() == ambient_dimension);
175+
point_to_draw.head(ambient_dimension) = conf_ambient;
155176
options.meshcat->SetTransform(path, RigidTransform<double>(point_to_draw));
156177
}
157178

@@ -174,7 +195,8 @@ HPolyhedron IrisZo(const planning::CollisionChecker& checker,
174195
log()->info("IrisZo worst case test requires {} samples.", N_max);
175196
}
176197

177-
std::vector<Eigen::VectorXd> particles(N_max, Eigen::VectorXd::Zero(dim));
198+
std::vector<Eigen::VectorXd> particles(
199+
N_max, Eigen::VectorXd::Zero(parameterized_dimension));
178200

179201
int iteration = 0;
180202
HPolyhedron P = domain;
@@ -184,7 +206,7 @@ HPolyhedron IrisZo(const planning::CollisionChecker& checker,
184206
// TODO(wernerpe): Find a better solution than hardcoding 300.
185207
constexpr int kNumFacesToPreAllocate = 300;
186208
Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> A(
187-
P.A().rows() + kNumFacesToPreAllocate, dim);
209+
P.A().rows() + kNumFacesToPreAllocate, parameterized_dimension);
188210
Eigen::VectorXd b(P.A().rows() + kNumFacesToPreAllocate);
189211

190212
while (true) {
@@ -228,15 +250,24 @@ HPolyhedron IrisZo(const planning::CollisionChecker& checker,
228250
options.mixing_steps);
229251
}
230252

231-
// Copy top slice of particles, due to collision checker only accepting
232-
// vectors of configurations.
233-
// TODO(wernerpe): Remove this copy operation.
234-
std::vector<Eigen::VectorXd> particles_step(particles.begin(),
235-
particles.begin() + N_k);
253+
// Copy top slice of particles, applying thet parameterization function to
254+
// each one, due to collision checker only accepting vectors of
255+
// configurations.
256+
// TODO(wernerpe, cohnt): Remove this copy operation.
257+
// TODO(cohnt): Consider parallelizing the parameterization calls, in case
258+
// it's an expensive operation.
259+
std::vector<Eigen::VectorXd> ambient_particles(N_k);
260+
std::transform(particles.begin(), particles.begin() + N_k,
261+
ambient_particles.begin(), options.get_parameterization());
262+
263+
for (int i = 0; i < ssize(ambient_particles); ++i) {
264+
// Only run this check in debug mode, because it's expensive.
265+
DRAKE_ASSERT(ambient_particles[i].size() == ambient_dimension);
266+
}
236267

237268
// Find all particles in collision
238269
std::vector<uint8_t> particle_col_free =
239-
checker.CheckConfigsCollisionFree(particles_step,
270+
checker.CheckConfigsCollisionFree(ambient_particles,
240271
options.parallelism);
241272
std::vector<Eigen::VectorXd> particles_in_collision;
242273
int number_particles_in_collision_unadaptive_test = 0;
@@ -280,34 +311,45 @@ HPolyhedron IrisZo(const planning::CollisionChecker& checker,
280311

281312
// For each particle in collision, we run a bisection search to find a
282313
// configuration on the boundary of the obstacle.
283-
const auto particle_update_work =
284-
[&checker, &particles_in_collision_updated, &particles_in_collision,
285-
&current_ellipsoid_center,
286-
&options](const int thread_num, const int64_t index) {
287-
const int point_idx = static_cast<int>(index);
288-
auto start_point = particles_in_collision[point_idx];
289-
290-
Eigen::VectorXd current_point = start_point;
291-
Eigen::VectorXd curr_pt_lower = current_ellipsoid_center;
292-
293-
// Update current point using a fixed number of bisection steps.
294-
if (!checker.CheckConfigCollisionFree(curr_pt_lower, thread_num)) {
295-
current_point = curr_pt_lower;
314+
const auto particle_update_work = [&checker,
315+
&particles_in_collision_updated,
316+
&particles_in_collision,
317+
&current_ellipsoid_center,
318+
&options](const int thread_num,
319+
const int64_t index) {
320+
const int point_idx = static_cast<int>(index);
321+
auto start_point = particles_in_collision[point_idx];
322+
323+
Eigen::VectorXd current_point = start_point;
324+
Eigen::VectorXd curr_pt_lower = current_ellipsoid_center;
325+
326+
// Update current point using a fixed number of bisection steps.
327+
Eigen::VectorXd current_point_ambient =
328+
options.get_parameterization()(curr_pt_lower);
329+
DRAKE_ASSERT(current_point_ambient.size() ==
330+
checker.plant().num_positions());
331+
if (!checker.CheckConfigCollisionFree(current_point_ambient,
332+
thread_num)) {
333+
current_point = curr_pt_lower;
334+
} else {
335+
Eigen::VectorXd curr_pt_upper = current_point;
336+
for (int i = 0; i < options.bisection_steps; ++i) {
337+
Eigen::VectorXd query = 0.5 * (curr_pt_upper + curr_pt_lower);
338+
Eigen::VectorXd query_ambient =
339+
options.get_parameterization()(query);
340+
DRAKE_ASSERT(query_ambient.size() ==
341+
checker.plant().num_positions());
342+
if (checker.CheckConfigCollisionFree(query_ambient, thread_num)) {
343+
curr_pt_lower = query;
296344
} else {
297-
Eigen::VectorXd curr_pt_upper = current_point;
298-
for (int i = 0; i < options.bisection_steps; ++i) {
299-
Eigen::VectorXd query = 0.5 * (curr_pt_upper + curr_pt_lower);
300-
if (checker.CheckConfigCollisionFree(query, thread_num)) {
301-
curr_pt_lower = query;
302-
} else {
303-
curr_pt_upper = query;
304-
current_point = query;
305-
}
306-
}
345+
curr_pt_upper = query;
346+
current_point = query;
307347
}
348+
}
349+
}
308350

309-
particles_in_collision_updated[point_idx] = current_point;
310-
};
351+
particles_in_collision_updated[point_idx] = current_point;
352+
};
311353

312354
// Update all particles in parallel.
313355
DynamicParallelForIndexLoop(DegreeOfParallelism(num_threads_to_use), 0,
@@ -378,15 +420,18 @@ HPolyhedron IrisZo(const planning::CollisionChecker& checker,
378420
}
379421

380422
// Fill in meshcat if added for debugging.
381-
if (options.meshcat && dim <= 3) {
423+
if (options.meshcat && ambient_dimension <= 3) {
382424
for (int pt_to_draw = 0; pt_to_draw < number_particles_in_collision;
383425
++pt_to_draw) {
384426
std::string path = fmt::format(
385427
"face_pt/iteration{:02}/sepit{:02}/{:03}/pt", iteration,
386428
num_iterations_separating_planes, current_num_faces);
387429
options.meshcat->SetObject(path, Sphere(0.03),
388430
geometry::Rgba(1, 1, 0.1, 1.0));
389-
point_to_draw.head(dim) = nearest_particle;
431+
Eigen::VectorXd ambient_particle =
432+
options.get_parameterization()(nearest_particle);
433+
DRAKE_ASSERT(ambient_particle.size() == ambient_dimension);
434+
point_to_draw.head(ambient_dimension) = ambient_particle;
390435
options.meshcat->SetTransform(
391436
path, RigidTransform<double>(point_to_draw));
392437
}

planning/iris/iris_zo.h

+66-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include <filesystem>
4+
#include <functional>
45
#include <memory>
56
#include <optional>
67
#include <utility>
@@ -22,7 +23,10 @@ namespace planning {
2223
* @experimental
2324
* @see IrisZo for more details.
2425
**/
25-
struct IrisZoOptions {
26+
class IrisZoOptions {
27+
public:
28+
DRAKE_DEFAULT_COPY_AND_MOVE_AND_ASSIGN(IrisZoOptions);
29+
2630
/** Passes this object to an Archive.
2731
Refer to @ref yaml_serialization "YAML Serialization" for background.
2832
Note: This only serializes options that are YAML built-in types. */
@@ -92,7 +96,8 @@ struct IrisZoOptions {
9296

9397
/** Number of threads to use when updating the particles. If the user requests
9498
* more threads than the CollisionChecker supports, that number of threads
95-
* will be used instead. */
99+
* will be used instead. However, see also `parameterization_is_threadsafe`.
100+
*/
96101
Parallelism parallelism{Parallelism::Max()};
97102

98103
/** Enables print statements indicating the progress of IrisZo. */
@@ -128,6 +133,62 @@ struct IrisZoOptions {
128133
currently and when the
129134
configuration space is <= 3 dimensional.*/
130135
std::shared_ptr<geometry::Meshcat> meshcat{};
136+
137+
typedef std::function<Eigen::VectorXd(const Eigen::VectorXd&)>
138+
ParameterizationFunction;
139+
140+
/** Ordinarily, IRIS-ZO grows collision free regions in the robot's
141+
* configuration space C. This allows the user to specify a function f:Q→C ,
142+
* and grow the region in Q instead. The function should be a map R^m to
143+
* R^n, where n is the dimension of the plant configuration space, determined
144+
* via `checker.plant().num_positions()` and m is `parameterization_dimension`
145+
* if specified. The user must provide `parameterization`, which is the
146+
* function f, `parameterization_is_threadsafe`, which is whether or not
147+
* `parametrization` can be called concurrently, and
148+
* `parameterization_dimension`, the dimension of the input space Q. */
149+
void set_parameterization(const ParameterizationFunction& parameterization,
150+
bool parameterization_is_threadsafe,
151+
int parameterization_dimension) {
152+
parameterization_ = parameterization;
153+
parameterization_is_threadsafe_ = parameterization_is_threadsafe;
154+
parameterization_dimension_ = parameterization_dimension;
155+
}
156+
157+
/** Get the parameterization function.
158+
* @note If the user has not specified this with `set_parameterization()`,
159+
* then the default value of `parameterization_` is the identity function,
160+
* indicating that the regions should be grown in the full configuration space
161+
* (in the standard coordinate system). */
162+
const ParameterizationFunction& get_parameterization() const {
163+
return parameterization_;
164+
}
165+
166+
/** Returns whether or not the user has specified the parameterization to be
167+
* threadsafe.
168+
* @note The default `parameterization_` is the identity function, which is
169+
* threadsafe. */
170+
bool get_parameterization_is_threadsafe() const {
171+
return parameterization_is_threadsafe_;
172+
}
173+
174+
/** Returns what the user has specified as the input dimension for the
175+
* parameterization function, or std::nullopt if it has not been set. A
176+
* std::nullopt value indicates that
177+
* IrisZo should use the ambient configuration space dimension as the input
178+
* dimension to the parameterization. */
179+
std::optional<int> get_parameterization_dimension() const {
180+
return parameterization_dimension_;
181+
}
182+
183+
private:
184+
bool parameterization_is_threadsafe_{true};
185+
186+
std::optional<int> parameterization_dimension_{std::nullopt};
187+
188+
std::function<Eigen::VectorXd(const Eigen::VectorXd&)> parameterization_{
189+
[](const Eigen::VectorXd& q) -> Eigen::VectorXd {
190+
return q;
191+
}};
131192
};
132193

133194
/** The IRIS-ZO (Iterative Regional Inflation by Semidefinite programming - Zero
@@ -160,7 +221,9 @@ box representing joint limits (e.g. from HPolyhedron::MakeBox).
160221
fraction, confidence level, and various algorithmic settings.
161222
162223
The @p starting_ellipsoid and @p domain must describe elements in the same
163-
ambient dimension as the configuration space of the robot.
224+
ambient dimension as the configuration space of the robot, unless a
225+
parameterization is specified (in which case, they must match
226+
`options.parameterization_dimension`).
164227
@return A HPolyhedron representing the computed collision-free region in
165228
configuration space.
166229
@ingroup robot_planning

0 commit comments

Comments
 (0)