lsst.afw g714e0ed6de+61745a5d47
Loading...
Searching...
No Matches
ChebyshevBoundedField.cc
Go to the documentation of this file.
1// -*- LSST-C++ -*-
2/*
3 * LSST Data Management System
4 * Copyright 2008-2014 LSST Corporation.
5 *
6 * This product includes software developed by the
7 * LSST Project (http://www.lsst.org/).
8 *
9 * This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the LSST License Statement and
20 * the GNU General Public License along with this program. If not,
21 * see <http://www.lsstcorp.org/LegalNotices/>.
22 */
23
24#include <memory>
25
26#include "ndarray/eigen.h"
34
35namespace lsst {
36namespace afw {
37
38template std::shared_ptr<math::ChebyshevBoundedField> table::io::PersistableFacade<
39 math::ChebyshevBoundedField>::dynamicCast(std::shared_ptr<table::io::Persistable> const&);
40
41namespace math {
42
44
45// ------------------ Constructors and helpers ---------------------------------------------------------------
46
47namespace {
48
49// Compute an affine transform that maps an arbitrary box to [-1,1]x[-1,1]
50lsst::geom::AffineTransform makeChebyshevRangeTransform(lsst::geom::Box2D const bbox) {
53 lsst::geom::Extent2D(-(2.0 * bbox.getCenterX()) / bbox.getWidth(),
54 -(2.0 * bbox.getCenterY()) / bbox.getHeight()));
55}
56
57} // namespace
58
60 ndarray::Array<double const, 2, 2> const& coefficients)
61 : BoundedField(bbox),
62 _toChebyshevRange(makeChebyshevRangeTransform(lsst::geom::Box2D(bbox))),
63 _coefficients(coefficients) {}
64
66 : BoundedField(bbox), _toChebyshevRange(makeChebyshevRangeTransform(lsst::geom::Box2D(bbox))) {}
67
68ChebyshevBoundedField::ChebyshevBoundedField(ChebyshevBoundedField const&) = default;
69ChebyshevBoundedField::ChebyshevBoundedField(ChebyshevBoundedField&&) = default;
71
72// ------------------ fit() and helpers ---------------------------------------------------------------------
73
74namespace {
75
76using Control = ChebyshevBoundedField::Control;
77using Packer = detail::TrapezoidalPacker;
78
79// fill an array with 1-d Chebyshev functions of the 1st kind T(x), evaluated at the given point x
80void evaluateBasis1d(ndarray::Array<double, 1, 1> const& t, double x) {
81 int const n = t.getSize<0>();
82 if (n > 0) {
83 t[0] = 1.0;
84 }
85 if (n > 1) {
86 t[1] = x;
87 }
88 for (int i = 2; i < n; ++i) {
89 t[i] = 2.0 * x * t[i - 1] - t[i - 2];
90 }
91}
92
93// Create a matrix of 2-d Chebyshev functions evaluated at a set of positions, with
94// Chebyshev order along columns and evaluation positions along rows. We pack the
95// 2-d functions using the TrapezoidalPacker class, because we don't want any columns
96// that correspond to coefficients that should be set to zero.
97ndarray::Array<double, 2, 2> makeMatrix(ndarray::Array<double const, 1> const& x,
98 ndarray::Array<double const, 1> const& y,
99 lsst::geom::AffineTransform const& toChebyshevRange,
100 Packer const& packer, Control const& ctrl) {
101 int const nPoints = x.getSize<0>();
102 ndarray::Array<double, 1, 1> tx = ndarray::allocate(packer.nx);
103 ndarray::Array<double, 1, 1> ty = ndarray::allocate(packer.ny);
104 ndarray::Array<double, 2, 2> out = ndarray::allocate(nPoints, packer.size);
105 // Loop over x and y together, computing T_i(x) and T_j(y) arrays for each point,
106 // then packing them together.
107 for (int p = 0; p < nPoints; ++p) {
108 lsst::geom::Point2D sxy = toChebyshevRange(lsst::geom::Point2D(x[p], y[p]));
109 evaluateBasis1d(tx, sxy.getX());
110 evaluateBasis1d(ty, sxy.getY());
111 packer.pack(out[p], tx, ty); // this sets a row of out to the packed outer product of tx and ty
112 }
113 return out;
114}
115
116// Create a matrix of 2-d Chebyshev functions evaluated on a grid of positions, with
117// Chebyshev order along columns and evaluation positions along rows. We pack the
118// 2-d functions using the TrapezoidalPacker class, because we don't want any columns
119// that correspond to coefficients that should be set to zero.
120ndarray::Array<double, 2, 2> makeMatrix(lsst::geom::Box2I const& bbox,
121 lsst::geom::AffineTransform const& toChebyshevRange,
122 Packer const& packer, Control const& ctrl) {
123 // Create a 2-d array that contains T_j(x) for each x value, with x values in rows and j in columns
124 ndarray::Array<double, 2, 2> tx = ndarray::allocate(bbox.getWidth(), packer.nx);
125 for (int x = bbox.getBeginX(), p = 0; p < bbox.getWidth(); ++p, ++x) {
126 evaluateBasis1d(tx[p], toChebyshevRange[lsst::geom::AffineTransform::XX] * x +
127 toChebyshevRange[lsst::geom::AffineTransform::X]);
128 }
129
130 // Loop over y values, and at each point, compute T_i(y), then loop over x and multiply by the T_j(x)
131 // we already computed and stored above.
132 ndarray::Array<double, 2, 2> out = ndarray::allocate(bbox.getArea(), packer.size);
133 ndarray::Array<double, 2, 2>::Iterator outIter = out.begin();
134 ndarray::Array<double, 1, 1> ty = ndarray::allocate(ctrl.orderY + 1);
135 for (int y = bbox.getBeginY(), i = 0; i < bbox.getHeight(); ++i, ++y) {
136 evaluateBasis1d(ty, toChebyshevRange[lsst::geom::AffineTransform::YY] * y +
137 toChebyshevRange[lsst::geom::AffineTransform::Y]);
138 for (int j = 0; j < bbox.getWidth(); ++j, ++outIter) {
139 // this sets a row of out to the packed outer product of tx and ty
140 packer.pack(*outIter, tx[j], ty);
141 }
142 }
143 return out;
144}
145
146} // namespace
147
149 ndarray::Array<double const, 1> const& x,
150 ndarray::Array<double const, 1> const& y,
151 ndarray::Array<double const, 1> const& z,
152 Control const& ctrl) {
153 // Initialize the result object, so we can make use of the AffineTransform it builds
155 // This packer object knows how to map the 2-d Chebyshev functions onto a 1-d array,
156 // using only those that the control says should have nonzero coefficients.
157 Packer const packer(ctrl);
158 // Create a "design matrix" for the linear least squares problem (A in min||Ax-b||)
159 ndarray::Array<double, 2, 2> matrix = makeMatrix(x, y, result->_toChebyshevRange, packer, ctrl);
160 // Solve the linear least squares problem.
162 // Unpack the solution into a 2-d matrix, with zeros for values we didn't fit.
163 result->_coefficients = packer.unpack(lstsq.getSolution());
164 return result;
165}
166
168 ndarray::Array<double const, 1> const& x,
169 ndarray::Array<double const, 1> const& y,
170 ndarray::Array<double const, 1> const& z,
171 ndarray::Array<double const, 1> const& w,
172 Control const& ctrl) {
173 // Initialize the result object, so we can make use of the AffineTransform it builds
175 // This packer object knows how to map the 2-d Chebyshev functions onto a 1-d array,
176 // using only those that the control says should have nonzero coefficients.
177 Packer const packer(ctrl);
178 // Create a "design matrix" for the linear least squares problem ('A' in min||Ax-b||)
179 ndarray::Array<double, 2, 2> matrix = makeMatrix(x, y, result->_toChebyshevRange, packer, ctrl);
180 // We want to do weighted least squares, so we multiply both the data vector 'b' and the
181 // matrix 'A' by the weights.
182 ndarray::asEigenArray(matrix).colwise() *= ndarray::asEigenArray(w);
183 ndarray::Array<double, 1, 1> wz = ndarray::copy(z);
184 ndarray::asEigenArray(wz) *= ndarray::asEigenArray(w);
185 // Solve the linear least squares problem.
187 // Unpack the solution into a 2-d matrix, with zeros for values we didn't fit.
188 result->_coefficients = packer.unpack(lstsq.getSolution());
189 return result;
190}
191
192ndarray::Array<double, 2, 2> ChebyshevBoundedField::makeFitMatrix(
193 lsst::geom::Box2I const& bbox,
194 ndarray::Array<double const, 1> const& x,
195 ndarray::Array<double const, 1> const& y,
196 Control const& ctrl
197) {
198 // Initialize a temporary result object, so we can make use of the AffineTransform it builds
200 // This packer object knows how to map the 2-d Chebyshev functions onto a 1-d array,
201 // using only those that the control says should have nonzero coefficients.
202 Packer const packer(ctrl);
203 // Create a "design matrix" for the linear least squares problem (A in min||Ax-b||)
204 return makeMatrix(x, y, result->_toChebyshevRange, packer, ctrl);
205}
206
207template <typename T>
209 Control const& ctrl) {
210 // Initialize the result object, so we can make use of the AffineTransform it builds
213 // This packer object knows how to map the 2-d Chebyshev functions onto a 1-d array,
214 // using only those that the control says should have nonzero coefficients.
215 Packer const packer(ctrl);
216 ndarray::Array<double, 2, 2> matrix = makeMatrix(bbox, result->_toChebyshevRange, packer, ctrl);
217 // Flatten the data image into a 1-d vector.
218 ndarray::Array<double, 2, 2> imgCopy = ndarray::allocate(img.getArray().getShape());
219 imgCopy.deep() = img.getArray();
220 ndarray::Array<double const, 1, 1> z = ndarray::flatten<1>(imgCopy);
221 // Solve the linear least squares problem.
223 // Unpack the solution into a 2-d matrix, with zeros for values we didn't fit.
224 result->_coefficients = packer.unpack(lstsq.getSolution());
225 return result;
226}
227
228// ------------------ modifier factories ---------------------------------------------------------------
229
231 if (static_cast<std::size_t>(ctrl.orderX) >= _coefficients.getSize<1>()) {
233 (boost::format("New x order (%d) exceeds old x order (%d)") % ctrl.orderX %
234 (_coefficients.getSize<1>() - 1))
235 .str());
236 }
237 if (static_cast<std::size_t>(ctrl.orderY) >= _coefficients.getSize<0>()) {
239 (boost::format("New y order (%d) exceeds old y order (%d)") % ctrl.orderY %
240 (_coefficients.getSize<0>() - 1))
241 .str());
242 }
243 ndarray::Array<double, 2, 2> coefficients = ndarray::allocate(ctrl.orderY + 1, ctrl.orderX + 1);
244 coefficients.deep() = _coefficients[ndarray::view(0, ctrl.orderY + 1)(0, ctrl.orderX + 1)];
245 if (ctrl.triangular) {
246 Packer packer(ctrl);
247 ndarray::Array<double, 1, 1> packed = ndarray::allocate(packer.size);
248 packer.pack(packed, coefficients);
249 packer.unpack(coefficients, packed);
250 }
251 return std::make_shared<ChebyshevBoundedField>(getBBox(), coefficients);
252}
253
257
258// ------------------ evaluate() and helpers ---------------------------------------------------------------
259
260namespace {
261
262// To evaluate a 1-d Chebyshev function without needing to have workspace, we use the
263// Clenshaw algorith, which is like going through the recurrence relation in reverse.
264// The CoeffGetter argument g is something that behaves like an array, providing access
265// to the coefficients.
266template <typename CoeffGetter>
267double evaluateFunction1d(CoeffGetter g, double x, int size) {
268 double b_kp2 = 0.0, b_kp1 = 0.0;
269 for (int k = (size - 1); k > 0; --k) {
270 double b_k = g[k] + 2 * x * b_kp1 - b_kp2;
271 b_kp2 = b_kp1;
272 b_kp1 = b_k;
273 }
274 return g[0] + x * b_kp1 - b_kp2;
275}
276
277// This class imitates a 1-d array, by running evaluateFunction1d on a nested dimension;
278// this lets us reuse the logic in evaluateFunction1d for both dimensions. Essentially,
279// we run evaluateFunction1d on a column of coefficients to evaluate T_i(x), then pass
280// the result of that to evaluateFunction1d with the results as the "coefficients" associated
281// with the T_j(y) functions.
282struct RecursionArrayImitator {
283 double operator[](int i) const {
284 return evaluateFunction1d(coefficients[i], x, coefficients.getSize<1>());
285 }
286
287 RecursionArrayImitator(ndarray::Array<double const, 2, 2> const& coefficients_, double x_)
288 : coefficients(coefficients_), x(x_) {}
289
290 ndarray::Array<double const, 2, 2> coefficients;
291 double x;
292};
293
294} // namespace
295
297 lsst::geom::Point2D p = _toChebyshevRange(position);
298 return evaluateFunction1d(RecursionArrayImitator(_coefficients, p.getX()), p.getY(),
299 _coefficients.getSize<0>());
300}
301
302// The integral of T_n(x) over [-1,1]:
303// https://en.wikipedia.org/wiki/Chebyshev_polynomials#Differentiation_and_integration
304double integrateTn(int n) {
305 if (n % 2 == 1)
306 return 0;
307 else
308 return 2.0 / (1.0 - double(n * n));
309}
310
312 double result = 0;
313 double determinant = getBBox().getArea() / 4.0;
314 for (ndarray::Size j = 0; j < _coefficients.getSize<0>(); j++) {
315 for (ndarray::Size i = 0; i < _coefficients.getSize<1>(); i++) {
316 result += _coefficients[j][i] * integrateTn(i) * integrateTn(j);
317 }
318 }
319 return result * determinant;
320}
321
322double ChebyshevBoundedField::mean() const { return integrate() / getBBox().getArea(); }
323
324// ------------------ persistence ---------------------------------------------------------------------------
325
326namespace {
327
328struct PersistenceHelper {
329 table::Schema schema;
330 table::Key<int> orderX;
331 table::Box2IKey bbox;
332 table::Key<table::Array<double> > coefficients;
333
334 PersistenceHelper(int nx, int ny)
335 : schema(),
336 orderX(schema.addField<int>("order_x", "maximum Chebyshev function order in x")),
337 bbox(table::Box2IKey::addFields(schema, "bbox", "bounding box", "pixel")),
338 coefficients(schema.addField<table::Array<double> >(
339 "coefficients", "Chebyshev function coefficients, ordered by y then x", nx * ny)) {}
340
341 PersistenceHelper(table::Schema const& s)
342 : schema(s), orderX(s["order_x"]), bbox(s["bbox"]), coefficients(s["coefficients"]) {}
343};
344
345class ChebyshevBoundedFieldFactory : public table::io::PersistableFactory {
346public:
347 explicit ChebyshevBoundedFieldFactory(std::string const& name)
348 : afw::table::io::PersistableFactory(name) {}
349
350 std::shared_ptr<table::io::Persistable> read(InputArchive const& archive,
351 CatalogVector const& catalogs) const override {
352 LSST_ARCHIVE_ASSERT(catalogs.size() == 1u);
353 LSST_ARCHIVE_ASSERT(catalogs.front().size() == 1u);
354 table::BaseRecord const& record = catalogs.front().front();
355 PersistenceHelper const keys(record.getSchema());
356 lsst::geom::Box2I bbox(record.get(keys.bbox));
357 std::size_t nx = record.get(keys.orderX) + 1;
358 std::size_t ny = keys.coefficients.getSize() / nx;
359 LSST_ARCHIVE_ASSERT(nx * ny == keys.coefficients.getSize());
360 ndarray::Array<double, 2, 2> coefficients = ndarray::allocate(ny, nx);
361 ndarray::flatten<1>(coefficients) = record.get(keys.coefficients);
362 return std::make_shared<ChebyshevBoundedField>(bbox, coefficients);
363 }
364};
365
366std::string getChebyshevBoundedFieldPersistenceName() { return "ChebyshevBoundedField"; }
367
368ChebyshevBoundedFieldFactory registration(getChebyshevBoundedFieldPersistenceName());
369
370} // namespace
371
373 return getChebyshevBoundedFieldPersistenceName();
374}
375
376std::string ChebyshevBoundedField::getPythonModule() const { return "lsst.afw.math"; }
377
379 PersistenceHelper const keys(_coefficients.getSize<1>(), _coefficients.getSize<0>());
380 table::BaseCatalog catalog = handle.makeCatalog(keys.schema);
382 record->set(keys.orderX, _coefficients.getSize<1>() - 1);
383 record->set(keys.bbox, getBBox());
384 (*record)[keys.coefficients].deep() = ndarray::flatten<1>(_coefficients);
385 handle.saveCatalog(catalog);
386}
387
388// ------------------ operators -----------------------------------------------------------------------------
389
393
395 auto rhsCasted = dynamic_cast<ChebyshevBoundedField const*>(&rhs);
396 if (!rhsCasted) return false;
397
398 return (getBBox() == rhsCasted->getBBox()) &&
399 (_coefficients.getShape() == rhsCasted->_coefficients.getShape()) &&
400 all(equal(_coefficients, rhsCasted->_coefficients));
401}
402
405 os << "ChebyshevBoundedField (" << _coefficients.getShape() << " coefficients in y,x)";
406 return os.str();
407}
408
409// ------------------ explicit instantiation ----------------------------------------------------------------
410
411#ifndef DOXYGEN
412
413#define INSTANTIATE(T) \
414 template std::shared_ptr<ChebyshevBoundedField> ChebyshevBoundedField::fit(image::Image<T> const& image, \
415 Control const& ctrl)
416
417INSTANTIATE(float);
418INSTANTIATE(double);
419
420#endif
421} // namespace math
422} // namespace afw
423} // namespace lsst
#define INSTANTIATE(FROMSYS, TOSYS)
Definition Detector.cc:509
#define LSST_EXCEPT(type,...)
#define LSST_ARCHIVE_ASSERT(EXPR)
An assertion macro used to validate the structure of an InputArchive.
Definition Persistable.h:48
lsst::geom::Box2I getBBox(ImageOrigin origin=PARENT) const
Definition ImageBase.h:445
A class to represent a 2-dimensional array of pixels.
Definition Image.h:51
An abstract base class for 2-d functions defined on an integer bounding boxes.
lsst::geom::Box2I getBBox() const
Return the bounding box that defines the region where the field is valid.
BoundedField(BoundedField const &)=default
int computeSize() const
Return the number of nonzero coefficients in the Chebyshev function defined by this object.
bool triangular
"if true, only include terms where the sum of the x and y order " "is less than or equal to max(order...
int orderY
"maximum Chebyshev function order in y" ;
int orderX
"maximum Chebyshev function order in x" ;
A BoundedField based on 2-d Chebyshev polynomials of the first kind.
static std::shared_ptr< ChebyshevBoundedField > fit(lsst::geom::Box2I const &bbox, ndarray::Array< double const, 1 > const &x, ndarray::Array< double const, 1 > const &y, ndarray::Array< double const, 1 > const &z, Control const &ctrl)
Fit a Chebyshev approximation to non-gridded data with equal weights.
std::shared_ptr< ChebyshevBoundedField > relocate(lsst::geom::Box2I const &bbox) const
Return a new ChebyshevBoundedField with domain set to the given bounding box.
bool operator==(BoundedField const &rhs) const override
BoundedFields (of the same sublcass) are equal if their bounding boxes and parameters are equal.
ndarray::Array< double const, 2, 2 > getCoefficients() const
Return the coefficient matrix.
std::shared_ptr< BoundedField > operator*(double const scale) const override
Return a scaled BoundedField.
void write(OutputArchiveHandle &handle) const override
Write the object to one or more catalogs.
static ndarray::Array< double, 2, 2 > makeFitMatrix(lsst::geom::Box2I const &bbox, ndarray::Array< double const, 1 > const &x, ndarray::Array< double const, 1 > const &y, Control const &ctrl)
Construct a matrix that can be used to fit or evaluate a Chebyshev polynomial at a set of points.
std::string getPythonModule() const override
Return the fully-qualified Python module that should be imported to guarantee that its factory is reg...
double evaluate(lsst::geom::Point2D const &position) const override
Evaluate the field at the given point.
double mean() const override
Compute the mean of this function over its bounding-box.
std::shared_ptr< ChebyshevBoundedField > truncate(Control const &ctrl) const
Return a new ChebyshevBoudedField with maximum orders set by the given control object.
std::string getPersistenceName() const override
Return the unique name used to persist this object and look up its factory.
ChebyshevBoundedField(lsst::geom::Box2I const &bbox, ndarray::Array< double const, 2, 2 > const &coefficients)
Initialize the field from its bounding box an coefficients.
double integrate() const override
Compute the integral of this function over its bounding-box.
Solver for linear least-squares problems.
ndarray::Array< double const, 1, 1 > getSolution()
Return the vector solution to the least squares problem.
static LeastSquares fromDesignMatrix(ndarray::Array< T1, 2, C1 > const &design, ndarray::Array< T2, 1, C2 > const &data, Factorization factorization=NORMAL_EIGENSYSTEM)
Initialize from the design matrix and data vector given as ndarrays.
@ NORMAL_EIGENSYSTEM
Use the normal equations with a symmetric Eigensystem decomposition.
std::shared_ptr< RecordT > addNew()
Create a new record, add it to the end of the catalog, and return a pointer to it.
Definition Catalog.h:490
A class used as a handle to a particular field in a table.
Definition Key.h:53
Defines the fields and offsets for a table.
Definition Schema.h:51
void saveCatalog(BaseCatalog const &catalog)
Save a catalog in the archive.
BaseCatalog makeCatalog(Schema const &schema)
Return a new, empty catalog with the given schema.
A CRTP facade class for subclasses of Persistable.
io::OutputArchiveHandle OutputArchiveHandle
double getWidth() const noexcept
double getCenterX() const noexcept
double getHeight() const noexcept
double getCenterY() const noexcept
int getBeginX() const noexcept
int getHeight() const noexcept
int getWidth() const noexcept
int getArea() const
int getBeginY() const noexcept
static LinearTransform makeScaling(double s) noexcept
T make_shared(T... args)
CatalogT< BaseRecord > BaseCatalog
Definition fwd.h:72
BoxKey< lsst::geom::Box2I > Box2IKey
Definition aggregates.h:283
Extent< double, 2 > Extent2D
Point< double, 2 > Point2D
T str(T... args)
A helper class ChebyshevBoundedField, for mapping trapezoidal matrices to 1-d arrays.
std::shared_ptr< table::io::Persistable > read(table::io::InputArchive const &archive, table::io::CatalogVector const &catalogs) const override