EpiNow2 Stan Functions
R_to_r.hpp
Go to the documentation of this file.
1#ifndef EPINOW2_R_TO_R_HPP
2#define EPINOW2_R_TO_R_HPP
3
4/**
5 * Growth rate from the reproduction number with a reverse-mode adjoint.
6 *
7 * Backs the Stan function `R_to_r(R, gt_rev_pmf, abs_tol)` declared in
8 * inst/stan/functions/rt.stan.
9 *
10 * Notation: p is the generation time PMF, the reverse of gt_rev_pmf, with
11 * p_k the weight of a gap of k = 0, ..., G - 1 days. The growth rate r is
12 * the root of
13 * f(r; R, p) = R sum_k p_k exp(-r k) - 1.
14 *
15 * Forward. Newton's method on doubles. Start at
16 * r = fmax((R - 1) / (R m), -1), with m = sum_k k p_k the mean generation
17 * time, and take steps f / f_r until a step is no larger than abs_tol in
18 * absolute value or 100 steps have been taken. The value
19 * returned is r after the last step. f is convex and decreasing in r, so
20 * from the first step on Newton approaches the root from one side and
21 * converges quadratically; the cap only guards against pathological inputs.
22 * This code is based on Julia code from
23 * https://github.com/CDCgov/Rt-without-renewal/blob/d6344cc6e451e3e6c4188e4984247f890ae60795/EpiAware/test/predictive_checking/fast_approx_for_r.jl
24 * under Apache license 2.0.
25 *
26 * Reverse. By the implicit function theorem, at the returned r
27 * dr/dR = -f_R / f_r = S0 / (R S1),
28 * dr/dp_k = -f_p_k / f_r = exp(-r k) / S1,
29 * with S0 = sum_k p_k exp(-r k), S1 = sum_k k p_k exp(-r k) and
30 * f_r = -R S1. So rbar is passed back as Rbar += rbar S0 / (R S1) and
31 * gt_rev_pmfbar[G - 1 - k] += rbar exp(-r k) / S1.
32 *
33 * This is the gradient of the root, which does not require recording the
34 * Newton steps on the autodiff tape. It agrees with the gradient through
35 * the steps to within the solver tolerance.
36 *
37 * Either of R and gt_rev_pmf may be var or double; only the gradients of
38 * var inputs are computed. As with convolve_with_rev_pmf(), the function
39 * lives in namespace epinow2 and is made visible in stan::math.
40 */
41
42#include <stan/math.hpp>
43#include <cmath>
44#include <ostream>
45
46namespace epinow2 {
47namespace internal {
48
49/**
50 * Sums used by the Newton step and the gradient.
51 *
52 * Computes S0 = sum_k p_k exp(-r k) and S1 = sum_k k p_k exp(-r k), with
53 * p_k = g[G - 1 - k].
54 *
55 * @param r Growth rate.
56 * @param g Reversed generation time PMF.
57 * @param s0 Set to S0.
58 * @param s1 Set to S1.
59 * @param e If not null, set to exp(-r k) in e[G - 1 - k].
60 */
61inline void R_to_r_sums(double r, const Eigen::VectorXd& g, double& s0,
62 double& s1, Eigen::VectorXd* e = nullptr) {
63 const int G = g.size();
64 s0 = 0.0;
65 s1 = 0.0;
66 for (int k = 0; k < G; ++k) {
67 const double p = g(G - 1 - k);
68 const double ek = std::exp(-r * k);
69 s0 += p * ek;
70 s1 += p * k * ek;
71 if (e) {
72 (*e)(G - 1 - k) = ek;
73 }
74 }
75}
76
77/** Maximum number of Newton steps taken by R_to_r_newton(). */
78constexpr int R_to_r_max_steps = 100;
79
80/**
81 * Solve for the growth rate by Newton's method on doubles.
82 *
83 * @param R Reproduction number.
84 * @param g Reversed generation time PMF.
85 * @param abs_tol Stop once a step is no larger than this in absolute value.
86 * @return The growth rate after the last step.
87 */
88inline double R_to_r_newton(double R, const Eigen::VectorXd& g,
89 double abs_tol) {
90 const int G = g.size();
91 double mean_gt = 0.0;
92 for (int k = 0; k < G; ++k) {
93 mean_gt += g(G - 1 - k) * k;
94 }
95 double r = std::fmax((R - 1) / (R * mean_gt), -1.0);
96 double step = abs_tol + 1;
97 for (int i = 0; i < R_to_r_max_steps && std::abs(step) > abs_tol; ++i) {
98 double s0, s1;
99 R_to_r_sums(r, g, s0, s1);
100 step = (R * s0 - 1) / (-R * s1);
101 r -= step;
102 }
103 return r;
104}
105
106} // namespace internal
107
108/**
109 * Estimate the growth rate r from the reproduction number R.
110 *
111 * The last argument is the output stream of the Stan calling convention,
112 * which is not used.
113 *
114 * @param R Reproduction number (var or double).
115 * @param gt_rev_pmf Reversed generation time PMF (var or double).
116 * @param abs_tol Absolute tolerance of the Newton solver (double).
117 * @return The growth rate r.
118 */
119template <typename T0, typename T1,
120 stan::require_stan_scalar_t<T0>* = nullptr,
121 stan::require_eigen_col_vector_t<T1>* = nullptr>
122inline stan::return_type_t<T0, T1> R_to_r(const T0& R, const T1& gt_rev_pmf,
123 const double& abs_tol,
124 std::ostream* /* pstream__ */) {
125 using stan::arena_t;
126 using stan::math::value_of;
127 using stan::math::var;
128 constexpr bool R_var = stan::is_var<T0>::value;
129 constexpr bool g_var = stan::is_var<stan::value_type_t<T1>>::value;
130 const double R_d = value_of(R);
131 if constexpr (!R_var && !g_var) {
132 return internal::R_to_r_newton(R_d, value_of(gt_rev_pmf), abs_tol);
133 } else {
134 arena_t<Eigen::Matrix<stan::value_type_t<T1>, -1, 1>> g_a = gt_rev_pmf;
135 arena_t<Eigen::VectorXd> g_val = value_of(g_a);
136 const double r = internal::R_to_r_newton(R_d, g_val, abs_tol);
137 var res(r);
138 T0 R_v = R;
139 stan::math::reverse_pass_callback([=]() mutable {
140 const double rbar = res.adj();
141 double s0, s1;
142 Eigen::VectorXd e(g_var ? g_val.size() : 0);
143 internal::R_to_r_sums(r, g_val, s0, s1, g_var ? &e : nullptr);
144 if constexpr (R_var) {
145 R_v.adj() += rbar * s0 / (R_d * s1);
146 }
147 if constexpr (g_var) {
148 g_a.adj() += (rbar / s1) * e;
149 }
150 });
151 return res;
152 }
153}
154
155} // namespace epinow2
156
157namespace stan {
158namespace math {
159using ::epinow2::R_to_r;
160} // namespace math
161} // namespace stan
162
163#endif
void R_to_r_sums(double r, const Eigen::VectorXd &g, double &s0, double &s1, Eigen::VectorXd *e=nullptr)
Definition R_to_r.hpp:61
double R_to_r_newton(double R, const Eigen::VectorXd &g, double abs_tol)
Definition R_to_r.hpp:88
constexpr int R_to_r_max_steps
Definition R_to_r.hpp:78
stan::return_type_t< T0, T1 > R_to_r(const T0 &R, const T1 &gt_rev_pmf, const double &abs_tol, std::ostream *)
Definition R_to_r.hpp:122