@@ -481,4 +481,95 @@ TEST_F(AgradRev, StanMath_integrate_1d_gk_rev_TestUniform) {
481481 EXPECT_FLOAT_EQ (1 , 1 + g[1 ]);
482482}
483483
484- } // namespace integrate_1d_gk_test
484+ // ── gradient-integrand shift (integrate_1d_adjoint<true>) ────────────
485+ //
486+ // The wrapper integrates (d f / d theta_i + f) and subtracts the
487+ // value integral. These tests pin (a) the evaluation count on such a
488+ // component, which the accuracy tests above cannot see, and (b) the
489+ // guard paths: an integral that is exactly zero (shift disabled) and
490+ // a negative integral (shift active with I < 0).
491+
492+ long n_evals = 0 ;
493+
494+ // d f / d theta is analytically zero (cos^2 + sin^2 = 1) but autodiff
495+ // evaluates it as round-off noise of order 1e-16 * x * f.
496+ struct f_noisy_gradient_counted {
497+ template <typename T1 , typename T2 , typename T3 >
498+ inline stan::return_type_t <T1 , T2 , T3 > operator ()(
499+ const T1 &x, const T2 &xc, std::ostream *msgs,
500+ const std::vector<T3 > &theta, const std::vector<double > &x_r,
501+ const std::vector<int > &x_i) const {
502+ ++n_evals;
503+ auto tx = theta[0 ] * x;
504+ return exp (-x * x) * (cos (tx) * cos (tx) + sin (tx) * sin (tx));
505+ }
506+ };
507+
508+ TEST_F (AgradRev, StanMath_integrate_1d_gk_rev_GradientShift_noisy_gradient) {
509+ using stan::math::var;
510+ const double I_ref = std::sqrt (stan::math::pi ()) * std::erf (1.0 );
511+ std::vector<var> theta = {2.5 };
512+ n_evals = 0 ;
513+ var I = stan::math::integrate_1d_gauss_kronrod_tol (
514+ f_noisy_gradient_counted{}, -1.0 , 1.0 , 1e-6 , 0.0 , 15 , msgs, theta,
515+ std::vector<double >{}, std::vector<int >{});
516+ std::vector<double > g;
517+ I.grad (theta, g);
518+ EXPECT_NEAR (I_ref, I.val (), 1e-6 );
519+ EXPECT_NEAR (0.0 , g[0 ], 1e-6 );
520+ // Value + one gradient component. Without the shift the gradient
521+ // integral hits max_depth (2^15 * 21 ≈ 6.9e5 evaluations).
522+ EXPECT_LT (n_evals, 5000L );
523+ }
524+
525+ struct f_odd {
526+ template <typename T1 , typename T2 , typename T3 >
527+ inline stan::return_type_t <T1 , T2 , T3 > operator ()(
528+ const T1 &x, const T2 &xc, std::ostream *msgs,
529+ const std::vector<T3 > &theta, const std::vector<double > &x_r,
530+ const std::vector<int > &x_i) const {
531+ return theta[0 ] * x * exp (-x * x); // odd: integral over [-1, 1] is 0
532+ }
533+ };
534+
535+ struct f_negative {
536+ template <typename T1 , typename T2 , typename T3 >
537+ inline stan::return_type_t <T1 , T2 , T3 > operator ()(
538+ const T1 &x, const T2 &xc, std::ostream *msgs,
539+ const std::vector<T3 > &theta, const std::vector<double > &x_r,
540+ const std::vector<int > &x_i) const {
541+ return -theta[0 ] * exp (-x * x); // negative everywhere, I < 0
542+ }
543+ };
544+
545+ TEST_F (AgradRev, StanMath_integrate_1d_gk_rev_GradientShift_guards) {
546+ using stan::math::var;
547+ // (a) exactly-zero integral: the shift is disabled and the gradient is
548+ // the (zero) integral of x exp(-x^2).
549+ {
550+ std::vector<var> theta = {2.5 };
551+ var I;
552+ EXPECT_NO_THROW (I = stan::math::integrate_1d_gauss_kronrod_tol (
553+ f_odd{}, -1.0 , 1.0 , 1e-6 , 0.0 , 15 , msgs, theta,
554+ std::vector<double >{}, std::vector<int >{}));
555+ std::vector<double > g;
556+ I.grad (theta, g);
557+ EXPECT_NEAR (0.0 , I.val (), 1e-8 );
558+ EXPECT_NEAR (0.0 , g[0 ], 1e-8 );
559+ }
560+ // (b) negative integral: shift active with I < 0; d I / d theta = I / theta.
561+ {
562+ const double th = 2.5 ;
563+ const double I_ref = -th * std::sqrt (stan::math::pi ()) * std::erf (1.0 );
564+ std::vector<var> theta = {th};
565+ var I = stan::math::integrate_1d_gauss_kronrod_tol (
566+ f_negative{}, -1.0 , 1.0 , 1e-8 , 0.0 , 15 , msgs, theta,
567+ std::vector<double >{}, std::vector<int >{});
568+ std::vector<double > g;
569+ I.grad (theta, g);
570+ EXPECT_NEAR (I_ref, I.val (), 1e-7 );
571+ EXPECT_NEAR (I_ref / th, g[0 ], 1e-7 );
572+ }
573+ }
574+
575+ }
0 commit comments