8

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...

Avatar
  • 14,622
  • 9
  • 119
  • 198
  • 5
    Cubic equations can have up to three roots. A negative or complex radius makes no physical sense: you need a real, positive root. This suggests to me that you either chose the wrong root or your choice of parameters makes no physical sense. – duffymo Aug 15 '14 at 16:01
  • This Mozilla article describes NaN (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN) which may help to solve your problem. –  Aug 15 '14 at 16:03
  • 1
    You are trying to solve a cubic. Thus you have up to three possible answers. Not all the possible answers wioll be real, but the one you are interested in will be. What are the chances of getting two complex parts which add to a real? Can you just ignore those answers as unreal - at least if one of the answers is real? – Adam Aug 15 '14 at 16:03
  • Ah, I start to understand. I check all 3 equations given by wolframalpha. If I get a NaN, I ignore it, if I get a real value, I can assign it to the radius. – Avatar Aug 15 '14 at 16:06
  • Then arises the question how do I calculate the 2nd and 3rd formulas for the radius that contain the complex numbers, e.g. [third solution for r](http://i.imgur.com/aTmdqTZ.png) that holds `i√3`, using Javascript. – Avatar Aug 15 '14 at 16:10
  • 1
    Seems like this is a math question more than a programming question. – Derek 朕會功夫 Aug 15 '14 at 16:11
  • @EchtEinfachTV You may be interested in http://mathjs.org/, and in [this question](http://stackoverflow.com/q/15399340/1883647). – ajp15243 Aug 15 '14 at 16:14
  • Hmm.. It looks like you may need to find a javascript math library which supports complex numbers. Alternatively (possible easier) write a server-side script in your langauge of choice to do the calculation and make an ajax call to get the radius. – Adam Aug 15 '14 at 16:21
  • Possible duplicate of [this question](http://stackoverflow.com/questions/15399340/how-to-calculate-with-imaginary-numbers-in-javascript) Albeit more complex (no pun intended) – Adam Aug 15 '14 at 16:38
  • 1
    @Adam: I do not want to ignore the complex parts, I would like to know all 3 exact answers. This is why I need to **prevent the NaN** and transform the value to a complex number. `Math.sqrt(-9)` should result in `3*i`. Since Javascript does not include calculating with complex numbers, I am searching for an easy solution. Furthermore, operations +-*/ with complex numbers should be possible. I guess we could use vectors here somehow. I have found this online tool http://www.1728.org/cubic.htm - here they use JS to calculate complex, but they **don't deal** with `NaN` as it seems! – Avatar Aug 16 '14 at 21:38
  • 1
    Instead they check `h = (g²/4) + (f³/27)` to determine if the result is complex or not, see [on their site](http://www.1728.org/cubic2.htm). Well, this is one way to solve cubic equations. The solution for my equation above, however, let's say "in the direct way" is still not found. @all: The question is still open. – Avatar Aug 16 '14 at 21:40
  • @Derek朕會功夫 I disagree. OP understands the math involved. It is a programming question because OP is trying to handle complex numbers in a programming environment that does not handle them natively. – chiliNUT Aug 16 '14 at 22:57
  • Hm... How you get first formula for `r` from `V` and `A` formulas? I can only find `r = - 2 * V * h / ( 2 * V - A * h )`. I can't find `r` without `h`. Can you explain? – ostapische Aug 16 '14 at 23:31
  • @ostapische You take the formulas `h = r / (A·r/(2·V)-1)` and `h = A/(2πr) - r`. Then `h = h` and solve for r. – Avatar Aug 17 '14 at 07:05
  • 1
    For all math enthusiasts, here is a [complete way of calculation](http://bit.ly/cylcalc) how to get the cubic formula from above: `0 = r^3 + A/(-2*pi)*r + V/pi`. – Avatar Oct 03 '14 at 10:44

3 Answers3

10

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.

chiliNUT
  • 18,989
  • 14
  • 66
  • 106
  • 1
    @chiliNUT Incredible answer. This will not only help on this specific problem but gives newcomers to NaN and complex numbers (and mathjs) a head start! Perfect shot. Thanks^10 – Avatar Aug 19 '14 at 07:50
  • 1
    And by the way, great that you have seen the common term, marked and simplified it. And pointing out `spaces implicitly mean to multiply terms` was essential. – Avatar Aug 19 '14 at 08:27
  • 1
    Cannot upvote this amazing answer hard enough. Good job! – jamesh Apr 25 '15 at 03:10
  • Thanks! Its probably my favorite answer on here. – chiliNUT Apr 25 '15 at 03:28
-1

use isNaN() to test if the value is NaN without errors

RAEC
  • 332
  • 1
  • 8
-2

Edit2: sorry I misread the original question, you cannot get a square root of a negative number in JS. Multiplying by -1 and then multiplying by 'i' (square root of -1) is the way it would be done in maths iirc, but that is not available in JS either.

Edit: Pi is Math.PI in Javascript, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math for all the possible functions and properties, including various constants.

You can't continue with calculations once you've got NaN, but you can find where you're getting NaN and fix.

You'll be wanting these:

parseInt(variablename, 10);

Converts the value of 'variablename' into an integer. The second argument is the radix, which here I've put as 10 as I'm assuming you're using decimals. If you didn't have this it might try to convert it depending on what it gets, so something like parseInt('08') is not the same as 8! More: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt

the string '10px' will get changed to 10 by parseInt, but 'foo20' will return NaN.

parseFloat(variableName);

converts the value of 'variablename' into a floating point number.

Dawn
  • 941
  • 6
  • 11
  • 1
    The OP says he is getting NaN because he is trying to take a square root of a negative number, not when parsing strings to integers. Everything you say is true (and the radix is a bit of a gotcha in js that is good to mention) but I don't see the relevance to this question. – Adam Aug 15 '14 at 16:09