Skip to content

Commit 90f6f33

Browse files
authored
Merge pull request #2335 from borglab/fix/NonlinearEquality
Deprecate NonlinearEquality and Fix Constrained Covariances
2 parents aa9f684 + 59856b6 commit 90f6f33

16 files changed

+629
-165
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
For reviewing PRs:
2-
* All functions in header files should have doxygen-style API docs
2+
* All functions in header files should have doxygen-style API docs, /** */ style, except small functions like getters which can have single line /// comments, no need for @brief, @params etc
33
* Use /// for single-line comments rather than /** */
44
* Use meaningful variable names, e.g. `measurement` not `msm`, avoid abbreviations.
55
* Flag overly complex or long/functions: break up in smaller functions

gtsam/constrained/NonlinearEqualityConstraint.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class ExpressionEqualityConstraint : public NonlinearEqualityConstraint {
101101

102102
/** Equality constraint that enforce the cost factor with zero error.
103103
* e.g., for a factor with unwhitened cost 2x-1, the constraint enforces the
104-
* equlity 2x-1=0.
104+
* equality 2x-1=0.
105105
*/
106106
class GTSAM_EXPORT ZeroCostConstraint : public NonlinearEqualityConstraint {
107107
public:

gtsam/linear/JacobianFactor.cpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -515,9 +515,13 @@ double JacobianFactor::error(const VectorValues& c) const {
515515
/* ************************************************************************* */
516516
Matrix JacobianFactor::augmentedInformation() const {
517517
if (model_) {
518-
Matrix AbWhitened = Ab_.full();
519-
model_->WhitenInPlace(AbWhitened);
520-
return AbWhitened.transpose() * AbWhitened;
518+
Matrix Ab = Ab_.full();
519+
if (model_->isConstrained()) {
520+
auto constrained = std::static_pointer_cast<noiseModel::Constrained>(model_);
521+
return constrained->informationFromA(Ab);
522+
}
523+
model_->WhitenInPlace(Ab);
524+
return Ab.transpose() * Ab;
521525
} else {
522526
return Ab_.full().transpose() * Ab_.full();
523527
}
@@ -526,9 +530,13 @@ Matrix JacobianFactor::augmentedInformation() const {
526530
/* ************************************************************************* */
527531
Matrix JacobianFactor::information() const {
528532
if (model_) {
529-
Matrix AWhitened = this->getA();
530-
model_->WhitenInPlace(AWhitened);
531-
return AWhitened.transpose() * AWhitened;
533+
Matrix A = this->getA();
534+
if (model_->isConstrained()) {
535+
auto constrained = std::static_pointer_cast<noiseModel::Constrained>(model_);
536+
return constrained->informationFromA(A);
537+
}
538+
model_->WhitenInPlace(A);
539+
return A.transpose() * A;
532540
} else {
533541
return this->getA().transpose() * this->getA();
534542
}

gtsam/linear/NoiseModel.cpp

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -341,12 +341,16 @@ double Diagonal::logDetR() const {
341341
/* ************************************************************************* */
342342

343343
namespace internal {
344-
// switch precisions and invsigmas to finite value
345-
// TODO: why?? And, why not just ask s==0.0 below ?
344+
// Keep invsigmas finite for constrained entries while preserving infinite
345+
// precision so downstream information matrices reflect determinism.
346346
static void fix(const Vector& sigmas, Vector& precisions, Vector& invsigmas) {
347+
static const double kInfinity = std::numeric_limits<double>::infinity();
347348
for (Vector::Index i = 0; i < sigmas.size(); ++i)
348349
if (!std::isfinite(1. / sigmas[i])) {
349-
precisions[i] = 0.0;
350+
// Preserve the infinite precision so downstream information matrices
351+
// reflect deterministic constraints. We still zero invsigmas to avoid
352+
// scaling constraint rows during whitening.
353+
precisions[i] = kInfinity;
350354
invsigmas[i] = 0.0;
351355
}
352356
}
@@ -457,6 +461,40 @@ void Constrained::WhitenInPlace(Eigen::Block<Matrix> H) const {
457461
H.row(i) *= invsigmas_(i);
458462
}
459463

464+
/* ************************************************************************* */
465+
Matrix Constrained::informationFromA(const Matrix& A) const {
466+
const double kZeroTol = 1e-12;
467+
const double kInfinity = std::numeric_limits<double>::infinity();
468+
Matrix info = Matrix::Zero(A.cols(), A.cols());
469+
assert(static_cast<DenseIndex>(precisions_.size()) == A.rows());
470+
// Accumulate row-wise contributions so constrained rows can mark infinite entries.
471+
for (DenseIndex row = 0; row < A.rows(); ++row) {
472+
const double precision = precisions_(row);
473+
if (precision == 0.0) {
474+
continue;
475+
}
476+
const auto a = A.row(row);
477+
if (std::isinf(precision)) {
478+
// Constrained rows force infinite information wherever they have support.
479+
for (DenseIndex i = 0; i < a.cols(); ++i) {
480+
if (std::abs(a(i)) <= kZeroTol) {
481+
continue;
482+
}
483+
for (DenseIndex j = 0; j < a.cols(); ++j) {
484+
if (std::abs(a(j)) <= kZeroTol) {
485+
continue;
486+
}
487+
info(i, j) = kInfinity;
488+
}
489+
}
490+
} else {
491+
// Finite precisions reduce to the standard weighted outer product.
492+
info.noalias() += precision * a.transpose() * a;
493+
}
494+
}
495+
return info;
496+
}
497+
460498
/* ************************************************************************* */
461499
Constrained::shared_ptr Constrained::unit() const {
462500
Vector sigmas = Vector::Ones(dim());

gtsam/linear/NoiseModel.h

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -408,8 +408,8 @@ namespace gtsam {
408408
Vector mu_; ///< Penalty function weight - needs to be large enough to dominate soft constraints
409409

410410
/**
411-
* Constructor that prevents any inf values
412-
* from appearing in invsigmas or precisions.
411+
* Constructor that prevents inf values from appearing in invsigmas,
412+
* while preserving infinite precisions for constrained entries.
413413
* Allows for specifying mu.
414414
*/
415415
Constrained(const Vector& mu, const Vector& sigmas);
@@ -439,25 +439,25 @@ namespace gtsam {
439439

440440
/**
441441
* A diagonal noise model created by specifying a Vector of
442-
* standard devations, some of which might be zero
442+
* standard deviations, some of which might be zero
443443
*/
444444
static shared_ptr MixedSigmas(const Vector& mu, const Vector& sigmas);
445445

446446
/**
447447
* A diagonal noise model created by specifying a Vector of
448-
* standard devations, some of which might be zero
448+
* standard deviations, some of which might be zero
449449
*/
450450
static shared_ptr MixedSigmas(const Vector& sigmas);
451451

452452
/**
453453
* A diagonal noise model created by specifying a Vector of
454-
* standard devations, some of which might be zero
454+
* standard deviations, some of which might be zero
455455
*/
456456
static shared_ptr MixedSigmas(double m, const Vector& sigmas);
457457

458458
/**
459459
* A diagonal noise model created by specifying a Vector of
460-
* standard devations, some of which might be zero
460+
* standard deviations, some of which might be zero
461461
*/
462462
static shared_ptr MixedVariances(const Vector& mu, const Vector& variances);
463463
static shared_ptr MixedVariances(const Vector& variances);
@@ -470,7 +470,7 @@ namespace gtsam {
470470
static shared_ptr MixedPrecisions(const Vector& precisions);
471471

472472
/**
473-
* The squaredMahalanobisDistance function for a constrained noisemodel,
473+
* The squaredMahalanobisDistance function for a constrained noise model,
474474
* for non-constrained versions, uses sigmas, otherwise
475475
* uses the penalty function with mu
476476
*/
@@ -502,6 +502,12 @@ namespace gtsam {
502502
void WhitenInPlace(Matrix& H) const override;
503503
void WhitenInPlace(Eigen::Block<Matrix> H) const override;
504504

505+
/**
506+
* Compute A' * diag(precisions) * A using constrained precisions.
507+
* Infinite precisions yield infinite entries where the row has support.
508+
*/
509+
Matrix informationFromA(const Matrix& A) const;
510+
505511
/**
506512
* Apply QR factorization to the system [A b], taking into account constraints
507513
* Q' * [A b] = [R d]
@@ -514,7 +520,7 @@ namespace gtsam {
514520
Diagonal::shared_ptr QR(Matrix& Ab) const override;
515521

516522
/**
517-
* Returns a Unit version of a constrained noisemodel in which
523+
* Returns a Unit version of a constrained noise model in which
518524
* constrained sigmas remain constrained and the rest are unit scaled
519525
*/
520526
shared_ptr unit() const;
@@ -559,7 +565,7 @@ namespace gtsam {
559565
typedef std::shared_ptr<Isotropic> shared_ptr;
560566

561567
/**
562-
* An isotropic noise model created by specifying a standard devation sigma
568+
* An isotropic noise model created by specifying a standard deviation sigma
563569
*/
564570
static shared_ptr Sigma(size_t dim, double sigma, bool smart = true);
565571

@@ -719,7 +725,7 @@ namespace gtsam {
719725
return robust_->loss(std::sqrt(squared_distance));
720726
}
721727

722-
// NOTE: This is special because in whiten the base version will do the reweighting
728+
// NOTE: This is special because in whiten the base version will do the re-weighting
723729
// which is incorrect!
724730
double squaredMahalanobisDistance(const Vector& v) const override {
725731
return noise_->squaredMahalanobisDistance(v);
@@ -773,5 +779,3 @@ namespace gtsam {
773779
template<> struct traits<noiseModel::Unit> : public Testable<noiseModel::Unit> {};
774780

775781
} //\ namespace gtsam
776-
777-

gtsam/linear/tests/testNoiseModel.cpp

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
#include <CppUnitLite/TestHarness.h>
2525

26+
#include <cmath>
2627
#include <iostream>
2728
#include <limits>
2829

@@ -129,9 +130,9 @@ TEST(NoiseModel, equals)
129130
///* ************************************************************************* */
130131
//TEST(NoiseModel, ConstrainedSmart )
131132
//{
132-
// Gaussian::shared_ptr nonconstrained = Constrained::MixedSigmas((Vector3(sigma, 0.0, sigma), true);
133-
// Diagonal::shared_ptr n1 = std::dynamic_pointer_cast<Diagonal>(nonconstrained);
134-
// Constrained::shared_ptr n2 = std::dynamic_pointer_cast<Constrained>(nonconstrained);
133+
// Gaussian::shared_ptr unconstrained = Constrained::MixedSigmas((Vector3(sigma, 0.0, sigma), true);
134+
// Diagonal::shared_ptr n1 = std::dynamic_pointer_cast<Diagonal>(unconstrained);
135+
// Constrained::shared_ptr n2 = std::dynamic_pointer_cast<Constrained>(unconstrained);
135136
// EXPECT(n1);
136137
// EXPECT(!n2);
137138
//
@@ -148,14 +149,15 @@ TEST(NoiseModel, ConstrainedConstructors )
148149
Constrained::shared_ptr actual;
149150
size_t d = 3;
150151
double m = 100.0;
152+
const double kInfinity = std::numeric_limits<double>::infinity();
151153
Vector3 sigmas(kSigma, 0.0, 0.0);
152154
Vector3 mu(200.0, 300.0, 400.0);
153155
actual = Constrained::All(d);
154156
// TODO: why should this be a thousand ??? Dummy variable?
155157
EXPECT(assert_equal(Vector::Constant(d, 1000.0), actual->mu()));
156158
EXPECT(assert_equal(Vector::Constant(d, 0), actual->sigmas()));
157159
EXPECT(assert_equal(Vector::Constant(d, 0), actual->invsigmas())); // Actually zero as dummy value
158-
EXPECT(assert_equal(Vector::Constant(d, 0), actual->precisions())); // Actually zero as dummy value
160+
EXPECT(assert_equal(Vector::Constant(d, kInfinity), actual->precisions())); // Infinite precision for hard constraints
159161

160162
actual = Constrained::All(d, m);
161163
EXPECT(assert_equal(Vector::Constant(d, m), actual->mu()));
@@ -201,6 +203,34 @@ TEST(NoiseModel, ConstrainedAll )
201203
DOUBLES_EQUAL(0.0, i->loss(0.0),1e-9);
202204
}
203205

206+
/* ************************************************************************* */
207+
TEST(NoiseModel, ConstrainedInformationFromA) {
208+
// Use one constrained row and one finite-precision row.
209+
Vector2 sigmas(0.0, 2.0);
210+
Constrained::shared_ptr model = Constrained::MixedSigmas(sigmas);
211+
212+
Matrix A(2, 2);
213+
A << 1.0, 0.0, 0.0, 2.0;
214+
215+
Matrix info = model->informationFromA(A);
216+
217+
EXPECT(std::isinf(info(0, 0)));
218+
DOUBLES_EQUAL(0.0, info(0, 1), 1e-12);
219+
DOUBLES_EQUAL(0.0, info(1, 0), 1e-12);
220+
DOUBLES_EQUAL(1.0, info(1, 1), 1e-12);
221+
222+
// Constrained row with support in multiple columns should mark cross-terms.
223+
Matrix A_dense(2, 2);
224+
A_dense << 1.0, 1.0, 0.0, 2.0;
225+
226+
Matrix info_dense = model->informationFromA(A_dense);
227+
228+
EXPECT(std::isinf(info_dense(0, 0)));
229+
EXPECT(std::isinf(info_dense(0, 1)));
230+
EXPECT(std::isinf(info_dense(1, 0)));
231+
EXPECT(std::isinf(info_dense(1, 1)));
232+
}
233+
204234
/* ************************************************************************* */
205235
namespace exampleQR {
206236
// create a matrix to eliminate

gtsam/nonlinear/Marginals.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,15 @@ Matrix Marginals::marginalInformation(Key variable) const {
131131

132132
/* ************************************************************************* */
133133
Matrix Marginals::marginalCovariance(Key variable) const {
134-
return marginalInformation(variable).inverse();
134+
Matrix info = marginalInformation(variable);
135+
136+
// Deterministic constraints (e.g., NonlinearEquality) produce infinite
137+
// information for the constrained variable. Treat those as zero covariance
138+
// rather than attempting to invert an infinite matrix.
139+
if (!info.allFinite())
140+
return Matrix::Zero(info.rows(), info.cols());
141+
142+
return info.inverse();
135143
}
136144

137145
/* ************************************************************************* */

0 commit comments

Comments
 (0)