Skip to content

Commit 970e930

Browse files
Fix wiener_lpdf derivative with respect to w
Correct the reverse-mode derivative of wiener_lpdf with respect to the relative starting point parameter w. The previous implementation disagreed with finite differences of Stan Math's own scalar value function in the 5-parameter overload and in the full overload. The fix evaluates the w derivative in the same coordinate and branch convention as the density calculation. Add expect_ad regression tests for the failing 5-parameter case, an sv=0 control, the full sw>0, st0=0 case, and the existing full-Wiener rows. Update the stale full-Wiener row 4 w-gradient reference to the corrected finite-difference/fixed-AD value.
1 parent 27d5dc9 commit 970e930

3 files changed

Lines changed: 180 additions & 31 deletions

File tree

stan/math/prim/prob/wiener5_lpdf.hpp

Lines changed: 100 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -516,40 +516,110 @@ template <bool WrtLog = false, typename T_y, typename T_a, typename T_w,
516516
inline auto wiener5_grad_w(const T_y& y, const T_a& a, const T_v& v,
517517
const T_w& w, const T_sv& sv,
518518
T_err log_err = log(1e-12)) noexcept {
519-
const auto two_log_a = 2.0 * log(a);
520-
const auto log_y_asq = log(y) - two_log_a;
521-
const auto log_error_term = wiener5_compute_log_error_term(y, a, v, w, sv);
522-
const auto one_m_w = 1.0 - w;
519+
using ret_t = return_type_t<T_y, T_a, T_w, T_v, T_sv, T_err>;
520+
521+
const auto y_asq = y / square(a);
522+
const auto q = 1.0 - w;
523523
const auto sv_sqr = square(sv);
524524
const auto one_plus_svsqr_y = 1.0 + sv_sqr * y;
525-
const auto density_part_one
526-
= (v * a + sv_sqr * square(a) * one_m_w) / one_plus_svsqr_y;
527-
const auto log_error = (log_err - log_error_term);
528525

529-
const auto n_terms_small_t
530-
= wiener5_n_terms_small_t<GradientCalc::OFF, GradientCalc::ON>(y, a, w,
531-
log_error);
532-
const auto n_terms_large_t
533-
= wiener5_gradient_large_reaction_time_terms<GradientCalc::ON>(y, a, w,
534-
log_error);
535-
auto wiener_res = wiener5_log_sum_exp<GradientCalc::OFF, GradientCalc::ON>(
536-
y, a, w, n_terms_small_t, n_terms_large_t);
537-
auto&& result = wiener_res.first;
538-
auto&& newsign = wiener_res.second;
539-
const auto log_density = wiener5_density<GradientCalc::OFF>(
540-
y, a, v, w, sv, log_err - log(fabs(density_part_one)));
541-
if (2.0 * n_terms_small_t < n_terms_large_t) {
542-
auto ans = -(density_part_one
543-
- newsign
544-
* exp(result - (log_density - log_error_term)
545-
- 2.5 * log_y_asq - 0.5 * LOG_TWO - 0.5 * LOG_PI));
546-
return WrtLog ? ans * exp(log_density) : ans;
526+
const auto log_error_term = wiener5_compute_log_error_term(y, a, v, w, sv);
527+
const auto log_error = log_err - log_error_term;
528+
529+
// d/dw of
530+
//
531+
// -2 log(a)
532+
// - 0.5 log(1 + sv^2 y)
533+
// + [-v^2 y + 2 a v (1-w) + a^2 (1-w)^2 sv^2]
534+
// / [2 (1 + sv^2 y)].
535+
const auto pref_grad_w
536+
= (-a * v - square(a) * sv_sqr * q) / one_plus_svsqr_y;
537+
538+
// Use the density branch decision. The derivative is the derivative of
539+
// the same scalar value, so do not let the w-gradient path switch to a
540+
// different underconverged representation.
541+
const auto n_small_density
542+
= wiener5_n_terms_small_t<true, GradientCalc::OFF>(y, a, w, log_error);
543+
const auto n_large_density
544+
= wiener5_density_large_reaction_time_terms(y, a, w, log_error);
545+
546+
const auto n_small_grad
547+
= wiener5_n_terms_small_t<false, GradientCalc::ON>(y, a, w, log_error);
548+
const auto n_large_grad
549+
= wiener5_gradient_large_reaction_time_terms<GradientCalc::ON>(
550+
y, a, w, log_error);
551+
552+
const int n_small = static_cast<int>(
553+
fmax(value_of_rec(n_small_density), value_of_rec(n_small_grad)));
554+
const int n_large = static_cast<int>(
555+
fmax(value_of_rec(n_large_density), value_of_rec(n_large_grad)));
556+
557+
ret_t series_grad_w = 0.0;
558+
559+
if (2.0 * n_small_density <= n_large_density) {
560+
// Small-time representation.
561+
//
562+
// R_s = sum_{k=-K}^{K} z_k exp(-z_k^2 / (2 t*)),
563+
// z_k = 1 - w + 2k.
564+
//
565+
// dR_s/dw = sum_k (z_k^2 / t* - 1)
566+
// exp(-z_k^2 / (2 t*)).
567+
ret_t max_log = NEGATIVE_INFTY;
568+
569+
for (int k = -n_small; k <= n_small; ++k) {
570+
const double kd = static_cast<double>(k);
571+
const auto z = q + 2.0 * kd;
572+
const auto log_e = -square(z) / (2.0 * y_asq);
573+
max_log = fmax(max_log, log_e);
574+
}
575+
576+
ret_t raw = 0.0;
577+
ret_t draw_dw = 0.0;
578+
579+
for (int k = -n_small; k <= n_small; ++k) {
580+
const double kd = static_cast<double>(k);
581+
const auto z = q + 2.0 * kd;
582+
const auto e = exp(-square(z) / (2.0 * y_asq) - max_log);
583+
584+
raw += z * e;
585+
draw_dw += (square(z) / y_asq - 1.0) * e;
586+
}
587+
588+
series_grad_w = draw_dw / raw;
547589
} else {
548-
auto ans = -(
549-
density_part_one
550-
+ newsign
551-
* exp(result - (log_density - log_error_term) + 2.0 * LOG_PI));
552-
return WrtLog ? ans * exp(log_density) : ans;
590+
// Large-time representation in the same upper-bound coordinate used
591+
// by the density code: q = 1 - w.
592+
//
593+
// R_l = sum_{k=1}^{K}
594+
// k sin(k pi q)
595+
// exp(-(k^2 - 1) pi^2 t* / 2).
596+
//
597+
// dR_l/dw = -sum_{k=1}^{K}
598+
// k^2 pi cos(k pi q)
599+
// exp(-(k^2 - 1) pi^2 t* / 2).
600+
ret_t raw = 0.0;
601+
ret_t draw_dw = 0.0;
602+
603+
const auto half_pi2_y = 0.5 * square(pi()) * y_asq;
604+
605+
for (int k = 1; k <= n_large; ++k) {
606+
const double kd = static_cast<double>(k);
607+
const auto exp_term = exp(-(square(kd) - 1.0) * half_pi2_y);
608+
const auto angle = kd * pi() * q;
609+
610+
raw += kd * sin(angle) * exp_term;
611+
draw_dw += -square(kd) * pi() * cos(angle) * exp_term;
612+
}
613+
614+
series_grad_w = draw_dw / raw;
615+
}
616+
617+
const auto ans = pref_grad_w + series_grad_w;
618+
619+
if constexpr (WrtLog) {
620+
return ans * wiener5_density<true>(y, a, v, w, sv, log_err);
621+
} else {
622+
return ans;
553623
}
554624
}
555625

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#include <stan/math/mix.hpp>
2+
#include <test/unit/math/test_ad.hpp>
3+
#include <gtest/gtest.h>
4+
5+
TEST(MathMixProbWienerLpdf, fiveParamWGradientExpectAd) {
6+
auto f = [](const auto& w) {
7+
return stan::math::wiener_lpdf(6.0, 10.0, 0.01, w, -3.0, 0.2);
8+
};
9+
10+
stan::test::ad_tolerances tols;
11+
tols.gradient_grad_ = 1e-5;
12+
stan::test::expect_ad(tols, f, 0.1);
13+
}
14+
15+
TEST(MathMixProbWienerLpdf, fiveParamZeroSvWGradientExpectAd) {
16+
auto f = [](const auto& w) {
17+
return stan::math::wiener_lpdf(6.0, 10.0, 0.01, w, -3.0, 0.0);
18+
};
19+
20+
stan::test::ad_tolerances tols;
21+
tols.gradient_grad_ = 1e-5;
22+
stan::test::expect_ad(tols, f, 0.1);
23+
}
24+
25+
TEST(MathMixProbWienerLpdf, fullParamWGradientExpectAd) {
26+
auto f = [](const auto& w) {
27+
return stan::math::wiener_lpdf(6.0, 10.0, 0.01, w, -3.0, 0.2, 0.1,
28+
0.0);
29+
};
30+
31+
stan::test::ad_tolerances tols;
32+
tols.gradient_grad_ = 1e-5;
33+
stan::test::expect_ad(tols, f, 0.1);
34+
}
35+
36+
TEST(MathMixProbWienerLpdf, existingFullRowsWGradientExpectAd) {
37+
struct Case {
38+
const char* name;
39+
double y;
40+
double a;
41+
double t0;
42+
double w;
43+
double v;
44+
double sv;
45+
double sw;
46+
double st0;
47+
};
48+
49+
const std::vector<Case> cases = {
50+
{"row_0", 2.0, 2.0, 1e-9, 0.10, 2.0, 0.0, 0.00, 0.000},
51+
{"row_1", 3.0, 2.0, 0.01, 0.50, 2.0, 0.2, 0.00, 0.000},
52+
{"row_2", 4.0, 10.0, 0.01, 0.80, 4.0, 0.0, 0.10, 0.000},
53+
{"row_3", 5.0, 4.0, 0.01, 0.70, 3.0, 0.0, 0.00, 0.007},
54+
{"row_4", 6.0, 10.0, 0.01, 0.10, -3.0, 0.2, 0.10, 0.000},
55+
{"row_5", 7.0, 1.0, 0.01, 0.90, 1.0, 0.2, 0.00, 0.007},
56+
{"row_6", 8.0, 3.0, 0.01, 0.70, -1.0, 0.0, 0.10, 0.007},
57+
{"row_7", 8.85, 1.7, 0.01, 0.92, -7.3, 0.7, 0.01, 0.009},
58+
{"row_8", 8.9, 2.4, 0.01, 0.90, -4.9, 0.0, 0.00, 0.009},
59+
{"row_9", 9.0, 11.0, 0.01, 0.12, 4.5, 0.7, 0.10, 0.009},
60+
{"row_10", 1.0, 1.5, 0.10, 0.50, 3.0, 0.5, 0.20, 0.000},
61+
};
62+
63+
stan::test::ad_tolerances tols;
64+
tols.gradient_grad_ = 1e-4;
65+
66+
for (const auto& c : cases) {
67+
SCOPED_TRACE(c.name);
68+
69+
auto f = [c](const auto& w) {
70+
return stan::math::wiener_lpdf(c.y, c.a, c.t0, w, c.v, c.sv, c.sw,
71+
c.st0);
72+
};
73+
74+
// The row sweep is intended to check the reverse-mode w adjoint against
75+
// finite differences. Some full-Wiener rows are not stable enough for
76+
// higher-order mixed-mode finite-difference checks
77+
stan::test::expect_ad<true>(tols, f, c.w);
78+
}
79+
}

test/unit/math/prim/prob/wiener_full_lpdf_test.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ TEST(mathPrimCorrectValues, wiener_lpdf) {
320320
12.8617364931501, 1.12047317491985, 5.68799957241344};
321321
std::vector<double> true_grad_w
322322
= {5.67120184517318, -3.64396221090076, -38.7775057146792,
323-
-14.1837930137393, -34.5869239580708, -10.4535345681946,
323+
-14.1837930137393, 35.71918681520359, -10.4535345681946,
324324
0.679597983582904, -9.93144540834201, 2.09117200953597,
325325
-6.0858540417876, -3.74870310978083};
326326
std::vector<double> true_grad_v = {

0 commit comments

Comments
 (0)