Automatic Differentiation
 
Loading...
Searching...
No Matches
laplace_marginal_density_estimator.hpp
Go to the documentation of this file.
1#ifndef STAN_MATH_MIX_FUNCTOR_LAPLACE_MARGINAL_DENSITY_ESTIMATOR_HPP
2#define STAN_MATH_MIX_FUNCTOR_LAPLACE_MARGINAL_DENSITY_ESTIMATOR_HPP
13#include <algorithm>
14#include <cmath>
15#include <limits>
16#include <mutex>
17#include <iomanip>
18
26namespace stan {
27namespace math {
28
33 /* Size of the blocks in block diagonal hessian*/
59 /* Maximum number of steps*/
65 laplace_options_base(int hessian_block_size_, int solver_, double tolerance_,
66 int max_num_steps_, bool allow_fallthrough_,
67 int max_steps_line_search_)
68 : hessian_block_size(hessian_block_size_),
69 solver(solver_),
70 tolerance(tolerance_),
71 max_num_steps(max_num_steps_),
72 allow_fallthrough(allow_fallthrough_),
73 line_search(max_steps_line_search_) {}
74};
75
76template <bool HasInitTheta>
78
79template <>
80struct laplace_options<false> : public laplace_options_base {
81 laplace_options() = default;
82
83 explicit laplace_options(int hessian_block_size_) {
84 hessian_block_size = hessian_block_size_;
85 }
86};
87
88template <>
90 /* Value for user supplied initial theta */
91 Eigen::VectorXd theta_0{0}; // 6
92
93 template <typename ThetaVec>
94 laplace_options(ThetaVec&& theta_0_, double tolerance_, int max_num_steps_,
95 int hessian_block_size_, int solver_,
96 int max_steps_line_search_, bool allow_fallthrough_)
97 : laplace_options_base(hessian_block_size_, solver_, tolerance_,
98 max_num_steps_, allow_fallthrough_,
99 max_steps_line_search_),
100 theta_0(value_of(std::forward<ThetaVec>(theta_0_))) {}
101};
102
105
106namespace internal {
107
108template <typename Options>
109inline constexpr auto tuple_to_laplace_options(Options&& ops) {
110 using Ops = std::decay_t<Options>;
111 if constexpr (is_tuple_v<Ops>) {
112 if constexpr (!is_eigen_v<std::tuple_element_t<0, std::decay_t<Ops>>>) {
113 static_assert(
114 sizeof(std::decay_t<Ops>*) == 0,
115 "ERROR:(laplace_marginal_lpdf) The first laplace argument is "
116 "expected to be an Eigen vector of dynamic size representing the "
117 "initial theta_0.");
118 }
119 if constexpr (!stan::is_inner_tuple_type_v<1, Ops, double>) {
120 static_assert(
121 sizeof(std::decay_t<Ops>*) == 0,
122 "ERROR:(laplace_marginal_lpdf) The second laplace argument is "
123 "expected to be a double representing the tolerance.");
124 }
125 if constexpr (!stan::is_inner_tuple_type_v<2, Ops, int>) {
126 static_assert(
127 sizeof(std::decay_t<Ops>*) == 0,
128 "ERROR:(laplace_marginal_lpdf) The third laplace argument is "
129 "expected to be an int representing the maximum number of steps for "
130 "the laplace approximation.");
131 }
132 if constexpr (!stan::is_inner_tuple_type_v<3, Ops, int>) {
133 static_assert(
134 sizeof(std::decay_t<Ops>*) == 0,
135 "ERROR:(laplace_marginal_lpdf) The fourth laplace argument is "
136 "expected to be an int representing the solver.");
137 }
138 if constexpr (!stan::is_inner_tuple_type_v<4, Ops, int>) {
139 static_assert(
140 sizeof(std::decay_t<Ops>*) == 0,
141 "ERROR:(laplace_marginal_lpdf) The fifth laplace argument is "
142 "expected to be an int representing the max steps for the laplace "
143 "approximaton's wolfe line search.");
144 }
145 constexpr bool is_fallthrough
147 5, Ops, int> || stan::is_inner_tuple_type_v<5, Ops, bool>;
148 if constexpr (!is_fallthrough) {
149 static_assert(
150 sizeof(std::decay_t<Ops>*) == 0,
151 "ERROR:(laplace_marginal_lpdf) The sixth laplace argument is "
152 "expected to be an int representing allow fallthrough (0/1).");
153 }
154 auto defaults = laplace_options_default{};
156 value_of(std::get<0>(std::forward<Options>(ops))),
157 std::get<1>(ops),
158 std::get<2>(ops),
159 defaults.hessian_block_size,
160 std::get<3>(ops),
161 std::get<4>(ops),
162 (std::get<5>(ops) > 0) ? true : false,
163 };
164 } else {
165 return std::forward<Options>(ops);
166 }
167}
168
169template <typename ThetaVec, typename WR, typename L_t, typename A_vec,
170 typename ThetaGrad, typename LU_t, typename KRoot>
172 /* log marginal density */
173 double lmd{std::numeric_limits<double>::infinity()};
174 /* ThetaVec at the mode */
175 ThetaVec theta;
183 WR W_r;
190 L_t L;
195 A_vec a;
197 ThetaGrad theta_grad;
198 /* LU matrix from solver 3 */
199 LU_t LU;
206 KRoot K_root;
208 laplace_density_estimates(double lmd_, ThetaVec&& theta_, WR&& W_r_, L_t&& L_,
209 A_vec&& a_, ThetaGrad&& theta_grad_, LU_t&& LU_,
210 KRoot&& K_root_, int solver_used_)
211 : lmd(lmd_),
212 theta(std::move(theta_)),
213 W_r(std::move(W_r_)),
214 L(std::move(L_)),
215 a(std::move(a_)),
216 theta_grad(std::move(theta_grad_)),
217 LU(std::move(LU_)),
218 K_root(std::move(K_root_)),
219 solver_used(solver_used_) {}
220};
221
241template <typename WRootMat>
242inline void block_matrix_sqrt(WRootMat& W_root,
243 const Eigen::SparseMatrix<double>& W,
244 const Eigen::Index block_size) {
245 const Eigen::Index n_block = W.cols() / block_size;
246 Eigen::MatrixXd local_block(block_size, block_size);
247 Eigen::MatrixXd local_block_sqrt(block_size, block_size);
248 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> eigensolver;
249 // No block operation available for sparse matrices, so we have to loop
250 // See https://eigen.tuxfamily.org/dox/group__TutorialSparse.html#title7
251 for (Eigen::Index i = 0; i < n_block; i++) {
252 local_block
253 = W.block(i * block_size, i * block_size, block_size, block_size);
254 if (unlikely(!local_block.array().isFinite().all())) {
255 [](auto i) STAN_COLD_PATH {
256 throw std::domain_error(
257 std::string("Error in block_matrix_sqrt: "
258 "non-finite values detected in block diagonal "
259 "starting at (")
260 + std::to_string(i) + ", " + std::to_string(i) + ")");
261 }(i);
262 }
263 local_block_sqrt = 0.5 * (local_block + local_block.transpose());
264 eigensolver.compute(local_block_sqrt);
265 if (unlikely(eigensolver.info() != Eigen::Success)) {
266 [](auto i) STAN_COLD_PATH {
267 throw std::domain_error(
268 std::string("Error in block_matrix_sqrt: "
269 "eigendecomposition failed for block diagonal "
270 "starting at (")
271 + std::to_string(i) + ", " + std::to_string(i) + ")");
272 }(i);
273 }
274 const Eigen::VectorXd eigenvalues = eigensolver.eigenvalues();
275 const double tolerance = block_size * std::numeric_limits<double>::epsilon()
276 * eigenvalues.cwiseAbs().maxCoeff();
277 if (unlikely(eigenvalues.minCoeff() < -tolerance)) {
278 [](auto&& i, auto&& eigenvalues) {
279 throw std::domain_error(
280 std::string("Error in block_matrix_sqrt: block diagonal starting "
281 "at (")
282 + std::to_string(i) + ", " + std::to_string(i)
283 + ") is not positive semi-definite (smallest eigenvalue "
284 + std::to_string(eigenvalues.minCoeff()) + ")");
285 }(i, eigenvalues);
286 }
287 local_block_sqrt.noalias()
288 = eigensolver.eigenvectors()
289 * eigenvalues.cwiseMax(0.0).cwiseSqrt().asDiagonal()
290 * eigensolver.eigenvectors().transpose();
291 for (Eigen::Index k = 0; k < block_size; k++) {
292 for (Eigen::Index j = 0; j < block_size; j++) {
293 W_root.coeffRef(i * block_size + j, i * block_size + k)
294 = local_block_sqrt(j, k);
295 }
296 }
297 }
298}
299
309template <bool InitTheta, typename CovarMat>
310inline void validate_laplace_options(const char* frame_name,
311 const laplace_options<InitTheta>& options,
312 const CovarMat& covariance) {
313 if constexpr (InitTheta) {
314 check_nonzero_size(frame_name, "initial guess", options.theta_0);
315 check_finite(frame_name, "initial guess", options.theta_0);
316 if (unlikely(options.theta_0.size() != covariance.rows())) {
317 std::stringstream msg;
318 msg << frame_name << ": The size of the initial theta ("
319 << options.theta_0.size()
320 << ") vector must match the rows and columns of the covariance "
321 "matrix ("
322 << covariance.rows() << ", " << covariance.cols() << ").";
323 throw std::domain_error(msg.str());
324 }
325 }
326 check_nonnegative(frame_name, "tolerance", options.tolerance);
327 check_positive(frame_name, "max_num_steps", options.max_num_steps);
328 check_positive(frame_name, "hessian_block_size", options.hessian_block_size);
329 check_square(frame_name, "covariance", covariance);
330
331 const Eigen::Index theta_size = covariance.rows();
332 if (unlikely(theta_size % options.hessian_block_size != 0
333 || theta_size < options.hessian_block_size)) {
334 throw std::domain_error(
335 "laplace_marginal_density: Hessian block size mismatch.");
336 }
337
338 if (unlikely(options.solver < 1 || options.solver > 3)) {
339 throw std::domain_error(
340 "laplace_marginal_density: solver must be 1, 2, or 3. Got: "
341 + std::to_string(options.solver));
342 }
343}
344
359
362
365
367 Eigen::VectorXd b;
368
370 Eigen::MatrixXd B;
371
373 Eigen::VectorXd prev_g;
381 bool final_loop = false;
382
400 template <typename ObjFun, typename ThetaGradFun, typename CovarianceT,
401 typename ThetaInitializer>
402 NewtonState(int theta_size, ObjFun&& obj_fun, ThetaGradFun&& theta_grad_f,
403 CovarianceT&& covariance, ThetaInitializer&& theta_init)
404 : wolfe_info(std::forward<ObjFun>(obj_fun),
405 covariance.llt().solve(theta_init),
406 std::forward<ThetaInitializer>(theta_init),
407 std::forward<ThetaGradFun>(theta_grad_f)),
408 proposal(theta_size),
409 b(theta_size),
410 B(theta_size, theta_size),
411 prev_g(theta_size) {
412 wolfe_status.num_backtracks_ = -1; // Safe initial value for BB step
413 }
414
419 auto& curr() & { return wolfe_info.curr_; }
420
425 const auto& curr() const& { return wolfe_info.curr_; }
426 auto&& curr() && { return std::move(wolfe_info).curr(); }
431 auto& prev() & { return wolfe_info.prev_; }
432
437 const auto& prev() const& { return wolfe_info.prev_; }
438 auto&& prev() && { return std::move(wolfe_info).prev(); }
439 auto& proposal_step() & { return proposal; }
440 const auto& proposal_step() const& { return proposal; }
441 auto&& proposal_step() && { return std::move(proposal); }
442 template <typename Options>
443 inline void update_next_step(const Options& options) {
444 this->prev().swap(this->curr());
445 this->curr().alpha()
446 = std::clamp(this->curr().alpha(), 0.0, options.line_search.max_alpha);
447 }
448};
449
459template <typename LLT, typename B_t>
460inline void llt_with_jitter(LLT& llt_B, B_t& B, double min_jitter = 1e-10,
461 double max_jitter = 1e-5) {
462 llt_B.compute(B);
463 if (llt_B.info() != Eigen::Success) {
464 double prev_jitter = 0.0;
465 double jitter_try = min_jitter;
466 for (; jitter_try < max_jitter; jitter_try *= 10) {
467 // Remove previously added jitter before adding the new (larger) amount,
468 // so that the total diagonal perturbation is exactly jitter_try.
469 B.diagonal().array() += (jitter_try - prev_jitter);
470 prev_jitter = jitter_try;
471 llt_B.compute(B);
472 if (llt_B.info() == Eigen::Success) {
473 break;
474 }
475 }
476 if (llt_B.info() != Eigen::Success) {
477 throw std::domain_error(
478 "laplace_marginal_density: Cholesky failed after adding jitter up to "
479 + std::to_string(jitter_try));
480 }
481 }
482}
483
499 Eigen::VectorXd W_r_diag;
500
502 Eigen::VectorXd W_diag;
503
505 Eigen::LLT<Eigen::MatrixXd> llt_B;
506
507 template <typename NewtonStateT, typename CovarMat>
508 CholeskyWSolverDiag(const NewtonStateT& state, const CovarMat& covariance)
509 : W_r_diag(Eigen::VectorXd::Zero(state.b.size())), W_diag(0), llt_B() {}
530 template <typename NewtonStateT, typename LLFun, typename LLTupleArgs,
531 typename CovarMat>
532 void solve_step(NewtonStateT& state, const LLFun& ll_fun,
533 const LLTupleArgs& ll_args, const CovarMat& covariance,
534 int /*hessian_block_size*/, std::ostream* msgs) {
535 const Eigen::Index theta_size = state.b.size();
536
537 // 1. Compute diagonal Hessian
538 W_diag = laplace_likelihood::diagonal_hessian(ll_fun, state.prev().theta(),
539 ll_args, msgs);
540 for (Eigen::Index j = 0; j < W_diag.size(); j++) {
541 if (W_diag.coeff(j) < 0 || !std::isfinite(W_diag.coeff(j))) {
542 throw std::domain_error(
543 "laplace_marginal_density: Hessian matrix is not positive "
544 "definite");
545 } else {
546 W_r_diag.coeffRef(j) = std::sqrt(W_diag.coeff(j));
547 }
548 }
549
550 // 2. Formulate B = I + W_r * Sigma * W_r
551 state.B.noalias()
552 = Eigen::MatrixXd::Identity(theta_size, theta_size)
553 + W_r_diag.asDiagonal() * covariance * W_r_diag.asDiagonal();
554
555 // 3. Factorize B with jittering fallback
556 llt_with_jitter(llt_B, state.B);
557 // 4. Solve for the raw Newton proposal in a-space.
558 state.b.noalias() = (W_diag.array() * state.prev().theta().array()).matrix()
559 + state.prev().theta_grad();
560 auto L = llt_B.matrixL();
561 auto LT = llt_B.matrixU();
562 state.proposal_step().a().noalias()
563 = state.b
564 - W_r_diag.asDiagonal()
565 * LT.solve(
566 L.solve(W_r_diag.cwiseProduct(covariance * state.b)));
567 }
568
573 double compute_log_determinant() const {
574 return 2.0 * llt_B.matrixLLT().diagonal().array().log().sum();
575 }
576
585 template <typename NewtonStateT>
586 auto build_result(NewtonStateT& state, double log_det) {
588 state.prev().obj() - 0.5 * log_det,
589 std::move(state).prev().theta(),
590 Eigen::SparseMatrix<double>(W_r_diag.asDiagonal()),
591 Eigen::MatrixXd(llt_B.matrixL()),
592 std::move(state).prev().a(),
593 std::move(state).prev().theta_grad(),
594 Eigen::PartialPivLU<Eigen::MatrixXd>{},
595 Eigen::MatrixXd(0, 0),
596 1};
597 }
598};
599
615 Eigen::SparseMatrix<double> W_r;
616
618 Eigen::SparseMatrix<double> W_block;
619
621 Eigen::LLT<Eigen::MatrixXd> llt_B;
622
623 template <typename NewtonStateT>
624 CholeskyWSolverBlock(const NewtonStateT& state, int hessian_block_size)
625 : W_r(state.b.size(), state.b.size()) {
626 const Eigen::Index theta_size = state.b.size();
627 W_r.reserve(Eigen::VectorXi::Constant(theta_size, hessian_block_size));
628 const Eigen::Index n_block = theta_size / hessian_block_size;
629 for (Eigen::Index ii = 0; ii < n_block; ii++) {
630 for (Eigen::Index k = 0; k < hessian_block_size; k++) {
631 for (Eigen::Index j = 0; j < hessian_block_size; j++) {
632 W_r.insert(ii * hessian_block_size + j, ii * hessian_block_size + k)
633 = 1.0;
634 }
635 }
636 }
637 W_r.makeCompressed();
638 }
639
662 template <typename NewtonStateT, typename LLFun, typename LLTupleArgs,
663 typename CovarMat>
664 void solve_step(NewtonStateT& state, const LLFun& ll_fun,
665 const LLTupleArgs& ll_args, const CovarMat& covariance,
666 int hessian_block_size, std::ostream* msgs) {
667 const Eigen::Index theta_size = state.b.size();
668 // 1. Compute block Hessian
670 ll_fun, state.prev().theta(), hessian_block_size, ll_args, msgs);
671
672 for (Eigen::Index j = 0; j < W_block.rows(); j++) {
673 if (W_block.coeff(j, j) < 0 || !std::isfinite(W_block.coeff(j, j))) {
674 throw std::domain_error(
675 "laplace_marginal_density: Hessian matrix is not positive "
676 "definite");
677 }
678 }
679
680 // 2. Compute W_r = sqrt(W)
681 block_matrix_sqrt(W_r, W_block, hessian_block_size);
682
683 // 3. Formulate B = I + W_r * Sigma * W_r
684 state.B.noalias() = Eigen::MatrixXd::Identity(theta_size, theta_size)
685 + W_r * (covariance * W_r);
686
687 // 4. Factorize B with jittering fallback
688 llt_with_jitter(llt_B, state.B);
689
690 // 5. Solve for the raw Newton proposal in a-space.
691 state.b.noalias()
692 = W_block * state.prev().theta() + state.prev().theta_grad();
693 auto L = llt_B.matrixL();
694 auto LT = llt_B.matrixU();
695 state.proposal_step().a().noalias()
696 = state.b - W_r * LT.solve(L.solve(W_r * (covariance * state.b)));
697 }
698
703 double compute_log_determinant() const {
704 return 2.0 * llt_B.matrixLLT().diagonal().array().log().sum();
705 }
706
715 template <typename NewtonStateT>
716 auto build_result(NewtonStateT& state, double log_det) {
717 return laplace_density_estimates{state.prev().obj() - 0.5 * log_det,
718 std::move(state).prev().theta(),
719 std::move(W_r),
720 Eigen::MatrixXd(llt_B.matrixL()),
721 std::move(state).prev().a(),
722 std::move(state).prev().theta_grad(),
723 Eigen::PartialPivLU<Eigen::MatrixXd>{},
724 Eigen::MatrixXd(0, 0),
725 1};
726 }
727};
728
743 Eigen::MatrixXd K_root;
744
746 Eigen::SparseMatrix<double> W_full;
747
749 Eigen::LLT<Eigen::MatrixXd> llt_B;
750
751 template <typename NewtonStateT, typename CovarMat>
752 CholeskyKSolver(const NewtonStateT& state, const CovarMat& covariance)
753 : K_root(0, 0), W_full(0, 0), llt_B() {
754 auto K_root_llt = covariance.template selfadjointView<Eigen::Lower>().llt();
755 if (K_root_llt.info() != Eigen::Success) {
756 throw std::domain_error(
757 "laplace_marginal_density: Cholesky of covariance failed at start");
758 }
759 K_root = std::move(K_root_llt.matrixL());
760 }
761
782 template <typename NewtonStateT, typename LLFun, typename LLTupleArgs,
783 typename CovarMat>
784 void solve_step(NewtonStateT& state, const LLFun& ll_fun,
785 const LLTupleArgs& ll_args, const CovarMat& covariance,
786 int hessian_block_size, std::ostream* msgs) {
787 const Eigen::Index theta_size = state.b.size();
788
789 // 1. Compute Hessian
791 ll_fun, state.prev().theta(), hessian_block_size, ll_args, msgs);
792
793 // 2. Formulate B = I + K^T * W * K
794 state.B.noalias() = Eigen::MatrixXd::Identity(theta_size, theta_size)
795 + K_root.transpose() * (W_full * K_root);
796
797 // 3. Factorize B with jittering fallback
798 llt_with_jitter(llt_B, state.B);
799
800 // 4. Solve for the raw Newton proposal in a-space.
801 state.b.noalias()
802 = W_full * state.prev().theta() + state.prev().theta_grad();
803 auto L = llt_B.matrixL();
804 auto LT = llt_B.matrixU();
805 state.proposal_step().a().noalias()
806 = K_root.transpose().template triangularView<Eigen::Upper>().solve(
807 LT.solve(L.solve(K_root.transpose() * state.b)));
808 }
809
814 double compute_log_determinant() const {
815 return 2.0 * llt_B.matrixLLT().diagonal().array().log().sum();
816 }
817
826 template <typename NewtonStateT>
827 auto build_result(NewtonStateT& state, double log_det) {
828 return laplace_density_estimates{state.prev().obj() - 0.5 * log_det,
829 std::move(state.prev().theta()),
830 std::move(W_full),
831 Eigen::MatrixXd(llt_B.matrixL()),
832 std::move(state.prev().a()),
833 std::move(state.prev().theta_grad()),
834 Eigen::PartialPivLU<Eigen::MatrixXd>{},
835 std::move(K_root),
836 2};
837 }
838};
839
853struct LUSolver {
855 Eigen::PartialPivLU<Eigen::MatrixXd> lu;
856
858 Eigen::SparseMatrix<double> W_full;
859
877 template <typename NewtonStateT, typename LLFun, typename LLTupleArgs,
878 typename CovarMat>
879 void solve_step(NewtonStateT& state, const LLFun& ll_fun,
880 const LLTupleArgs& ll_args, const CovarMat& covariance,
881 int hessian_block_size, std::ostream* msgs) {
882 const Eigen::Index theta_size = state.b.size();
883
884 // 1. Compute Hessian
886 ll_fun, state.prev().theta(), hessian_block_size, ll_args, msgs);
887
888 // 2. Factorize B = I + Sigma * W
889 lu.compute(Eigen::MatrixXd::Identity(theta_size, theta_size)
890 + covariance * W_full);
891
892 // 3. Solve for the raw Newton proposal in a-space.
893 state.b.noalias()
894 = W_full * state.prev().theta() + state.prev().theta_grad();
895 state.proposal_step().a().noalias()
896 = state.b - W_full * lu.solve(covariance * state.b);
897 }
898
908 double compute_log_determinant() const {
909 return lu.matrixLU().diagonal().array().log().sum();
910 }
911
920 template <typename NewtonStateT>
921 auto build_result(NewtonStateT& state, double log_det) {
922 return laplace_density_estimates{state.prev().obj() - 0.5 * log_det,
923 std::move(state).prev().theta(),
924 std::move(W_full),
925 Eigen::MatrixXd(0, 0),
926 std::move(state).prev().a(),
927 std::move(state).prev().theta_grad(),
928 std::move(lu),
929 Eigen::MatrixXd(0, 0),
930 3};
931 }
932};
933
956template <typename SolverPolicy, typename NewtonStateT, typename OptionsT,
957 typename LLFunT, typename LLTupleArgsT, typename CovarMatT,
958 typename UpdateFun>
959inline auto run_newton_loop(SolverPolicy& solver, NewtonStateT& state,
960 const OptionsT& options, Eigen::Index& step_iter,
961 const LLFunT& ll_fun, const LLTupleArgsT& ll_args,
962 const CovarMatT& covariance, UpdateFun&& update_fun,
963 std::ostream* msgs) {
964 bool finish_update = false;
965 for (; step_iter <= options.max_num_steps; step_iter++) {
966 solver.solve_step(state, ll_fun, ll_args, covariance,
967 options.hessian_block_size, msgs);
968 if (!state.final_loop) {
969 auto&& proposal = state.proposal_step();
970 state.wolfe_info.p_ = proposal.a() - state.prev().a();
971 state.prev_g.noalias() = -covariance * state.prev().a()
972 + covariance * state.prev().theta_grad();
973 state.wolfe_info.init_dir_ = state.prev_g.dot(state.wolfe_info.p_);
974 // Flip direction if not ascending
975 state.wolfe_info.flip_direction();
976 auto&& scratch = state.wolfe_info.scratch_;
977 proposal.eval_.alpha() = 1.0;
978 const bool proposal_valid
979 = update_fun(proposal, state.curr(), state.prev(), proposal.eval_,
980 state.wolfe_info.p_);
981 const bool cached_proposal_ok
982 = proposal_valid && std::isfinite(proposal.obj())
983 && std::isfinite(proposal.dir())
984 && proposal.alpha() > options.line_search.min_alpha;
985 if (!cached_proposal_ok) {
986 state.wolfe_status
988 } else if (options.line_search.max_iterations == 0) {
989 state.curr().update(proposal);
990 state.wolfe_status = WolfeStatus{WolfeReturn::Continue, 1, 0, true};
991 } else {
992 Eigen::VectorXd s = proposal.a() - state.prev().a();
993 auto full_step_grad
994 = (-covariance * proposal.a() + covariance * proposal.theta_grad())
995 .eval();
996 state.curr().alpha() = barzilai_borwein_step_size(
997 s, full_step_grad, state.prev_g, state.prev().alpha(),
998 state.wolfe_status.num_backtracks_, options.line_search.min_alpha,
999 options.line_search.max_alpha);
1000 state.wolfe_status = internal::wolfe_line_search(
1001 state.wolfe_info, update_fun, options.line_search, msgs);
1002 }
1003 bool search_failed = !state.wolfe_status.accept_;
1004 const bool proposal_armijo_ok
1005 = cached_proposal_ok
1007 proposal.obj(), state.prev().obj(), proposal.alpha(),
1008 state.wolfe_info.init_dir_, options.line_search);
1009 if (search_failed && proposal_armijo_ok) {
1010 state.curr().update(proposal);
1011 state.wolfe_status
1012 = WolfeStatus{WolfeReturn::Armijo, state.wolfe_status.num_evals_,
1013 state.wolfe_status.num_backtracks_, true};
1014 search_failed = false;
1015 }
1016 bool objective_converged
1017 = state.wolfe_status.accept_
1018 && std::abs(state.curr().obj() - state.prev().obj())
1019 < options.tolerance;
1020 finish_update = objective_converged || search_failed;
1021 }
1022 if (finish_update) {
1023 if (!state.final_loop && state.wolfe_status.accept_) {
1024 // Do one final loop with exact wolfe conditions
1025 state.final_loop = true;
1026 state.update_next_step(options);
1027 continue;
1028 }
1029 return solver.build_result(state, solver.compute_log_determinant());
1030 } else {
1031 state.update_next_step(options);
1032 }
1033 }
1034 if (msgs) {
1035 (*msgs)
1036 << std::string(
1037 "WARNING(laplace_marginal_density): max number of iterations: ")
1038 + std::to_string(options.max_num_steps) + " exceeded.";
1039 }
1040 return solver.build_result(state, solver.compute_log_determinant());
1041}
1042
1051[[noreturn]] inline void throw_solver_failure(std::string_view context,
1052 Eigen::Index iter,
1053 std::string_view failed_solver,
1054 const std::exception& e) {
1055 std::ostringstream os;
1056 os << context << ": " << failed_solver << " failed at iteration " << iter
1057 << " and allow_fallthrough is false. Reason: " << e.what();
1058 throw std::domain_error(os.str());
1059}
1060
1070inline void log_solver_fallback(std::ostream* msgs, std::string_view context,
1071 Eigen::Index iter,
1072 std::string_view failed_solver,
1073 std::string_view next_solver,
1074 const std::exception& e) {
1075 if (!msgs) {
1076 return;
1077 }
1078 // Build once so we don't interleave with other logs.
1079 std::ostringstream os;
1080 os << "[" << context << "] WARNING: solver fallback\n"
1081 << " " << std::left << std::setw(12) << "iteration:" << iter << "\n"
1082 << " " << std::left << std::setw(12) << "failed:" << failed_solver << "\n"
1083 << " " << std::left << std::setw(12) << "reason:" << e.what() << "\n"
1084 << " " << std::left << std::setw(12) << "action:"
1085 << "trying " << next_solver << "\n"
1086 << "note: this warning message will only be displayed once."
1087 << "\n";
1088 (*msgs) << os.str();
1089}
1090
1091template <bool InitTheta, typename Opts>
1092inline decltype(auto) theta_init_impl(Eigen::Index theta_size, Opts&& options) {
1093 if constexpr (InitTheta) {
1094 // If requested, use the prior mean as the initial value
1095 return std::decay_t<decltype(options)>(options).theta_0;
1096 } else {
1097 return Eigen::MatrixXd::Zero(theta_size, 1);
1098 }
1099}
1100
1119template <typename ObjFun, typename ThetaGradFun, typename Covariance,
1120 typename Options>
1121inline auto create_update_fun(ObjFun&& obj_fun, ThetaGradFun&& theta_grad_f,
1122 Covariance&& covariance, Options&& options) {
1123 auto update_step = [&covariance, &obj_fun, &theta_grad_f](
1124 auto& proposal, auto&& /* curr */, auto&& prev,
1125 auto& eval_in, auto&& p) {
1126 try {
1127 proposal.a() = prev.a() + eval_in.alpha() * p;
1128 proposal.theta().noalias() = covariance * proposal.a();
1129 proposal.theta_grad() = theta_grad_f(proposal.theta());
1130 eval_in.obj() = obj_fun(proposal.a(), proposal.theta());
1131 eval_in.dir()
1132 = (-covariance * proposal.a() + covariance * proposal.theta_grad())
1133 .dot(p);
1134 return std::isfinite(eval_in.obj()) && std::isfinite(eval_in.dir());
1135 } catch (const std::exception&) {
1136 return false;
1137 }
1138 };
1139 auto backoff = [&options](auto& eval) {
1140 eval.alpha() *= options.line_search.tau;
1141 return eval.alpha() > options.line_search.min_alpha;
1142 };
1143 return
1144 [update_step_ = std::move(update_step), backoff_ = std::move(backoff)](
1145 auto& proposal, auto&& curr, auto&& prev, auto& eval_in, auto&& p) {
1146 return internal::retry_evaluate(update_step_, proposal, curr, prev,
1147 eval_in, p, backoff_);
1148 };
1149}
1150
1204template <typename LLFun, typename LLTupleArgs, typename CovarMat,
1205 bool InitTheta,
1208 LLFun&& ll_fun, LLTupleArgs&& ll_args, CovarMat&& covariance,
1209 const laplace_options<InitTheta>& options, std::ostream* msgs) {
1210 internal::validate_laplace_options("laplace_marginal_density", options,
1211 covariance);
1212 const Eigen::Index theta_size = covariance.rows();
1213 // Wolfe optimizes over the latent 'a' space
1214 auto obj_fun = [&ll_fun, &ll_args, &msgs](const Eigen::VectorXd& a_val,
1215 auto&& theta_val) -> double {
1216 return -0.5 * a_val.dot(theta_val)
1217 + laplace_likelihood::log_likelihood(ll_fun, theta_val, ll_args,
1218 msgs);
1219 };
1220 auto theta_grad_f = [&ll_fun, &ll_args, &msgs](auto&& theta_val) {
1221 return laplace_likelihood::theta_grad(ll_fun, theta_val, ll_args, msgs);
1222 };
1223 decltype(auto) theta_init = theta_init_impl<InitTheta>(theta_size, options);
1224 // When the user supplies a non-zero theta_init, we must initialise a
1225 // consistently so that the invariant theta = Sigma * a holds. Otherwise
1226 // the prior term -0.5 * a'*theta vanishes (a=0 while theta!=0), inflating
1227 // the initial objective and causing the Wolfe line search to reject the
1228 // first Newton step.
1229 auto state
1230 = NewtonState(theta_size, obj_fun, theta_grad_f, covariance, theta_init);
1231 // Start with safe step size
1232 auto update_fun = create_update_fun(
1233 std::move(obj_fun), std::move(theta_grad_f), covariance, options);
1234 Eigen::Index step_iter = 0;
1235 try {
1236 if (options.solver == 1) {
1237 if (options.hessian_block_size == 1) {
1238 CholeskyWSolverDiag solver(state, covariance);
1239 return run_newton_loop(solver, state, options, step_iter, ll_fun,
1240 ll_args, covariance, update_fun, msgs);
1241 } else {
1242 CholeskyWSolverBlock solver(state, options.hessian_block_size);
1243 return run_newton_loop(solver, state, options, step_iter, ll_fun,
1244 ll_args, covariance, update_fun, msgs);
1245 }
1246 }
1247 } catch (const std::exception& e) {
1248 const std::string solver_type
1249 = (options.hessian_block_size == 1) ? "Diagonal" : "Block";
1250 std::string failed = "solver 1 (" + solver_type + " Hessian-root Cholesky)";
1251 if (!options.allow_fallthrough) {
1252 throw_solver_failure("laplace_marginal_density", step_iter, failed, e);
1253 }
1254 std::call_once(
1256 [](auto&&... args) STAN_COLD_PATH {
1257 log_solver_fallback(std::forward<decltype(args)>(args)...);
1258 },
1259 msgs, "laplace_marginal_density", step_iter, std::move(failed),
1260 "solver 2 (Covariance-root Cholesky)", e);
1261 }
1262 try {
1263 if (options.solver == 2 || options.allow_fallthrough) {
1264 CholeskyKSolver solver(state, covariance);
1265 return run_newton_loop(solver, state, options, step_iter, ll_fun, ll_args,
1266 covariance, update_fun, msgs);
1267 }
1268 } catch (const std::exception& e) {
1269 if (!options.allow_fallthrough) {
1270 throw_solver_failure("laplace_marginal_density", step_iter,
1271 "solver 2 (Covariance-root Cholesky)", e);
1272 }
1273 std::call_once(
1275 [](auto&&... args) STAN_COLD_PATH {
1276 log_solver_fallback(std::forward<decltype(args)>(args)...);
1277 },
1278 msgs, "laplace_marginal_density", step_iter,
1279 "solver 2 (Covariance-root Cholesky)", "solver 3 (General LU solver)",
1280 e);
1281 }
1282 if (options.solver == 3 || options.allow_fallthrough) {
1283 LUSolver solver;
1284 return run_newton_loop(solver, state, options, step_iter, ll_fun, ll_args,
1285 covariance, update_fun, msgs);
1286 }
1287 throw std::domain_error(
1288 std::string("You chose a solver (") + std::to_string(options.solver)
1289 + ") that is not valid. Please choose either 1, 2, or 3.");
1290}
1291} // namespace internal
1292} // namespace math
1293} // namespace stan
1294#endif
#define STAN_THREADS_DEF
#define STAN_COLD_PATH
#define unlikely(x)
int64_t size(const T &m)
Returns the size (number of the elements) of a matrix_cl or var_value<matrix_cl<T>>.
Definition size.hpp:19
(Expert) Numerical traits for algorithmic differentiation variables.
WolfeStatus wolfe_line_search(Info &wolfe_info, UpdateFun &&update_fun, Options &&opt, Stream *msgs)
Strong Wolfe line search for maximization.
static thread_local std::once_flag fallback_warning_2_3
auto create_update_fun(ObjFun &&obj_fun, ThetaGradFun &&theta_grad_f, Covariance &&covariance, Options &&options)
Create the update function for the line search, capturing necessary references.
auto run_newton_loop(SolverPolicy &solver, NewtonStateT &state, const OptionsT &options, Eigen::Index &step_iter, const LLFunT &ll_fun, const LLTupleArgsT &ll_args, const CovarMatT &covariance, UpdateFun &&update_fun, std::ostream *msgs)
Run a Newton loop with a solver policy, updating the shared state.
void log_solver_fallback(std::ostream *msgs, std::string_view context, Eigen::Index iter, std::string_view failed_solver, std::string_view next_solver, const std::exception &e)
Log a solver fallback event to the provided stream, if any.
double barzilai_borwein_step_size(const Eigen::VectorXd &s, const Eigen::VectorXd &g_curr, const Eigen::VectorXd &g_prev, double prev_step, int last_backtracks, double min_alpha, double max_alpha)
Curvature-aware Barzilai–Borwein (BB) step length with robust safeguards.
decltype(auto) theta_init_impl(Eigen::Index theta_size, Opts &&options)
auto check_armijo(double obj_next, double obj_init, double alpha_next, double dir0, Option &&opt)
constexpr double laplace_default_tolerance
void throw_solver_failure(std::string_view context, Eigen::Index iter, std::string_view failed_solver, const std::exception &e)
Throw for a solver failure when falling through to the next solver is not allowed.
constexpr auto tuple_to_laplace_options(Options &&ops)
constexpr int laplace_default_max_steps_line_search
constexpr int laplace_default_hessian_block_size
void validate_laplace_options(const char *frame_name, const laplace_options< InitTheta > &options, const CovarMat &covariance)
Validates the options for the Laplace approximation.
constexpr int laplace_default_allow_fallthrough
static thread_local std::once_flag fallback_warning_1_2
auto retry_evaluate(Update &&update, Proposal &&proposal, Curr &&curr, Prev &&prev, Eval &eval, P &&p, Backoff &&backoff)
Retry evaluation of a step until it passes a validity check.
auto laplace_marginal_density_est(LLFun &&ll_fun, LLTupleArgs &&ll_args, CovarMat &&covariance, const laplace_options< InitTheta > &options, std::ostream *msgs)
For a latent Gaussian model with hyperparameters phi and latent variables theta, and observations y,...
void llt_with_jitter(LLT &llt_B, B_t &B, double min_jitter=1e-10, double max_jitter=1e-5)
Factorize B with jittering fallback.
void block_matrix_sqrt(WRootMat &W_root, const Eigen::SparseMatrix< double > &W, const Eigen::Index block_size)
Returns the principal square root of a symmetric positive semi-definite block diagonal matrix.
auto diagonal_hessian(F &&f, Theta &&theta, TupleArgs &&ll_tuple, Stream *msgs)
auto log_likelihood(F &&f, Theta &&theta, TupleArgs &&ll_tup, Stream *msgs)
A wrapper that accepts a tuple as arguments.
auto block_hessian(F &&f, Theta &&theta, const Eigen::Index hessian_block_size, TupleArgs &&ll_tuple, Stream *msgs)
auto theta_grad(F &&f, Theta &&theta, TupleArgs &&ll_tup, Stream *msgs=nullptr)
A wrapper that accepts a tuple as arguments.
Eigen::Matrix< complex_return_t< value_type_t< EigMat > >, -1, 1 > eigenvalues(EigMat &&m)
Return the eigenvalues of a (real-valued) matrix.
void check_square(const char *function, const char *name, const T_y &y)
Check if the specified matrix is square.
void check_nonnegative(const char *function, const char *name, const T_y &y)
Check if y is non-negative.
static constexpr double e()
Return the base of the natural logarithm.
Definition constants.hpp:20
T eval(T &&arg)
Inputs which have a plain_type equal to the own time are forwarded unmodified (for Eigen expressions ...
Definition eval.hpp:20
T value_of(const fvar< T > &v)
Return the value of the specified variable.
Definition value_of.hpp:18
void check_finite(const char *function, const char *name, const T_y &y)
Return true if all values in y are finite.
void check_nonzero_size(const char *function, const char *name, const T_y &y)
Check if the specified matrix/vector is of non-zero size.
void check_positive(const char *function, const char *name, const T_y &y)
Check if y is positive.
double dot(const std::vector< double > &x, const std::vector< double > &y)
Definition dot.hpp:11
constexpr bool is_inner_tuple_type_v
Checks if the N-th element of a tuple is of the same type as CheckType.
Definition is_tuple.hpp:97
std::enable_if_t< Check::value > require_t
If condition is true, template is enabled.
The lgamma implementation in stan-math is based on either the reentrant safe lgamma_r implementation ...
STL namespace.
void solve_step(NewtonStateT &state, const LLFun &ll_fun, const LLTupleArgs &ll_args, const CovarMat &covariance, int hessian_block_size, std::ostream *msgs)
Perform one Newton step using covariance Cholesky solver.
Eigen::MatrixXd K_root
Lower Cholesky factor of covariance: Sigma = K_root * K_root^T.
Eigen::LLT< Eigen::MatrixXd > llt_B
Cholesky factorization of B = I + K_root^T * W * K_root.
Eigen::SparseMatrix< double > W_full
Full (block) Hessian matrix from likelihood.
double compute_log_determinant() const
Compute log determinant of B from Cholesky factor.
auto build_result(NewtonStateT &state, double log_det)
Build the final result structure.
CholeskyKSolver(const NewtonStateT &state, const CovarMat &covariance)
Solver Policy 2: Cholesky decomposition of K (Covariance).
Eigen::LLT< Eigen::MatrixXd > llt_B
Cholesky factorization of B = I + W_r * Sigma * W_r.
Eigen::SparseMatrix< double > W_block
Sparse block-diagonal Hessian from likelihood.
Eigen::SparseMatrix< double > W_r
Sparse square root of block Hessian.
double compute_log_determinant() const
Compute log determinant of B from Cholesky factor.
void solve_step(NewtonStateT &state, const LLFun &ll_fun, const LLTupleArgs &ll_args, const CovarMat &covariance, int hessian_block_size, std::ostream *msgs)
Perform one Newton step using block-diagonal Hessian solver.
auto build_result(NewtonStateT &state, double log_det)
Build the final result structure.
CholeskyWSolverBlock(const NewtonStateT &state, int hessian_block_size)
Solver Policy 1 (Block): Cholesky decomposition using block W.
void solve_step(NewtonStateT &state, const LLFun &ll_fun, const LLTupleArgs &ll_args, const CovarMat &covariance, int, std::ostream *msgs)
Perform one Newton step using diagonal Hessian solver.
Eigen::LLT< Eigen::MatrixXd > llt_B
Cholesky factorization of B = I + W_r * Sigma * W_r.
CholeskyWSolverDiag(const NewtonStateT &state, const CovarMat &covariance)
Eigen::VectorXd W_r_diag
Square root of diagonal Hessian: W_r[j] = sqrt(W[j])
auto build_result(NewtonStateT &state, double log_det)
Build the final result structure.
Eigen::VectorXd W_diag
Diagonal Hessian values from the likelihood.
double compute_log_determinant() const
Compute log determinant of B from Cholesky factor.
Solver Policy 1 (Diagonal): Cholesky decomposition using W.
auto build_result(NewtonStateT &state, double log_det)
Build the final result structure.
void solve_step(NewtonStateT &state, const LLFun &ll_fun, const LLTupleArgs &ll_args, const CovarMat &covariance, int hessian_block_size, std::ostream *msgs)
Perform one Newton step using LU decomposition solver.
double compute_log_determinant() const
Compute log determinant from LU factorization.
Eigen::SparseMatrix< double > W_full
Full Hessian matrix from likelihood.
Eigen::PartialPivLU< Eigen::MatrixXd > lu
LU factorization of B = I + Sigma * W.
WolfeData proposal
Cached proposal evaluated before the Wolfe line search.
auto & prev() &
Access the previous step state (mutable).
Eigen::MatrixXd B
Workspace matrix: B = I + W_r * Sigma * W_r (or similar)
WolfeStatus wolfe_status
Status of the most recent Wolfe line search.
auto & curr() &
Access the current step state (mutable).
Eigen::VectorXd b
Workspace vector: b = W * theta + grad(log_lik)
WolfeInfo wolfe_info
Wolfe line search state including current/previous steps.
const auto & curr() const &
Access the current step state (const).
NewtonState(int theta_size, ObjFun &&obj_fun, ThetaGradFun &&theta_grad_f, CovarianceT &&covariance, ThetaInitializer &&theta_init)
Constructs Newton state with a consistent (a_init, theta_init) pair.
Eigen::VectorXd prev_g
Previous gradient for Barzilai-Borwein step calculation.
bool final_loop
On the final loop if we found a better wolfe step, but we are going to exit, we want to make sure all...
const auto & prev() const &
Access the previous step state (const).
Holds the state for the Newton-Raphson optimization loop.
Data used in current evaluation of wolfe line search at a particular stepsize.
Data object used in wolfe line search.
Struct to hold the result status of the Wolfe line search.
L_t L
Solver-dependent factorization of the system matrix B.
KRoot K_root
Lower Cholesky factor of the covariance matrix.
A_vec a
Mode in the a parameterization, where theta = covariance * a.
ThetaGrad theta_grad
Gradient of the log-likelihood with respect to theta at the mode.
laplace_density_estimates(double lmd_, ThetaVec &&theta_, WR &&W_r_, L_t &&L_, A_vec &&a_, ThetaGrad &&theta_grad_, LU_t &&LU_, KRoot &&K_root_, int solver_used_)
Options for Wolfe line search during optimization.
laplace_options(ThetaVec &&theta_0_, double tolerance_, int max_num_steps_, int hessian_block_size_, int solver_, int max_steps_line_search_, bool allow_fallthrough_)
double tolerance
Iterations end when the absolute change in the optimization objective is less than this tolerance.
int solver
Which linear solver to use inside the Newton step.
laplace_options_base(int hessian_block_size_, int solver_, double tolerance_, int max_num_steps_, bool allow_fallthrough_, int max_steps_line_search_)
Options for the Laplace approximation.