smooth
A C++ library for Lie theory
Loading...
Searching...
No Matches
lp2d.hpp
1// Copyright (C) 2021-2022 Petter Nilsson. MIT License.
2
3#pragma once
4
5#include <algorithm>
6#include <cstdint>
7#include <limits>
8#include <numeric>
9#include <queue>
10#include <ranges>
11#include <tuple>
12#include <utility>
13#include <vector>
14
15namespace lp2d {
16
17using Scalar = double;
18
19enum class Status { Optimal, PrimaryInfeasible, DualInfeasible };
20
24
25namespace detail {
26
27inline constexpr auto eps = 100 * std::numeric_limits<Scalar>::epsilon();
28inline constexpr auto inf = std::numeric_limits<Scalar>::infinity();
29
32{
33 Scalar a, b, c;
34 bool active{true};
35};
36
37inline std::tuple<Scalar, Scalar, Status> solve_impl(std::vector<HalfPlane> &);
38
39} // namespace detail
40
44
57template<std::ranges::range R>
58inline std::tuple<Scalar, Scalar, Status> solve(Scalar cx, Scalar cy, const R & rows)
59 requires(std::tuple_size_v<std::ranges::range_value_t<R>> == 3)
60{
61 const Scalar sqnorm = cx * cx + cy * cy;
62
63 if (sqnorm < detail::eps) { return {0, 0, Status::Optimal}; }
64
65 if (std::ranges::empty(rows)) { return {0, 0, Status::DualInfeasible}; }
66
67 // transformation matrix:
68 // [x; y] = [cP -sP; sP cP] [xt; yt]
69 const Scalar cP = cy / sqnorm;
70 const Scalar sP = -cx / sqnorm;
71
72 // insert rotated halfplanes with unit vector norm 1
73 std::vector<detail::HalfPlane> input;
74 input.reserve(std::ranges::size(rows));
75 for (const auto [a, b, c] : rows) {
76 const double ra = cP * a + sP * b;
77 const double rb = -sP * a + cP * b;
78
79 const double norm = ra * ra + rb * rb;
80
81 if (norm > detail::eps && c < detail::inf) {
82 input.push_back(detail::HalfPlane{
83 .a = ra / norm,
84 .b = rb / norm,
85 .c = c / norm,
86 .active = true,
87 });
88 }
89 }
90
91 // scale factor
92 Scalar lambda{1};
93 for (const auto & hp : input) { lambda = std::max(lambda, std::abs(hp.c)); }
94
95 for (auto & hp : input) { hp.c /= lambda; }
96
97 const auto [xt_opt, yt_opt, status] = solve_impl(input);
98
99 // multiplication that returns 0 for 0 * inf (regular multiplication returns nan)
100 const auto mul = [](Scalar a, Scalar b) { return std::abs(a) > detail::eps ? a * b : 0; };
101
102 // return solution in original coordinates
103 return {
104 lambda * (mul(cP, xt_opt) - mul(sP, yt_opt)),
105 lambda * (mul(sP, xt_opt) + mul(cP, yt_opt)),
106 status,
107 };
108}
109
113
114namespace detail {
115
116// value and derivative
117using ValDer = std::tuple<Scalar, Scalar>;
118
119// value and subderivative (upper,lower)
120using ValSubDer = std::tuple<Scalar, Scalar, Scalar>;
121
122// halfplanes that define upper and lower bounds on y (as a function of x)
123inline constexpr auto active_y_upper = [](const auto & hp) { return hp.active && hp.b > 0; };
124inline constexpr auto active_y_lower = [](const auto & hp) { return hp.active && hp.b < 0; };
125
126// halfplane as bounds on y
127inline constexpr auto hp_to_yslope = [](const HalfPlane & hp, Scalar x) -> std::pair<Scalar, Scalar> {
128 // ax + by <=> c for b != 0 <==> y <=> c/b - (a/b) x
129 const Scalar alpha = -hp.a / hp.b;
130 const Scalar beta = hp.c / hp.b;
131
132 // ensure we can evaluate at \pm inf
133 if (std::abs(alpha) <= eps) { return {beta, alpha}; }
134
135 return {alpha * x + beta, alpha};
136};
137
141inline std::optional<Scalar> intersection(const HalfPlane & hp1, const HalfPlane & hp2)
142{
143 const Scalar lhs = hp1.a * hp2.b - hp2.a * hp1.b;
144 if (std::abs(lhs) > eps) { return (hp2.b * hp1.c - hp1.b * hp2.c) / lhs; }
145 return {};
146}
147
148// g(x) = max { ai * x + bi }, and its subderivative
149inline const auto gfun = [](const std::ranges::range auto & hps, const Scalar x) -> ValSubDer {
150 ValSubDer ret{-inf, 0, 0};
151 for (const auto & hp : hps | std::views::filter(active_y_lower)) {
152 const auto [yx, dyx] = hp_to_yslope(hp, x);
153 if (std::get<0>(ret) > yx + eps) {
154 // do noting
155 } else if (yx > std::get<0>(ret) + eps) {
156 ret = {yx, dyx, dyx};
157 } else {
158 std::get<1>(ret) = std::min(std::get<1>(ret), dyx);
159 std::get<2>(ret) = std::max(std::get<2>(ret), dyx);
160 }
161 }
162 return ret;
163};
164
165// h(x) = min { a * x + b }, and its subderivative
166inline const auto hfun = [](const std::ranges::range auto & hps, const Scalar x) -> ValSubDer {
167 ValSubDer ret{inf, 0, 0};
168 for (const auto & hp : hps | std::views::filter(active_y_upper)) {
169 const auto [yx, dyx] = hp_to_yslope(hp, x);
170 if (std::get<0>(ret) + eps < yx) {
171 // do noting
172 } else if (yx + eps < std::get<0>(ret)) {
173 ret = {yx, dyx, dyx};
174 } else {
175 std::get<1>(ret) = std::min(std::get<1>(ret), dyx);
176 std::get<2>(ret) = std::max(std::get<2>(ret), dyx);
177 }
178 }
179 return ret;
180};
181
183inline std::optional<Scalar> find_candidate(std::vector<HalfPlane> & hps, Scalar a, Scalar b)
184{
185 std::optional<typename std::vector<HalfPlane>::iterator> it1_store{};
186
187 // keep track of median using two priority queues
188 std::priority_queue<Scalar> small, large;
189 const auto addnum = [&small, &large](Scalar d) {
190 small.push(d);
191 large.push(-small.top());
192 small.pop();
193 while (small.size() < large.size()) {
194 small.push(-large.top());
195 large.pop();
196 }
197 };
198
199 // INTERSECT LOWERS AMONGST THEMSELVES
200
201 for (auto it2 = hps.begin(); it2 != hps.end(); ++it2) {
202 if (!active_y_lower(*it2)) { continue; }
203
204 if (!it1_store.has_value()) {
205 it1_store = it2;
206 continue;
207 }
208
209 auto it1 = *it1_store;
210
211 const auto isec = intersection(*it1, *it2);
212
213 int8_t redundant = 0; // 0 (none), 1, or 2
214
215 if (isec.has_value()) {
216 if (a + eps < *isec && *isec + eps < b) {
217 addnum(*isec);
218 it1_store = {};
219 } else { // intersection outside--one is redundant
220 if (a + eps >= *isec) { // check for redundancy at a
221 const auto [v1, dv1] = hp_to_yslope(*it1, a);
222 const auto [v2, dv2] = hp_to_yslope(*it2, a);
223 if (v1 + eps < v2) {
224 redundant = 1;
225 } else if (v2 + eps < v1) {
226 redundant = 2;
227 } else {
228 redundant = dv1 <= dv2 ? 1 : 2;
229 }
230 } else if (*isec + eps >= b) { // check for redundancy at b
231 const auto [v1, dv1] = hp_to_yslope(*it1, b);
232 const auto [v2, dv2] = hp_to_yslope(*it2, b);
233 if (v1 + eps < v2) {
234 redundant = 1;
235 } else if (v2 + eps < v1) {
236 redundant = 2;
237 } else {
238 redundant = dv1 >= dv2 ? 1 : 2;
239 }
240 }
241 }
242 } else { // parallel--so one is redundant
243 redundant = hp_to_yslope(*it1, 0) < hp_to_yslope(*it2, 0) ? 1 : 2;
244 }
245
246 if (redundant == 1) {
247 it1->active = false;
248 it1_store = it2;
249 } else if (redundant == 2) {
250 it2->active = false;
251 }
252 }
253
254 // INTERSECT UPPERS AMONGST THEMSELVES
255
256 it1_store = {};
257
258 for (auto it2 = hps.begin(); it2 != hps.end(); ++it2) {
259
260 if (!active_y_upper(*it2)) { continue; }
261
262 if (!it1_store.has_value()) {
263 it1_store = it2;
264 continue;
265 }
266
267 auto it1 = *it1_store;
268
269 const auto isec = intersection(*it1, *it2);
270
271 int8_t redundant = 0; // 0 (none), 1, or 2
272
273 if (isec.has_value()) {
274 if (a + eps < *isec && *isec + eps < b) {
275 addnum(*isec);
276 it1_store = {};
277 } else { // intersection outside--one is redundant
278 if (a + eps >= *isec) { // check for redundancy at a
279 const auto [v1, dv1] = hp_to_yslope(*it1, a);
280 const auto [v2, dv2] = hp_to_yslope(*it2, a);
281 if (v1 + eps < v2) {
282 redundant = 2;
283 } else if (v2 + eps < v1) {
284 redundant = 1;
285 } else {
286 redundant = dv1 <= dv2 ? 2 : 1;
287 }
288 } else if (*isec + eps >= b) { // check for redundancy at b
289 const auto [v1, dv1] = hp_to_yslope(*it1, b);
290 const auto [v2, dv2] = hp_to_yslope(*it2, b);
291 if (v1 + eps < v2) {
292 redundant = 2;
293 } else if (v2 + eps < v1) {
294 redundant = 1;
295 } else {
296 redundant = dv1 >= dv2 ? 2 : 1;
297 }
298 }
299 }
300 } else { // parallel--so one is redundant
301 redundant = hp_to_yslope(*it1, 0) < hp_to_yslope(*it2, 0) ? 2 : 1;
302 }
303
304 if (redundant == 1) {
305 it1->active = false;
306 it1_store = it2;
307 } else if (redundant == 2) {
308 it2->active = false;
309 }
310 }
311
312 // IF NO POINTS WERE FOUND AND THERE'S A SINGLE LOWER, INTERSECT IT WITH THE UPPERS
313
314 if (small.empty() && std::count_if(hps.cbegin(), hps.cend(), active_y_lower) == 1) {
315 const auto hp_l = *std::find_if(std::ranges::begin(hps), std::ranges::end(hps), active_y_lower);
316 for (auto & hp_u : hps | std::views::filter(active_y_upper)) {
317 const auto isec = intersection(hp_l, hp_u);
318 if (isec.has_value() && a + eps < *isec && *isec + eps < b) { addnum(*isec); }
319 }
320 }
321
322 if (small.empty()) { return {}; }
323
324 // return median element
325 return small.top();
326}
327
338inline uint8_t check(const std::vector<HalfPlane> & hps, const Scalar x)
339{
340 const auto [gx, sg, Sg] = gfun(hps, x);
341 const auto [hx, sh, Sh] = hfun(hps, x);
342
343 if (gx <= hx + eps) { // FEASIBLE
344 if (gx + eps < hx) { // there's slack, only g matters
345 if (sg > 0) {
346 return 1;
347 } else if (Sg < 0) {
348 return 2;
349 } else {
350 return 0;
351 }
352 } else { // no slack
353 if (sg > 0 && sg >= Sh) {
354 return 1;
355 } else if (Sg < 0 && Sg < sh) {
356 return 2;
357 } else {
358 return 0;
359 }
360 }
361 } else { // INFEASIBLE
362 if (Sg < sh) {
363 return 2;
364 } else if (sg > Sh) {
365 return 1;
366 } else {
367 return 3;
368 }
369 }
370}
371
383inline std::tuple<Scalar, Scalar, Status> solve_impl(std::vector<HalfPlane> & hps)
384{
385 // halfplanes that define a lower bound on x (independent of y)
386 auto hps_x_lower =
387 hps | std::views::filter([](const auto & hp) { return hp.active && std::abs(hp.b) < eps && hp.a < 0; });
388
389 // initial lower bound on x
390 Scalar a = std::transform_reduce(
391 std::ranges::begin(hps_x_lower),
392 std::ranges::end(hps_x_lower),
393 -inf,
394 [](const Scalar avar, const Scalar bvar) { return std::max(avar, bvar); },
395 [](const HalfPlane & hp) { return hp.c / hp.a; });
396
397 // halfplanes that define an upper bound on x (independent of y)
398 auto hps_x_upper =
399 hps | std::views::filter([](const auto & hp) { return hp.active && std::abs(hp.b) < eps && hp.a > 0; });
400
401 // initial upper bound on x
402 Scalar b = std::transform_reduce(
403 std::ranges::begin(hps_x_upper),
404 std::ranges::end(hps_x_upper),
405 inf,
406 [](const Scalar avar, const Scalar bvar) { return std::min(avar, bvar); },
407 [](const HalfPlane & hp) { return hp.c / hp.a; });
408
409 // we remove at least one halfplane per iterations, so need at most N iterations
410 for (auto iter = hps.size(); iter > 0; --iter) {
411 const auto x = find_candidate(hps, a, b);
412
413 // remove hps that were marked as not active
414 hps.erase(std::remove_if(hps.begin(), hps.end(), [](const auto & hp) { return !hp.active; }), hps.end());
415
416 if (!x.has_value()) { break; }
417
418 switch (check(hps, *x)) {
419 case 0:
420 return {*x, std::get<0>(gfun(hps, *x)), Status::Optimal};
421 break;
422 case 1:
423 b = *x;
424 break;
425 case 2:
426 a = *x;
427 break;
428 case 3:
429 return {0, inf, Status::PrimaryInfeasible};
430 break;
431 }
432 }
433
434 // no intersection points, only need to consider boundaries
435 const auto [ga, sga, Sga] = gfun(hps, a);
436 const auto [ha, sha, Sha] = hfun(hps, a);
437
438 const auto [gb, sgb, Sgb] = gfun(hps, b);
439 const auto [hb, shb, Shb] = hfun(hps, b);
440
441 if (ga == ha && gb == hb && std::abs(ga) == inf && std::abs(gb) == inf) {
442 // special case where bounds are equal and \pm inf
443 if (gfun(hps, 0) <= hfun(hps, 0)) {
444 return {0., 0., Status::DualInfeasible};
445 } else {
446 return {0., 0., Status::PrimaryInfeasible};
447 }
448 }
449
450 if (ga > ha && gb > hb) {
451 return {0, 0, Status::PrimaryInfeasible};
452 } else if ((ga <= ha && gb > hb) || ga < gb) {
453 return {a, ga, ga == -inf ? Status::DualInfeasible : Status::Optimal};
454 } else {
455 return {b, gb, gb == -inf ? Status::DualInfeasible : Status::Optimal};
456 }
457}
458
459} // namespace detail
460
461} // namespace lp2d
typename traits::man< M >::Scalar Scalar
Manifold scalar type.
Definition manifold.hpp:88
Halfplane represented as inequality ax + by <= c.
Definition lp2d.hpp:32