def normal_lines(self, p, prec=None): """Normal lines between `p` and the ellipse. Parameters ========== p : Point Returns ======= normal_lines : list with 1, 2 or 4 Lines Examples ======== >>> from sympy import Line, Point, Ellipse >>> e = Ellipse((0, 0), 2, 3) >>> c = e.center >>> e.normal_lines(c + Point(1, 0)) [Line(Point(0, 0), Point(1, 0))] >>> e.normal_lines(c) [Line(Point(0, 0), Point(0, 1)), Line(Point(0, 0), Point(1, 0))] Off-axis points require the solution of a quartic equation. This often leads to very large expressions that may be of little practical use. An approximate solution of `prec` digits can be obtained by passing in the desired value: >>> e.normal_lines((3, 3), prec=2) [Line(Point(-38/47, -85/31), Point(9/47, -21/17)), Line(Point(19/13, -43/21), Point(32/13, -8/3))] Whereas the above solution has an operation count of 12, the exact solution has an operation count of 2020. """ p = Point(p) # XXX change True to something like self.angle == 0 if the arbitrarily # rotated ellipse is introduced. # https://github.com/sympy/sympy/issues/2815) if True: rv = [] if p.x == self.center.x: rv.append(Line(self.center, slope=oo)) if p.y == self.center.y: rv.append(Line(self.center, slope=0)) if rv: # at these special orientations of p either 1 or 2 normals # exist and we are done return rv # find the 4 normal points and construct lines through them with # the corresponding slope x, y = Dummy('x', real=True), Dummy('y', real=True) eq = self.equation(x, y) dydx = idiff(eq, y, x) norm = -1 / dydx slope = Line(p, (x, y)).slope seq = slope - norm points = [] if prec is not None: yis = solve(seq, y)[0] xeq = eq.subs(y, yis).as_numer_denom()[0].expand() try: iv = list(zip(*Poly(xeq).intervals()))[0] # bisection is safest here since other methods may miss root xsol = [ S(nroot(lambdify(x, xeq), i, solver="anderson")) for i in iv ] points = [ Point(i, solve(eq.subs(x, i), y)[0]).n(prec) for i in xsol ] except PolynomialError: pass if not points: points = solve((seq, eq), (x, y)) # complicated expressions may not be decidably real so evaluate to # check whether they are real or not points = [ Point(i).n(prec) if prec is not None else Point(i) for i in points if all(j.n(2).is_real for j in i) ] slopes = [norm.subs(zip((x, y), pt.args)) for pt in points] if prec is not None: slopes = [ i.n(prec) if i not in (-oo, oo, zoo) else i for i in slopes ] return [Line(pt, slope=s) for pt, s in zip(points, slopes)]
def normal_lines(self, p, prec=None): """Normal lines between `p` and the ellipse. Parameters ========== p : Point Returns ======= normal_lines : list with 1, 2 or 4 Lines Examples ======== >>> from sympy import Line, Point, Ellipse >>> e = Ellipse((0, 0), 2, 3) >>> c = e.center >>> e.normal_lines(c + Point(1, 0)) [Line(Point(0, 0), Point(1, 0))] >>> e.normal_lines(c) [Line(Point(0, 0), Point(0, 1)), Line(Point(0, 0), Point(1, 0))] Off-axis points require the solution of a quartic equation. This often leads to very large expressions that may be of little practical use. An approximate solution of `prec` digits can be obtained by passing in the desired value: >>> e.normal_lines((3, 3), prec=2) [Line(Point(-38/47, -85/31), Point(9/47, -21/17)), Line(Point(19/13, -43/21), Point(32/13, -8/3))] Whereas the above solution has an operation count of 12, the exact solution has an operation count of 2020. """ p = Point(p) # XXX change True to something like self.angle == 0 if the arbitrarily # rotated ellipse is introduced. # https://github.com/sympy/sympy/issues/2815) if True: rv = [] if p.x == self.center.x: rv.append(Line(self.center, slope=oo)) if p.y == self.center.y: rv.append(Line(self.center, slope=0)) if rv: # at these special orientations of p either 1 or 2 normals # exist and we are done return rv # find the 4 normal points and construct lines through them with # the corresponding slope x, y = Dummy('x', real=True), Dummy('y', real=True) eq = self.equation(x, y) dydx = idiff(eq, y, x) norm = -1/dydx slope = Line(p, (x, y)).slope seq = slope - norm points = [] if prec is not None: yis = solve(seq, y)[0] xeq = eq.subs(y, yis).as_numer_denom()[0].expand() try: iv = list(zip(*Poly(xeq).intervals()))[0] # bisection is safest here since other methods may miss root xsol = [S(nroot(lambdify(x, xeq), i, solver="anderson")) for i in iv] points = [Point(i, solve(eq.subs(x, i), y)[0]).n(prec) for i in xsol] except PolynomialError: pass if not points: points = [Point(*s) for s in solve((seq, eq), (x, y))] # complicated expressions may not be decidably real so evaluate to # check whether they are real or not points = [i.n(prec) if prec is not None else i for i in points if all(j.n(2).is_real for j in i.args)] slopes = [norm.subs(zip((x, y), pt.args)) for pt in points] if prec is not None: slopes = [i.n(prec) if i not in (-oo, oo, zoo) else i for i in slopes] return [Line(pt, slope=s) for pt,s in zip(points, slopes)]