Kai Noack Kai Noack - 25 days ago 8
Javascript Question

How to overcome the NaN and continue the calculation in Javascript - Negative value for radicand of sqrt

I have set up a js / three.js program for calculating cylinders by only two given values.

The only calculation that is quite difficult happens when volume and surface are given. From both values I need to calculate the radius or the height.

To recall the formulas:

Volume V = π·r²·h

Surface A = 2·π·r·(r+h)

If you do the math, you will get the cubic formula:

0 = r^3 + A/(-2*pi)*r + V/pi


which I honestly could not solve, so I used wolframalpha that gives this result for the radius r:

formula cylinder for r by V and A

Note: There are three formulas for r, this is the first of them. See wolframalpha.

By trying to implement this equation in Javascript I realized that the radicand of
√(54πV^2 - A^3)
is negative and Javascript is returning
NaN
.

This leads to my question: How can I overcome the
NaN
and continue the calculation - should I use complex numbers, how? What workarounds have you used? Can I just multiply the radicand by
*(-1)
, remember this value and consider it later on?

I am a bit lost here, this is the first time I have to defeat NaN :-)

Thanks in advance for all your tips, advices, solutions and code.


Edit (reaching the goal): Is someone living on this earth feasable of solving the three equations in Javascript and can post his code? I have generally googled "calculate cylinder by surface and volume" and it seems nobody has done it before...

Answer

So, discarding negative radicands is not the best solution because you still might rule out valid real solutions, since the radicands in the second term could cancel out the imaginary part from the first term. Additionally, the 2nd and 3rd roots have i in their formula, so you are sort of forced to deal with complex numbers there. These roots should also never be thrown out, because even for cubics with 3 real roots, 2 of the 3 roots are still calculated using complex numbers!

Dealing with complex numbers is something that

  1. JavaScript does not handle natively, and
  2. is non-trivial enough that you would not want to implement it yourself. That's where. math.js comes in.

Read here to learn about math.js. But for this question, you just need to know about one method. math.js does its work through its math object, and the method we are concerned with is math.eval(expr,scope) which will evaluate a string expression expr and use the variable assignments specified in scope.

So, initially looking at the 3 roots provided by wolfram:

roots

They are a little bit unwieldy. Upon closer inspection, they all have a common term:

common term

That term is an expression of A and V, so lets move that to a function of A and V, called f

f

So substitute that term for our new function f, and now the roots are a lot more manageable:

substituted with f

So, lets gets started. You just need to include math.js at the top of your project:

<script type="text/javascript" language="JavaScript" 
src="http://cdnjs.cloudflare.com/ajax/libs/mathjs/0.26.0/math.min.js"></script>

Onto the script. First, define the f function described above:

        var f = function(A, V) {
            var scope = {A: A, V: V};
            var expr = math.eval(
                    '(sqrt(6) pi^(3/2) sqrt(54 pi V^2-A^3)-18 pi^2 V)^(1/3)'
                     ,scope);
            return expr;
        };

Note that: spaces implicitly mean to multiply terms, ie a b=a*b, and that the cube root of a number n is equivalent to n^(1/3)

So f will evaluate our expr using the arguments A and V for area and volume.

Now we can use that to define functions that will generate the 3 roots, r1, r2, and r3, given any area A and volume V

        var r1 = function(A, V) {
            var scope = {A: A, V: V, f: f(A, V)};
            var expr = math.eval(
                    'A/(6^(1/3) f)+f/(6^(2/3) pi)'
                    , scope);
            return expr;
        };

        var r2 = function(A, V) {
            var scope = {A: A, V: V, f: f(A, V)};
            var expr = math.eval(
                    '-((1+i sqrt(3)) A)/(2*6^(1/3) f) - ((1-i sqrt(3)) f)/(2*6^(2/3) pi)'
                    , scope);
            return expr;
        };

        var r3 = function(A, V) {
            var scope = {A: A, V: V, f: f(A, V)};
            var expr = math.eval(
                    '-((1-i sqrt(3)) A)/(2*6^(1/3) f) - ((1+i sqrt(3)) f)/(2*6^(2/3) pi)'
                    , scope);
            return expr;
        };

So now, lets test it out. Using the values from the link you provided, say the radius r is 2, and the height h is 1.5

Then, the volume V=pi*r^2 is approximately 18.85, and the surface area A=2pi*r(r+h) is approximately 43.982. Using the methods defined above, we can get the roots.

Note that result is the result of evaluating r^3 + A/(-2*pi)*r + V/pi using the given root, so if the result is 0, the root was calculated correctly. Actual values will be accurate to about ~15 digits due to round off error.

var A, V, r, scope;
A = 43.982, V = 18.85;

        //test r1
        r = r1(A, V);

        scope = {A: A, V: V, r: r};
        console.log('r1', r, 'result: ',math.eval('r^3+A/(-2pi) r+V/pi', scope));
        //r1 1.9999528096882697 - 2.220446049250313e-16i result: 4.440892098500626e-15 - 1.1101077869995534e-15i
        //round to 5 decimals:
        console.log('rounded r1:', math.round(r,5), 'rounded result: ',math.round(math.eval('r^3+A/(-2pi) r+V/pi', scope),5));
        //rounded r1:1.99995 rounded result: 0


        //test r2
        r = r2(A, V);

        scope = {A: A, V: V, r: r};
        console.log('r2', r,'result: ', math.eval('r^3+A/(-2pi) r+V/pi', scope));
        //r2 -2.9999999737884457 - 1.6653345369377348e-16i result: 2.6645352591003757e-15 - 8.753912513083332e-15i
        //round to 5 decimals:
        console.log('rounded r2:', math.round(r,5),'rounded result: ', math.round(math.eval('r^3+A/(-2pi) r+V/pi', scope),5));
        //rounded r2: -3 rounded result: 0

        //test r3
        r = r3(A, V);

        scope = {A: A, V: V, r: r};
        console.log('r3', r, 'result: ',math.eval('r^3+A/(-2pi) r+V/pi', scope));
        //r3 1.000047164100176 + 4.440892098500626e-16i result: -1.7762101637478832e-15i
        //round to 5 decimals
        console.log('rounded r3:', math.round(r,5), 'rounded result: ',math.round(math.eval('r^3+A/(-2pi) r+V/pi', scope),5));
        //rounded r3: 1.00005 rounded result: 0

And this agrees with the roots provided by wolfram alpha. {-3,1.00005,1.99995}

Also note that most of the results of console.log() of math.js objects will log the entire object, something like this:

r1 Complex { re=1.9999528096882697, im=-2.220446049250313e-16, toPolar=function(), more...} 

So I applied a toString() to the results I included, for readability.

Comments