Skip to content

Aligned normal_lccdf and std_normal_lccdf with the lcdf functions - #3363

Open
jaburgoyne wants to merge 7 commits into
stan-dev:developfrom
jaburgoyne:fix/normal_lccdf
Open

Aligned normal_lccdf and std_normal_lccdf with the lcdf functions#3363
jaburgoyne wants to merge 7 commits into
stan-dev:developfrom
jaburgoyne:fix/normal_lccdf

Conversation

@jaburgoyne

@jaburgoyne jaburgoyne commented Aug 21, 2026

Copy link
Copy Markdown

Summary

This pull request addresses #1985 by updating normal_lccdf and std_normal_lccdf to call normal_lcdf and std_normal_lcdf with the signs of x and mu reversed.

Tests

The tests have been updated to use the latest values of pnorm in R 4.6.1. These are more direct tests than the previous versions, which worked via an exp call, somewhat defeating the purpose of having separate lcdf and lccdf functions.

Side Effects

None.

Release notes

normal_lccdf and std_normal_lccdf updated to use normal_lcdf and std_normal_lcdf implementations

Checklist

  • Copyright holder: John Ashley Burgoyne

    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

    • unit tests pass (to run, use: ./runTests.py test/unit)
    • header checks pass, (make test-headers)
    • dependencies checks pass, (make test-math-dependencies)
    • docs build, (make doxygen)
    • code passes the built in C++ standards checks (make cpplint)
  • the code is written in idiomatic C++ and changes are documented in the doxygen

  • the new changes are tested

@SteveBronder

Copy link
Copy Markdown
Collaborator

Fyi our backend CI is migrating to a new server cluster so the jenkins will hiccup for a minute while we get everything settled

@SteveBronder

Copy link
Copy Markdown
Collaborator

It looks like your changes exposed an issue in the opencl code. Let me look into this

@SteveBronder

SteveBronder commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

@jaburgoyne this is interesting, for the tests it looks like there may be an autodiff issue with using the symmetry trick here (which implies there is something wrong with our code that your code is showing us). At y=-50 we are getting an error for the hessian calculation vs finite difference.

./test/unit/math/expect_near_rel.hpp:90: Failure
Value of: stan::math::is_nan(x1) && stan::math::is_nan(x2)
  Actual: false
Expected: true
expect_near_rel(0, nan)
expect_near_rel; require items x1(0, 0) = x2(0, 0): hessian() Hessian for finite_diff vs fvar<var>

./test/unit/math/expect_near_rel.hpp:90: Failure
Value of: stan::math::is_nan(x1) && stan::math::is_nan(x2)
  Actual: false
Expected: true
expect_near_rel(0, nan)
expect_near_rel; require items x1(0, 0) = x2(0, 0): hessian_fvar() Hessian for finite_diff vs fvar<fvar<double>>

[  FAILED  ] AgradRev.mathMixScalFun_normal_lccdf (1 ms)

This passed at y=50 previously and your code changes test the negative boundary which is why the error shows up now. So this issue existed before your changes. I need to dive into this a bit more, would you mind taking a look as well? Claude says it has a solution that looks simple'ish and I'll make a little branch for it

@jaburgoyne

Copy link
Copy Markdown
Author

Thanks for the insight and interest, @SteveBronder ! I saw the errors and wanted to have a look but haven't yet had a chance with post-conference catch-up. I think I can make some time tomorrow to try to replicate the error on my machine and start looking at solutions. I had actually already extended the range of the test beyond what is in this pull request – the current std_normal_lcdf actually matches R all the way out past -1e8 – but it is interesting to look at where and why autodiff would break down.

@jaburgoyne

Copy link
Copy Markdown
Author

I'm not sure why those tests were passing before, as y=50 is well into the range where even the LCCDF underflows to 0. I've pushed some new commits that do three things:

  • extend the prim unit tests to cover the full useful range of the normal and standard normal LCDF and LCCDF;
  • reverse the signs of the failing LCCDF mix tests such that they are actually testing the same thing as their LCDF counterparts; and
  • added a commented-out Hessian test to each of the mix tests.

Does anybody know the history of the Hessian test? The normal_lccdf_log_test was the only one of the four that had it. It fails on my machine for the LCDFs but works for LCCDFs. Before merging, it would be good to know whether it should be there. Removing it altogether would be the most consistent with the codebase as it stood before this pull request...but perhaps we should be testing the Hessians?

@jaburgoyne

Copy link
Copy Markdown
Author

On a similar note, would it also be a good idea to extend the expect_ad tests in mix to cover a larger range, as I did for the tests in prim? I'm not sure where the development community lands on speed vs. completeness for the test suite.

SteveBronder added a commit that referenced this pull request Aug 26, 2026
PR #3363 rewrote normal_lccdf as normal_lcdf(-y, -mu, sigma), which made
every lccdf error report the wrong function:

  normal_lccdf(1.0, 0.0, -1.0)
  -> "normal_lcdf: Scale parameter is -1, but must be positive!"

Thread the reporting name through as a leading defaulted non-type template
parameter, so the reflected distributions pass their own name and the checks
still run exactly once. Same shape as the existing opencl_code<const char*>
kernel-string parameter in stan/math/opencl/kernel_generator/opencl_code.hpp.

Putting the parameter first keeps T_y/T_loc/T_scale deducible, at the cost of
breaking explicit-type-argument calls such as
normal_lcdf<double, double, double>(y, mu, sigma). That idiom should not
compile anyway; the two in-repo users were the deprecated *_cdf_log wrappers,
which never needed the explicit arguments.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SteveBronder added a commit that referenced this pull request Aug 26, 2026
PR #3363 rewrote the prim lccdf functions as lcdf(-y, -mu, sigma) but left
the OpenCL kernels on the old erf-based algorithm, which hard-truncates the
upper tail:

  select(scaled_diff > 8.25 * INV_SQRT_TWO, 0.0, 1.0 - erf(scaled_diff))

log(0) is -inf, so for (y - mu) / sigma > 8.25 OpenCL returned -inf while the
new CPU path returned a finite value. That is the ProbDistributionsNormalLccdf
CI failure: OpenCL -inf vs CPU -136.16378053699881 at z = (2, 16, -0.01).

Three cases in test/unit/math/opencl/rev/normal_lccdf_test.cpp were affected
(opencl_matches_cpu_small, opencl_broadcast_y, opencl_matches_cpu_big). The
std_normal_lccdf tests passed only because their inputs stay in [-1, 1]; the
same 8.25 cutoff was latent there.

Delegating fixes it by construction: the OpenCL normal_lcdf kernel is a
line-by-line transliteration of the prim implementation, so CPU/OpenCL
agreement for lccdf reduces to the already-passing normal_lcdf comparison.
Verified by emulating the patched kernels on CPU: values and all three
gradients match prim to machine precision on every previously-failing case
(0/459 gradients outside 1e-8 on the N=153 case, worst relative diff 2.2e-16).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SteveBronder added a commit that referenced this pull request Aug 26, 2026
PR #3363 rewrites normal_lccdf as normal_lcdf(-y, -mu, sigma), which routes
lccdf into argument regions of lcdf that lccdf never touched before and that
lcdf's own mix test barely probes -- it stops at y = +10. Three distinct
defects live in those regions, all pre-existing on develop.

These tests are RED. Each fix lands in its own commit after this one.

P1  exp(x2) sits in a denominator in the scaled_diff > 2.9 derivative branch.
    Autodiff of 1/w needs w^(k+1) representable, so the k-th derivative dies
    at x2 > 709.78/(k+1). Measured, at mu=0 sigma=1:
      d2 fvar<fvar<double>>      silent 0 for y in [26.6, 37.5], NaN y >= 37.6
      d2 fvar<var>               NaN y >= 37.7
      d3 fvar<fvar<fvar<double>>> silent 0 at y = 20, NaN y >= 22
      d3 fvar<fvar<var>>         NaN y >= 37.6
    The silent zeros are ~1e-158 and numerically harmless; the NaNs poison
    whole Hessians. Note the silent-zero band is invisible to expect_ad, so
    it needs a direct fwd assertion.

P2  erfc(|s|)^2 underflows inside log(erfc(-s)), giving a NaN third-order
    mixed derivative. Reachable ONLY in fvar<fvar<var>>, i.e. expect_ad's
    grad_hessian; fvar<var> and fvar<fvar<double>> are both clean. Window
    bisects to s in (-20, -19.2103473480), i.e. y in
    (-28.2842712475, -27.1675337575). The existing y = -20*sqrt(2) test point
    sits exactly on the lower edge and takes the other branch, missing it.

P3  The scaled_diff < -29 residual correction adds 0.0015065154280332*x2,
    which grows quadratically, but d/ds log Phi grows linearly (-> 2|s|, DLMF
    7.12.1). A quadratic fit cannot track a linear asymptote. Measured
    relative gradient error: 1.00e-4 at y=-54.46, 1.00e-3 at -66.47, 8.2e-3
    at -100, 1.00e-2 at -106.06, 9.6e-2 at -300. This corrupts plain var
    gradients, i.e. HMC transitions, not just Hessians.

Tests are placed in the tier that exhibits the defect: fwd for the pure
forward orders, rev for the first-order gradient, mix for expect_ad. Reference
values come from mpmath at 60 significant digits; the generating snippet is
committed alongside the tables. EXPECT_FLOAT_EQ is deliberately avoided for
the tail derivatives -- those magnitudes are below the float minimum, so it
would compare 0 == 0 and pass vacuously.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SteveBronder added a commit that referenced this pull request Aug 26, 2026
The scaled_diff < -29 branch took the Abramowitz & Stegun negative-tail
approximation and added a cubic residual fit whose leading term is
0.0015065154280332 * x2. That term grows quadratically in scaled_diff.

DLMF 7.12.1 gives, for y = -scaled_diff > 0,

  erfc(y) ~ (exp(-y^2) / (sqrt(pi) y)) * sum_m (-1)^m (1/2)_m / y^(2m)

and since dncdf_log = (2/sqrt(pi)) exp(-y^2) / erfc(y), this is

  dncdf_log ~ 2y / (1 - 1/(2y^2) + 3/(4y^4) - 15/(8y^6) + ...)

so dncdf_log grows LINEARLY, as -2*scaled_diff. A quadratic residual fit
cannot track a linear asymptote; it must eventually dominate. Measured
relative error of the old branch against a 60-digit mpmath reference:

  scaled_diff   -29       -40       -75      -100     -1000
  old         9.5e-06   1.9e-04   1.0e-02   2.3e-02   6.8e-01
  new         1.3e-11   1.0e-12   6.5e-15   7.0e-16   7.8e-18

At scaled_diff = -1000 the old branch is 68% wrong. In y terms (mu=0,
sigma=1) the gradient passes 1e-4 relative error at y = -54.46 and 1e-2 at
y = -106.06. This is a plain first-order var gradient, so it corrupts HMC
transitions, not just Hessians -- and PR #3363's reflection newly exposes it
on the lccdf side at large positive y.

Replacing the branch with the truncated asymptotic is also exponential-free:
no erf, no exp, nothing that can overflow. DLMF states the remainder is
bounded by the first neglected term for real argument, which at
scaled_diff = -29 is 6.5625/29^8 = 1.3e-11, matching the measured error.

The seam at scaled_diff = -29 moves from 3.3e-06 to 6.2e-06 relative, still
well inside the existing inter-branch jumps (~2.7e-5 at scaled_diff = 2.9).

Also corrects the sqrt(2) scaling in the new rev sweep assertions: dncdf_log
is d/d(scaled_diff), while the reported partial is dncdf_log/(sigma*sqrt(2)).
The discrete mpmath-referenced cases were unaffected. Verified the corrected
sweep is still red without this fix and green with it.

All P1, P2 and P3 tests now pass across prim, rev, fwd and mix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SteveBronder and others added 3 commits August 26, 2026 13:55
PR stan-dev#3363 rewrites normal_lccdf as normal_lcdf(-y, -mu, sigma), which routes
lccdf into argument regions of lcdf that lccdf never touched before and that
lcdf's own mix test barely probed -- it stopped at y = +10. Three defects live
in those regions, all pre-existing on develop. Verified with git diff that
normal_lcdf.hpp was untouched by stan-dev#3363: the PR reaches these, it does not
introduce them.

Notation: s = scaled_diff, z = (y - mu) / sigma = s * sqrt(2).

P1  exp(x2) sat in a denominator in the s > 2.9 derivative branch. Autodiff of
    1/w needs w^(k+1) representable, so the k-th derivative dies at
    x2 > 709.78/(k+1). Measured at mu=0 sigma=1: d2 silently 0 for
    y in [26.6, 37.5] then NaN from 37.6; d3 silently 0 at y = 20, NaN from 22.
    Abramowitz & Stegun (1964) 7.1.26 publishes exp(-x^2) as a NUMERATOR
    factor; Stan had algebraically inverted it. Restoring A&S's arrangement is
    an exact identity -- max 6 ulp over a 200,001-point scan of
    s in [2.9, 26.64] -- and lets the exponential underflow instead of
    overflowing. R's pnorm does the same, forming the Gaussian factor only in
    the numerator (do_del) and guarding underflow, never overflow.

P2  erfc(|s|)^2 underflowed inside log(erfc(-s)), giving a NaN third-order
    mixed derivative. Reachable ONLY in fvar<fvar<var>>, i.e. expect_ad's
    grad_hessian; fvar<var> and fvar<fvar<double>> are both clean. Window
    bisects to s in (-20.0000000000, -19.2103473480). Moved the crossover to
    R pnorm's own cutoff: it switches to the Cody tail form at y > M_SQRT_32,
    i.e. |x| > sqrt(32), which in scaled_diff is exactly
    sqrt(32)/sqrt(2) = 4. Our Cody coefficient set differs from R's p[]/q[]
    (ours is in s, theirs in x), so its range was measured separately: <=
    9.593e-17 relative down to s = -4, degrading past -3.52.

P3  The s < -29 residual correction added 0.0015065154280332 * x2, growing
    quadratically, but d/ds log Phi grows linearly, to 2|s| (DLMF 7.12.1). A
    quadratic fit cannot track a linear asymptote. Relative gradient error was
    1.0e-4 at y = -54.46, 8.2e-3 at -100, and 68% at s = -1000. This corrupted
    plain var gradients, i.e. HMC transitions. Replaced with the truncated
    asymptotic Mills ratio, which needs no exponential at all: 1.3e-11 at
    s = -29 improving to 7.8e-18 at s = -1000.

All three land at four sites together -- prim normal_lcdf, prim
std_normal_lcdf, and the two OpenCL kernels, which are line-by-line
transliterations whose results are compared against CPU in opencl/rev.

Also in this change:

- OpenCL normal_lccdf and std_normal_lccdf now reflect through lcdf like prim
  does. They had been left on the old erf algorithm, whose select(scaled_diff >
  8.25 * INV_SQRT_TWO, 0.0, ...) makes log(0) = -inf. That is the reported CI
  failure: OpenCL -inf against CPU -136.16378053699881 at z = (2, 16, -0.01).
- A const char* template parameter carries the caller's name into the error
  checks, so normal_lccdf(1, 0, -1) reports normal_lccdf rather than
  normal_lcdf. This breaks normal_lcdf<double, double, double>(...), an idiom
  that should not compile anyway; the two in-repo users were the deprecated
  *_cdf_log wrappers.
- normal_lcdf's doc block records what each cutoff rests on: A&S 7.1.26,
  Cody (1969) and DLMF 7.12.1 with links, the R pnorm and SciPy log_ndtr
  cross-references, and the stan-dev#1411 origin of the interior Taylor
  cutoffs, which are original and undocumented -- the PR describes them as
  "original Taylor expansions ... to bridge the gap where the numerical
  approximations are unstable". std_normal_lcdf points at it rather than
  duplicating; the duplicate had already drifted.
- Doxygen completed on both: \ingroup prob_dists, @return, and removal of
  @tparam T_loc, @tparam T_scale, @PARAM mu and @PARAM sigma from
  std_normal_lcdf, which does not take them.

Tests, all in prim and mix:

- prim: 16 rows of log Phi against references carried to 40 significant digits
  in comments beside the correctly-rounded doubles, spanning all three value
  branches out to s = -212. Mutation-checked: a 0.15% perturbation of temp_p's
  leading coefficient fires all six Cody rows. One row is asserted loosely and
  documented -- at s = 11.31 this platform's libm erfc is itself 8.0e-06 out,
  which passes straight through.
- mix: expect_ad at every branch cutoff, bracketed rather than landing on the
  seam, since finite_diff_grad_hessian_auto's stencil would straddle it.
- mix: per-branch derivative accuracy at each branch's own measured error,
  because expect_ad's blanket 1e-4 leaves the (0.8, 1.5] branch only 1.6x.
- mix: order-2 and order-3 tail derivatives against 60-digit references, plus
  a finiteness sweep over y in [-120, 120]. These are asserted directly rather
  than through expect_ad because expect_ad cannot see either failure: the
  silent zeros are ~1e-158, invisible to a finite-difference comparison whose
  relative tolerance floors its denominator at 1, and test_ad.hpp stops at
  fvar<fvar<var>>, so fvar<fvar<fvar<double>>> is never instantiated.

OpenCL cannot be executed on the development machine -- the only local device
has no fp64 -- so those two kernels were verified by transliterating them back
to C++ and diffing against prim over 16,000 points in y in [-400, 400]: value
bit-identical, gradient worst 3.6e-16. Jenkins remains the real check.

Known and deliberately untouched: the s > 2.9 branch is ~6e-5 relatively
inaccurate because it uses P(t)/t where A&S 7.1.26 requires P(t)/2. This
change preserves that bit-for-bit and makes no accuracy claim. Fixing it means
replacing all five piecewise Taylor branches with
INV_SQRT_PI * exp(-x2) / (1 - 0.5 * erfc(scaled_diff)), reusing the erfc the
value branch already computes, which would shift many gradient expectations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four mix/prob normal tests had grown four copies of the same thing: the
same d2 and d3 extractors, the same branch_ref/tail_ref/far_ref structs, the
same cutoff array, and the same reference tables differing only by a sign.

Adds test/unit/math/mix/prob/normal_lcdf_tail_test_helpers.hpp holding the
references once and the checks as functors:

  d2, d3                        pure-forward derivative extractors
  value_and_grad                reverse-mode value and gradient
  mills_dlogphi_dy              five-term DLMF 7.12.1 asymptotic
  expect_ad_across_cutoffs      expect_ad bracketing every branch cutoff
  expect_ad_at_defect_inputs    expect_ad at the three defects' inputs
  expect_branch_accuracy        per-branch accuracy vs 60-digit references
  expect_tail_derivatives       order-2 and order-3 tail references
  expect_derivatives_finite     NaN sweep over y in [-120, 120]
  expect_far_tail_gradient      far-tail references plus a Mills sweep

All four functions share one piecewise implementation, so one set of
references covers them. They are stated for normal_lcdf(y, 0, 1); a reflected
function -- lccdf(y) == lcdf(-y) -- is exercised at -y with odd-order
derivatives flipped. That is the `dir` argument, spelled
orientation::lcdf or orientation::lccdf at each call site, which is also what
makes the reflection explicit rather than buried in four hand-negated tables.

Each test file drops to roughly 50 lines: its own expect_ad points, then six
one-line calls. The helper is 260 lines, against about 700 of duplication it
replaces.

Mutation-checked after the refactor, so the move did not weaken anything:
reverting the exp(-x2) rearrangement fails defect_inputs, tail_derivatives
(naming y = 27, 30, 33, 37 for order 2 and 20, 22 for order 3) and
derivatives_are_finite; re-centring the 1.85 series on 1.80 fails
branch_cutoffs and branch_accuracy. Same failures as before the refactor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
runTests.py --jumbo concatenates test file bodies into one .cpp, hoisting and
de-duplicating the includes (generateJumboTests). Anonymous namespaces from
every file in a folder therefore land in the same translation unit, so the
`lcdf`, `lccdf` and `dir` objects I put in `namespace { ... }` collided.

Reproduced by concatenating the four files the way generateJumboTests does:

  error: redefinition of 'dir'      (x3)
  error: redefinition of 'lcdf'
  error: redefinition of 'lccdf'

Each file now uses a namespace named after itself -- normal_lcdf_mix_test,
normal_lccdf_mix_test, std_normal_lcdf_mix_test, std_normal_lccdf_mix_test --
holding `fn` and `dir`, matching the dirichlet_test convention already in this
folder. The same concatenation now compiles clean.

Checked the prim tests for the same hazard: their reference structs are
declared inside the TEST bodies, so they are function-local and a prim jumbo
concatenates cleanly as-is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SteveBronder

SteveBronder commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Does anybody know the history of the Hessian test? The normal_lccdf_log_test was the only one of the four that had it. It fails on my machine for the LCDFs but works for LCCDFs. Before merging, it would be good to know whether it should be there. Removing it altogether would be the most consistent with the codebase as it stood before this pull request...but perhaps we should be testing the Hessians?

So in general the hessian tests are checking a finite diff for the hessian vs. Stan's AD computation of the Hessian. Does that answer your question?

I was also really curious about where the cutoff points for the Taylor expansions in the adjoint calculations in normal_lcdf came from. I had claude dig backwards and found the original PR where those were written and the answer is, "Just made sense" lol. Which I think they do! They were written so that at the cutoff points we would have at least ~1e-4 relative tolerance for gradient and hessian calculations. I have a branch below that adds a lot of doc to the normal_lcdf to explain the cutoff points for the cdf and adjoint calculations. It also aligns the cdf calculations cutoff points to be more in line with R's pnorm.

e0e8ee2...fix/normal-lcdf-tails#diff-fc7a099d7fc782572adda6e43861b346142f7690b57a8d2277d9eadb2f71253d

The main change is stan/math/prim/prob/normal_lcdf.hpp. There I added a bunch of doc explaining the cutoffs, adjusted one of the cutoffs for the cdf to be in line with R's pnorm, and fixed an overflow issue in one of the adjoint calculations that was causing the hessian failure.

The tests in that are mostly to make sure we catch the hessian and gradient calculations correctly. The opencl changes are just to make sure the opencl code stays in parity with the cpu code.

You can check it out locally via the below (assuming your git remote for stan-dev is named upstream)

git fetch upstream
git switch -c normal-lcdf-tails upstream/fix/normal-lcdf-tails   # or: git reset --hard upstream/... if he already had it
git log --oneline fix/normal_lccdf..normal-lcdf-tails            # 15 commits

If you are cool with the changes you can merge and push them to your branch via

git switch fix/normal_lccdf
git merge --ff-only upstream/fix/normal-lcdf-tails

fyi I had claude help a good bit with this, but the docs show provenance for the actual code changes that I think make a lot of sense / align with R's pnorm.

@SteveBronder

Copy link
Copy Markdown
Collaborator

@jaburgoyne just a sanity check, did you add my branch to this branch? If not I must have fat fingered something and I can remove / revert those changes off of this branch

@jaburgoyne

Copy link
Copy Markdown
Author

@SteveBronder: Yes, I just added your branch!

@SteveBronder SteveBronder self-assigned this Aug 26, 2026
@jaburgoyne

Copy link
Copy Markdown
Author

The extra documentation is a great idea! Thank you for fixing up the OpenCL bits – I had not originally realised that I needed to look there, too.

I don't completely understand all of the new tests you added, but assuming they do what they say, they are certainly more rigorous than what was there before.

As for the Hessian tests – my question wasn't what they were doing, but rather why they had been removed everywhere except normal_ccdf_log_test. As written, they pass for normal_lccdf and std_normal_lccdf but fail for normal_lcdf and std_normal_lcdf. Should you, I, or Claude try to make a cleaner version?

@SteveBronder

Copy link
Copy Markdown
Collaborator

I don't completely understand all of the new tests you added, but assuming they do what they say, they are certainly more rigorous than what was there before.

Those tests are purely to try to 'eek out the poor hessian calculation by making sure we have better coverage on each of the cutoff points. expect_ad_across_cutoffs goes +/- 0.01 across each cutoff point to check that each one does the adjoint calculations correctly

As for the Hessian tests – my question wasn't what they were doing, but rather why they had been removed everywhere except normal_ccdf_log_test. As written, they pass for normal_lccdf and std_normal_lccdf but fail for normal_lcdf and std_normal_lcdf. Should you, I, or Claude try to make a cleaner version?

That is a great question... maybe the first questions is whether they existed 😅

Looking at git blame for the mix tests it looks like normal_lcdf never had these checks. So it is good we are catching it now

https://github.com/stan-dev/math/blame/develop/test/unit/math/mix/prob/normal_cdf_log_test.cpp#L12

@jaburgoyne

Copy link
Copy Markdown
Author

Those tests are purely to try to 'eek out the poor hessian calculation by making sure we have better coverage on each of the cutoff points. expect_ad_across_cutoffs goes +/- 0.01 across each cutoff point to check that each one does the adjoint calculations correctly

Right! So this is actually a much better test than what was in there before, anyway. I apologise for being so slow to piece this together – newbie growing pains! – but I think that I finally see the big picture.

Looking at git blame for the mix tests it looks like normal_lcdf never had these checks. So it is good we are catching it now

Given the other tests that you added, let's just remove these.

I pushed one more commit taking those Hessian tests out, and expanding the other tests slightly to:

  • check values other than mu = 0 and sigma = 1 in the prim tests and
  • test the derivatives of mu and sigma in the mix tests.

Claude caught that second point, and so far as I can see, it was right: the suite was never checking whether autodiff on mu and sigma were working.

Given the way that this pull request has developed, perhaps one other question that I originally put into #3364 but now fits better here: is it good to keep std_normal_lcdf and normal_lcdf as independent implementations, complicated branching and all, or is it worth it in this pull request (or a new one) to try to make normal_lcdf call std_normal_lcdf, with the necessary corrections for the presence of mu and sigma? I'm happy to have a go at that and now have a better sense of how to make sure the results fits better into the whole test apparatus.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants