We all remember those days in school where the maths teacher would try to show us how great trigonometry was, drawing lots of triangles,
working out angles and writing all those confusing equations;
sin, cos, tan and so on, all very puzzling.
But over the years, as we begin to understand it all, more or less, we are quite happy to press those sin
and cos
buttons on our calculator,
without giving much thought about what is going on behind the scenes.
If you are a software developer then you would have come across some maths library with a whole host of functions,
including those sin
and cos
ones.
Somewhere at some point those functions are performed; some type of algorithm must be used; some process must be followed to calculate the actual values.
But who cares, there is a library that does everything for you, right? Yes, however...
In Web Assembly there are some maths functions, add, subtract, multiply and so on, even a square root function, but for some reason,
most of the normal maths related functions you find in a standard maths library are not included.
That means no sin
or cos
functions either.
So I decided to create my own; how hard can it be; must be simple surely, right? Well, yes and no.
The first place I went looking was Wikipedia: Sine and cosine page. There is more to these functions than I would ever want to know. Still, I did find two important equations that helped to explain how it is all worked out.
The “x” angle values are in radians, not degrees, if that helps.
All simple stuff right?
No, me either to start with.
I was hoping for software algorithms not mathematical formulae.
However, if we remember our days learning maths, we can slowly work our way through this and create the functions ourselves.
To start with, we will look at the sin
function.
Looking at the diagram above, we need to do the following steps, which I have written in a software-like way, to help you understand it better.
sin(x) = x - (pow(x, 3) / factorial(3)) + (pow(x, 5) / factorial(5)) - (pow(x, 7) / factorial(7)) + (and so on, in odd numbers, switching from minusing to adding as you go)
But how long should we continue calculating? It looks like the more levels you use the greater the precision. But we have limits. A 64 bit floating point number can only be so precise and continuing too long would be a waste of time. So what is the magic number? I couldn't even begin to know, but from other examples I have found, written in C, they limit themselves to using only 9 terms, so that is what we will use too.
I will be showing you some code examples, all written in WAT, which can look a bit scary, but I have created the same functions in JavaScript to help you better understand what is going on.
(func $get_sin (param $x f64) (result f64) ;; Set locals (local $result f64) (local $term i32) (local $termResult f64) (local $term2 i32) (local $term2p1 i32) (local $fractorial f64)
This is the start of the get_sin
function.
It has an $x
f64 parameter (in radians) and returns a f64 result.
Below the function declaration is a list of local variables.
;; Set $result to the starting $x value local.get $x local.set $result
The next step is to set the $result
to the starting $x
value.
This is the starting amount set out in the sin
formula we looked at before.
The next process is to loop between $term
1 to 9.
;; Set $term to 1 i32.const 1 local.set $term ;; Loop for each $term (1 to 9) loop $for_each_term ... do $term related calculations ;; Increase the $term local.get $term i32.const 1 i32.add local.set $term ;; We need to check the $term is less than or equal to 9 local.get $term i32.const 9 i32.le_u ;; If so then continue on with the next $term br_if $for_each_term end
The looping parts increase the $term
value from 1 to 9 and perform the required calculations for each one.
For each $term
we need to do some maths.
;; Workout $term * 2 (we can shift left to do this) local.get $term i32.const 1 i32.shl local.set $term2 ;; Workout ($term * 2) + 1 local.get $term2 i32.const 1 i32.add local.set $term2p1 ;; Workout pow($x, $term2p1) local.get $x local.get $term2p1 call $math_pow local.set $termResult ;; Get the $fractorial for $term2p1 local.get $term2p1 call $get_fractorial local.set $fractorial ;; Workout $termResult /= $fractorial local.get $termResult local.get $fractorial f64.div local.set $termResult
The inner loop calculation is used to find a term's result value. You can look at this in the following way.
$term2p1 = ($term * 2) + 1; $termResult = pos($x, $term2p1) / factorial($term2p1);
The $term
value is 1, 2, 3, and so on, but we need to process it in 3!, 5!, 7!, order.
So for $term
1, we workout $term2p1
to be (1 * 2) + 1 = 3, and then use that to work out the term's result.
;; Workout if $term is odd local.get $term i32.const 1 i32.and ;; If odd if ;; Adjust the $result by removing the $termResult local.get $result local.get $termResult f64.sub local.set $result else ;; Adjust the $result by adding the $termResult local.get $result local.get $termResult f64.add local.set $result end
We now need to work out if the term is odd or even.
This is so we know if we need to minus the term result from the current sin
result or add to it.
If you look at the equation again you will see that it switches between adding and minusing to the final result.
That is all there is to it.
With the cos
function everything is similar, except you start off with the value 1.0, not the $x
parameter,
and you move along term by term in even numbers.
So that is it, all done.
Well yes, but sadly no.
After creating some testing software, comparing my sin
and cos
functions to JavaScripts Math versions, I found out that the numbers do match,
to start with, but when the degrees get closer to 360 and beyond, the results start to drift.
It seemed my journey was not yet over.
Looking over the Wikipedia page again it looks like I needed to do two more things. The first thing was to find the modulo of the given angle, so that it is between 0 and 360 degrees (nothing above or below that), and the second thing was something to do with quadrants.
Going back to those school days again, you'll hopefully remember that there are only 360 degrees in a circle. If you have more than that, then you have just gone around the circle once and you can just use another, smaller, angle to represent the same thing. For example, the angle 370 degrees is the same as 10 degrees. Also, if you go backwards around the circle, you can still represent that angle as a positive amount. For example, -30 degrees is the same as 330 degrees.
The next thing we can do is split the circle into 4 parts. In each quadrant we perform different sin or cos functions to get the result we want. In a way we are only using the sin and cos functions between 0 and 90 degrees, which helps to keep our calculations accurate. Below is a list of what we do within each quadrant.
Quadrant (in degrees) | Calculation (in degrees) |
---|---|
0 to 90 | sin($x) |
90 to 180 | cos($x - 90) |
180 to 270 | -sin($x - 180) |
270 to 360 | -cos($x - 270) |
Quadrant (in degrees) | Calculation (in degrees) |
---|---|
0 to 90 | cos($x) |
90 to 180 | -sin($x - 90) |
180 to 270 | -cos($x - 180) |
270 to 360 | sin($x - 270) |
Taking everything we have learnt so far, we can now create our final version of the sin
and cos
functions.
Let us take a look at the whole function first and then go through some of it in detail.
(func $math_sin (param $x f64) (result f64) ;; Set local variables (local $modRadian f64) ;; Mod the radian angle (0 to 360 degrees) local.get $x call $get_radian_mod local.set $modRadian ;; Check angle is less than or equal to 90 degrees local.get $modRadian f64.const 1.57079632679489661923 f64.le ;; If it is if ;; Call the sin function as it is local.get $modRadian call $get_sin ;; The result is on the stack which is this functions return result too return end ;; Check angle is less than or equal to 180 degrees local.get $modRadian f64.const 3.14159265358979323846 f64.le ;; If it is if ;; Set angle local.get $modRadian f64.const 1.57079632679489661923 f64.sub ;; Call the cos function call $get_cos ;; The result is on the stack which is this functions return result too return end ;; Check angle is less than or equal to 270 degrees local.get $modRadian f64.const 4.71238898038468985769 f64.le ;; If it is if ;; Set angle local.get $modRadian f64.const 3.14159265358979323846 f64.sub ;; Call the sin function call $get_sin ;; Negate the result f64.neg ;; The result is on the stack which is this functions return result too return end ;; Otherwise the angle is less than 360 degrees ;; Set angle local.get $modRadian f64.const 4.71238898038468985769 f64.sub ;; Call the cos function call $get_cos ;; Negate the result f64.neg ;; The result is on the stack which is this functions return result too )
We are calling the function math_sin
because it will be the function you will want to call.
We still have the get_sin
function, we are not replacing it, because we still need to call it as it is.
The first step is to get the modulo of the angle, so that the given value is between 0 and 360 degrees.
Now we check to see which quadrant the angle is in, adjust the angle to use, and then call the sin
or cos
function as required.
Both the sin
and cos
functions look similar.
You can take a look for yourself; it is all on GitHub
So there we have it.
All the tests seem to work correctly.
We have our own WAT sin
and cos
maths functions.
A fun journey indeed.