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
Add double factorial function #72
Conversation
…dlib into feature/factorial2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you very much for this pull request! I left some comments.
|
|
||
| The double factorial should not be confused with the factorial function iterated twice, written as (n!)! and not n!! | ||
|
|
||
| The [double factorial][double-factorial-function] is implemented for integer values in terms of the [Factorial][@stdlib/math/base/special/factorial] function using the relation: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing [@stdlib/math/base/special/factorial]: https://github.com/stdlib-js/stdlib in the link section at the end of the file.
| 'use strict'; | ||
|
|
||
| var incrspace = require( '@stdlib/math/generics/utils/incrspace' ); | ||
| var factorial = require( './../lib' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
factorial should be replaced by factorial2 in this file.
| function factorial2( x ) { | ||
| var n; | ||
| if ( | ||
| (isInteger( x ) && x < 0) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use isNegativeInteger module instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, keep isInteger, but cache the result so you can use the boolean later at line 66.
var isInt;
isInt = isInteger( x );
if ( isInt && x < 0 ) {
return NaN;
}
...
if ( isInt ) {
...
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call :)
| @@ -0,0 +1,38 @@ | |||
| { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The contents of the package.json files have changed, take a look at the snippet. Fields that need to be updated: author, contributors, repository, homepage, bugs, and license.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not so sure what values that need to be updated for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{
"name": "@stdlib/math/base/special/factorial2",
"version": "0.0.0",
"description": "Evaluate a double factorial.",
"author": {
"name": "The Stdlib Authors",
"url": "https://github.com/stdlib-js/stdlib/graphs/contributors"
},
"contributors": [
{
"name": "The Stdlib Authors",
"url": "https://github.com/stdlib-js/stdlib/graphs/contributors"
}
],
"scripts": {},
"main": "./lib",
"repository": {
"type": "git",
"url": "git://github.com/stdlib-js/stdlib.git"
},
"homepage": "https://github.com/stdlib-js/stdlib",
"keywords": [
"stdlib",
"stdmath",
"mathematics",
"math",
"special functions",
"special",
"function",
"factorial",
"double factorial",
"fact",
"combinatorics",
"gamma",
"number"
],
"bugs": {
"url": "https://github.com/stdlib-js/stdlib/issues"
},
"dependencies": {},
"devDependencies": {},
"engines": {
"node": ">=0.10.0"
},
"license": "Apache-2.0"
}
|
|
||
| var integers = require( './fixtures/julia/integers.json' ); | ||
| var decimals = require( './fixtures/julia/decimals.json' ); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add empty line before // TESTS //.
| } else { | ||
| delta = abs( v - expected[ i ] ); | ||
| tol = EPS * Math.max( 1.0, abs( v ), abs( expected[ i ] ) ); | ||
| t.ok( delta <= tol, 'within tolerance. x: ' + x[ i ] + '. Value: ' + v + '. Expected: ' + expected[ i ] + '. Tolerance: ' + tol + '.' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you replace the last two lines with
tol = EPS * abs( expected[ i ] );
t.ok( delta <= tol, 'within tolerance. x: '+x[i]+'. Value: '+v+'. E: '+expected[i]+'. Δ: '+delta+'. tol: '+tol );and check whether the tests still pass?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This passes.
| v = factorial2( x[ i ] ); | ||
| delta = abs( v - expected[ i ] ); | ||
| tol = 2.0e-14 * Math.max( 1.0, abs( v ), abs( expected[ i ] ) ); | ||
| t.ok( delta <= tol, 'within tolerance. x: ' + x[ i ] + '. Value: ' + v + '. Expected: ' + expected[ i ] + '. Tolerance: ' + tol + '.' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you replace the last two lines with
tol = 2.0e-14 * abs( expected[ i ] );
t.ok( delta <= tol, 'within tolerance. x: '+x[i]+'. Value: '+v+'. E: '+expected[i]+'. Δ: '+delta+'. tol: '+tol );and check whether the tests still pass?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tolerance should be scaled via epsilon.
tol = <scalar> * EPS * abs( expected[ i ] );There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I scale the tolerance with epsilon, the tests break.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain the rationale behind scaling the tolerance with epsilon? Also, why can't we apply the same for the factorial tests?
|
Awesome. @chrisdamba Can you also complete the PR template checklist and update the description section. |
|
|
||
| <!-- </equation> --> | ||
|
|
||
| The double factorial should not be confused with the factorial function iterated twice, written as (n!)! and not n!! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the double factorial should not...
| <br> | ||
| </div> | ||
|
|
||
| The [Gamma][@stdlib/math/base/special/gamma] function extends the [double factorial][double-factorial-function] function for non-integer values. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...function to non-integer values.
| var DOUBLE_FACTORIALS = require( './doublefactorials.json' ); | ||
| var NINF = require( '@stdlib/math/constants/float64-ninf' ); | ||
| var PINF = require( '@stdlib/math/constants/float64-pinf' ); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add another newline.
| var isInteger = require( '@stdlib/math/base/utils/is-integer' ); | ||
| var isNegativeZero = require( '@stdlib/math/base/utils/is-negative-zero' ); | ||
| var sqrt = require( '@stdlib/math/base/special/sqrt' ); | ||
| var DOUBLE_FACTORIALS = require( './doublefactorials.json' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be the last require statement. Order of requires:
- Builtins; e.g.,
path,Buffer, etc. - External packages; e.g.,
debug,object-keys, etc. - Internal packages; e.g.,
@stdlib/math/base/special/sqrt. - Relative modules; e.g.,
./doublefactorials.json.
| @@ -0,0 +1,97 @@ | |||
| #!/usr/bin/env julia | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be better to use Python and SciPy for generating test fixtures, as SciPy actually has a factorial2 implementation against which we can test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will try to reproduce locally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did some investigating. Based on the SciPy and Boost implementations, the double factorial is simply not implemented for doubles. In the code above, if you cast x to integers, then the fixtures work.
As a result, unlikely to get either of those two reference implementations to provide test fixtures.
Further, I am willing to hazard a guess that the Gamma relation is not straightforward. Boost only applies a Gamma approximation for odd integers. SciPy applies two different approximations based on whether an input integer is odd or even. While the double factorial can be extended to reals, neither of those two implementations can serve as reference implementations for that particular use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So in this case do we leave the fixtures as they are?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on my assessment, looks like the package implementation needs refactoring. Notably, unless we know of some implementation which actually does extend the double factorial function to all reals, we should stick to Boost and Scipy and only support integer inputs. For large inputs, similar to Boost, if not covered by the table, we can then apply a Gamma approximation*.
Once that is updated, the fixtures can be updated, and we'll have a better idea of where we stand.
- NOTE: I would be curious to know how the table lookup compares to integer multiplication in terms of performance as done in SciPy. Did you ever investigate this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely concur, needs some refactoring. I haven't actually done the investigation but I believe it's worth doing the comparison. Will do so and update you the results.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome. Thanks! Really appreciate your tending to this. 👍
|
@chrisdamba Once you make the requested changes, if you could Thanks again for all your work! |
…the test fixtures
Current coverage is 88.2077% (diff: 96.6666%)@@ develop #72 diff @@
==========================================
Files 2202 2204 +2
Lines 29201 29231 +30
Methods 0 0
Messages 0 0
Branches 0 0
==========================================
+ Hits 25755 25784 +29
- Misses 3446 3447 +1
Partials 0 0
|
| @@ -0,0 +1,38 @@ | |||
| { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{
"name": "@stdlib/math/base/special/factorial2",
"version": "0.0.0",
"description": "Evaluate a double factorial.",
"author": {
"name": "The Stdlib Authors",
"url": "https://github.com/stdlib-js/stdlib/graphs/contributors"
},
"contributors": [
{
"name": "The Stdlib Authors",
"url": "https://github.com/stdlib-js/stdlib/graphs/contributors"
}
],
"scripts": {},
"main": "./lib",
"repository": {
"type": "git",
"url": "git://github.com/stdlib-js/stdlib.git"
},
"homepage": "https://github.com/stdlib-js/stdlib",
"keywords": [
"stdlib",
"stdmath",
"mathematics",
"math",
"special functions",
"special",
"function",
"factorial",
"double factorial",
"fact",
"combinatorics",
"gamma",
"number"
],
"bugs": {
"url": "https://github.com/stdlib-js/stdlib/issues"
},
"dependencies": {},
"devDependencies": {},
"engines": {
"node": ">=0.10.0"
},
"license": "Apache-2.0"
}
|
|
||
| Note that the double factorial should not be confused with the factorial function iterated twice, written as (n!)! and not n!! | ||
|
|
||
| The [double factorial][double-factorial-function] is implemented for integer values in terms of the [Factorial][@stdlib/math/base/special/factorial] function using the relation: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lowercase "factorial":
...in terms of the factorial function using the relation:
|
|
||
| The [double factorial][double-factorial-function] is implemented for integer values in terms of the [Factorial][@stdlib/math/base/special/factorial] function using the relation: | ||
|
|
||
| <!-- <equation class="equation" label="eq:double_factorial_function_and_factorial" align="center" raw="(2n)!! = 2^n * n!" alt="Factorial function extension via the Factorial function"> --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lowercase "Factorial"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2^n \cdot n!
|
|
||
| The [Gamma][@stdlib/math/base/special/gamma] function extends the [double factorial][double-factorial-function] function to non-integer values. | ||
|
|
||
| <!-- <equation class="equation" label="eq:double_factorial_function_and_gamma" align="center" raw="(2n-1)!! = \Gamma \left( \frac{(2n+1)}{2} \right) * \frac{2^n}{\sqrt{\pi}}" alt="Factorial function extension via the Gamma function"> --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not believe you need the * (asterisk) in your equation. This applies here and immediately below.
|
|
||
| <!-- <equation class="equation" label="eq:double_factorial_function_and_factorial" align="center" raw="(2n)!! = 2^n * n!" alt="Factorial function extension via the Factorial function"> --> | ||
|
|
||
| <div class="equation" align="center" data-raw-text="(2n)!! = 2^n * n!" data-equation="eq:double_factorial_function_and_factorial"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2^n \cdot n!
| * // returns NaN | ||
| */ | ||
| function factorial2( x ) { | ||
| var n; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variables should be listed in order of length.
var isInt;
var n;
| @@ -0,0 +1,97 @@ | |||
| #!/usr/bin/env julia | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will try to reproduce locally.
| @@ -96,7 +97,7 @@ tape( 'the function evaluates the double factorial function (positive integers < | |||
| } else { | |||
| delta = abs( v - expected[ i ] ); | |||
| tol = EPS * Math.max( 1.0, abs( v ), abs( expected[ i ] ) ); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line should be
tol = EPS * abs( expected[ i ] );
| @@ -126,7 +127,7 @@ tape( 'the function evaluates the factorial function (decimal values)', function | |||
| v = factorial2( x[ i ] ); | |||
| delta = abs( v - expected[ i ] ); | |||
| tol = 2.0e-14 * Math.max( 1.0, abs( v ), abs( expected[ i ] ) ); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line should be
tol = <num> * EPS * abs( expected[ i ] );
For example,
tol = 100.0 * EPS * abs( expected[ i ] );
The reason why we scale epsilon is that doing so provides more context as to how much results are off by and lessens the presence of so-called "magic" numbers. For instance, if we know that
actual = <number>;
expected = 1.0;
tol = 1.0 * EPS * abs( expected );
delta = abs( actual - expected );
bool = ( delta <= tol );
we know that actual value differs from the expected by only 1 bit. Similarly for other values, although the correspondence between units epsilon and number of differing bits is not 1:1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great explanation, thank you for this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem! :)
| // VARIABLES // | ||
|
|
||
| var MAX_FACTORIAL = 170; // TODO: consider extracting as a constant | ||
| var PIO2 = 1.57079632679489661923; // pi/2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be replaced by using @stdlib/math/constants/float64-half-pi.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can see there are other areas in the library which need to be updated as well to use the constant e.g in acos, asin and atan.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. We have an internal backlog of various clean-up tasks. :( Currently picking them off as we have time. Thanks for the reminder!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't mind picking up this clean-up task if there's an issue not assigned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can create an issue to this end, and, if you want to submit a separate PR addressing them, that would be great!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chrisdamba See issue #74.
|
I'd love to help get this merged. Am I to conclude that, at least for a start, restricting the implementation to integers is the right move? Is the function then strictly a lookup table? |
|
Yes, restricting to integer inputs should be fine. A table lookup and for large inputs a Gamma approximation should work, as Athan pointed out. Maybe have a look at how Boost does it. |
|
Closing this PR due to inactivity. |



Resolves #44 .
Checklist
develop.developbranch.Description
This pull request:
Related Issues
This pull request:
Questions
Follow-up questions are in the responses to the review suggestions.
Other
Encountering errors when trying to use
factorial2method in Scipy for generating the fixtures.@stdlib-js/reviewers