You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Make normal_lcdf(x | 0, 1) equivalent to Phi() #2470
Since #1411normal_lcdf is more robust in the tails than Phi or even Phi_approx. Update Phi() to be as robust as normal_lcdf and add log_Phi() as discussed in the linked PR above (should this be a separate issue?)
It looks while log(Phi(x)) does not perform as well as std_normal_lcdf(x), there is only a marginal difference between Phi(x) and exp(std_normal_lcdf(x)). Also, log(Phi_approx(x)) performs better (but not equivalently) when log_inv_logit is used.
Testing
Model functions
funs="functions { real log_phi_old(real x) { return log(Phi(x)); } real log_phi_new(real x) { return std_normal_lcdf(x|); } real log_phi_approx_old(real x) { return log(Phi_approx(x)); } real log_phi_approx_new(real x) { return log_inv_logit(0.07056 * pow(x, 3.0) + 1.5976 * x); } real phi_old(real x) { return Phi(x); } real phi_new(real x) { return exp(std_normal_lcdf(x|)); } real phi_approx_old(real x) { return Phi_approx(x); } real phi_approx_new(real x) { return exp(log_inv_logit(0.07056 * pow(x, 3.0) + 1.5976 * x)); }}"rstan::expose_stan_functions(rstan::stanc(model_code=funs))
I'm not sure if there's a way to get log(Phi(x)) to replicate the precision of normal_lcdf(x|0,1). The precision is achieved through analytic approximations of the log function. Using log() severely reduces the precision and leads to faster overflow to 0. A log_Phi() function definitely makes sense though, as does updating normal_lccdf().
Also, I just happened to check the function reference doc and it still quotes the old values for underflow / overflow (-37.5 and 8.25 respectively) so this should probably be updated too.
I checked the three requests in this issue against mpmath, on develop with PR #3406.
Phi as robust as normal_lcdf. With Expanded numerical stability and performance for std_normal_lcdf, reused across distributions & functions #3406, Phi has no cutoffs. Before, it returned 0 below -37.5 and 1 above 8.25. Its relative error for z in [-37.5, 8] went from 4e-11 (at z = -5) to 4e-16. On a grid from -40 to 40 with step 1/64, there is now no point where one of Phi(z) and exp(std_normal_lcdf(z)) is 0 and the other is not. Before there were 63, all between -38.5 and -37.5. In the deep lower tail Phi is the more accurate of the two, because exp turns the error of a log value near -600 into a relative error of about 2e-13.
The upper tail cannot be fixed in Phi. Above z = 8.30, Phi(z) rounds to 1 in double precision, so log(Phi(z)) is 0 there with any implementation of Phi. Only std_normal_lcdf keeps that information, or the complement std_normal_lccdf and Phi(-z).
log_Phi(). This is still missing. std_normal_lcdf(x) gives log Phi for a scalar, but for a vector it returns the sum, so Stan has no elementwise log Phi. The copula use case in Phi_approx is not as robust in the tails as normal_lcdf docs#352 would benefit from one. The inverse already exists as the elementwise std_normal_log_qf. Following log_inv_logit and log1m_inv_logit, the new functions would be log_Phi(T x) and log1m_Phi(T x). After Expanded numerical stability and performance for std_normal_lcdf, reused across distributions & functions #3406 the Stan Math part is small, because internal::std_normal_lcdf_value_grad already computes the elementwise values and slopes, and the OpenCL device functions exist. The functions also need a stanc3 signature and a docs entry. I can make the Stan Math PR if you want these functions.
Activity
andrjohns commentedon Apr 16, 2021
It looks while
log(Phi(x))does not perform as well asstd_normal_lcdf(x), there is only a marginal difference betweenPhi(x)andexp(std_normal_lcdf(x)). Also,log(Phi_approx(x))performs better (but not equivalently) whenlog_inv_logitis used.Testing
Model functions
log(Phi(x))Results:Phi(x)Results:bbbales2 commentedon Apr 16, 2021
Also normal_lccdf should be updated too -- there might be a separate issue for that. Tnx for looking at this.
PhilClemson commentedon Apr 19, 2021
I'm not sure if there's a way to get
log(Phi(x))to replicate the precision ofnormal_lcdf(x|0,1). The precision is achieved through analytic approximations of the log function. Usinglog()severely reduces the precision and leads to faster overflow to 0. Alog_Phi()function definitely makes sense though, as does updatingnormal_lccdf().Also, I just happened to check the function reference doc and it still quotes the old values for underflow / overflow (-37.5 and 8.25 respectively) so this should probably be updated too.
spinkney commentedon Apr 19, 2021
Here is the docs issue :) stan-dev/docs#352
avehtari commentedon Oct 23, 2026
I checked the three requests in this issue against mpmath, on
developwith PR #3406.Phias robust asnormal_lcdf. With Expanded numerical stability and performance forstd_normal_lcdf, reused across distributions & functions #3406,Phihas no cutoffs. Before, it returned 0 below -37.5 and 1 above 8.25. Its relative error for z in [-37.5, 8] went from 4e-11 (at z = -5) to 4e-16. On a grid from -40 to 40 with step 1/64, there is now no point where one ofPhi(z)andexp(std_normal_lcdf(z))is 0 and the other is not. Before there were 63, all between -38.5 and -37.5. In the deep lower tailPhiis the more accurate of the two, becauseexpturns the error of a log value near -600 into a relative error of about 2e-13.The upper tail cannot be fixed in
Phi. Above z = 8.30,Phi(z)rounds to 1 in double precision, solog(Phi(z))is 0 there with any implementation ofPhi. Onlystd_normal_lcdfkeeps that information, or the complementstd_normal_lccdfandPhi(-z).log_Phi(). This is still missing.std_normal_lcdf(x)gives log Phi for a scalar, but for a vector it returns the sum, so Stan has no elementwise log Phi. The copula use case in Phi_approx is not as robust in the tails as normal_lcdf docs#352 would benefit from one. The inverse already exists as the elementwisestd_normal_log_qf. Followinglog_inv_logitandlog1m_inv_logit, the new functions would belog_Phi(T x)andlog1m_Phi(T x). After Expanded numerical stability and performance forstd_normal_lcdf, reused across distributions & functions #3406 the Stan Math part is small, becauseinternal::std_normal_lcdf_value_gradalready computes the elementwise values and slopes, and the OpenCL device functions exist. The functions also need a stanc3 signature and a docs entry. I can make the Stan Math PR if you want these functions.normal_lccdf. Aligned normal_lccdf and std_normal_lccdf with the lcdf functions #3363 fixed this (merged 2026-08-30) and closed Accuracy of normal_lccdf much worse than normal_lcdf #1985.The functions reference still gave the old limits and said that
Phi_approxis more robust in the tails. stan-dev/docs#990 corrects both.