def __init__(self, funcs): # To minimize the number of symbolic comparisons, all function arguments # get assigned a value number. self.value_numbers = {} self.value_number_to_value = [] # Both of these maps use integer indices for arguments / functions. self.arg_to_funcset = [] self.func_to_argset = [] for func_i, func in enumerate(funcs): func_argset = OrderedSet() for func_arg in func.args: arg_number = self.get_or_add_value_number(func_arg) func_argset.add(arg_number) self.arg_to_funcset[arg_number].add(func_i) self.func_to_argset.append(func_argset)
def opt_cse(exprs, order='canonical'): """Find optimization opportunities in Adds, Muls, Pows and negative coefficient Muls Parameters ---------- exprs : list of sympy expressions The expressions to optimize. order : string, 'none' or 'canonical' The order by which Mul and Add arguments are processed. For large expressions where speed is a concern, use the setting order='none'. Returns ------- opt_subs : dictionary of expression substitutions The expression substitutions which can be useful to optimize CSE. Examples ======== >>> from sympy.simplify.cse_main import opt_cse >>> from sympy.abc import x >>> opt_subs = opt_cse([x**-2]) >>> k, v = list(opt_subs.keys())[0], list(opt_subs.values())[0] >>> print((k, v.as_unevaluated_basic())) (x**(-2), 1/(x**2)) """ from sympy.matrices.expressions import MatAdd, MatMul, MatPow opt_subs = dict() adds = OrderedSet() muls = OrderedSet() seen_subexp = set() def _find_opts(expr): if not isinstance(expr, (Basic, Unevaluated)): return if expr.is_Atom or expr.is_Order: return if iterable(expr): list(map(_find_opts, expr)) return if expr in seen_subexp: return expr seen_subexp.add(expr) list(map(_find_opts, expr.args)) if _coeff_isneg(expr): neg_expr = -expr if not neg_expr.is_Atom: opt_subs[expr] = Unevaluated(Mul, (S.NegativeOne, neg_expr)) seen_subexp.add(neg_expr) expr = neg_expr if isinstance(expr, (Mul, MatMul)): muls.add(expr) elif isinstance(expr, (Add, MatAdd)): adds.add(expr) elif isinstance(expr, (Pow, MatPow)): base, exp = expr.base, expr.exp if _coeff_isneg(exp): opt_subs[expr] = Unevaluated(Pow, (Pow(base, -exp), -1)) for e in exprs: if isinstance(e, (Basic, Unevaluated)): _find_opts(e) # split muls into commutative commutative_muls = OrderedSet() for m in muls: c, nc = m.args_cnc(cset=False) if c: c_mul = m.func(*c) if nc: if c_mul == 1: new_obj = m.func(*nc) else: new_obj = m.func(c_mul, m.func(*nc), evaluate=False) opt_subs[m] = new_obj if len(c) > 1: commutative_muls.add(c_mul) match_common_args(Add, adds, opt_subs) match_common_args(Mul, commutative_muls, opt_subs) return opt_subs
def match_common_args(func_class, funcs, opt_subs): """ Recognize and extract common subexpressions of function arguments within a set of function calls. For instance, for the following function calls:: x + z + y sin(x + y) this will extract a common subexpression of `x + y`:: w = x + y w + z sin(w) The function we work with is assumed to be associative and commutative. :arg func_class: The function class (e.g. Add, Mul) :arg funcs: A list of function calls :arg opt_subs: A dictionary of substitutions which this function may update """ # Sort to ensure that whole-function subexpressions come before the items # that use them. funcs = sorted(funcs, key=lambda f: len(f.args)) arg_tracker = FuncArgTracker(funcs) changed = OrderedSet() for i in range(len(funcs)): common_arg_candidates_counts = arg_tracker.get_common_arg_candidates( arg_tracker.func_to_argset[i], min_func_i=i + 1) # Sort the candidates in order of match size. # This makes us try combining smaller matches first. common_arg_candidates = OrderedSet( sorted(common_arg_candidates_counts.keys(), key=lambda k: (common_arg_candidates_counts[k], k))) while common_arg_candidates: j = common_arg_candidates.pop(last=False) com_args = arg_tracker.func_to_argset[i].intersection( arg_tracker.func_to_argset[j]) if len(com_args) <= 1: # This may happen if a set of common arguments was already # combined in a previous iteration. continue # For all sets, replace the common symbols by the function # over them, to allow recursive matches. diff_i = arg_tracker.func_to_argset[i].difference(com_args) if diff_i: # com_func needs to be unevaluated to allow for recursive matches. com_func = Unevaluated( func_class, arg_tracker.get_args_in_value_order(com_args)) com_func_number = arg_tracker.get_or_add_value_number(com_func) arg_tracker.update_func_argset( i, diff_i | OrderedSet([com_func_number])) changed.add(i) else: # Treat the whole expression as a CSE. # # The reason this needs to be done is somewhat subtle. Within # tree_cse(), to_eliminate only contains expressions that are # seen more than once. The problem is unevaluated expressions # do not compare equal to the evaluated equivalent. So # tree_cse() won't mark funcs[i] as a CSE if we use an # unevaluated version. com_func = funcs[i] com_func_number = arg_tracker.get_or_add_value_number(funcs[i]) diff_j = arg_tracker.func_to_argset[j].difference(com_args) arg_tracker.update_func_argset( j, diff_j | OrderedSet([com_func_number])) changed.add(j) for k in arg_tracker.get_subset_candidates(com_args, common_arg_candidates): diff_k = arg_tracker.func_to_argset[k].difference(com_args) arg_tracker.update_func_argset( k, diff_k | OrderedSet([com_func_number])) changed.add(k) if i in changed: opt_subs[funcs[i]] = Unevaluated( func_class, arg_tracker.get_args_in_value_order( arg_tracker.func_to_argset[i])) arg_tracker.stop_arg_tracking(i)
def convex_hull(*args, **kwargs): """The convex hull surrounding the Points contained in the list of entities. Parameters ========== args : a collection of Points, Segments and/or Polygons Returns ======= convex_hull : Polygon if ``polygon`` is True else as a tuple `(U, L)` where ``L`` and ``U`` are the lower and upper hulls, respectively. Notes ===== This can only be performed on a set of points whose coordinates can be ordered on the number line. References ========== [1] https://en.wikipedia.org/wiki/Graham_scan [2] Andrew's Monotone Chain Algorithm (A.M. Andrew, "Another Efficient Algorithm for Convex Hulls in Two Dimensions", 1979) http://geomalgorithms.com/a10-_hull-1.html See Also ======== sympy.geometry.point.Point, sympy.geometry.polygon.Polygon Examples ======== >>> from sympy.geometry import Point, convex_hull >>> points = [(1, 1), (1, 2), (3, 1), (-5, 2), (15, 4)] >>> convex_hull(*points) Polygon(Point2D(-5, 2), Point2D(1, 1), Point2D(3, 1), Point2D(15, 4)) >>> convex_hull(*points, **dict(polygon=False)) ([Point2D(-5, 2), Point2D(15, 4)], [Point2D(-5, 2), Point2D(1, 1), Point2D(3, 1), Point2D(15, 4)]) """ from .entity import GeometryEntity from .point import Point from .line import Segment from .polygon import Polygon polygon = kwargs.get("polygon", True) p = OrderedSet() for e in args: if not isinstance(e, GeometryEntity): try: e = Point(e) except NotImplementedError: raise ValueError( "%s is not a GeometryEntity and cannot be made into Point" % str(e)) if isinstance(e, Point): p.add(e) elif isinstance(e, Segment): p.update(e.points) elif isinstance(e, Polygon): p.update(e.vertices) else: raise NotImplementedError("Convex hull for %s not implemented." % type(e)) # make sure all our points are of the same dimension if any(len(x) != 2 for x in p): raise ValueError("Can only compute the convex hull in two dimensions") p = list(p) if len(p) == 1: return p[0] if polygon else (p[0], None) elif len(p) == 2: s = Segment(p[0], p[1]) return s if polygon else (s, None) def _orientation(p, q, r): """Return positive if p-q-r are clockwise, neg if ccw, zero if collinear.""" return (q.y - p.y) * (r.x - p.x) - (q.x - p.x) * (r.y - p.y) # scan to find upper and lower convex hulls of a set of 2d points. U = [] L = [] try: p.sort(key=lambda x: x.args) except TypeError: raise ValueError("The points could not be sorted.") for p_i in p: while len(U) > 1 and _orientation(U[-2], U[-1], p_i) <= 0: U.pop() while len(L) > 1 and _orientation(L[-2], L[-1], p_i) >= 0: L.pop() U.append(p_i) L.append(p_i) U.reverse() convexHull = tuple(L + U[1:-1]) if len(convexHull) == 2: s = Segment(convexHull[0], convexHull[1]) return s if polygon else (s, None) if polygon: return Polygon(*convexHull) else: U.reverse() return (U, L)
def convex_hull(*args, **kwargs): """The convex hull surrounding the Points contained in the list of entities. Parameters ========== args : a collection of Points, Segments and/or Polygons Returns ======= convex_hull : Polygon if ``polygon`` is True else as a tuple `(U, L)` where ``L`` and ``U`` are the lower and upper hulls, respectively. Notes ===== This can only be performed on a set of points whose coordinates can be ordered on the number line. References ========== [1] https://en.wikipedia.org/wiki/Graham_scan [2] Andrew's Monotone Chain Algorithm (A.M. Andrew, "Another Efficient Algorithm for Convex Hulls in Two Dimensions", 1979) http://geomalgorithms.com/a10-_hull-1.html See Also ======== sympy.geometry.point.Point, sympy.geometry.polygon.Polygon Examples ======== >>> from sympy.geometry import Point, convex_hull >>> points = [(1, 1), (1, 2), (3, 1), (-5, 2), (15, 4)] >>> convex_hull(*points) Polygon(Point2D(-5, 2), Point2D(1, 1), Point2D(3, 1), Point2D(15, 4)) >>> convex_hull(*points, **dict(polygon=False)) ([Point2D(-5, 2), Point2D(15, 4)], [Point2D(-5, 2), Point2D(1, 1), Point2D(3, 1), Point2D(15, 4)]) """ from .entity import GeometryEntity from .point import Point from .line import Segment from .polygon import Polygon polygon = kwargs.get('polygon', True) p = OrderedSet() for e in args: if not isinstance(e, GeometryEntity): try: e = Point(e) except NotImplementedError: raise ValueError('%s is not a GeometryEntity and cannot be made into Point' % str(e)) if isinstance(e, Point): p.add(e) elif isinstance(e, Segment): p.update(e.points) elif isinstance(e, Polygon): p.update(e.vertices) else: raise NotImplementedError( 'Convex hull for %s not implemented.' % type(e)) # make sure all our points are of the same dimension if any(len(x) != 2 for x in p): raise ValueError('Can only compute the convex hull in two dimensions') p = list(p) if len(p) == 1: return p[0] if polygon else (p[0], None) elif len(p) == 2: s = Segment(p[0], p[1]) return s if polygon else (s, None) def _orientation(p, q, r): '''Return positive if p-q-r are clockwise, neg if ccw, zero if collinear.''' return (q.y - p.y)*(r.x - p.x) - (q.x - p.x)*(r.y - p.y) # scan to find upper and lower convex hulls of a set of 2d points. U = [] L = [] try: p.sort(key=lambda x: x.args) except TypeError: raise ValueError("The points could not be sorted.") for p_i in p: while len(U) > 1 and _orientation(U[-2], U[-1], p_i) <= 0: U.pop() while len(L) > 1 and _orientation(L[-2], L[-1], p_i) >= 0: L.pop() U.append(p_i) L.append(p_i) U.reverse() convexHull = tuple(L + U[1:-1]) if len(convexHull) == 2: s = Segment(convexHull[0], convexHull[1]) return s if polygon else (s, None) if polygon: return Polygon(*convexHull) else: U.reverse() return (U, L)
def match_common_args(func_class, funcs, opt_subs): """ Recognize and extract common subexpressions of function arguments within a set of function calls. For instance, for the following function calls:: x + z + y sin(x + y) this will extract a common subexpression of `x + y`:: w = x + y w + z sin(w) The function we work with is assumed to be associative and commutative. :arg func_class: The function class (e.g. Add, Mul) :arg funcs: A list of function calls :arg opt_subs: A dictionary of substitutions which this function may update """ # Sort to ensure that whole-function subexpressions come before the items # that use them. funcs = sorted(funcs, key=lambda f: len(f.args)) arg_tracker = FuncArgTracker(funcs) changed = OrderedSet() for i in range(len(funcs)): common_arg_candidates_counts = arg_tracker.get_common_arg_candidates( arg_tracker.func_to_argset[i], min_func_i=i + 1) # Sort the candidates in order of match size. # This makes us try combining smaller matches first. common_arg_candidates = OrderedSet(sorted( common_arg_candidates_counts.keys(), key=lambda k: (common_arg_candidates_counts[k], k))) while common_arg_candidates: j = common_arg_candidates.pop(last=False) com_args = arg_tracker.func_to_argset[i].intersection( arg_tracker.func_to_argset[j]) if len(com_args) <= 1: # This may happen if a set of common arguments was already # combined in a previous iteration. continue # For all sets, replace the common symbols by the function # over them, to allow recursive matches. diff_i = arg_tracker.func_to_argset[i].difference(com_args) if diff_i: # com_func needs to be unevaluated to allow for recursive matches. com_func = Unevaluated( func_class, arg_tracker.get_args_in_value_order(com_args)) com_func_number = arg_tracker.get_or_add_value_number(com_func) arg_tracker.update_func_argset(i, diff_i | OrderedSet([com_func_number])) changed.add(i) else: # Treat the whole expression as a CSE. # # The reason this needs to be done is somewhat subtle. Within # tree_cse(), to_eliminate only contains expressions that are # seen more than once. The problem is unevaluated expressions # do not compare equal to the evaluated equivalent. So # tree_cse() won't mark funcs[i] as a CSE if we use an # unevaluated version. com_func = funcs[i] com_func_number = arg_tracker.get_or_add_value_number(funcs[i]) diff_j = arg_tracker.func_to_argset[j].difference(com_args) arg_tracker.update_func_argset(j, diff_j | OrderedSet([com_func_number])) changed.add(j) for k in arg_tracker.get_subset_candidates( com_args, common_arg_candidates): diff_k = arg_tracker.func_to_argset[k].difference(com_args) arg_tracker.update_func_argset(k, diff_k | OrderedSet([com_func_number])) changed.add(k) if i in changed: opt_subs[funcs[i]] = Unevaluated(func_class, arg_tracker.get_args_in_value_order(arg_tracker.func_to_argset[i])) arg_tracker.stop_arg_tracking(i)