January 2024¶
core
1D meshes: the
mesh
module explicitly supports 1D meshes (intervals). Before of this update, the only way to handle one dimensional intervals was to employ a degenerate linear network. NowMesh<1, 1>
is provided to support this functionality. In addition, point location over 1D interval is implemented using a fast \(O(\log(n))\) binary search without additional memory storage. The class supports also a convenient linspaced constructor for meshing intervals \([a,b]\) with equispaced nodesMesh<1, 1>::Mesh(double a, double b, int n_nodes)
.Discretization of 1D PDEs using B-spline basis: PDEs can be discretized using a B-spline basis expansion of the solution. The
spline
module allows to define a PDE on aMesh<1, 1>
with the following API, using a B-spline discretization:Mesh<1, 1> unit_interval(0, 1, 10); // SPLINE declares the intention to discretize this operator using a B-spline basis expansion // of its solution. auto Lt = -bilaplacian<SPLINE>(); // strong formulation of the differential operator PDE<Mesh<1, 1>, decltype(Lt), DMatrix<double>, SPLINE, spline_order<3>> time_penalty(unit_interval, Lt);
Still missing
Diffusion and transport operators, non-homogeneous forcing terms, Dirichlet and non-homogeneous Neumann boundary conditions, time-dependent problems, non-linearities.
Binary matrices: the linear algebra module supports the definition and manipulation of binary valued matrices, via the template
BinaryMatrix<Rows, Cols>
. Template parametersRows
andCols
can be set tofdapde::Dynamic
to express a matrix whose dimesions are not known at compile time. Due to its particularly efficient implementation, binary matrices should always be preferred overstd::vector<bool>
orDMatrix<bool>
in the library, whenever there is the need to manipulate vectors (or matrices) of binary values.// a statically stored binary matrix (coefficients set to 0 by default) BinaryMatrix<2,2> m1; m1.set(0,0); // set to 1 the coefficient in position (0,0) // another statically stored binary matrix BinaryMatrix<2,2> m2; m2.set(1,1); // bitwise operations BinaryMatrix<2,2> m3 = m1 | m2; // bitwise or (addition modulo 2) BinaryMatrix<2,2> m4 = m1 & m2; // bitwise and (product modulo 2) BinaryMatrix<2,2> m5 = m1 ^ m2; // bitwise xor BinaryMatrix<2,2> m6 = ~m1; // binary negation // bitwise expression templates :) auto e = (m1 | m2) & ~m1; // a 5 x 100, dynamically sized, binary matrix BinaryMatrix<fdapde::Dynamic> m7(5, 100); m7.set(4, 70); // block operations auto r = m7.row(2); // extract the third row auto c = m7.col(4); // extract the fifth column auto b = m7.block(2, 40, 3, 30); // extract a 3 x 30 block starting at position (2,40) auto static_block = m7.block<3, 30>(2, 40); // static sized version of the above // obtain a new binary matrix by repating m7 2 times by rows and 4 times by columns BinaryMatrix<Dynamic> m8 = m7.blk_repeat(2, 4); // visitors bool v1 = m3.all(); // are all the coefficients of m3 set to true? bool v2 = m3.any(); // is there at least one coefficient of m3 set to true? std::size_t count = m3.count(); // how many coefficients of m3 set to true? // binary vectors are defined as BinaryMatrix<Rows, 1>, all the API above remains valid BinaryVector<Dynamic> vec(500); vec.set(10); vec = m7.row(3);
Tip
Binary matrices are expecially convenient to express bitmasks, e.g., to express the presence or absence of an observation at a given location.
BinaryMatrix<Rows, Cols>
exposes aselect()
method which can be used to mask a given dense or sparse Eigen expression.SpMatrix<double> A(10, 10); BinaryMatrix<fdapde::Dynamic> mask(10, 10); // produce a (sparse) matrix B keeping only those coefficients of A which matches with ones in the mask, // sets all the others to zero SpMatrix<double> B = mask.select(A); // the same holds for dense expressions.
Info
A
BinaryMatrix<Rows, Cols>
does not store its coefficients using one integer for each coefficient. Instead, each integer is used to store8*sizeof(std::uintmax_t)
coefficients (this value is architecture dependent, for instance, each integer can store 64 bits on a 64-bit architecture). This means that a binary matrix with less than 64 coefficients is stored using a single integer (with a space-consumption of 8 bytes on a 64-bit architecture).This memory representation makes the datatype extremely efficient. Indeed, operations between binary matrices are performed at batches of
8*sizeof(std::uintmax_t)
coefficients, e.g., the logical sum (addition modulo 2) between two binary matrices with less than8*sizeof(std::uintmax_t)
is performed with one single machine instruction, instead of using a costly loop coefficient by coefficient.Mass lumping: the linear algebra module supports the computation of the lumped matrix of a given Eigen expression. Both sparse and dense expressions are supported. The implemented lumped operator is the classical row-sum operator.
SpMatrix<double> R0; // some sparse matrix SpMatrix<double> R0_lumped = lump(R0); // mass-lumped R0 // obtain the mass lumped matrix of eigen expressions SpMatrix<double> lumped_matrix = lump(2*R0 + R0); // the above holds also for dense expresions.
Info
lump(A)
returns the mass-lumped matrix of A, not the inverse of its mass-lumped matrix.Optimizers can be type-erased: the optimization module provides a template
Optimizer<F>
which is a type-erasure wrapper for optimization algorithms optimizing functors of typeF
.Optimizer<F>
exposes the standard API of the optimization module. Check any optimizer in the optimization module for details.Example
Thanks to the type-erasure technique, optimizers can be set and assigned using run-time decisions.
ScalarField<2> f([](const SVector<2>& p) -> double { return p[0] + 2*p[1]; }); // an optimizer for 2D scalar fields Optimizer<ScalarField<2>> opt; // bound to opt any optimization algorithm at runtime if(some_runtime_condition) { opt = BFGS<2, WolfeLineSearch>(max_iter, tolerance, step); // BFGS with Wolfe step } else { opt = Newton<2, BacktrackingLineSearch> opt(max_iter, tolerance, step); // Newton with Backtracking step } // this works whenever f is a ScalarField<2>, independently on the implementation of f opt.optimize(f, SVector<2>(1,1));
The above is used, e.g., in
calibration::GCV
(see below) to set at run-time the type of optimizer used for GCV minimization.calibration::GCV
stores a member of typeOptimizer<GCV>
, to enable the optimization of the GCV objective using any optimization strategy.
cpp
General PDEs for space-time separable penalized problems: it is now possible to provide a generic 1D PDE as time penalty in a space-time separable penalized problem.
Note
The functionality is not tested outside the classical time-penalty usually encountered in literature, e.g. \(\int_{\mathcal{D} \times T} (\frac{\partial^2 f}{\partial t^2})^2\), neverthless from this update on the internal infrastructure allows for generic operators in time.
Example
// a spatio-temporal STRPDE model with separable penalty (details omitted) // define temporal and spatial domain... // spatial regularization auto Ld = -laplacian<FEM>(); // simple laplacian penalty in space PDE<Mesh<2, 2>, decltype(Ld), DMatrix<double>, FEM, fem_order<1>> space_penalty(space_domain, Ld, u); // temporal regularization auto Lt = -bilaplacian<SPLINE>(); // penalty on the second derivative in time PDE<Mesh<1, 1>, decltype(Lt), DMatrix<double>, SPLINE, spline_order<3>> time_penalty(time_domain, Lt); STRPDE<SpaceTimeSeparable, fdapde::monolithic> model(space_penalty, time_penalty, Sampling::mesh_nodes);
The writing above implements an STRPDE model as usually encountered in literature. Neverthless
Lt
can now be any operator time. It is also worth to mention that-bilaplacian<SPLINE>
refers to the fourth order problem one gets by developing the math. This might be misleading, as we are actually penalizing for a laplacian (second order derivative in time). Name changes are possible in this respect.K-fold Cross Validation: support for a general implementation of a K-fold cross validation strategy with random partition in train and test set.
KCV
fulfills the calibrator concept (see below for details).template <typename ModelType, typename ScoreType> DVector<double> fit(ModelType& model, const std::vector<DVector<double>>& lambdas, ScoreType cv_score);
Specifically
ScoreType
must be a functor with the following singaturedouble operator()( const DVector<double>& lambda, const BinaryVector<fdapde::Dynamic>& train_mask, const BinaryVector<fdapde::Dynamic>& test_mask);
and must return the model score for a given smoothing parameter and train/test partition. Check
RMSE
for an example.Info
KCV
splits the data (previously shuffled if requested) in K folds, and just invokes the provided cross validation index with the currently explored smoothing parameter and train/test partition. As such, the specific scoring logic, i.e., the core of the calibration strategy, is completely moved on theScoreType
data type.Moreover, there is no actual data splitting, nor data replication, while producing the data folds. Instead, properly defined masks, implemented as
BinaryVector<Dynamic>
, are produced to implement the partitioning in train and test sets.Example
The code below shows how to calibrate the smoothing parameter of an SRPDE model using a 10-fold CV strategy minimizing the model’s RMSE.
// define some statistical model SRPDE model(problem, Sampling::mesh_nodes); // define KCV engine and search for best lambda which minimizes the model's RMSE std::size_t n_folds = 10; KCV kcv(n_folds); std::vector<DVector<double>> lambdas; for (double x = -6.0; x <= -3.0; x += 0.25) lambdas.push_back(SVector<1>(std::pow(10, x))); kcv.fit(model, lambdas, RMSE(model));
For an higher-level API, check the calibrator concept below.
Warning
The functionality is still considered unstable, as extensive numerical tests for all the supported models are required.
Calibrators: the calibrator concept introduces a unified way to calibrate a statistical model (e.g. select its smoothing parameters). The only requirement for a type T to be a calibrator is to expose a
fit
method with the following signaturetemplate <typename ModelType, typename... Args> DVector<double> fit(ModelType& model, Args&&... args);
fit
takes the model whose parameters must be selected and additional arguments required for the specific calibration algorithm. It returns the selected smoothing parameter. Are examples of calibrators,calibration::KCV
andcalibration::GCV
.some details on the GCV calibrator
calibration::GCV
must not be confused withmodel::GCV
. While the latter is a functor representing the GCV objective, the former represents a calibrator.model::GCV
offers a lower-level API than its calibrator. To see the differences, check the following code snippets:// define some statistical model SRPDE model(pde, Sampling::mesh_nodes); // request its GCV objective (use approximated Tr[S]) std::size_t seed = 476813; auto GCV = model.gcv<StochasticEDF>(100, seed); // optimize GCV (require a grid optimization) DVector<double> opt_lambda = core::Grid<fdapde::Dynamic>{}.optimize(GCV, lambda_grid);
// define some statistical model SRPDE model(pde, Sampling::mesh_nodes); // define GCV calibrator (pay attention that a calibrator is model independent) std::size_t seed = 476813; auto calibrator = calibration::GCV {Grid<fdapde::Dynamic> {}, StochasticEDF(100, seed)}; // fit the model using the calibrator DVector<double> opt_lambda = calibrator(lambda_grid).fit(model);
Pay attention that a calibrator never depends on a statistical model. It allows for a functional way to express a calibration strategy which does not depend on a specific model instance. For instance
auto calibrator = calibration::GCV {Grid<fdapde::Dynamic> {}, StochasticEDF(100, seed)};
represents a calibration strategy for a (regression) model based on GCV minimization, optimized over a grid of smoothing parameters, and using a stochastic approximation for the edfs. Note that in the above definition no model is specified. Moreover, it is copy/move assignable, i.e., it can be stored and given as argument to other functions.
The first argument of
calibrator::GCV
can be any optimizer in the core module, for instance a calibrator so definedauto calibrator = calibration::GCV {Newton<fdapde::Dynamic, BacktrackingLineSearch> (10, 0.05, 1), StochasticEDF(100, seed)};
express a calibration strategy for a (regression) model whose GCV is optimized using a newton method with adaptive step size (backtracking line search), using a stochastic approximation for the edfs. Check the optimization module for further details.
calibration::GCV
is a functor, exposing a call operator which forwards its arguments to the optimizer (e.g., the initial point for an iterative optimization routine, or a grid of points for a brute force optimization). The result is an instance ofConfiguredCalibrator
with afit
method accepting the model instance. The calibration is lazily evaluated, e.g., computation starts only when fit is invoked.// set up the internal optimization algorithm with the choosen grid of smoothing parameters and fit the model DVector<double> opt_lambda = calibrator(lambdas).fit(model);
some details on the KCV calibrator
calibration::KCV
allows for the selection of the smoothing parameter of a statistical model, using a K-Fold Cross Validation approach. Observe that thanks to the low requirements for the model type accepted bycalibration::KCV
, any model class (not only regression models) can be provided to this calibrator. The snippet below shows the provided API// define some statistical model SRPDE model(pde, Sampling::mesh_nodes); // define KCV calibrator minimizing the Root Mean Squared Error (RMSE) of the model std::size_t n_folds = 10; std::size_t seed = 476813; auto calibrator = calibration::KCV {n_folds, seed}(lambda_grid, RMSE()); // fit the model with the selected calibration strategy DVector<double> opt_lambda = calibrator.fit(model);
Functions accepting a calibration strategy should accept a
ConfiguredCalibrator
instance. In this way, the routine is abstracted from the calibration strategy, allowing to provide any type of calibration to the algorithm. For an example, see thecenter
routine for the functional centering of a data matrix.Functional centering: the functional module now offer support for the smooth centering of a given data matrix \(X\) via the
center
routine. It returns the centered data togheter with the expansion coefficients of the mean field.Example
The functional centering of a data matrix \(X\), which provides the following signature
template <typename SmootherType_, typename CalibrationType_> CenterReturnType center(const DMatrix<double>& X, SmootherType_&& smoother, CalibrationType_&& calibration);
is an example of the flexibility of the calibrator concept. The
center
function does not assume any type of smoothing algorithm to produce the smooth mean, nor any type of calibration strategy to find the optimal smoothing parameter for the smoother. Users of the centering algorithm define whatever they find more appropriate for their use case.// center the data matrix X, using the smooth mean field obtained from an SRPDE model tuned according to its GCV index // (optimized over a grid of smoothing parameters) applied on the pointwise mean estimator of X auto centered_data = center( X, SRPDE {pde, Sampling::mesh_nodes}, calibration::GCV {Grid<fdapde::Dynamic> {}, StochasticEDF(100)}(lambda_grid)); // centered_data is of type CenterReturnType, a struct providing access to: centred_data.fitted // centred data X - \mu centred_data.mean // mean field expansion coefficients
Note
The requirements on the smoother are so low that also a
RegressionModel<void>
instance (type-erased wrapper for a regression model without any assumption on its penalty) is a valid smoother.functional PCA: official support for sequential fPCA (Lila, E., Aston, J.A.D., Sangalli, L.M. (2016), Smooth Principal Component Analysis over two-dimensional manifolds with an application to Neuroimaging, Annals of Applied Statistics, 10 (4), 1854-1879.) with GCV and KCV calibration of the optimal smoothing parameter for each component. Space-only version.
In the initialization step, SVD is now placed outside the PC functions computational for loop.
PC functions are always normalized with respect to the functional \(L^2\) norm, loadings are the evaluation of these \(L^2\)-normalized fields at the data locations (they are no more normalized in euclidean norm).
Official support for monolithic fPCA based on Regularized SVD with fixed smoothing parameter.
// fPCA with fixed lambda for each component, sequential solver FPCA<SpaceOnly, fdapde::sequential> model(pde, Sampling::mesh_nodes, Calibration::off); // replacing Calibration::off, with Calibration::gcv or Calibration::kcv makes the model to // switch the selection of the level of smoothing for each compoent to the desired strategy // solve the same problem with a monolithic (RSVD-based) solver FPCA<SpaceOnly, fdapde::monolithic> model(pde, Sampling::mesh_nodes);
Check
test/src/fpca_test.cpp
for the detailed API.
R (base)
no notable changings. Moving the internal implementation to R6 classes. At this stage still in an early development phase.