// cc -o ellipse-perimeter -Wall ellipse-perimeter.c -lm // The code below calculates the 'exact' perimeter of an ellipse, at least to the // precision of the machine word, by evaluating the sum of an infinite series, and // stopping once a sum term is smaller than the lowest bit in the floating point // representation. Surprisingly very few terms are needed (7 for "long double") // and the calculation is quite efficient. The floating point type can be changed // by the programmer to "float" or "double" if desired. // I started writing this after reading somewhere that there was no exact solution to // the elliptical perimeter problem. In hindsight I realise now they meant no exact // solution that does not involve an infinite series, or that if you are using an // infinite series to calculate the perimeter, it is invalid because no-one can // ever fully evaluate an infinite series. However this is academic because our // computers cannot calculate with infinite precision and a converging infinite series // *can* be evaluated down to the last bit of precision supported by our hardware. // // So ... my approach to calculating the perimeter of an ellipse was this: // // We know the perimeter of a circle (2*Pi*r) and we know that any ellipse can // be formed by applying a shear on one axis to a circle and then rotating // the axes. The rotation is trivial and we can make our calcuation simpler // by assuming an axis-aligned ellipse. // // When a circle of radius 'r' is stretched uniformly in one dimension by a // factor of k, it transforms into an ellipse with semi-major axis a = kr and // semi-minor axis b = r. // // While searching for an equation to calculate the effect of the shear, I found // the web page: https://www.mathsisfun.com/geometry/ellipse-perimeter.html // (which gave a formula for the perimeter of an ellipse directly and doesn't // mention the shear transformation) so I ended up implementing that instead! // (using the infinite series #2 from that page) // // To compute the perimeter using the information from www.mathsisfun.com, first we // calculate h: // // (a-b)^2 // h = ------- // (a+b)^2 // // Then we use the sum of the infinite series: // // p = Pi*(a+b) * Sigma(n={0,Inf}, Binom(0.5,n)^2*h^n) // // which expands to: // // 1 1 1 25 49 441 // p = Pi*(a+b) * ( --- * h^0 + --- * h^1 + --- * h^2 + --- * h^4 + ----- * h^5 + ----- * h^6 + ... ) // 1 4 64 256 16384 65536 // // We will add terms until the terms are so small that they no longer affect the cumulative sum. // This is sure to happen as the series is known to converge monotonically. // // To calculate this in a C program we need to generate the numerators and denominators of this series. // Because of the size of the numbers generated in these sequences we must use floating point data types // rather than integers. // // The denominators are relatively easy: the numbers are all of the form 4^n and taking log_4 of these numbers // reveals the n's to be: 0, 1, 3, 4, 7, 8, 10, 11, 15, // // This series (which we can find at: https://oeis.org/A005187 ) can be formed by a recurrence relation, // A005187(n) = n + A005187(floor(n/2)); // // The numerators are a little more tricky... // // We find the sequence at https://oeis.org/A056981 where it is defined in terms of another series as: // // A056981(n) = A002596(n)^2. // // The other series is found at https://oeis.org/A002596 where it is defined as: // // A002596(n+2) = C(n+1)/2^k(n+1) // // when n >= 0. C(n) = A000108(n), k(n) = A048881(n). ('C()' are the Catalan numbers) // // ( i.e. expressed alternatively, we have: A002596(n+2) = A000108(n+1)/2^A048881(n+1) ) // // These are defined in turn by: // // A000108(n) = (2*n)!/(n!*(n+1)!); // However we will use the gamma function here instead of factorial, because our data type is floating point: // A000108(n) = f(2*n)/(f(n)*f(n+1)); f(n) is available in C as 'tgamma(n+1)'. // // and // // A048881(n) = A000120(n+1) - 1 // // Once again we need another series: // // A000120: 1's-counting sequence: number of 1's in binary expansion of n (or the binary weight of n). // Since our data is floating point we can't use bit operations to determine this so our function 'wt' // uses division by two and remainder modulo two, to perform an analagous computation. // // Putting these all together does indeed reproduce the series 1, 1, 1, 1, 25, 49, 441, 1089, 184041, etc. // // Having worked all that out, I also found an approximate formula for the perimeter of the ellipse and // used the exact code to check it. The results of the approximation are very accurate and the approximation // can probably be used for all real-world applications. #include <stdio.h> #include <stdlib.h> #include <float.h> #include <math.h> #define LONG_DOUBLE 1 #define DOUBLE 2 #define FLOAT 3 #ifndef PRECISION // can be overridden on command-line with -DPRECISION="FLOAT" #define PRECISION LONG_DOUBLE // Pick a precision! #endif #define NAME1(x) #x #define NAME(x) NAME1(x) #if PRECISION == LONG_DOUBLE #undef PRECISION #define PRECISION long double #define FORMAT "%.16Lf" #define POW(x,y) powl(x,y) #define FABS(x) fabsl(x) #define SQRT(x) sqrtl(x) #define TGAMMA(x) tgammal(x) #elif PRECISION == DOUBLE #undef PRECISION #define PRECISION double #define FORMAT "%.16lf" #define POW(x,y) pow(x,y) #define FABS(x) fabs(x) #define SQRT(x) sqrt(x) #define TGAMMA(x) tgamma(x) #elif PRECISION == FLOAT #undef PRECISION #define PRECISION float #define FORMAT "%.16f" #define POW(x,y) powf(x,y) #define FABS(x) fabsf(x) #define SQRT(x) sqrtf(x) #define TGAMMA(x) tgammaf(x) #endif #define pow(x,y) ((y)<=0.0 ? 1.0 : POW(x,y)) // correct a problem with pow. X^0 should always be 1. #define fabs(x) FABS(x) #define sqrt(x) SQRT(x) #define tgamma(x) TGAMMA(x) static PRECISION Den(PRECISION n) { // calculate the denominator of the coefficient of each term // see https://oeis.org/A005187 if (n == 0.0) return 0.0; return Den(floor(n/2)) + n; } static PRECISION f(PRECISION x) { PRECISION gamma = tgamma(fabs(x) + 1.0); if (isnan(gamma) || isinf(gamma)) { fprintf(stderr, "f(" FORMAT ") = NAN -> 1.0\n", x); return 1.0; } // fprintf(stderr, "f(" FORMAT ") = " FORMAT "\n", x, gamma); return gamma; } static PRECISION C(PRECISION n) { PRECISION r = floor(f(2.0*n))/(floor(f(n))*floor(f(n+1.0))); //fprintf(stderr, "C(" FORMAT ") -> " FORMAT "\n", n, r); return round(r); } static PRECISION wt(PRECISION n) { // wt(n) = 1's-counting sequence: number of 1's in binary expansion of n PRECISION tot = 0.0; if (n < 0.0) n = -n; while (n > 0.25) { if (round(n - floor(n/2.0)*2.0) == 1.0) tot++; n = floor(n/2.0); } //fprintf(stderr, "wt(" FORMAT ") -> " FORMAT "\n", n, tot); return tot; } static PRECISION k(PRECISION n) { if (n <= 0.0) return 1.0; PRECISION r1 = wt(n+1.0); PRECISION r = r1 - 1.0; //fprintf(stderr, "k(" FORMAT ") -> wt(" FORMAT ")-1.0 -> " FORMAT " - 1.0 -> " FORMAT "\n", n, n+1.0, r1, r); return r; } static PRECISION a(PRECISION n) { PRECISION r, t1, t2, tk; n = n-2.0; t1=C(n+1.0); tk=k(n+1.0); t2=pow(2.0, tk); // n >= 0; where C(n) = A000108(n), k(n) = A048881(n) r = round(t1/t2); //fprintf(stderr, "a(" FORMAT ") -> C(" FORMAT ")/pow(2, k(" FORMAT ")) -> " FORMAT "/pow(2, " FORMAT ") -> " FORMAT "/" FORMAT " -> " FORMAT "^2 -> " FORMAT "\n", // n, n+1, n+1, t1, tk, t1, t2, r, r*r); return r*r; } static PRECISION Num(int n) { // calculate the numerator of the coefficient of each term. see https://oeis.org/A056981 PRECISION r; r = a(n); //fprintf(stderr, "COMPARE: " FORMAT " vs " FORMAT "\n", lookup[n], a(n)); return r; } // Axis-aligned. If not, rotate it first until it *is* axis-aligned. // If the input ellipse is the result of a shear, use Nate's formula // to convert to a rotation first. Need to experiment to determine // if a>b or b>a leads to a more accurate or quicker-to-compute result. PRECISION exact_ellipse_perimeter(PRECISION a, PRECISION b) { int term; if (a == 0.0) return b*4.0; // Special-case for ellipse flattened to a line! if (b == 0.0) return a*4.0; PRECISION h = ((a - b) * (a - b)) / ((a + b) * (a + b)); PRECISION p = M_PI * (a + b); PRECISION result, numerator, denominator, coefficient, infinite_sum = 0.0, prev_result = 0.0; // for (term = 0; term < sizeof(lookup)/sizeof(lookup[0]); term++) { for (term = 0; ; term++) { numerator = Num(term); denominator = pow(4.0, Den(term)); coefficient = numerator/denominator; infinite_sum += coefficient * pow(h, term); result = p * infinite_sum; if (result == prev_result) { fprintf(stderr, "\nWe hit the limit of machine accuracy (using type \"" NAME(PRECISION) "\") after %d terms in the expansion of the infinite series\n", term-1); break; } prev_result = result; } return result; } PRECISION approximate_ellipse_perimeter(PRECISION a, PRECISION b) { // This is pretty accurate and can probably be used in every real-world application. PRECISION h = pow((a-b)/(a+b), 2.0); return M_PI*(a + b)*(1 + (3*h)/(10 + sqrt(4 - 3*h))); } int main(int argc, char **argv) { // Use the 'exact' calculation to verify the approximate calculation above. PRECISION a = 1.2, b = 1.0; // later: get from argv instead. PRECISION approximate_p = approximate_ellipse_perimeter(a,b); PRECISION exact_p = exact_ellipse_perimeter(a,b); PRECISION err = fabs((exact_p - approximate_p)/exact_p); fprintf(stderr, "\nThe perimeter of an ellipse whose major axis is rx=" FORMAT " and ry=" FORMAT " is " FORMAT "\n", a, b, exact_p); fprintf(stderr, "\nA good approximation to the perimeter is " FORMAT " with error=" FORMAT "%%\n", approximate_p, err*100.0); fprintf(stderr, "\nThe cruder approximation of 2 * PI * sqrt( (rx + ry) / 2 ) is " FORMAT " and should never be used!\n\n", 2 * M_PI * sqrt( (a + b) / 2 )); exit(EXIT_SUCCESS); return 0; }