Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/log bessel i #640

Merged
merged 11 commits into from
Oct 22, 2017
Merged

Feature/log bessel i #640

merged 11 commits into from
Oct 22, 2017

Conversation

bgoodri
Copy link
Contributor

@bgoodri bgoodri commented Oct 7, 2017

Submission Checklist

  • Run unit tests: ./runTests.py test/unit
  • Run cpplint: make cpplint
  • Declare copyright holder and open-source license: see below

Summary:

Introduce log_modified_bessel_first_kind(T1 v, T2 z) function that uses log_sum_exp to avoid overflow and can be evaluated for non-integer v.

Intended Effect:

Not overflowing while expanding the number of situations where the Bessel I function can be used.

How to Verify:

Run the unit tests

Side Effects:

Inconsistency with other Bessel functions, which currently are restricted to integer orders and do not have log_ versions.

Fixes #639

Documentation:

I am not sure log_modified_bessel_first_kind should necessarily be exposed in the Stan language since the only place it is used in Stan is von_mises_lpdf, which only calls it with v = 0 and double-precision z. Thus, I have not implemented rev or fwd specializations because the derivative with respect to v is a lot of work for no known use case. It probably wouldn't hurt anything to expose it in the Stan language, but any derivatives would be calculated by autodiffing a truncated infinite sum.

Copyright and Licensing

Please list the copyright holder for the work you are submitting (this will be you or your assignee, such as a university or company): Trustees of Columbia University

By submitting this pull request, the copyright holder is agreeing to license the submitted work under the following licenses:

Copy link
Contributor

@bob-carpenter bob-carpenter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this looks great. The changes requested are some minor efficiency issues, some needless includes, and abit of doc on template parameters.

check_greater_or_equal("log_modified_bessel_first_kind", "v", v, -1);

using std::log;
using stan::math::log_sum_exp;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symbols in the same namespace get brought in automatically---so you don't need the stan::math:: using statements. They won't hurt anything being there.

}

T log_half_z = log(0.5 * z);
T lgam = lgamma(v + 1.0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't lgam be set inside the v > -1 branch? The other branch sets it to 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boost's lgamma function stupidly throws an exception if v = -1 and thus lgamma is evaluated at 0. It should be returning positive infinity, in which case the first term in the denominator of the Bessel I function is just zero. So, we avoid that, handle the m = 1 case manually, and start the infinity sum with the m = 2 case.

double m = 2;
T lfac = 0;
T old_out;
do {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't the test just out != old_out.

I always find the do-while convention takes me a while to get my head around, but I could get used to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I forgot that NaN != NaN is false.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon further review, I was right the first time: NaN != NaN is true because NaN == NaN is false. So, if out and old_out somehow both go NaN then the loop would never terminate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reviewing this. I somehow thought any comparisons involving NaN were automatically false, but you're right that != is an exception to this general rule and always returns true.

@@ -67,7 +68,7 @@ namespace stan {
kappa_dbl[i] = value_of(kappa_vec[i]);
if (include_summand<propto, T_scale>::value)
log_bessel0[i]
= log(modified_bessel_first_kind(0, value_of(kappa_vec[i])));
= log_modified_bessel_first_kind(0, value_of(kappa_vec[i]));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

/* Log of the modified Bessel function of the first kind,
* which is better known as the Bessel I function. See
* modified_bessel_first_kind.hpp for the function definition.
*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to add doc for the template parameters,

@tparam T1 type of the order (v)
@tparam T2 type of argument (z)

template <typename T1, typename T2>
inline typename boost::math::tools::promote_args<T1, T2, double>::type
log_modified_bessel_first_kind(const T1 v, const T2 z) {
check_not_nan("log_modified_bessel_first_kind", "v", v);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments can't really use v since that's not what the user is necessarily going to have typed into their program, so it gets confusing.

It's better if you can replace v with first argument and z with second argument (order) in the error messages.

if (stan::math::is_inf(z)) return z;
if (z > 100) {
// Boost does something like this in asymptotic_bessel_i_large_x
T lim = (4 * square(v) + 10) / (8 * z);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be more efficient as (square(v) + 2.5) / (2 * z).

}
}

T log_half_z = log(0.5 * z);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type can be tighter here as T2. That way, if T2 == double, this won't be overpromoted.

Same thing goes for the next one (lgam) being just T2. You only need the promoted T when v and z are involved (directly or indirectly).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you have to metaprogram here. If T1 or T2 is int then we want the left-hand side to be a double.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point about integers; it'll need to be

typename boost::math::tools::promote_args<T1>::type log_half_z

num *= mu - 25;
denom *= ex * 3;
s -= num / denom;
s = z - log(std::sqrt(2 * z * M_PI)) + log(s);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be using std::sqrt and then just sqrt here if z is going to be anything other than double or int.

T out;
if (v > -1) {
out = log_sum_exp(v * log_half_z - lgam, lcons - lgamma(v + 2));
lgam += log(v + 1.0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The righthand side can be log1p(v) --- this may need an include and a using std::log1p if there's not a primitive stan::math::log1p.

@bgoodri
Copy link
Contributor Author

bgoodri commented Oct 7, 2017

I think I responded to all of your comments, but in the latest push I also copied special cases from Boost when v = 0 or v = 1.

@bob-carpenter
Copy link
Contributor

The failing test is a failed instantiation of the log-Bessel for fvar<var>, I think.

@bob-carpenter
Copy link
Contributor

bob-carpenter commented Oct 8, 2017 via email

@bgoodri
Copy link
Contributor Author

bgoodri commented Oct 8, 2017

I don't understand the error message. It just says over and over again that it can't do the template instantiation. But it has T1 and T2, so I don't know what is stopping it from working in fwd mode.

@bob-carpenter
Copy link
Contributor

I'll check it out and have a look. Probably just a missing include somewhere.

@bob-carpenter
Copy link
Contributor

bob-carpenter commented Oct 9, 2017 via email

@bob-carpenter
Copy link
Contributor

bob-carpenter commented Oct 9, 2017 via email

@bgoodri
Copy link
Contributor Author

bgoodri commented Oct 9, 2017

It won't autodiff correctly at v = 0 or v = 1 but other than that it should be fine.

@bob-carpenter
Copy link
Contributor

OK, let me write a finer test for this case, then, and see if that works.

@bob-carpenter
Copy link
Contributor

I'm pushing a version that passes tests locally for me.

There are still some known issues and then a bunch of cases where the double version throws an exception but the autodiff version does not.

  // TODO(carpenter):  need to fix boundary conditions in function
  //   50 : tolerance too tight for autodiff, but basically OK
  //   0 : known that won't work for finite diffs test
  //   -0.5  : autodiff doesn't throw
  //   stan::math::not_a_number() : autodiff doesn't throw
  //   stan::math::negative_infinity() : autodiff doesn't throw

@bob-carpenter
Copy link
Contributor

Jenkins, retest this please.

@bob-carpenter
Copy link
Contributor

@seantalts --- any idea what's up? My request to Jenkins to retest this didn't kick off a new testing round. Looks like the failure's a Jenkins issue of some kind.

@seantalts
Copy link
Member

It did kick off a new testing round. It's just queued. I don't think the link will work until the job actually begins. You can see it in the job queue on the home page of Jenkins and you can see its description by hovering over it.

image

@bob-carpenter
Copy link
Contributor

bob-carpenter commented Oct 9, 2017 via email

@seantalts
Copy link
Member

Jenkins, retest this please.

@seantalts
Copy link
Member

My bad. Jenkins, retest this please.

@syclik
Copy link
Member

syclik commented Oct 22, 2017

@bob-carpenter, can you change your review to approved? @bgoodri went through and updated the pull request.

@bob-carpenter bob-carpenter merged commit 62a8c51 into develop Oct 22, 2017
@bob-carpenter bob-carpenter deleted the feature/log_besselI branch October 22, 2017 03:41
@seantalts
Copy link
Member

Hey @bgoodri, looks like there's a compiler warning in develop on Stan during test-headers that's breaking the build: http://d1m1s1b1.stat.columbia.edu:8080/job/Stan/job/develop/11/execution/node/83/log/

This wasn't caught because our current Math build doesn't test the full Stan test suite when it tests Stan Upstream. The new pipelines fix this but aren't in yet.

Can you make a PR to fix this and we can roll forward?

@aaronjg
Copy link

aaronjg commented Mar 28, 2018

It looks like this is currently not exposed. I just wanted to mention that the log_bessel_k is used in other distributions beyond the von Misses, such as the Generalized Inverse Gaussian Distribution, and having log_bessel_i exposed could be helpful in implementing these.

@bgoodri
Copy link
Contributor Author

bgoodri commented Mar 28, 2018 via email

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.

None yet

5 participants