Repository navigation
Conversation
This was referenced Oct 13, 2026
SteveBronder
added this pull request to stack #3458
October 14, 2026 19:39
SteveBronder
self-requested a review
October 14, 2026 19:39
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3218.
@jachymb pointed out the fwd
tanhissue in the logistic distribution PR #3398. I checked that in addition tologistic_lpdf,tanhis used bycholesky_factor_corrandcorr_matrixviacorr_constrain(no other Stan Math function callstanh. User code that callstanhgets the fix too).This PR repairs the cancellations in the forward-mode derivative of
tanhand in the transforms ofcholesky_factor_corrandcorr_matrix(values and log Jacobians). It fixes the initialization failure ofcholesky_factor_corr[K]for K >= 40 reported in #3218, and adds the functionlog_cosh. The fix forcholesky_factor_corrandcorr_matrixhas to be made at the same time as the fix fortanh: their old forward-mode Jacobian gradient was correct only because the cancellation errors oftanhand of the Jacobian divided out. #3398 (logistic) is based on this PR and gets thetanhfix without further changes.This PR was assisted by Claude.
Summary
Forward-mode
tanh.stan/math/fwd/fun/tanh.hppcomputes the derivative as1 - u * uwithu = tanh(x). The difference cancels: against mpmath the error is 5.7e3 ulp for|x|from 1 to 5, and the derivative is exactly 0 from|x| = 19.06(for example, the second derivative oftanh(20)infvar<fvar<double>>is 0; the exact value is -3.4e-17). This PR keeps1 - u * ufor|x| < 0.5, where it has an error of at most 1.4 ulp, and uses4 e / (1 + e)^2withe = exp(-2 |x|)above. The result has an error of at most 3.8 ulp for|x|up to 350, and the higher derivatives are correct down to underflow infvar<fvar<double>>andfvar<var>. Two other forms were rejected:1 / cosh(x)^2, which the reverse-modetanhuses, givesNaNsecond derivatives from|x| = 360infvar<fvar<double>>and from|x| = 720infvar<var>: the quotient rule evaluates0 * infoncecosh(x)^2overflows.4 e / (1 + e)^2for allx, withfabs(x): the derivative offabsat exactly 0 is 0 in Stan, so the third derivative oftanhat 0 was 0 instead of -2 infvar<fvar<var>>.The correlation transforms. Both transforms form partial correlations
z = tanh(y)and then1 - z^2and1 - sum_sqs, in the values (L(i, j) = z sqrt(1 - sum_sqs),L(i, i) = sqrt(1 - sum_sqs)) and in the log Jacobian (log1m(square(z)),0.5 * log1m(sum_sqs), and theread_corr_Lterm(K - k - 1) * log1m(square(z))). These differences cancel in two cases:|y| = 10, 1.7e-4 at 15, 0.044 at 18, and is-inffrom 19.1 (from 18 withvar_value, whosetanhis Eigen's vectorizedtanh);1 - sum_sqsis the product of all1 - z^2in the row, so it cancels also with moderatey. With Stan's default inits (yuniform in [-2, 2]), a diagonal element ofLrounds to 0 in 6 % of the inits at K = 30, 98 % at K = 40 and all at K = 64;lkj_corr_cholesky_lpdfrejects such anL, so initialization fails (cholesky_factor_corr[n]reliably fails to initialize (from [-2,+2]) for "moderate"n#3218).The repair.
1 - tanh(y)^2 = exp(-2 log(cosh(y))), so afterjelements of a row,1 - sum_sqs = exp(-2 a)withathe sum oflog(cosh(y))over these elements. This PR computes the Cholesky factor asL(i, j) = tanh(y_j) exp(-a)andL(i, i) = exp(-a)over the whole row,without a subtraction (for
corr_matrix_constrainthe same in the column order ofread_corr_L, thenOmega = L L'), and the log Jacobian as-sum_n c_n log(cosh(y_n))with integer coefficients:c = 2forcorr_constrain,c = i + 1 - jfor element(i, j)ofcholesky_corr_constrain,c = K - jfor columnjofcorr_matrix_constrain(equation 11 of Lewandowski, Kurowicka and Joe, 2009). A test checkslpagainstlog |det J|of the autodiff Jacobian of the map fromyto the strictly lower triangle (agreement 1.8e-15 at K = 4). Thevar_valueoverloads have a new hand-written reverse pass; in rowi,L(i, j)depends on its ownywith derivativesech(y)^2 exp(-a)and every later element of the row on it throughexp(-a)with derivative-tanh(y)times the element. Its gradient equals theEigen::Matrix<var, ...>gradient to 2e-16 relative up to K = 128.log_cosh. New function inprim,fwdandrev. For|x| < 1it usesu = expm1(|x|),log1p(u^2 / (2 (1 + u)))andtanh(|x|) = u (u + 2) / (u (u + 2) + 2); otherwiset = expm1(-2 |x|),|x| + log1p(t / 2)andtanh(|x|) = -t / (2 + t). Against mpmath on 70 002 points from 1e-150 to 400: at most 2.4 ulp for the value and 1.7 ulp for the derivative.prim/constraint/cholesky_corr_constrain.hpprev/constraint/cholesky_corr_constrain.hppprim/constraint/corr_matrix_constrain.hpprev/constraint/corr_matrix_constrain.hppprim/constraint/corr_constrain.hppprim/fun/log_cosh.hpp(new)rev/fun/log_cosh.hpp(new)fwd/fun/log_cosh.hpp(new)fwd/fun/tanh.hppIn addition
prim/fun.hpp,fwd/fun.hppandrev/fun.hppget one include line each. Tests add 536 lines and change 1.Results
Accuracy of the values (K = 4,
y = (15, 12, -0.5, 9, 2, -14), against mpmath): every element ofLwithin 1.0e-15 relative (develop: up to 3.8e-4 relative, fromsqrt(1 - z^2)of a roundedz); the correlations ofcorr_matrix_constrainwithin 1.1e-16 absolute (develop: 2e-10). At K = 40 and 128 with default-like inits, the rows ofLhave unit norm within 7e-16 and the smallest diagonal elements are 8e-11 and 3e-33, where develop gives 0.Prior-only check with HMC in CmdStan 2.40.0:
L ~ lkj_corr_cholesky(eta)(cholesky_factor_corr[K]) andOmega ~ lkj_corr(eta)(corr_matrix[K]). Under LKJ(eta),(rho + 1) / 2 ~ Beta(a, a)witha = eta - 1 + K / 2for every correlationrho, soE[rho^2] = 1 / (2a + 1)is known exactly;z = (E[rho^2] - exact) / MCSEper seed.Initialization of
cholesky_factor_corr[K](eta = 1 and 2, 10 seeds, 4 chains, 40 chains per row; chains that failed / mean number of rejected initial values):"Value fix only" failures at K = 64 and 128 come from develop's Jacobian terms, which are
-infthere. "Jacobian fix only" failures come fromL(i, i)rounding to 0, whichlkj_corr_cholesky_lpdfrejects. Both fixes are needed. Chains that start, sample correctly in all versions.Sampling, eta = 0.1 to 2, K = 2, 5, 10, 20 seeds per setting (the same for all versions), 4 chains of 1000 warmup and 4000 draws; totals over K and seeds (240 000 draws per row and band): divergences after warmup / largest per seed / seeds with
|z| > 3:cholesky_factor_corrcholesky_factor_corrcholesky_factor_corrcorr_matrixcorr_matrixcorr_matrixWith this PR the share of
cholesky_factor_corrdraws at|y| > 15matches its prior probability (0.0504 to 0.0508 against 0.0505 at eta = 0.1, 0.0112 to 0.0117 against 0.0115 at eta = 0.15); a prototype of the same transform in the Stan language, which keepsy, reached|y|up to 65. With develop the chains stop near|y| = 18.7. Forcorr_matrixthis PR changes nothing measurable:lkj_corr_lpdfworks withOmegaitself, and its positive-definiteness check rejectsOmegaonce the smallest eigenvalue is below what double precision resolves (about 1e-20 at a K = 40 init), whatever the accuracy ofL. The MCSE is from batch means and is not reliable in runs with many divergences.For usual priors with eta >= 1, there is no practical difference in HMC behavior, but the fix for initialization helps.
Time per call, on one whole node of AMD EPYC 9965 (g++ 11.5,
-O3), median of 5 interleaved rounds (the spread between rounds of one binary is at most 9 %, mostly below 3 %). Reverse-mode kernels compute the gradient oflp + sum(L)(oflp + lkj_corr_cholesky_lpdf(L, 2)in thelkjrow); the forward-mode kernels compute one directional derivative. The middle column is this PR without the value fix.corr_constrainwithlp,var, 45 valuescholesky_corr_constrain,var, K = 5cholesky_corr_constrain,var, K = 10cholesky_corr_constrain,var, K = 25cholesky_corr_constrain,var_value, K = 10cholesky_corr_constrain,var_value, K = 25corr_matrix_constrain,var, K = 5corr_matrix_constrain,var, K = 10corr_matrix_constrain,var_value, K = 10cholesky_corr_constrain+lkj_corr_cholesky_lpdf,var, K = 10cholesky_corr_constrain,fvar<double>, K = 10corr_matrix_constrain,fvar<double>, K = 10Reasons for the changes:
Eigen::Matrix<var, ...>arguments use the genericprimcode. The Jacobian fix alone makes these transforms 9 % to 14 % faster, because the Jacobian becomes onelog_coshnode per element and adot_product, instead of thelog1m(square(z))and0.5 * log1m(sum_sqs)nodes. The value fix computestanh(y),exp(-a)andlog(cosh(y))per element instead oftanh(y)andsqrt(1 - sum_sqs), which costs more than the Jacobian fix saves at small K; the net change is -3 % to +13 %.var_valuearguments use the new hand-written reverse pass: the forward pass needs oneexpm1orexpper element (tanh(y)andsech(y)together, andexp(-a)as a running product ofsech(y)), and the reverse pass has no transcendental function. Develop evaluated a vectorizedtanhwith its own reverse pass andsqrt(1 - sum_sqs). The result is 11 % to 21 % faster than develop.corr_constrainalone (no value change) is slower becauselog_coshcosts anexpm1and alog1pper element, againstlog1m(square(tanh(y)))from the already computedtanh.fvar<double>arguments are slower because the forward-modetanhderivative needs anexpabove|y| = 0.5, and the value fix addsexp(-a)andlog_cosh(which also evaluatestanh) per element. At K = 10 the times are below 5 us, so this is unlikely to dominate in a model; forward mode is used in nested autodiff only.Limits that remain
corr_matrixat large K: as above, the initialization and small-eta failures come from the positive-definiteness check ofOmegainlkj_corr_lpdf.cholesky_factor_corrwithlkj_corr_choleskyis the type to use for large K.read_corr_L(CPCs, K, lp)andread_corr_matrixkeep their formulas, because they receive only the partial correlations;corr_matrix_constrainno longer calls them.cov_matrix_constrain_lkj, which stanc3 does not emit, still uses them.cholesky_corr_freeandcorr_matrix_freeare unchanged.Tests
test/unit/math/mix/fun/tanh_test.cpp: first and second derivatives atx = 3, ±20, 300, 1000infvar<double>,fvar<fvar<double>>andfvar<var>, and first to third derivatives infvar<fvar<var>>atx = 0, 0.25, 0.4999, ±0.5, 3, -20, against mpmath.test/unit/math/mix/fun/log_cosh_test.cpp(new):expect_adfor scalars, containers andvar_value; values and derivatives against mpmath on both sides of the branch, at1e-10and at 800.test/unit/math/mix/constraint/{corr_constrain,cholesky_corr_constrain,corr_matrix_constrain}_test.cpp:lpand its gradient at largeyagainst mpmath indouble,var,var_valueandfvar<double>;lpagainstlog |det J|; the values ofLandOmegaat largeyagainst mpmath in all four modes; K = 40 with default-like inits (positive diagonal, unit rows, finitelpandlkj_corr_cholesky_lpdf,var_valuegradient equal to theEigen::Matrix<var, ...>gradient).test/unit/math/rev/prob/lkj_corr_test.cpp:ProbDistributionsLkjCorrCholesky_gradients1compares finite differences with autodiff at an input with an exact 0, where the exact derivative is 0; autodiff now returns exactly 0 and the finite difference has rounding noise of 1.2e-13, so the comparison uses an absolute tolerance of 1e-10 instead of float equality.tanhtests and the large-argument tests fail; with the headers before the value repair, the three value tests (cholesky_corr_constrain_values_large_argument,cholesky_corr_constrain_long_rows,corr_matrix_constrain_values_large_argument) fail.tanh, the three transforms,cov_matrix_constrain_lkj,read_corr_L,read_corr_matrix,read_cov_Landlkj_corr(prim, fwd, rev, mix; 19 suites) pass.Tested with develop
43bc5684a3, the libraries of the 2.40.0 release (Eigen 5.0.1, Boost 1.87.0), g++ 11.5.Side Effects
lp__and the values ofcholesky_factor_corrandcorr_matrixparameters change in the last digits for moderate unconstrained values. Models withcholesky_factor_corr[K]for K >= 40 now initialize with default inits.Release notes
Improved the accuracy of the forward-mode derivative of
tanhand of thecholesky_factor_corrandcorr_matrixtransforms. Addedlog_cosh.Checklist
Copyright holder: Aalto University
The copyright holder is typically you or your assignee, such as a university or company. By submitting this pull request, the copyright holder is agreeing to the license the submitted work under the following licenses:
- Code: BSD 3-clause (https://opensource.org/licenses/BSD-3-Clause)
- Documentation: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
the basic tests are passing
./runTests.py test/unit)make test-headers)make test-math-dependencies)make doxygen)make cpplint)the code is written in idiomatic C++ and changes are documented in the doxygen
the new changes are tested