-
-
Notifications
You must be signed in to change notification settings - Fork 870
GSoC 2025 ‐ Karan Anand
Hi, I’m Karan Anand, a fourth-year undergraduate student at the University of British Columbia (UBC), pursuing a combined major in Computer Science and Math. Originally from India, I’ve always been fascinated by the intersection of mathematics and computing, particularly in numerical methods, scientific computing, and high-performance software.
Through my work as a teaching assistant, researcher, and open-source contributor, I’ve had the chance to explore how theoretical concepts can be turned into practical implementations. Google Summer of Code with stdlib was my first step into large-scale open-source collaboration, and it has been an incredibly meaningful experience, both technically and personally.
Have you ever opened a math textbook, seen an equation like x^y
or sin(x)
, and wondered: How does this actually run on a computer? That was the spirit behind my Google Summer of Code project with stdlib. And trust me, making it work under the hood was nowhere near as simple as it looks (but that’s what made it fun!).
The main goal was to strengthen stdlib’s collection of special mathematical functions: the building blocks behind scientific computing, simulations, and machine learning, while making sure they worked efficiently in both JavaScript and C.
Concretely, the project focused on:
-
Single-precision functions: Implementing new math functions in JavaScript and C so developers can work with faster, lighter numerical code (e.g.,
sinf(x)
,cosf(x)
,powf(x, y)
). -
Double-precision functions: Adding C implementations to existing functions that previously only had JavaScript versions, improving speed and consistency (e.g.,
gammainc(x)
,heaviside(x)
,roundsd(x, y)
). -
Supporting pieces: Filling in missing constants, NAPI macros, and complex number utilities (like
cabs2f
,cabsf
, etc.) to ensure everything worked smoothly together. - Quality improvements: Verifying accuracy against trusted upstream libraries (e.g., Cephes, Boost.Math, FreeBSD libm), fixing bugs, improving test coverage (including IEEE 754 compliance), and refactoring random number generation.
- Extras: Contributing to documentation, adding benchmarks, improving test coverage, and doing code refactorings.
In short, the project was about turning abstract mathematical definitions into reliable, well-tested code that developers across fields from scientific research to engineering can use with confidence.
One of the main highlights of my project was adding support for single-precision implementations of mathematical functions in both JavaScript and C.
JavaScript has only one numeric type: Number
, which is always a 64-bit double-precision floating-point value (IEEE-754). That means whether you’re adding two integers, working with decimals, or doing heavy numerical computing, JavaScript always allocates 64
bits.
This design choice makes the language simpler, but it has trade-offs:
-
Memory overhead: Using
64
bits even when32
would suffice can be wasteful, especially in large arrays or tight loops. - Performance: Many CPUs and GPUs are optimized for 32-bit (single-precision) math, especially in areas like graphics, physics simulations, and machine learning. Doubling the precision unnecessarily can slow things down.
-
Interfacing with other systems: In scientific workloads, libraries and file formats often use single-precision floats (
float32
). JavaScript’s lack of a nativefloat32
math layer makes interoperability trickier.
By adding single-precision implementations to stdlib, we fill this gap. Developers can now choose between fast, lightweight float32
math when precision requirements are lower, or the default float64
math when higher accuracy is needed. This makes stdlib more versatile and closer to what developers expect in scientific computing environments (where both float32
and float64
are standard).
Here’s a quick example that shows the difference. In JavaScript, all numbers are normally stored as double precision:
// Double-precision in JavaScript:
var a = 0.123456789;
var b = 1.23e4;
var c = a + b;
console.log( c );
// returns 12300.123456789
The result can be confirmed by comparing it with C’s double
type:
#include <stdio.h>
int main() {
double a = 0.123456789;
double b = 1.23e4;
double c = a + b;
printf( "%.10f\n", c );
// prints: 12300.1234567890
return 0;
}
Both JavaScript’s Number
and C’s double
give the same high-precision result.
But when working with single-precision, we explicitly downcast the numbers after each arithmetic operation to maintain true single-precision semantics, using stdlib’s in-house function:
var float64ToFloat32 = require( '@stdlib/number/float64/base/to-float32' );
// Single-precision in JavaScript:
var a = float64ToFloat32( 0.123456789 );
var b = float64ToFloat32( 1.23e4 );
var c = float64ToFloat32( a + b );
console.log( c );
// returns 12300.123046875
This casting step is essential to preserve true Float32 behavior everywhere, ensuring consistency with C implementations. The following C code demonstrates the same behavior:
#include <stdio.h>
int main() {
float a = 0.123456789f;
float b = 1.23e4f;
float c = a + b;
printf( "%.10f\n", c );
// prints: 12300.123456789000
return 0;
}
Another subtle challenge was preserving signed vs unsigned behavior when working with Float32 “words” (the raw bit patterns of floats). For example, in implementations like sinf
, cosf
, or sincosf
, it was important to interpret the bits correctly.
Here is a quick example:
var toWordf = require( '@stdlib/number/float32/base/to-word' );
var f32 = require( '@stdlib/number/float64/base/to-float32' );
var x = f32( -15.75 );
// Get the raw 32-bit word:
var raw = toWordf( x );
console.log( raw );
// returns 3246129152 (unsigned)
// Interpret as signed 32-bit:
var signedWord = raw | 0;
console.log( signedWord );
// returns -1048838144
// Interpret as unsigned 32-bit:
var unsignedWord = signedWord >>> 0;
console.log( unsignedWord );
// returns 3246129152 (gets back to original unsigned representation)
This careful handling guaranteed correctness across different platforms and made the implementations robust.
In addition to the JavaScript implementations, I also wrote the corresponding C implementations to ensure performance and consistency.
For example, the sinf
function:
JavaScript:
var sinf = require( '@stdlib/math/base/special/sinf' );
var v = sinf( 0.0 );
// returns 0.0
C:
#include "stdlib/math/base/special/sinf.h"
#include <stdio.h>
int main() {
float v;
v = stdlib_base_sinf( 0.0f );
printf( "sinf( 0.0 ) = %f\n", v );
// returns 0.0
}
This dual implementation ensures that developers can use the same reliable function in both JS and C contexts, depending on their performance needs.
A second big piece of the project was adding missing C implementations for functions that already existed in JavaScript, so developers get the same behavior with better performance where native code is appropriate.
For example, the gammainc
function:
In JavaScript:
var gammainc = require( '@stdlib/math/base/special/gammainc' );
var v = gammainc( 1.0, 2.0, true, true );
// returns ~0.7358
New addition for C:
#include "stdlib/math/base/special/gammainc.h"
#include <stdio.h>
#include <stdbool.h>
int main() {
double v;
v = stdlib_base_gammainc( 1.0, 2.0, true, true );
printf( "gammainc( 1.0, 2.0, true, true ) = %f\n", v );
// returns ~0.7358
}
-
JS and C in sync: Whenever upstream references (algorithms, coefficients, thresholds) changed, I updated the JS versions while adding the C ones, ensuring both produce consistent results.
-
API and behavior parity: Options and flags in JS were mirrored in C, with the same semantics for edge cases (
NaN
,±∞
, signed zeros, etc.), so users can expect identical behavior. -
Thorough testing: Expanded test coverage to compare JS vs C across typical inputs, tricky boundaries, and IEEE-754 edge cases, targeting “same inputs -> same results” within floating-point tolerance.
-
Numerical stability: Chose algorithms that minimize cancellation and preserve accuracy, while keeping branching decisions consistent with the JS logic.
Another improvement I worked on was adding support for ULP (Units in the Last Place) difference testing.
ULP measures how many “steps” apart two floating-point numbers are in memory. Instead of comparing numbers by absolute or relative error, ULP comparison asks: are these two numbers adjacent (or very close) in their binary representation?
For example, consider 1.0
and 1.0 + EPS
in double precision (where EPS
is the smallest representable increment after 1.0):
1.0 -> 0 01111111111 0000000000000000000000000000000000000000000000000000
1.0 + EPS -> 0 01111111111 0000000000000000000000000000000000000000000000000001
Even though they look almost the same, the bit patterns differ by exactly 1 ULP.
Why was this needed?
Previously, tests compared outputs against references using relative tolerances, like:
var abs = require( '@stdlib/math/base/special/abs' );
var delta = abs( y - expected[ i ] );
var tol = EPS * abs( expected[ i ] );
t.ok( delta <= tol, 'within tolerance. x: '+x[i]+'. y: '+y );
This worked, but was a bit clunky and sometimes tricky to tune (especially for very small or very large values).
With ULP support, we can now write simpler and more reliable checks:
var ulpdiff = require( '@stdlib/number/float64/base/ulp-difference' );
t.strictEqual( ulpdiff( y, expected[ i ] ) <= 1, true, 'returns expected value' );
This says: “the result is within 1 ULP of the expected value,” which is a very natural way to measure correctness in floating-point math.
I added ULP support for a wide range of types, including:
Float32
Complex64
/Complex128
-
Typed arrays like
Float64Array
,Float32Array
,Complex128Array
, etc.
Support for
Float64
was previously added by Athan.
This made it much easier to test both real and complex functions, across single- and double-precision, with consistent and trustworthy rules.
I also worked on improving benchmarks by fixing how random numbers were generated. Previously, random numbers were being generated inside the benchmarking loop, which interfered with the results:
bench( pkg, function benchmark( b ) {
var x;
var y;
var i;
b.tic();
for ( i = 0; i < b.iterations; i++ ) {
x = ( randu() * 10.0 ) - 5.0;
y = sind( x );
}
b.toc();
});
I helped in refactoring this so that the random values are generated once before the loop, and the benchmark only measures the function’s performance:
bench( pkg, function benchmark( b ) {
var x;
var y;
var i;
x = uniform( 100, -5.0, 5.0 );
b.tic();
for ( i = 0; i < b.iterations; i++ ) {
y = sind( x[ i % x.length ] );
}
b.toc();
});
This change makes the benchmarks more accurate, fair, and consistent because they’re no longer affected by random number generation overhead.
I also helped in refactoring examples to make them cleaner and easier to read.
We started using logEachMap
(added by Haris) instead of manually looping and printing values.
Before: we logged each result inside a loop, which was more verbose:
var randu = require( '@stdlib/random/base/randu' );
var exp = require( './../lib' );
var x;
var i;
for ( i = 0; i < 100; i++ ) {
x = ( randu() * 100.0 ) - 50.0;
console.log( 'e^%d = %d', x, exp( x ) );
}
After: we generate values with uniform
once, and use logEachMap
to neatly format all results:
var uniform = require( '@stdlib/random/array/uniform' );
var logEachMap = require( '@stdlib/console/log-each-map' );
var expm1 = require( './../lib' );
var opts = {
'dtype': 'float64'
};
var x = uniform( 100, -5.0, 5.0, opts );
logEachMap( 'e^%0.4f - 1 = %0.4f', x, expm1 );
This refactor makes the examples shorter, prettier, and more consistent.
All single-precision and double-precision implementations that are completed or in progress are tracked under the main tracking issue: Tracking Issue #649
A selection of merged implementation PRs includes:
- https://github.com/stdlib-js/stdlib/pull/7897
- https://github.com/stdlib-js/stdlib/pull/7893
- https://github.com/stdlib-js/stdlib/pull/7892
- https://github.com/stdlib-js/stdlib/pull/7868
- https://github.com/stdlib-js/stdlib/pull/7832
- https://github.com/stdlib-js/stdlib/pull/7817
- https://github.com/stdlib-js/stdlib/pull/7809
- https://github.com/stdlib-js/stdlib/pull/7808
- https://github.com/stdlib-js/stdlib/pull/7806
- https://github.com/stdlib-js/stdlib/pull/7805
- https://github.com/stdlib-js/stdlib/pull/7749
- https://github.com/stdlib-js/stdlib/pull/7741
- https://github.com/stdlib-js/stdlib/pull/7726
- https://github.com/stdlib-js/stdlib/pull/7720
- https://github.com/stdlib-js/stdlib/pull/7707
- https://github.com/stdlib-js/stdlib/pull/7619
- https://github.com/stdlib-js/stdlib/pull/7468
- https://github.com/stdlib-js/stdlib/pull/7419
- https://github.com/stdlib-js/stdlib/pull/7418
- https://github.com/stdlib-js/stdlib/pull/7408
- https://github.com/stdlib-js/stdlib/pull/7390
- https://github.com/stdlib-js/stdlib/pull/7381
- https://github.com/stdlib-js/stdlib/pull/7377
- https://github.com/stdlib-js/stdlib/pull/7371
- https://github.com/stdlib-js/stdlib/pull/7360
- https://github.com/stdlib-js/stdlib/pull/7358
- https://github.com/stdlib-js/stdlib/pull/7342
- https://github.com/stdlib-js/stdlib/pull/7341
- https://github.com/stdlib-js/stdlib/pull/7316
- https://github.com/stdlib-js/stdlib/pull/7303
- https://github.com/stdlib-js/stdlib/pull/7302
- https://github.com/stdlib-js/stdlib/pull/7263
- https://github.com/stdlib-js/stdlib/pull/7234
- https://github.com/stdlib-js/stdlib/pull/7229
- https://github.com/stdlib-js/stdlib/pull/7201
- https://github.com/stdlib-js/stdlib/pull/7158
- https://github.com/stdlib-js/stdlib/pull/7101
- https://github.com/stdlib-js/stdlib/pull/7097
- https://github.com/stdlib-js/stdlib/pull/7089
- https://github.com/stdlib-js/stdlib/pull/7081
- https://github.com/stdlib-js/stdlib/pull/7048
- https://github.com/stdlib-js/stdlib/pull/7043
- https://github.com/stdlib-js/stdlib/pull/7039
- https://github.com/stdlib-js/stdlib/pull/7022
- https://github.com/stdlib-js/stdlib/pull/7007
- https://github.com/stdlib-js/stdlib/pull/7000
- https://github.com/stdlib-js/stdlib/pull/6983
- https://github.com/stdlib-js/stdlib/pull/6965
- https://github.com/stdlib-js/stdlib/pull/6736
Note: Only merged PRs are listed here. Several others are currently in review or being updated.
Additional work on ULP difference support and related refactorings can be found in these PRs:
- https://github.com/stdlib-js/stdlib/pull/7871
- https://github.com/stdlib-js/stdlib/pull/7869
- https://github.com/stdlib-js/stdlib/pull/7690
- https://github.com/stdlib-js/stdlib/pull/7688
- https://github.com/stdlib-js/stdlib/pull/7687
- https://github.com/stdlib-js/stdlib/pull/7682
- https://github.com/stdlib-js/stdlib/pull/7649
- https://github.com/stdlib-js/stdlib/pull/7622
- https://github.com/stdlib-js/stdlib/pull/7620
- https://github.com/stdlib-js/stdlib/pull/7517
- https://github.com/stdlib-js/stdlib/pull/7473
- https://github.com/stdlib-js/stdlib/pull/7451
- https://github.com/stdlib-js/stdlib/pull/7446
Direct Commits
Most of the refactoring work for documentation fixes, benchmark updates, and example improvements were committed directly to the develop
branch without separate PRs. Link
Most of the single-precision implementations have been completed, and the same goes for the double-precision C implementations, which have been added and updated according to the latest project conventions.
All ongoing and future work continues to be tracked under the main tracking issue: Tracking Issue #649
Regarding ULP support, in addition to is-almost-equal
, the remaining work involves adding support for is-almost-same-value
.
Working across single- and double-precision implementations, ULP comparisons, benchmarks, and examples has been both rewarding and at times, surprisingly tricky.
-
Precision is picky. I learned that computers don’t always agree on what “equal” means. Figuring out when two floating-point numbers are “close enough” required digging into ULP differences. It was eye-opening to see how something as small as one bit flip could make or break a test.
-
Benchmarks need honesty. Originally, our benchmarks were testing not just the functions, but also how good the random number generator was at running inside a loop. (Spoiler: not very.) Refactoring them so randomness happened outside the loop made the results more trustworthy and taught me the importance of isolating what you’re actually measuring.
-
Examples should be friendly. Console logs with endless loops and cryptic formatting made examples harder to read. Switching to
logEachMap
made things cleaner, prettier, and honestly felt like going from “math class chalkboard” to “slides with nice fonts.”
And the most important...
-
Teamwork matters. Float64 ULP support wasn’t mine; it came from Athan.
logEachMap
was added by Haris. My part was building on and integrating those contributions. My own PRs were reviewed by Philipp, and I also reviewed PRs contributed by others that became part of my project. It was a good reminder that open source is less about lone-wolf coding and more about weaving together everyone’s work.
In short, I learned that small details (like when you generate a random number or how you compare floats) can make a big difference. And sometimes the biggest challenge isn’t the math itself. It’s convincing your code to stop doing sneaky little side quests while you’re trying to measure something else.
Contributing to stdlib through Google Summer of Code has been one of the most meaningful experiences of my undergraduate journey. It gave me the chance to bridge the gap between abstract mathematical theory and practical, production-level code, turning textbook formulas into building blocks that scientists, engineers, and developers can rely on.
Along the way, I learned that even small details, like how you generate random numbers in a benchmark or how you compare two floating-point values, can have a big impact on correctness and performance. More importantly, I discovered how open source thrives on collaboration. Each contribution by everyone became a part of a larger tapestry that I was lucky enough to help weave.
Looking ahead, I want to continue exploring the intersection of mathematics, computing, and open-source software. My next steps include diving deeper into numerical methods, scientific computing, and high-performance libraries, with the goal of contributing to projects that push the boundaries of what’s possible in research, data science, and engineering applications.
This project has shown me not just how math runs on a computer, but also how communities come together to make it run better. And that, for me, is the most exciting part of the journey still ahead.
stdlib, I’ll be back! This is only the beginning.
Thanks everyone for reading! Cheers!