5

I want to be able to calculate the exact length of an SVG Arc. I can do all the manipulations rather easily. But, I am unsure of whether there is a solution at all or the exact implementation of the solution.

Here's the exact solution for the circumference of the ellipse. Using popular libraries is fine. I fully grasp that there are no easy solution as they will all require hypergeometric functions to be exact.

from scipy import pi, sqrt
from scipy.special import hyp2f1

def exact(a, b):
    t = ((a - b) / (a + b)) ** 2
    return pi * (a + b) * hyp2f1(-0.5, -0.5, 1, t)

a = 2.667950e9
b = 6.782819e8
print(exact(a, b))

My idea is to have this as opt-in code if you happen to have scipy installed it'll use the exact super-fancy solution, else it'll fall back to the weaker approximation code (progressively smaller line segments until error is small). The problem is the math level here is above me. And I don't know if there's some ways to specify a start and stop point for that.

Most of the approximation solutions are for ellipses, but I only want the arc. There may also be a solution unknown to me, for calculating the length of an arc on an ellipse but since the start and end position can be anywhere. It doesn't seem to be instantly viable to say a sweep angle is 15% of the total possible angle therefore it's 15% of the ellipse circumference.

A more effective less fancy arc approximation might also be nice. There are progressively better ellipse approximations but I can't go from ellipse circumference to arc length, so those are currently not helpful.


Let's say the arc parameterization is the start and end points on the ellipse. Since that's how SVG is parameterized. But, anything that isn't tautological like arc_length parameterization is a correct answer.

Tatarize
  • 10,238
  • 4
  • 58
  • 64
  • How is the target arc defined? – hidefromkgb Nov 03 '19 at 22:40
  • Either angle from center to point on ellipse, a particular start and end point on the ellipse. Let's say since I said 'svg' it's parameterized the way svg is with a start and end position on the arc. Basically anything that isn't arc_length parameterization is fine, since that'd be a bit tautological. – Tatarize Nov 04 '19 at 01:36
  • 1
    By «super-fancy solution» you mean [`scipy.special.ellipeinc`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.ellipeinc.html), I presume? – hidefromkgb Nov 04 '19 at 20:57
  • Something like that, properly implemented to easily produce the correct answer for the correct arc. The functionality of the hypergeometric functions is beyond me. That an answer can be produced seems likely, what that answer is. I dunno. – Tatarize Nov 05 '19 at 04:13
  • You probably want to look here: https://math.stackexchange.com/questions/433094/how-to-determine-the-arc-length-of-ellipse and use https://docs.scipy.org/doc/scipy/reference/integrate.html#module-scipy.integrate for the numerical integration. I don't think it can be done with `ellipeinc`. – fpbhb Nov 06 '19 at 23:59
  • 1
    Correction: it can be done with `ellipenc` by subtracting for phi1 and phi2. – fpbhb Nov 07 '19 at 00:07

1 Answers1

3

If you want to calculate this with your bare hands and the std lib, you can base your calculation on the following formula. This is only valid for two points on the upper half of the ellipse because of the acos but we're going to use it with the angles directly.

enter image description here

The calculation consists in these steps:

  1. Start with the SVG data: start point, a ,b rotation, long arc, sweep, end point
  2. Rotate the coordinate system to match the horizontal axis of the ellipse.
  3. Solve a system of 4 equations with 4 unknowns to get the centre point and the angles corresponding to the start and end point
  4. Approximate the integral by a discreet sum over small segments. This is where you could use scipy.special.ellipeinc, as suggested in the comments.

Step 2 is easy, just use a rotation matrix (note the angle rot is positive in the clockwise direction):

 m = [
        [math.cos(rot), math.sin(rot)], 
        [-math.sin(rot), math.cos(rot)]
   ]

Step 3 is very well explained in this answer. Note the value obtained for a1 is modulo pi because it is obtained with atan. That means that you need to calculate the centre points for the two angles t1 and t2 and check they match. If they don't, add pi to a1 and check again.

Step 4 is quite straightforward. Divide the interval [t1, t2] into n segments, get the value of the function at the end of each segment, time it by the segment length and sum all this up. You can try refining this by taking the value of the function at the mid-point of each segment, but I'm not sure there is much benefit to that. The number of segments is likely to have more effect on the precision.

Here is a very rough Python version of the above (please bear with the ugly coding style, I was doing this on my mobile whilst traveling )

import math

PREC = 1E-6

# matrix vector multiplication
def transform(m, p):
    return ((sum(x * y for x, y in zip(m_r, p))) for m_r in m)

# the partial integral function        
def ellipse_part_integral(t1, t2, a, b, n=100):

    # function to integrate
    def f(t):
        return math.sqrt(1 - (1 - a**2 / b**2) * math.sin(t)**2)


    start = min(t1, t2)
    seg_len = abs(t1 - t2) / n
    return - b * sum(f(start + seg_len * (i + 1)) * seg_len for i in range(n))


def ellipse_arc_length(x1, y1, a, b, rot, large_arc, sweep, x2, y2):
    if abs(x1 - x2) < PREC and abs(y1 - y2) < PREC:
        return 0

    # get rot in radians
    rot = math.pi / 180 * rot
    # get the coordinates in the rotated coordinate system
    m = [
        [math.cos(rot), math.sin(rot)], 
        [- math.sin(rot), math.cos(rot)]
    ]
    x1_loc, y1_loc, x2_loc, y2_loc = *transform(m, (x1,y1)), *transform(m, (x2,y2))

    r1 = (x1_loc - x2_loc) / (2 * a)
    r2 = (y2_loc - y1_loc) / (2 * b)

    # avoid division by 0 if both points have same y coord
    if abs(r2) > PREC:
        a1 = math.atan(r1 / r2)
    else:
        a1 = r1 / abs(r1) * math.pi / 2

    if abs(math.cos(a1)) > PREC:
        a2 = math.asin(r2 / math.cos(a1))
    else:
        a2 = math.asin(r1 / math.sin(a1))

    # calculate the angle of start and end point
    t1 = a1 + a2
    t2 = a1 - a2

    # calculate centre point coords
    x0 = x1_loc - a * math.cos(t1)
    y0 = y1_loc - b * math.sin(t1)

    x0s = x2_loc - a * math.cos(t2)
    y0s = y2_loc - b * math.sin(t2)


    # a1 value is mod pi so the centres may not match
    # if they don't, check a1 + pi
    if abs(x0 - x0s) > PREC or abs(y0 - y0s) > PREC:
        a1 = a1 + math.pi
        t1 = a1 + a2
        t2 = a1 - a2

        x0 = x1_loc - a * math.cos(t1)
        y0 = y1_loc - b * math.sin(t1)

        x0s = x2_loc - a * math.cos(t2)
        y0s = y2_loc - b * math.sin(t2)

    # get the angles in the range [0, 2 * pi]
    if t1 < 0:
        t1 += 2 * math.pi
    if t2 < 0:
        t2 += 2 * math.pi

    # increase minimum by 2 * pi for a large arc
    if large_arc:
        if t1 < t2:
            t1 += 2 * math.pi 
        else:
            t2 += 2 * math.pi

    return ellipse_part_integral(t1, t2, a, b)

print(ellipse_arc_length(0, 0, 40, 40, 0, False, True, 80, 0))

The good news is that the sweep flag doesn't matter as long as you're just looking for the length of the arc.

I'm not 100% sure the modulo pi problem is handled correctly and the implementation above may have a few bugs. Nevertheless, it gave me a good approximation of the length in the simple case of a half circle, so I dare calling it WIP. Let me know if this is worth pursuing, I can have a further look when I'll be seated at a computer. Or maybe someone can come up with a clean way of doing this in the meantime?

Jacques Gaudin
  • 15,779
  • 10
  • 54
  • 75
  • 1
    Cursory check had it give me an answer fractionally different than my point calculated answer in a fraction of a second rather than much longer. I couldn't get even good ellipse estimates working other than step fractions of lines. – Tatarize Nov 10 '19 at 23:10
  • from svg.elements import * Arc((0, 0), 40, 32, 0, False, True, (80, 0)).length() 113.4466715495692 – Tatarize Nov 10 '19 at 23:11
  • print(ellipse_arc_length(0, 0, 40, 32, 0, False, True, 80, 0)) -113.4466715558979 --- Note I changed to 32 for second radus to avoid circle arc shortcutting that length does in svg.elements – Tatarize Nov 10 '19 at 23:12
  • The error seems to spike at high eccentricities. Usually it's 8 orders of magnitude lower. 200.000009720719476 == 200.000006916086477, dif: 0.000002804632999 Arc(start=0, radius=(50+0.004901195889745281j), rotation=0, arc=True, sweep=False, end=1e-14) – Tatarize Nov 11 '19 at 17:26
  • Maybe worth increasing the number of segments in `ellipse_part_integral`. Or using a better estimate of the integral with `ellipeinc`? Does that make any difference? – Jacques Gaudin Nov 11 '19 at 18:07
  • Increasing the segments makes the error for that kind of curve drop directly related to the number of segments. If I add a 0 to the number of segments it drops the error by 1 order of magnitude. Also, the error remains weirdly constant regardless where the random minor radius falls. – Tatarize Nov 11 '19 at 18:34
  • Ok, I'm still computerless for the next few days. I presume the error is due to the way the integral is calculated. I need to compare the theoretical perimeter with the one obtained with the segments and with the results from `ellipeinc`. What kind of precision are you looking for? – Jacques Gaudin Nov 11 '19 at 18:51
  • The error and everything else is fantastic, it's only extreme eccentricities that spikes an error. I think maybe it messes up at x = 0. Here's the error for a given pancaked example as N increases. https://gist.github.com/tatarize/f102438e8665328ba0653f2ad8dc4fb9 – Tatarize Nov 11 '19 at 19:12
  • I just tried to do all random arcs with my test case and it hit a pancaked arc and threw the error over my 1e-7 limit. Which was intentionally high given the routine typically had errors less than 1e-11. Worst case, I'll live with it it's still much better in 99.9998% of cases, and much faster in all of them. – Tatarize Nov 11 '19 at 19:23
  • Sometimes determining the position at the theta and theta+delta without the length had errors that spiked that hard too. So it might just be completely on my running it against many random arcs seemed sort of gold standard but it might just be revealing some instability in precision of determining a point by angle at very high curvature. – Tatarize Nov 12 '19 at 18:41
  • I've just been playing around a bit with this and found that `n=100000` (1E5) gives me an error of 1.154E-11 quite swiftly on my computer (for the parameters that you gave). There is definitely a trick with very flat ellipses but I presume an addition of 1e5 numbers is something that a modern computer can handle fast. I presume it depends on how many arcs you are dealing with. I think the `ellipeinc` algorithm is more sophisticated. Mine is a bit of a caveman's idea! – Jacques Gaudin Nov 14 '19 at 22:40
  • I messed around with it for circular, ellipe gets 1e-14, ints get 1e-11, lines gets 1e-9. For flats, ellipe gets 1e-15, ints gets 1e-9, lines gets 1e-15. For randoms: int gets 1e-6, lines: 1e-9 (ellipe used for correct). – Tatarize Nov 15 '19 at 01:48