Buckets:
MisterAI/LocalAI_Demo_backends / cpu-diffusers.upgrade-tmp /venv /lib /python3.10 /site-packages /sympy /plotting /series.py
| ### The base class for all series | |
| from collections.abc import Callable | |
| from sympy.calculus.util import continuous_domain | |
| from sympy.concrete import Sum, Product | |
| from sympy.core.containers import Tuple | |
| from sympy.core.expr import Expr | |
| from sympy.core.function import arity | |
| from sympy.core.sorting import default_sort_key | |
| from sympy.core.symbol import Symbol | |
| from sympy.functions import atan2, zeta, frac, ceiling, floor, im | |
| from sympy.core.relational import (Equality, GreaterThan, | |
| LessThan, Relational, Ne) | |
| from sympy.core.sympify import sympify | |
| from sympy.external import import_module | |
| from sympy.logic.boolalg import BooleanFunction | |
| from sympy.plotting.utils import _get_free_symbols, extract_solution | |
| from sympy.printing.latex import latex | |
| from sympy.printing.pycode import PythonCodePrinter | |
| from sympy.printing.precedence import precedence | |
| from sympy.sets.sets import Set, Interval, Union | |
| from sympy.simplify.simplify import nsimplify | |
| from sympy.utilities.exceptions import sympy_deprecation_warning | |
| from sympy.utilities.lambdify import lambdify | |
| from .intervalmath import interval | |
| import warnings | |
| class IntervalMathPrinter(PythonCodePrinter): | |
| """A printer to be used inside `plot_implicit` when `adaptive=True`, | |
| in which case the interval arithmetic module is going to be used, which | |
| requires the following edits. | |
| """ | |
| def _print_And(self, expr): | |
| PREC = precedence(expr) | |
| return " & ".join(self.parenthesize(a, PREC) | |
| for a in sorted(expr.args, key=default_sort_key)) | |
| def _print_Or(self, expr): | |
| PREC = precedence(expr) | |
| return " | ".join(self.parenthesize(a, PREC) | |
| for a in sorted(expr.args, key=default_sort_key)) | |
| def _uniform_eval(f1, f2, *args, modules=None, | |
| force_real_eval=False, has_sum=False): | |
| """ | |
| Note: this is an experimental function, as such it is prone to changes. | |
| Please, do not use it in your code. | |
| """ | |
| np = import_module('numpy') | |
| def wrapper_func(func, *args): | |
| try: | |
| return complex(func(*args)) | |
| except (ZeroDivisionError, OverflowError): | |
| return complex(np.nan, np.nan) | |
| # NOTE: np.vectorize is much slower than numpy vectorized operations. | |
| # However, this modules must be able to evaluate functions also with | |
| # mpmath or sympy. | |
| wrapper_func = np.vectorize(wrapper_func, otypes=[complex]) | |
| def _eval_with_sympy(err=None): | |
| if f2 is None: | |
| msg = "Impossible to evaluate the provided numerical function" | |
| if err is None: | |
| msg += "." | |
| else: | |
| msg += "because the following exception was raised:\n" | |
| "{}: {}".format(type(err).__name__, err) | |
| raise RuntimeError(msg) | |
| if err: | |
| warnings.warn( | |
| "The evaluation with %s failed.\n" % ( | |
| "NumPy/SciPy" if not modules else modules) + | |
| "{}: {}\n".format(type(err).__name__, err) + | |
| "Trying to evaluate the expression with Sympy, but it might " | |
| "be a slow operation." | |
| ) | |
| return wrapper_func(f2, *args) | |
| if modules == "sympy": | |
| return _eval_with_sympy() | |
| try: | |
| return wrapper_func(f1, *args) | |
| except Exception as err: | |
| return _eval_with_sympy(err) | |
| def _adaptive_eval(f, x): | |
| """Evaluate f(x) with an adaptive algorithm. Post-process the result. | |
| If a symbolic expression is evaluated with SymPy, it might returns | |
| another symbolic expression, containing additions, ... | |
| Force evaluation to a float. | |
| Parameters | |
| ========== | |
| f : callable | |
| x : float | |
| """ | |
| np = import_module('numpy') | |
| y = f(x) | |
| if isinstance(y, Expr) and (not y.is_Number): | |
| y = y.evalf() | |
| y = complex(y) | |
| if y.imag > 1e-08: | |
| return np.nan | |
| return y.real | |
| def _get_wrapper_for_expr(ret): | |
| wrapper = "%s" | |
| if ret == "real": | |
| wrapper = "re(%s)" | |
| elif ret == "imag": | |
| wrapper = "im(%s)" | |
| elif ret == "abs": | |
| wrapper = "abs(%s)" | |
| elif ret == "arg": | |
| wrapper = "arg(%s)" | |
| return wrapper | |
| class BaseSeries: | |
| """Base class for the data objects containing stuff to be plotted. | |
| Notes | |
| ===== | |
| The backend should check if it supports the data series that is given. | |
| (e.g. TextBackend supports only LineOver1DRangeSeries). | |
| It is the backend responsibility to know how to use the class of | |
| data series that is given. | |
| Some data series classes are grouped (using a class attribute like is_2Dline) | |
| according to the api they present (based only on convention). The backend is | |
| not obliged to use that api (e.g. LineOver1DRangeSeries belongs to the | |
| is_2Dline group and presents the get_points method, but the | |
| TextBackend does not use the get_points method). | |
| BaseSeries | |
| """ | |
| # Some flags follow. The rationale for using flags instead of checking base | |
| # classes is that setting multiple flags is simpler than multiple | |
| # inheritance. | |
| is_2Dline = False | |
| # Some of the backends expect: | |
| # - get_points returning 1D np.arrays list_x, list_y | |
| # - get_color_array returning 1D np.array (done in Line2DBaseSeries) | |
| # with the colors calculated at the points from get_points | |
| is_3Dline = False | |
| # Some of the backends expect: | |
| # - get_points returning 1D np.arrays list_x, list_y, list_y | |
| # - get_color_array returning 1D np.array (done in Line2DBaseSeries) | |
| # with the colors calculated at the points from get_points | |
| is_3Dsurface = False | |
| # Some of the backends expect: | |
| # - get_meshes returning mesh_x, mesh_y, mesh_z (2D np.arrays) | |
| # - get_points an alias for get_meshes | |
| is_contour = False | |
| # Some of the backends expect: | |
| # - get_meshes returning mesh_x, mesh_y, mesh_z (2D np.arrays) | |
| # - get_points an alias for get_meshes | |
| is_implicit = False | |
| # Some of the backends expect: | |
| # - get_meshes returning mesh_x (1D array), mesh_y(1D array, | |
| # mesh_z (2D np.arrays) | |
| # - get_points an alias for get_meshes | |
| # Different from is_contour as the colormap in backend will be | |
| # different | |
| is_interactive = False | |
| # An interactive series can update its data. | |
| is_parametric = False | |
| # The calculation of aesthetics expects: | |
| # - get_parameter_points returning one or two np.arrays (1D or 2D) | |
| # used for calculation aesthetics | |
| is_generic = False | |
| # Represent generic user-provided numerical data | |
| is_vector = False | |
| is_2Dvector = False | |
| is_3Dvector = False | |
| # Represents a 2D or 3D vector data series | |
| _N = 100 | |
| # default number of discretization points for uniform sampling. Each | |
| # subclass can set its number. | |
| def __init__(self, *args, **kwargs): | |
| kwargs = _set_discretization_points(kwargs.copy(), type(self)) | |
| # discretize the domain using only integer numbers | |
| self.only_integers = kwargs.get("only_integers", False) | |
| # represents the evaluation modules to be used by lambdify | |
| self.modules = kwargs.get("modules", None) | |
| # plot functions might create data series that might not be useful to | |
| # be shown on the legend, for example wireframe lines on 3D plots. | |
| self.show_in_legend = kwargs.get("show_in_legend", True) | |
| # line and surface series can show data with a colormap, hence a | |
| # colorbar is essential to understand the data. However, sometime it | |
| # is useful to hide it on series-by-series base. The following keyword | |
| # controls whether the series should show a colorbar or not. | |
| self.colorbar = kwargs.get("colorbar", True) | |
| # Some series might use a colormap as default coloring. Setting this | |
| # attribute to False will inform the backends to use solid color. | |
| self.use_cm = kwargs.get("use_cm", False) | |
| # If True, the backend will attempt to render it on a polar-projection | |
| # axis, or using a polar discretization if a 3D plot is requested | |
| self.is_polar = kwargs.get("is_polar", kwargs.get("polar", False)) | |
| # If True, the rendering will use points, not lines. | |
| self.is_point = kwargs.get("is_point", kwargs.get("point", False)) | |
| # some backend is able to render latex, other needs standard text | |
| self._label = self._latex_label = "" | |
| self._ranges = [] | |
| self._n = [ | |
| int(kwargs.get("n1", self._N)), | |
| int(kwargs.get("n2", self._N)), | |
| int(kwargs.get("n3", self._N)) | |
| ] | |
| self._scales = [ | |
| kwargs.get("xscale", "linear"), | |
| kwargs.get("yscale", "linear"), | |
| kwargs.get("zscale", "linear") | |
| ] | |
| # enable interactive widget plots | |
| self._params = kwargs.get("params", {}) | |
| if not isinstance(self._params, dict): | |
| raise TypeError("`params` must be a dictionary mapping symbols " | |
| "to numeric values.") | |
| if len(self._params) > 0: | |
| self.is_interactive = True | |
| # contains keyword arguments that will be passed to the rendering | |
| # function of the chosen plotting library | |
| self.rendering_kw = kwargs.get("rendering_kw", {}) | |
| # numerical transformation functions to be applied to the output data: | |
| # x, y, z (coordinates), p (parameter on parametric plots) | |
| self._tx = kwargs.get("tx", None) | |
| self._ty = kwargs.get("ty", None) | |
| self._tz = kwargs.get("tz", None) | |
| self._tp = kwargs.get("tp", None) | |
| if not all(callable(t) or (t is None) for t in | |
| [self._tx, self._ty, self._tz, self._tp]): | |
| raise TypeError("`tx`, `ty`, `tz`, `tp` must be functions.") | |
| # list of numerical functions representing the expressions to evaluate | |
| self._functions = [] | |
| # signature for the numerical functions | |
| self._signature = [] | |
| # some expressions don't like to be evaluated over complex data. | |
| # if that's the case, set this to True | |
| self._force_real_eval = kwargs.get("force_real_eval", None) | |
| # this attribute will eventually contain a dictionary with the | |
| # discretized ranges | |
| self._discretized_domain = None | |
| # whether the series contains any interactive range, which is a range | |
| # where the minimum and maximum values can be changed with an | |
| # interactive widget | |
| self._interactive_ranges = False | |
| # NOTE: consider a generic summation, for example: | |
| # s = Sum(cos(pi * x), (x, 1, y)) | |
| # This gets lambdified to something: | |
| # sum(cos(pi*x) for x in range(1, y+1)) | |
| # Hence, y needs to be an integer, otherwise it raises: | |
| # TypeError: 'complex' object cannot be interpreted as an integer | |
| # This list will contains symbols that are upper bound to summations | |
| # or products | |
| self._needs_to_be_int = [] | |
| # a color function will be responsible to set the line/surface color | |
| # according to some logic. Each data series will et an appropriate | |
| # default value. | |
| self.color_func = None | |
| # NOTE: color_func usually receives numerical functions that are going | |
| # to be evaluated over the coordinates of the computed points (or the | |
| # discretized meshes). | |
| # However, if an expression is given to color_func, then it will be | |
| # lambdified with symbols in self._signature, and it will be evaluated | |
| # with the same data used to evaluate the plotted expression. | |
| self._eval_color_func_with_signature = False | |
| def _block_lambda_functions(self, *exprs): | |
| """Some data series can be used to plot numerical functions, others | |
| cannot. Execute this method inside the `__init__` to prevent the | |
| processing of numerical functions. | |
| """ | |
| if any(callable(e) for e in exprs): | |
| raise TypeError(type(self).__name__ + " requires a symbolic " | |
| "expression.") | |
| def _check_fs(self): | |
| """ Checks if there are enough parameters and free symbols. | |
| """ | |
| exprs, ranges = self.expr, self.ranges | |
| params, label = self.params, self.label | |
| exprs = exprs if hasattr(exprs, "__iter__") else [exprs] | |
| if any(callable(e) for e in exprs): | |
| return | |
| # from the expression's free symbols, remove the ones used in | |
| # the parameters and the ranges | |
| fs = _get_free_symbols(exprs) | |
| fs = fs.difference(params.keys()) | |
| if ranges is not None: | |
| fs = fs.difference([r[0] for r in ranges]) | |
| if len(fs) > 0: | |
| raise ValueError( | |
| "Incompatible expression and parameters.\n" | |
| + "Expression: {}\n".format( | |
| (exprs, ranges, label) if ranges is not None else (exprs, label)) | |
| + "params: {}\n".format(params) | |
| + "Specify what these symbols represent: {}\n".format(fs) | |
| + "Are they ranges or parameters?" | |
| ) | |
| # verify that all symbols are known (they either represent plotting | |
| # ranges or parameters) | |
| range_symbols = [r[0] for r in ranges] | |
| for r in ranges: | |
| fs = set().union(*[e.free_symbols for e in r[1:]]) | |
| if any(t in fs for t in range_symbols): | |
| # ranges can't depend on each other, for example this are | |
| # not allowed: | |
| # (x, 0, y), (y, 0, 3) | |
| # (x, 0, y), (y, x + 2, 3) | |
| raise ValueError("Range symbols can't be included into " | |
| "minimum and maximum of a range. " | |
| "Received range: %s" % str(r)) | |
| if len(fs) > 0: | |
| self._interactive_ranges = True | |
| remaining_fs = fs.difference(params.keys()) | |
| if len(remaining_fs) > 0: | |
| raise ValueError( | |
| "Unknown symbols found in plotting range: %s. " % (r,) + | |
| "Are the following parameters? %s" % remaining_fs) | |
| def _create_lambda_func(self): | |
| """Create the lambda functions to be used by the uniform meshing | |
| strategy. | |
| Notes | |
| ===== | |
| The old sympy.plotting used experimental_lambdify. It created one | |
| lambda function each time an evaluation was requested. If that failed, | |
| it went on to create a different lambda function and evaluated it, | |
| and so on. | |
| This new module changes strategy: it creates right away the default | |
| lambda function as well as the backup one. The reason is that the | |
| series could be interactive, hence the numerical function will be | |
| evaluated multiple times. So, let's create the functions just once. | |
| This approach works fine for the majority of cases, in which the | |
| symbolic expression is relatively short, hence the lambdification | |
| is fast. If the expression is very long, this approach takes twice | |
| the time to create the lambda functions. Be aware of that! | |
| """ | |
| exprs = self.expr if hasattr(self.expr, "__iter__") else [self.expr] | |
| if not any(callable(e) for e in exprs): | |
| fs = _get_free_symbols(exprs) | |
| self._signature = sorted(fs, key=lambda t: t.name) | |
| # Generate a list of lambda functions, two for each expression: | |
| # 1. the default one. | |
| # 2. the backup one, in case of failures with the default one. | |
| self._functions = [] | |
| for e in exprs: | |
| # TODO: set cse=True once this issue is solved: | |
| # https://github.com/sympy/sympy/issues/24246 | |
| self._functions.append([ | |
| lambdify(self._signature, e, modules=self.modules), | |
| lambdify(self._signature, e, modules="sympy", dummify=True), | |
| ]) | |
| else: | |
| self._signature = sorted([r[0] for r in self.ranges], key=lambda t: t.name) | |
| self._functions = [(e, None) for e in exprs] | |
| # deal with symbolic color_func | |
| if isinstance(self.color_func, Expr): | |
| self.color_func = lambdify(self._signature, self.color_func) | |
| self._eval_color_func_with_signature = True | |
| def _update_range_value(self, t): | |
| """If the value of a plotting range is a symbolic expression, | |
| substitute the parameters in order to get a numerical value. | |
| """ | |
| if not self._interactive_ranges: | |
| return complex(t) | |
| return complex(t.subs(self.params)) | |
| def _create_discretized_domain(self): | |
| """Discretize the ranges for uniform meshing strategy. | |
| """ | |
| # NOTE: the goal is to create a dictionary stored in | |
| # self._discretized_domain, mapping symbols to a numpy array | |
| # representing the discretization | |
| discr_symbols = [] | |
| discretizations = [] | |
| # create a 1D discretization | |
| for i, r in enumerate(self.ranges): | |
| discr_symbols.append(r[0]) | |
| c_start = self._update_range_value(r[1]) | |
| c_end = self._update_range_value(r[2]) | |
| start = c_start.real if c_start.imag == c_end.imag == 0 else c_start | |
| end = c_end.real if c_start.imag == c_end.imag == 0 else c_end | |
| needs_integer_discr = self.only_integers or (r[0] in self._needs_to_be_int) | |
| d = BaseSeries._discretize(start, end, self.n[i], | |
| scale=self.scales[i], | |
| only_integers=needs_integer_discr) | |
| if ((not self._force_real_eval) and (not needs_integer_discr) and | |
| (d.dtype != "complex")): | |
| d = d + 1j * c_start.imag | |
| if needs_integer_discr: | |
| d = d.astype(int) | |
| discretizations.append(d) | |
| # create 2D or 3D | |
| self._create_discretized_domain_helper(discr_symbols, discretizations) | |
| def _create_discretized_domain_helper(self, discr_symbols, discretizations): | |
| """Create 2D or 3D discretized grids. | |
| Subclasses should override this method in order to implement a | |
| different behaviour. | |
| """ | |
| np = import_module('numpy') | |
| # discretization suitable for 2D line plots, 3D surface plots, | |
| # contours plots, vector plots | |
| # NOTE: why indexing='ij'? Because it produces consistent results with | |
| # np.mgrid. This is important as Mayavi requires this indexing | |
| # to correctly compute 3D streamlines. While VTK is able to compute | |
| # streamlines regardless of the indexing, with indexing='xy' it | |
| # produces "strange" results with "voids" into the | |
| # discretization volume. indexing='ij' solves the problem. | |
| # Also note that matplotlib 2D streamlines requires indexing='xy'. | |
| indexing = "xy" | |
| if self.is_3Dvector or (self.is_3Dsurface and self.is_implicit): | |
| indexing = "ij" | |
| meshes = np.meshgrid(*discretizations, indexing=indexing) | |
| self._discretized_domain = dict(zip(discr_symbols, meshes)) | |
| def _evaluate(self, cast_to_real=True): | |
| """Evaluation of the symbolic expression (or expressions) with the | |
| uniform meshing strategy, based on current values of the parameters. | |
| """ | |
| np = import_module('numpy') | |
| # create lambda functions | |
| if not self._functions: | |
| self._create_lambda_func() | |
| # create (or update) the discretized domain | |
| if (not self._discretized_domain) or self._interactive_ranges: | |
| self._create_discretized_domain() | |
| # ensure that discretized domains are returned with the proper order | |
| discr = [self._discretized_domain[s[0]] for s in self.ranges] | |
| args = self._aggregate_args() | |
| results = [] | |
| for f in self._functions: | |
| r = _uniform_eval(*f, *args) | |
| # the evaluation might produce an int/float. Need this correction. | |
| r = self._correct_shape(np.array(r), discr[0]) | |
| # sometime the evaluation is performed over arrays of type object. | |
| # hence, `result` might be of type object, which don't work well | |
| # with numpy real and imag functions. | |
| r = r.astype(complex) | |
| results.append(r) | |
| if cast_to_real: | |
| discr = [np.real(d.astype(complex)) for d in discr] | |
| return [*discr, *results] | |
| def _aggregate_args(self): | |
| """Create a list of arguments to be passed to the lambda function, | |
| sorted according to self._signature. | |
| """ | |
| args = [] | |
| for s in self._signature: | |
| if s in self._params.keys(): | |
| args.append( | |
| int(self._params[s]) if s in self._needs_to_be_int else | |
| self._params[s] if self._force_real_eval | |
| else complex(self._params[s])) | |
| else: | |
| args.append(self._discretized_domain[s]) | |
| return args | |
| def expr(self): | |
| """Return the expression (or expressions) of the series.""" | |
| return self._expr | |
| def expr(self, e): | |
| """Set the expression (or expressions) of the series.""" | |
| is_iter = hasattr(e, "__iter__") | |
| is_callable = callable(e) if not is_iter else any(callable(t) for t in e) | |
| if is_callable: | |
| self._expr = e | |
| else: | |
| self._expr = sympify(e) if not is_iter else Tuple(*e) | |
| # look for the upper bound of summations and products | |
| s = set() | |
| for e in self._expr.atoms(Sum, Product): | |
| for a in e.args[1:]: | |
| if isinstance(a[-1], Symbol): | |
| s.add(a[-1]) | |
| self._needs_to_be_int = list(s) | |
| # list of sympy functions that when lambdified, the corresponding | |
| # numpy functions don't like complex-type arguments | |
| pf = [ceiling, floor, atan2, frac, zeta] | |
| if self._force_real_eval is not True: | |
| check_res = [self._expr.has(f) for f in pf] | |
| self._force_real_eval = any(check_res) | |
| if self._force_real_eval and ((self.modules is None) or | |
| (isinstance(self.modules, str) and "numpy" in self.modules)): | |
| funcs = [f for f, c in zip(pf, check_res) if c] | |
| warnings.warn("NumPy is unable to evaluate with complex " | |
| "numbers some of the functions included in this " | |
| "symbolic expression: %s. " % funcs + | |
| "Hence, the evaluation will use real numbers. " | |
| "If you believe the resulting plot is incorrect, " | |
| "change the evaluation module by setting the " | |
| "`modules` keyword argument.") | |
| if self._functions: | |
| # update lambda functions | |
| self._create_lambda_func() | |
| def is_3D(self): | |
| flags3D = [self.is_3Dline, self.is_3Dsurface, self.is_3Dvector] | |
| return any(flags3D) | |
| def is_line(self): | |
| flagslines = [self.is_2Dline, self.is_3Dline] | |
| return any(flagslines) | |
| def _line_surface_color(self, prop, val): | |
| """This method enables back-compatibility with old sympy.plotting""" | |
| # NOTE: color_func is set inside the init method of the series. | |
| # If line_color/surface_color is not a callable, then color_func will | |
| # be set to None. | |
| setattr(self, prop, val) | |
| if callable(val) or isinstance(val, Expr): | |
| self.color_func = val | |
| setattr(self, prop, None) | |
| elif val is not None: | |
| self.color_func = None | |
| def line_color(self): | |
| return self._line_color | |
| def line_color(self, val): | |
| self._line_surface_color("_line_color", val) | |
| def n(self): | |
| """Returns a list [n1, n2, n3] of numbers of discratization points. | |
| """ | |
| return self._n | |
| def n(self, v): | |
| """Set the numbers of discretization points. ``v`` must be an int or | |
| a list. | |
| Let ``s`` be a series. Then: | |
| * to set the number of discretization points along the x direction (or | |
| first parameter): ``s.n = 10`` | |
| * to set the number of discretization points along the x and y | |
| directions (or first and second parameters): ``s.n = [10, 15]`` | |
| * to set the number of discretization points along the x, y and z | |
| directions: ``s.n = [10, 15, 20]`` | |
| The following is highly unreccomended, because it prevents | |
| the execution of necessary code in order to keep updated data: | |
| ``s.n[1] = 15`` | |
| """ | |
| if not hasattr(v, "__iter__"): | |
| self._n[0] = v | |
| else: | |
| self._n[:len(v)] = v | |
| if self._discretized_domain: | |
| # update the discretized domain | |
| self._create_discretized_domain() | |
| def params(self): | |
| """Get or set the current parameters dictionary. | |
| Parameters | |
| ========== | |
| p : dict | |
| * key: symbol associated to the parameter | |
| * val: the numeric value | |
| """ | |
| return self._params | |
| def params(self, p): | |
| self._params = p | |
| def _post_init(self): | |
| exprs = self.expr if hasattr(self.expr, "__iter__") else [self.expr] | |
| if any(callable(e) for e in exprs) and self.params: | |
| raise TypeError("`params` was provided, hence an interactive plot " | |
| "is expected. However, interactive plots do not support " | |
| "user-provided numerical functions.") | |
| # if the expressions is a lambda function and no label has been | |
| # provided, then its better to do the following in order to avoid | |
| # surprises on the backend | |
| if any(callable(e) for e in exprs): | |
| if self._label == str(self.expr): | |
| self.label = "" | |
| self._check_fs() | |
| if hasattr(self, "adaptive") and self.adaptive and self.params: | |
| warnings.warn("`params` was provided, hence an interactive plot " | |
| "is expected. However, interactive plots do not support " | |
| "adaptive evaluation. Automatically switched to " | |
| "adaptive=False.") | |
| self.adaptive = False | |
| def scales(self): | |
| return self._scales | |
| def scales(self, v): | |
| if isinstance(v, str): | |
| self._scales[0] = v | |
| else: | |
| self._scales[:len(v)] = v | |
| def surface_color(self): | |
| return self._surface_color | |
| def surface_color(self, val): | |
| self._line_surface_color("_surface_color", val) | |
| def rendering_kw(self): | |
| return self._rendering_kw | |
| def rendering_kw(self, kwargs): | |
| if isinstance(kwargs, dict): | |
| self._rendering_kw = kwargs | |
| else: | |
| self._rendering_kw = {} | |
| if kwargs is not None: | |
| warnings.warn( | |
| "`rendering_kw` must be a dictionary, instead an " | |
| "object of type %s was received. " % type(kwargs) + | |
| "Automatically setting `rendering_kw` to an empty " | |
| "dictionary") | |
| def _discretize(start, end, N, scale="linear", only_integers=False): | |
| """Discretize a 1D domain. | |
| Returns | |
| ======= | |
| domain : np.ndarray with dtype=float or complex | |
| The domain's dtype will be float or complex (depending on the | |
| type of start/end) even if only_integers=True. It is left for | |
| the downstream code to perform further casting, if necessary. | |
| """ | |
| np = import_module('numpy') | |
| if only_integers is True: | |
| start, end = int(start), int(end) | |
| N = end - start + 1 | |
| if scale == "linear": | |
| return np.linspace(start, end, N) | |
| return np.geomspace(start, end, N) | |
| def _correct_shape(a, b): | |
| """Convert ``a`` to a np.ndarray of the same shape of ``b``. | |
| Parameters | |
| ========== | |
| a : int, float, complex, np.ndarray | |
| Usually, this is the result of a numerical evaluation of a | |
| symbolic expression. Even if a discretized domain was used to | |
| evaluate the function, the result can be a scalar (int, float, | |
| complex). Think for example to ``expr = Float(2)`` and | |
| ``f = lambdify(x, expr)``. No matter the shape of the numerical | |
| array representing x, the result of the evaluation will be | |
| a single value. | |
| b : np.ndarray | |
| It represents the correct shape that ``a`` should have. | |
| Returns | |
| ======= | |
| new_a : np.ndarray | |
| An array with the correct shape. | |
| """ | |
| np = import_module('numpy') | |
| if not isinstance(a, np.ndarray): | |
| a = np.array(a) | |
| if a.shape != b.shape: | |
| if a.shape == (): | |
| a = a * np.ones_like(b) | |
| else: | |
| a = a.reshape(b.shape) | |
| return a | |
| def eval_color_func(self, *args): | |
| """Evaluate the color function. | |
| Parameters | |
| ========== | |
| args : tuple | |
| Arguments to be passed to the coloring function. Can be coordinates | |
| or parameters or both. | |
| Notes | |
| ===== | |
| The backend will request the data series to generate the numerical | |
| data. Depending on the data series, either the data series itself or | |
| the backend will eventually execute this function to generate the | |
| appropriate coloring value. | |
| """ | |
| np = import_module('numpy') | |
| if self.color_func is None: | |
| # NOTE: with the line_color and surface_color attributes | |
| # (back-compatibility with the old sympy.plotting module) it is | |
| # possible to create a plot with a callable line_color (or | |
| # surface_color). For example: | |
| # p = plot(sin(x), line_color=lambda x, y: -y) | |
| # This creates a ColoredLineOver1DRangeSeries with line_color=None | |
| # and color_func=lambda x, y: -y, which effectively is a | |
| # parametric series. Later we could change it to a string value: | |
| # p[0].line_color = "red" | |
| # However, this sets ine_color="red" and color_func=None, but the | |
| # series is still ColoredLineOver1DRangeSeries (a parametric | |
| # series), which will render using a color_func... | |
| warnings.warn("This is likely not the result you were " | |
| "looking for. Please, re-execute the plot command, this time " | |
| "with the appropriate an appropriate value to line_color " | |
| "or surface_color.") | |
| return np.ones_like(args[0]) | |
| if self._eval_color_func_with_signature: | |
| args = self._aggregate_args() | |
| color = self.color_func(*args) | |
| _re, _im = np.real(color), np.imag(color) | |
| _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan | |
| return _re | |
| nargs = arity(self.color_func) | |
| if nargs == 1: | |
| if self.is_2Dline and self.is_parametric: | |
| if len(args) == 2: | |
| # ColoredLineOver1DRangeSeries | |
| return self._correct_shape(self.color_func(args[0]), args[0]) | |
| # Parametric2DLineSeries | |
| return self._correct_shape(self.color_func(args[2]), args[2]) | |
| elif self.is_3Dline and self.is_parametric: | |
| return self._correct_shape(self.color_func(args[3]), args[3]) | |
| elif self.is_3Dsurface and self.is_parametric: | |
| return self._correct_shape(self.color_func(args[3]), args[3]) | |
| return self._correct_shape(self.color_func(args[0]), args[0]) | |
| elif nargs == 2: | |
| if self.is_3Dsurface and self.is_parametric: | |
| return self._correct_shape(self.color_func(*args[3:]), args[3]) | |
| return self._correct_shape(self.color_func(*args[:2]), args[0]) | |
| return self._correct_shape(self.color_func(*args[:nargs]), args[0]) | |
| def get_data(self): | |
| """Compute and returns the numerical data. | |
| The number of parameters returned by this method depends on the | |
| specific instance. If ``s`` is the series, make sure to read | |
| ``help(s.get_data)`` to understand what it returns. | |
| """ | |
| raise NotImplementedError | |
| def _get_wrapped_label(self, label, wrapper): | |
| """Given a latex representation of an expression, wrap it inside | |
| some characters. Matplotlib needs "$%s%$", K3D-Jupyter needs "%s". | |
| """ | |
| return wrapper % label | |
| def get_label(self, use_latex=False, wrapper="$%s$"): | |
| """Return the label to be used to display the expression. | |
| Parameters | |
| ========== | |
| use_latex : bool | |
| If False, the string representation of the expression is returned. | |
| If True, the latex representation is returned. | |
| wrapper : str | |
| The backend might need the latex representation to be wrapped by | |
| some characters. Default to ``"$%s$"``. | |
| Returns | |
| ======= | |
| label : str | |
| """ | |
| if use_latex is False: | |
| return self._label | |
| if self._label == str(self.expr): | |
| # when the backend requests a latex label and user didn't provide | |
| # any label | |
| return self._get_wrapped_label(self._latex_label, wrapper) | |
| return self._latex_label | |
| def label(self): | |
| return self.get_label() | |
| def label(self, val): | |
| """Set the labels associated to this series.""" | |
| # NOTE: the init method of any series requires a label. If the user do | |
| # not provide it, the preprocessing function will set label=None, which | |
| # informs the series to initialize two attributes: | |
| # _label contains the string representation of the expression. | |
| # _latex_label contains the latex representation of the expression. | |
| self._label = self._latex_label = val | |
| def ranges(self): | |
| return self._ranges | |
| def ranges(self, val): | |
| new_vals = [] | |
| for v in val: | |
| if v is not None: | |
| new_vals.append(tuple([sympify(t) for t in v])) | |
| self._ranges = new_vals | |
| def _apply_transform(self, *args): | |
| """Apply transformations to the results of numerical evaluation. | |
| Parameters | |
| ========== | |
| args : tuple | |
| Results of numerical evaluation. | |
| Returns | |
| ======= | |
| transformed_args : tuple | |
| Tuple containing the transformed results. | |
| """ | |
| t = lambda x, transform: x if transform is None else transform(x) | |
| x, y, z = None, None, None | |
| if len(args) == 2: | |
| x, y = args | |
| return t(x, self._tx), t(y, self._ty) | |
| elif (len(args) == 3) and isinstance(self, Parametric2DLineSeries): | |
| x, y, u = args | |
| return (t(x, self._tx), t(y, self._ty), t(u, self._tp)) | |
| elif len(args) == 3: | |
| x, y, z = args | |
| return t(x, self._tx), t(y, self._ty), t(z, self._tz) | |
| elif (len(args) == 4) and isinstance(self, Parametric3DLineSeries): | |
| x, y, z, u = args | |
| return (t(x, self._tx), t(y, self._ty), t(z, self._tz), t(u, self._tp)) | |
| elif len(args) == 4: # 2D vector plot | |
| x, y, u, v = args | |
| return ( | |
| t(x, self._tx), t(y, self._ty), | |
| t(u, self._tx), t(v, self._ty) | |
| ) | |
| elif (len(args) == 5) and isinstance(self, ParametricSurfaceSeries): | |
| x, y, z, u, v = args | |
| return (t(x, self._tx), t(y, self._ty), t(z, self._tz), u, v) | |
| elif (len(args) == 6) and self.is_3Dvector: # 3D vector plot | |
| x, y, z, u, v, w = args | |
| return ( | |
| t(x, self._tx), t(y, self._ty), t(z, self._tz), | |
| t(u, self._tx), t(v, self._ty), t(w, self._tz) | |
| ) | |
| elif len(args) == 6: # complex plot | |
| x, y, _abs, _arg, img, colors = args | |
| return ( | |
| x, y, t(_abs, self._tz), _arg, img, colors) | |
| return args | |
| def _str_helper(self, s): | |
| pre, post = "", "" | |
| if self.is_interactive: | |
| pre = "interactive " | |
| post = " and parameters " + str(tuple(self.params.keys())) | |
| return pre + s + post | |
| def _detect_poles_numerical_helper(x, y, eps=0.01, expr=None, symb=None, symbolic=False): | |
| """Compute the steepness of each segment. If it's greater than a | |
| threshold, set the right-point y-value non NaN and record the | |
| corresponding x-location for further processing. | |
| Returns | |
| ======= | |
| x : np.ndarray | |
| Unchanged x-data. | |
| yy : np.ndarray | |
| Modified y-data with NaN values. | |
| """ | |
| np = import_module('numpy') | |
| yy = y.copy() | |
| threshold = np.pi / 2 - eps | |
| for i in range(len(x) - 1): | |
| dx = x[i + 1] - x[i] | |
| dy = abs(y[i + 1] - y[i]) | |
| angle = np.arctan(dy / dx) | |
| if abs(angle) >= threshold: | |
| yy[i + 1] = np.nan | |
| return x, yy | |
| def _detect_poles_symbolic_helper(expr, symb, start, end): | |
| """Attempts to compute symbolic discontinuities. | |
| Returns | |
| ======= | |
| pole : list | |
| List of symbolic poles, possibly empty. | |
| """ | |
| poles = [] | |
| interval = Interval(nsimplify(start), nsimplify(end)) | |
| res = continuous_domain(expr, symb, interval) | |
| res = res.simplify() | |
| if res == interval: | |
| pass | |
| elif (isinstance(res, Union) and | |
| all(isinstance(t, Interval) for t in res.args)): | |
| poles = [] | |
| for s in res.args: | |
| if s.left_open: | |
| poles.append(s.left) | |
| if s.right_open: | |
| poles.append(s.right) | |
| poles = list(set(poles)) | |
| else: | |
| raise ValueError( | |
| f"Could not parse the following object: {res} .\n" | |
| "Please, submit this as a bug. Consider also to set " | |
| "`detect_poles=True`." | |
| ) | |
| return poles | |
| ### 2D lines | |
| class Line2DBaseSeries(BaseSeries): | |
| """A base class for 2D lines. | |
| - adding the label, steps and only_integers options | |
| - making is_2Dline true | |
| - defining get_segments and get_color_array | |
| """ | |
| is_2Dline = True | |
| _dim = 2 | |
| _N = 1000 | |
| def __init__(self, **kwargs): | |
| super().__init__(**kwargs) | |
| self.steps = kwargs.get("steps", False) | |
| self.is_point = kwargs.get("is_point", kwargs.get("point", False)) | |
| self.is_filled = kwargs.get("is_filled", kwargs.get("fill", True)) | |
| self.adaptive = kwargs.get("adaptive", False) | |
| self.depth = kwargs.get('depth', 12) | |
| self.use_cm = kwargs.get("use_cm", False) | |
| self.color_func = kwargs.get("color_func", None) | |
| self.line_color = kwargs.get("line_color", None) | |
| self.detect_poles = kwargs.get("detect_poles", False) | |
| self.eps = kwargs.get("eps", 0.01) | |
| self.is_polar = kwargs.get("is_polar", kwargs.get("polar", False)) | |
| self.unwrap = kwargs.get("unwrap", False) | |
| # when detect_poles="symbolic", stores the location of poles so that | |
| # they can be appropriately rendered | |
| self.poles_locations = [] | |
| exclude = kwargs.get("exclude", []) | |
| if isinstance(exclude, Set): | |
| exclude = list(extract_solution(exclude, n=100)) | |
| if not hasattr(exclude, "__iter__"): | |
| exclude = [exclude] | |
| exclude = [float(e) for e in exclude] | |
| self.exclude = sorted(exclude) | |
| def get_data(self): | |
| """Return coordinates for plotting the line. | |
| Returns | |
| ======= | |
| x: np.ndarray | |
| x-coordinates | |
| y: np.ndarray | |
| y-coordinates | |
| z: np.ndarray (optional) | |
| z-coordinates in case of Parametric3DLineSeries, | |
| Parametric3DLineInteractiveSeries | |
| param : np.ndarray (optional) | |
| The parameter in case of Parametric2DLineSeries, | |
| Parametric3DLineSeries or AbsArgLineSeries (and their | |
| corresponding interactive series). | |
| """ | |
| np = import_module('numpy') | |
| points = self._get_data_helper() | |
| if (isinstance(self, LineOver1DRangeSeries) and | |
| (self.detect_poles == "symbolic")): | |
| poles = _detect_poles_symbolic_helper( | |
| self.expr.subs(self.params), *self.ranges[0]) | |
| poles = np.array([float(t) for t in poles]) | |
| t = lambda x, transform: x if transform is None else transform(x) | |
| self.poles_locations = t(np.array(poles), self._tx) | |
| # postprocessing | |
| points = self._apply_transform(*points) | |
| if self.is_2Dline and self.detect_poles: | |
| if len(points) == 2: | |
| x, y = points | |
| x, y = _detect_poles_numerical_helper( | |
| x, y, self.eps) | |
| points = (x, y) | |
| else: | |
| x, y, p = points | |
| x, y = _detect_poles_numerical_helper(x, y, self.eps) | |
| points = (x, y, p) | |
| if self.unwrap: | |
| kw = {} | |
| if self.unwrap is not True: | |
| kw = self.unwrap | |
| if self.is_2Dline: | |
| if len(points) == 2: | |
| x, y = points | |
| y = np.unwrap(y, **kw) | |
| points = (x, y) | |
| else: | |
| x, y, p = points | |
| y = np.unwrap(y, **kw) | |
| points = (x, y, p) | |
| if self.steps is True: | |
| if self.is_2Dline: | |
| x, y = points[0], points[1] | |
| x = np.array((x, x)).T.flatten()[1:] | |
| y = np.array((y, y)).T.flatten()[:-1] | |
| if self.is_parametric: | |
| points = (x, y, points[2]) | |
| else: | |
| points = (x, y) | |
| elif self.is_3Dline: | |
| x = np.repeat(points[0], 3)[2:] | |
| y = np.repeat(points[1], 3)[:-2] | |
| z = np.repeat(points[2], 3)[1:-1] | |
| if len(points) > 3: | |
| points = (x, y, z, points[3]) | |
| else: | |
| points = (x, y, z) | |
| if len(self.exclude) > 0: | |
| points = self._insert_exclusions(points) | |
| return points | |
| def get_segments(self): | |
| sympy_deprecation_warning( | |
| """ | |
| The Line2DBaseSeries.get_segments() method is deprecated. | |
| Instead, use the MatplotlibBackend.get_segments() method, or use | |
| The get_points() or get_data() methods. | |
| """, | |
| deprecated_since_version="1.9", | |
| active_deprecations_target="deprecated-get-segments") | |
| np = import_module('numpy') | |
| points = type(self).get_data(self) | |
| points = np.ma.array(points).T.reshape(-1, 1, self._dim) | |
| return np.ma.concatenate([points[:-1], points[1:]], axis=1) | |
| def _insert_exclusions(self, points): | |
| """Add NaN to each of the exclusion point. Practically, this adds a | |
| NaN to the exclusion point, plus two other nearby points evaluated with | |
| the numerical functions associated to this data series. | |
| These nearby points are important when the number of discretization | |
| points is low, or the scale is logarithm. | |
| NOTE: it would be easier to just add exclusion points to the | |
| discretized domain before evaluation, then after evaluation add NaN | |
| to the exclusion points. But that's only work with adaptive=False. | |
| The following approach work even with adaptive=True. | |
| """ | |
| np = import_module("numpy") | |
| points = list(points) | |
| n = len(points) | |
| # index of the x-coordinate (for 2d plots) or parameter (for 2d/3d | |
| # parametric plots) | |
| k = n - 1 | |
| if n == 2: | |
| k = 0 | |
| # indices of the other coordinates | |
| j_indeces = sorted(set(range(n)).difference([k])) | |
| # TODO: for now, I assume that numpy functions are going to succeed | |
| funcs = [f[0] for f in self._functions] | |
| for e in self.exclude: | |
| res = points[k] - e >= 0 | |
| # if res contains both True and False, ie, if e is found | |
| if any(res) and any(~res): | |
| idx = np.nanargmax(res) | |
| # select the previous point with respect to e | |
| idx -= 1 | |
| # TODO: what if points[k][idx]==e or points[k][idx+1]==e? | |
| if idx > 0 and idx < len(points[k]) - 1: | |
| delta_prev = abs(e - points[k][idx]) | |
| delta_post = abs(e - points[k][idx + 1]) | |
| delta = min(delta_prev, delta_post) / 100 | |
| prev = e - delta | |
| post = e + delta | |
| # add points to the x-coord or the parameter | |
| points[k] = np.concatenate( | |
| (points[k][:idx], [prev, e, post], points[k][idx+1:])) | |
| # add points to the other coordinates | |
| c = 0 | |
| for j in j_indeces: | |
| values = funcs[c](np.array([prev, post])) | |
| c += 1 | |
| points[j] = np.concatenate( | |
| (points[j][:idx], [values[0], np.nan, values[1]], points[j][idx+1:])) | |
| return points | |
| def var(self): | |
| return None if not self.ranges else self.ranges[0][0] | |
| def start(self): | |
| if not self.ranges: | |
| return None | |
| try: | |
| return self._cast(self.ranges[0][1]) | |
| except TypeError: | |
| return self.ranges[0][1] | |
| def end(self): | |
| if not self.ranges: | |
| return None | |
| try: | |
| return self._cast(self.ranges[0][2]) | |
| except TypeError: | |
| return self.ranges[0][2] | |
| def xscale(self): | |
| return self._scales[0] | |
| def xscale(self, v): | |
| self.scales = v | |
| def get_color_array(self): | |
| np = import_module('numpy') | |
| c = self.line_color | |
| if hasattr(c, '__call__'): | |
| f = np.vectorize(c) | |
| nargs = arity(c) | |
| if nargs == 1 and self.is_parametric: | |
| x = self.get_parameter_points() | |
| return f(centers_of_segments(x)) | |
| else: | |
| variables = list(map(centers_of_segments, self.get_points())) | |
| if nargs == 1: | |
| return f(variables[0]) | |
| elif nargs == 2: | |
| return f(*variables[:2]) | |
| else: # only if the line is 3D (otherwise raises an error) | |
| return f(*variables) | |
| else: | |
| return c*np.ones(self.nb_of_points) | |
| class List2DSeries(Line2DBaseSeries): | |
| """Representation for a line consisting of list of points.""" | |
| def __init__(self, list_x, list_y, label="", **kwargs): | |
| super().__init__(**kwargs) | |
| np = import_module('numpy') | |
| if len(list_x) != len(list_y): | |
| raise ValueError( | |
| "The two lists of coordinates must have the same " | |
| "number of elements.\n" | |
| "Received: len(list_x) = {} ".format(len(list_x)) + | |
| "and len(list_y) = {}".format(len(list_y)) | |
| ) | |
| self._block_lambda_functions(list_x, list_y) | |
| check = lambda l: [isinstance(t, Expr) and (not t.is_number) for t in l] | |
| if any(check(list_x) + check(list_y)) or self.params: | |
| if not self.params: | |
| raise ValueError("Some or all elements of the provided lists " | |
| "are symbolic expressions, but the ``params`` dictionary " | |
| "was not provided: those elements can't be evaluated.") | |
| self.list_x = Tuple(*list_x) | |
| self.list_y = Tuple(*list_y) | |
| else: | |
| self.list_x = np.array(list_x, dtype=np.float64) | |
| self.list_y = np.array(list_y, dtype=np.float64) | |
| self._expr = (self.list_x, self.list_y) | |
| if not any(isinstance(t, np.ndarray) for t in [self.list_x, self.list_y]): | |
| self._check_fs() | |
| self.is_polar = kwargs.get("is_polar", kwargs.get("polar", False)) | |
| self.label = label | |
| self.rendering_kw = kwargs.get("rendering_kw", {}) | |
| if self.use_cm and self.color_func: | |
| self.is_parametric = True | |
| if isinstance(self.color_func, Expr): | |
| raise TypeError( | |
| "%s don't support symbolic " % self.__class__.__name__ + | |
| "expression for `color_func`.") | |
| def __str__(self): | |
| return "2D list plot" | |
| def _get_data_helper(self): | |
| """Returns coordinates that needs to be postprocessed.""" | |
| lx, ly = self.list_x, self.list_y | |
| if not self.is_interactive: | |
| return self._eval_color_func_and_return(lx, ly) | |
| np = import_module('numpy') | |
| lx = np.array([t.evalf(subs=self.params) for t in lx], dtype=float) | |
| ly = np.array([t.evalf(subs=self.params) for t in ly], dtype=float) | |
| return self._eval_color_func_and_return(lx, ly) | |
| def _eval_color_func_and_return(self, *data): | |
| if self.use_cm and callable(self.color_func): | |
| return [*data, self.eval_color_func(*data)] | |
| return data | |
| class LineOver1DRangeSeries(Line2DBaseSeries): | |
| """Representation for a line consisting of a SymPy expression over a range.""" | |
| def __init__(self, expr, var_start_end, label="", **kwargs): | |
| super().__init__(**kwargs) | |
| self.expr = expr if callable(expr) else sympify(expr) | |
| self._label = str(self.expr) if label is None else label | |
| self._latex_label = latex(self.expr) if label is None else label | |
| self.ranges = [var_start_end] | |
| self._cast = complex | |
| # for complex-related data series, this determines what data to return | |
| # on the y-axis | |
| self._return = kwargs.get("return", None) | |
| self._post_init() | |
| if not self._interactive_ranges: | |
| # NOTE: the following check is only possible when the minimum and | |
| # maximum values of a plotting range are numeric | |
| start, end = [complex(t) for t in self.ranges[0][1:]] | |
| if im(start) != im(end): | |
| raise ValueError( | |
| "%s requires the imaginary " % self.__class__.__name__ + | |
| "part of the start and end values of the range " | |
| "to be the same.") | |
| if self.adaptive and self._return: | |
| warnings.warn("The adaptive algorithm is unable to deal with " | |
| "complex numbers. Automatically switching to uniform meshing.") | |
| self.adaptive = False | |
| def nb_of_points(self): | |
| return self.n[0] | |
| def nb_of_points(self, v): | |
| self.n = v | |
| def __str__(self): | |
| def f(t): | |
| if isinstance(t, complex): | |
| if t.imag != 0: | |
| return t | |
| return t.real | |
| return t | |
| pre = "interactive " if self.is_interactive else "" | |
| post = "" | |
| if self.is_interactive: | |
| post = " and parameters " + str(tuple(self.params.keys())) | |
| wrapper = _get_wrapper_for_expr(self._return) | |
| return pre + "cartesian line: %s for %s over %s" % ( | |
| wrapper % self.expr, | |
| str(self.var), | |
| str((f(self.start), f(self.end))), | |
| ) + post | |
| def get_points(self): | |
| """Return lists of coordinates for plotting. Depending on the | |
| ``adaptive`` option, this function will either use an adaptive algorithm | |
| or it will uniformly sample the expression over the provided range. | |
| This function is available for back-compatibility purposes. Consider | |
| using ``get_data()`` instead. | |
| Returns | |
| ======= | |
| x : list | |
| List of x-coordinates | |
| y : list | |
| List of y-coordinates | |
| """ | |
| return self._get_data_helper() | |
| def _adaptive_sampling(self): | |
| try: | |
| if callable(self.expr): | |
| f = self.expr | |
| else: | |
| f = lambdify([self.var], self.expr, self.modules) | |
| x, y = self._adaptive_sampling_helper(f) | |
| except Exception as err: | |
| warnings.warn( | |
| "The evaluation with %s failed.\n" % ( | |
| "NumPy/SciPy" if not self.modules else self.modules) + | |
| "{}: {}\n".format(type(err).__name__, err) + | |
| "Trying to evaluate the expression with Sympy, but it might " | |
| "be a slow operation." | |
| ) | |
| f = lambdify([self.var], self.expr, "sympy") | |
| x, y = self._adaptive_sampling_helper(f) | |
| return x, y | |
| def _adaptive_sampling_helper(self, f): | |
| """The adaptive sampling is done by recursively checking if three | |
| points are almost collinear. If they are not collinear, then more | |
| points are added between those points. | |
| References | |
| ========== | |
| .. [1] Adaptive polygonal approximation of parametric curves, | |
| Luiz Henrique de Figueiredo. | |
| """ | |
| np = import_module('numpy') | |
| x_coords = [] | |
| y_coords = [] | |
| def sample(p, q, depth): | |
| """ Samples recursively if three points are almost collinear. | |
| For depth < 6, points are added irrespective of whether they | |
| satisfy the collinearity condition or not. The maximum depth | |
| allowed is 12. | |
| """ | |
| # Randomly sample to avoid aliasing. | |
| random = 0.45 + np.random.rand() * 0.1 | |
| if self.xscale == 'log': | |
| xnew = 10**(np.log10(p[0]) + random * (np.log10(q[0]) - | |
| np.log10(p[0]))) | |
| else: | |
| xnew = p[0] + random * (q[0] - p[0]) | |
| ynew = _adaptive_eval(f, xnew) | |
| new_point = np.array([xnew, ynew]) | |
| # Maximum depth | |
| if depth > self.depth: | |
| x_coords.append(q[0]) | |
| y_coords.append(q[1]) | |
| # Sample to depth of 6 (whether the line is flat or not) | |
| # without using linspace (to avoid aliasing). | |
| elif depth < 6: | |
| sample(p, new_point, depth + 1) | |
| sample(new_point, q, depth + 1) | |
| # Sample ten points if complex values are encountered | |
| # at both ends. If there is a real value in between, then | |
| # sample those points further. | |
| elif p[1] is None and q[1] is None: | |
| if self.xscale == 'log': | |
| xarray = np.logspace(p[0], q[0], 10) | |
| else: | |
| xarray = np.linspace(p[0], q[0], 10) | |
| yarray = list(map(f, xarray)) | |
| if not all(y is None for y in yarray): | |
| for i in range(len(yarray) - 1): | |
| if not (yarray[i] is None and yarray[i + 1] is None): | |
| sample([xarray[i], yarray[i]], | |
| [xarray[i + 1], yarray[i + 1]], depth + 1) | |
| # Sample further if one of the end points in None (i.e. a | |
| # complex value) or the three points are not almost collinear. | |
| elif (p[1] is None or q[1] is None or new_point[1] is None | |
| or not flat(p, new_point, q)): | |
| sample(p, new_point, depth + 1) | |
| sample(new_point, q, depth + 1) | |
| else: | |
| x_coords.append(q[0]) | |
| y_coords.append(q[1]) | |
| f_start = _adaptive_eval(f, self.start.real) | |
| f_end = _adaptive_eval(f, self.end.real) | |
| x_coords.append(self.start.real) | |
| y_coords.append(f_start) | |
| sample(np.array([self.start.real, f_start]), | |
| np.array([self.end.real, f_end]), 0) | |
| return (x_coords, y_coords) | |
| def _uniform_sampling(self): | |
| np = import_module('numpy') | |
| x, result = self._evaluate() | |
| _re, _im = np.real(result), np.imag(result) | |
| _re = self._correct_shape(_re, x) | |
| _im = self._correct_shape(_im, x) | |
| return x, _re, _im | |
| def _get_data_helper(self): | |
| """Returns coordinates that needs to be postprocessed. | |
| """ | |
| np = import_module('numpy') | |
| if self.adaptive and (not self.only_integers): | |
| x, y = self._adaptive_sampling() | |
| return [np.array(t) for t in [x, y]] | |
| x, _re, _im = self._uniform_sampling() | |
| if self._return is None: | |
| # The evaluation could produce complex numbers. Set real elements | |
| # to NaN where there are non-zero imaginary elements | |
| _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan | |
| elif self._return == "real": | |
| pass | |
| elif self._return == "imag": | |
| _re = _im | |
| elif self._return == "abs": | |
| _re = np.sqrt(_re**2 + _im**2) | |
| elif self._return == "arg": | |
| _re = np.arctan2(_im, _re) | |
| else: | |
| raise ValueError("`_return` not recognized. " | |
| "Received: %s" % self._return) | |
| return x, _re | |
| class ParametricLineBaseSeries(Line2DBaseSeries): | |
| is_parametric = True | |
| def _set_parametric_line_label(self, label): | |
| """Logic to set the correct label to be shown on the plot. | |
| If `use_cm=True` there will be a colorbar, so we show the parameter. | |
| If `use_cm=False`, there might be a legend, so we show the expressions. | |
| Parameters | |
| ========== | |
| label : str | |
| label passed in by the pre-processor or the user | |
| """ | |
| self._label = str(self.var) if label is None else label | |
| self._latex_label = latex(self.var) if label is None else label | |
| if (self.use_cm is False) and (self._label == str(self.var)): | |
| self._label = str(self.expr) | |
| self._latex_label = latex(self.expr) | |
| # if the expressions is a lambda function and use_cm=False and no label | |
| # has been provided, then its better to do the following in order to | |
| # avoid surprises on the backend | |
| if any(callable(e) for e in self.expr) and (not self.use_cm): | |
| if self._label == str(self.expr): | |
| self._label = "" | |
| def get_label(self, use_latex=False, wrapper="$%s$"): | |
| # parametric lines returns the representation of the parameter to be | |
| # shown on the colorbar if `use_cm=True`, otherwise it returns the | |
| # representation of the expression to be placed on the legend. | |
| if self.use_cm: | |
| if str(self.var) == self._label: | |
| if use_latex: | |
| return self._get_wrapped_label(latex(self.var), wrapper) | |
| return str(self.var) | |
| # here the user has provided a custom label | |
| return self._label | |
| if use_latex: | |
| if self._label != str(self.expr): | |
| return self._latex_label | |
| return self._get_wrapped_label(self._latex_label, wrapper) | |
| return self._label | |
| def _get_data_helper(self): | |
| """Returns coordinates that needs to be postprocessed. | |
| Depending on the `adaptive` option, this function will either use an | |
| adaptive algorithm or it will uniformly sample the expression over the | |
| provided range. | |
| """ | |
| if self.adaptive: | |
| np = import_module("numpy") | |
| coords = self._adaptive_sampling() | |
| coords = [np.array(t) for t in coords] | |
| else: | |
| coords = self._uniform_sampling() | |
| if self.is_2Dline and self.is_polar: | |
| # when plot_polar is executed with polar_axis=True | |
| np = import_module('numpy') | |
| x, y, _ = coords | |
| r = np.sqrt(x**2 + y**2) | |
| t = np.arctan2(y, x) | |
| coords = [t, r, coords[-1]] | |
| if callable(self.color_func): | |
| coords = list(coords) | |
| coords[-1] = self.eval_color_func(*coords) | |
| return coords | |
| def _uniform_sampling(self): | |
| """Returns coordinates that needs to be postprocessed.""" | |
| np = import_module('numpy') | |
| results = self._evaluate() | |
| for i, r in enumerate(results): | |
| _re, _im = np.real(r), np.imag(r) | |
| _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan | |
| results[i] = _re | |
| return [*results[1:], results[0]] | |
| def get_parameter_points(self): | |
| return self.get_data()[-1] | |
| def get_points(self): | |
| """ Return lists of coordinates for plotting. Depending on the | |
| ``adaptive`` option, this function will either use an adaptive algorithm | |
| or it will uniformly sample the expression over the provided range. | |
| This function is available for back-compatibility purposes. Consider | |
| using ``get_data()`` instead. | |
| Returns | |
| ======= | |
| x : list | |
| List of x-coordinates | |
| y : list | |
| List of y-coordinates | |
| z : list | |
| List of z-coordinates, only for 3D parametric line plot. | |
| """ | |
| return self._get_data_helper()[:-1] | |
| def nb_of_points(self): | |
| return self.n[0] | |
| def nb_of_points(self, v): | |
| self.n = v | |
| class Parametric2DLineSeries(ParametricLineBaseSeries): | |
| """Representation for a line consisting of two parametric SymPy expressions | |
| over a range.""" | |
| is_2Dline = True | |
| def __init__(self, expr_x, expr_y, var_start_end, label="", **kwargs): | |
| super().__init__(**kwargs) | |
| self.expr_x = expr_x if callable(expr_x) else sympify(expr_x) | |
| self.expr_y = expr_y if callable(expr_y) else sympify(expr_y) | |
| self.expr = (self.expr_x, self.expr_y) | |
| self.ranges = [var_start_end] | |
| self._cast = float | |
| self.use_cm = kwargs.get("use_cm", True) | |
| self._set_parametric_line_label(label) | |
| self._post_init() | |
| def __str__(self): | |
| return self._str_helper( | |
| "parametric cartesian line: (%s, %s) for %s over %s" % ( | |
| str(self.expr_x), | |
| str(self.expr_y), | |
| str(self.var), | |
| str((self.start, self.end)) | |
| )) | |
| def _adaptive_sampling(self): | |
| try: | |
| if callable(self.expr_x) and callable(self.expr_y): | |
| f_x = self.expr_x | |
| f_y = self.expr_y | |
| else: | |
| f_x = lambdify([self.var], self.expr_x) | |
| f_y = lambdify([self.var], self.expr_y) | |
| x, y, p = self._adaptive_sampling_helper(f_x, f_y) | |
| except Exception as err: | |
| warnings.warn( | |
| "The evaluation with %s failed.\n" % ( | |
| "NumPy/SciPy" if not self.modules else self.modules) + | |
| "{}: {}\n".format(type(err).__name__, err) + | |
| "Trying to evaluate the expression with Sympy, but it might " | |
| "be a slow operation." | |
| ) | |
| f_x = lambdify([self.var], self.expr_x, "sympy") | |
| f_y = lambdify([self.var], self.expr_y, "sympy") | |
| x, y, p = self._adaptive_sampling_helper(f_x, f_y) | |
| return x, y, p | |
| def _adaptive_sampling_helper(self, f_x, f_y): | |
| """The adaptive sampling is done by recursively checking if three | |
| points are almost collinear. If they are not collinear, then more | |
| points are added between those points. | |
| References | |
| ========== | |
| .. [1] Adaptive polygonal approximation of parametric curves, | |
| Luiz Henrique de Figueiredo. | |
| """ | |
| x_coords = [] | |
| y_coords = [] | |
| param = [] | |
| def sample(param_p, param_q, p, q, depth): | |
| """ Samples recursively if three points are almost collinear. | |
| For depth < 6, points are added irrespective of whether they | |
| satisfy the collinearity condition or not. The maximum depth | |
| allowed is 12. | |
| """ | |
| # Randomly sample to avoid aliasing. | |
| np = import_module('numpy') | |
| random = 0.45 + np.random.rand() * 0.1 | |
| param_new = param_p + random * (param_q - param_p) | |
| xnew = _adaptive_eval(f_x, param_new) | |
| ynew = _adaptive_eval(f_y, param_new) | |
| new_point = np.array([xnew, ynew]) | |
| # Maximum depth | |
| if depth > self.depth: | |
| x_coords.append(q[0]) | |
| y_coords.append(q[1]) | |
| param.append(param_p) | |
| # Sample irrespective of whether the line is flat till the | |
| # depth of 6. We are not using linspace to avoid aliasing. | |
| elif depth < 6: | |
| sample(param_p, param_new, p, new_point, depth + 1) | |
| sample(param_new, param_q, new_point, q, depth + 1) | |
| # Sample ten points if complex values are encountered | |
| # at both ends. If there is a real value in between, then | |
| # sample those points further. | |
| elif ((p[0] is None and q[1] is None) or | |
| (p[1] is None and q[1] is None)): | |
| param_array = np.linspace(param_p, param_q, 10) | |
| x_array = [_adaptive_eval(f_x, t) for t in param_array] | |
| y_array = [_adaptive_eval(f_y, t) for t in param_array] | |
| if not all(x is None and y is None | |
| for x, y in zip(x_array, y_array)): | |
| for i in range(len(y_array) - 1): | |
| if ((x_array[i] is not None and y_array[i] is not None) or | |
| (x_array[i + 1] is not None and y_array[i + 1] is not None)): | |
| point_a = [x_array[i], y_array[i]] | |
| point_b = [x_array[i + 1], y_array[i + 1]] | |
| sample(param_array[i], param_array[i], point_a, | |
| point_b, depth + 1) | |
| # Sample further if one of the end points in None (i.e. a complex | |
| # value) or the three points are not almost collinear. | |
| elif (p[0] is None or p[1] is None | |
| or q[1] is None or q[0] is None | |
| or not flat(p, new_point, q)): | |
| sample(param_p, param_new, p, new_point, depth + 1) | |
| sample(param_new, param_q, new_point, q, depth + 1) | |
| else: | |
| x_coords.append(q[0]) | |
| y_coords.append(q[1]) | |
| param.append(param_p) | |
| f_start_x = _adaptive_eval(f_x, self.start) | |
| f_start_y = _adaptive_eval(f_y, self.start) | |
| start = [f_start_x, f_start_y] | |
| f_end_x = _adaptive_eval(f_x, self.end) | |
| f_end_y = _adaptive_eval(f_y, self.end) | |
| end = [f_end_x, f_end_y] | |
| x_coords.append(f_start_x) | |
| y_coords.append(f_start_y) | |
| param.append(self.start) | |
| sample(self.start, self.end, start, end, 0) | |
| return x_coords, y_coords, param | |
| ### 3D lines | |
| class Line3DBaseSeries(Line2DBaseSeries): | |
| """A base class for 3D lines. | |
| Most of the stuff is derived from Line2DBaseSeries.""" | |
| is_2Dline = False | |
| is_3Dline = True | |
| _dim = 3 | |
| def __init__(self): | |
| super().__init__() | |
| class Parametric3DLineSeries(ParametricLineBaseSeries): | |
| """Representation for a 3D line consisting of three parametric SymPy | |
| expressions and a range.""" | |
| is_2Dline = False | |
| is_3Dline = True | |
| def __init__(self, expr_x, expr_y, expr_z, var_start_end, label="", **kwargs): | |
| super().__init__(**kwargs) | |
| self.expr_x = expr_x if callable(expr_x) else sympify(expr_x) | |
| self.expr_y = expr_y if callable(expr_y) else sympify(expr_y) | |
| self.expr_z = expr_z if callable(expr_z) else sympify(expr_z) | |
| self.expr = (self.expr_x, self.expr_y, self.expr_z) | |
| self.ranges = [var_start_end] | |
| self._cast = float | |
| self.adaptive = False | |
| self.use_cm = kwargs.get("use_cm", True) | |
| self._set_parametric_line_label(label) | |
| self._post_init() | |
| # TODO: remove this | |
| self._xlim = None | |
| self._ylim = None | |
| self._zlim = None | |
| def __str__(self): | |
| return self._str_helper( | |
| "3D parametric cartesian line: (%s, %s, %s) for %s over %s" % ( | |
| str(self.expr_x), | |
| str(self.expr_y), | |
| str(self.expr_z), | |
| str(self.var), | |
| str((self.start, self.end)) | |
| )) | |
| def get_data(self): | |
| # TODO: remove this | |
| np = import_module("numpy") | |
| x, y, z, p = super().get_data() | |
| self._xlim = (np.amin(x), np.amax(x)) | |
| self._ylim = (np.amin(y), np.amax(y)) | |
| self._zlim = (np.amin(z), np.amax(z)) | |
| return x, y, z, p | |
| ### Surfaces | |
| class SurfaceBaseSeries(BaseSeries): | |
| """A base class for 3D surfaces.""" | |
| is_3Dsurface = True | |
| def __init__(self, *args, **kwargs): | |
| super().__init__(**kwargs) | |
| self.use_cm = kwargs.get("use_cm", False) | |
| # NOTE: why should SurfaceOver2DRangeSeries support is polar? | |
| # After all, the same result can be achieve with | |
| # ParametricSurfaceSeries. For example: | |
| # sin(r) for (r, 0, 2 * pi) and (theta, 0, pi/2) can be parameterized | |
| # as (r * cos(theta), r * sin(theta), sin(t)) for (r, 0, 2 * pi) and | |
| # (theta, 0, pi/2). | |
| # Because it is faster to evaluate (important for interactive plots). | |
| self.is_polar = kwargs.get("is_polar", kwargs.get("polar", False)) | |
| self.surface_color = kwargs.get("surface_color", None) | |
| self.color_func = kwargs.get("color_func", lambda x, y, z: z) | |
| if callable(self.surface_color): | |
| self.color_func = self.surface_color | |
| self.surface_color = None | |
| def _set_surface_label(self, label): | |
| exprs = self.expr | |
| self._label = str(exprs) if label is None else label | |
| self._latex_label = latex(exprs) if label is None else label | |
| # if the expressions is a lambda function and no label | |
| # has been provided, then its better to do the following to avoid | |
| # surprises on the backend | |
| is_lambda = (callable(exprs) if not hasattr(exprs, "__iter__") | |
| else any(callable(e) for e in exprs)) | |
| if is_lambda and (self._label == str(exprs)): | |
| self._label = "" | |
| self._latex_label = "" | |
| def get_color_array(self): | |
| np = import_module('numpy') | |
| c = self.surface_color | |
| if isinstance(c, Callable): | |
| f = np.vectorize(c) | |
| nargs = arity(c) | |
| if self.is_parametric: | |
| variables = list(map(centers_of_faces, self.get_parameter_meshes())) | |
| if nargs == 1: | |
| return f(variables[0]) | |
| elif nargs == 2: | |
| return f(*variables) | |
| variables = list(map(centers_of_faces, self.get_meshes())) | |
| if nargs == 1: | |
| return f(variables[0]) | |
| elif nargs == 2: | |
| return f(*variables[:2]) | |
| else: | |
| return f(*variables) | |
| else: | |
| if isinstance(self, SurfaceOver2DRangeSeries): | |
| return c*np.ones(min(self.nb_of_points_x, self.nb_of_points_y)) | |
| else: | |
| return c*np.ones(min(self.nb_of_points_u, self.nb_of_points_v)) | |
| class SurfaceOver2DRangeSeries(SurfaceBaseSeries): | |
| """Representation for a 3D surface consisting of a SymPy expression and 2D | |
| range.""" | |
| def __init__(self, expr, var_start_end_x, var_start_end_y, label="", **kwargs): | |
| super().__init__(**kwargs) | |
| self.expr = expr if callable(expr) else sympify(expr) | |
| self.ranges = [var_start_end_x, var_start_end_y] | |
| self._set_surface_label(label) | |
| self._post_init() | |
| # TODO: remove this | |
| self._xlim = (self.start_x, self.end_x) | |
| self._ylim = (self.start_y, self.end_y) | |
| def var_x(self): | |
| return self.ranges[0][0] | |
| def var_y(self): | |
| return self.ranges[1][0] | |
| def start_x(self): | |
| try: | |
| return float(self.ranges[0][1]) | |
| except TypeError: | |
| return self.ranges[0][1] | |
| def end_x(self): | |
| try: | |
| return float(self.ranges[0][2]) | |
| except TypeError: | |
| return self.ranges[0][2] | |
| def start_y(self): | |
| try: | |
| return float(self.ranges[1][1]) | |
| except TypeError: | |
| return self.ranges[1][1] | |
| def end_y(self): | |
| try: | |
| return float(self.ranges[1][2]) | |
| except TypeError: | |
| return self.ranges[1][2] | |
| def nb_of_points_x(self): | |
| return self.n[0] | |
| def nb_of_points_x(self, v): | |
| n = self.n | |
| self.n = [v, n[1:]] | |
| def nb_of_points_y(self): | |
| return self.n[1] | |
| def nb_of_points_y(self, v): | |
| n = self.n | |
| self.n = [n[0], v, n[2]] | |
| def __str__(self): | |
| series_type = "cartesian surface" if self.is_3Dsurface else "contour" | |
| return self._str_helper( | |
| series_type + ": %s for" " %s over %s and %s over %s" % ( | |
| str(self.expr), | |
| str(self.var_x), str((self.start_x, self.end_x)), | |
| str(self.var_y), str((self.start_y, self.end_y)), | |
| )) | |
| def get_meshes(self): | |
| """Return the x,y,z coordinates for plotting the surface. | |
| This function is available for back-compatibility purposes. Consider | |
| using ``get_data()`` instead. | |
| """ | |
| return self.get_data() | |
| def get_data(self): | |
| """Return arrays of coordinates for plotting. | |
| Returns | |
| ======= | |
| mesh_x : np.ndarray | |
| Discretized x-domain. | |
| mesh_y : np.ndarray | |
| Discretized y-domain. | |
| mesh_z : np.ndarray | |
| Results of the evaluation. | |
| """ | |
| np = import_module('numpy') | |
| results = self._evaluate() | |
| # mask out complex values | |
| for i, r in enumerate(results): | |
| _re, _im = np.real(r), np.imag(r) | |
| _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan | |
| results[i] = _re | |
| x, y, z = results | |
| if self.is_polar and self.is_3Dsurface: | |
| r = x.copy() | |
| x = r * np.cos(y) | |
| y = r * np.sin(y) | |
| # TODO: remove this | |
| self._zlim = (np.amin(z), np.amax(z)) | |
| return self._apply_transform(x, y, z) | |
| class ParametricSurfaceSeries(SurfaceBaseSeries): | |
| """Representation for a 3D surface consisting of three parametric SymPy | |
| expressions and a range.""" | |
| is_parametric = True | |
| def __init__(self, expr_x, expr_y, expr_z, | |
| var_start_end_u, var_start_end_v, label="", **kwargs): | |
| super().__init__(**kwargs) | |
| self.expr_x = expr_x if callable(expr_x) else sympify(expr_x) | |
| self.expr_y = expr_y if callable(expr_y) else sympify(expr_y) | |
| self.expr_z = expr_z if callable(expr_z) else sympify(expr_z) | |
| self.expr = (self.expr_x, self.expr_y, self.expr_z) | |
| self.ranges = [var_start_end_u, var_start_end_v] | |
| self.color_func = kwargs.get("color_func", lambda x, y, z, u, v: z) | |
| self._set_surface_label(label) | |
| self._post_init() | |
| def var_u(self): | |
| return self.ranges[0][0] | |
| def var_v(self): | |
| return self.ranges[1][0] | |
| def start_u(self): | |
| try: | |
| return float(self.ranges[0][1]) | |
| except TypeError: | |
| return self.ranges[0][1] | |
| def end_u(self): | |
| try: | |
| return float(self.ranges[0][2]) | |
| except TypeError: | |
| return self.ranges[0][2] | |
| def start_v(self): | |
| try: | |
| return float(self.ranges[1][1]) | |
| except TypeError: | |
| return self.ranges[1][1] | |
| def end_v(self): | |
| try: | |
| return float(self.ranges[1][2]) | |
| except TypeError: | |
| return self.ranges[1][2] | |
| def nb_of_points_u(self): | |
| return self.n[0] | |
| def nb_of_points_u(self, v): | |
| n = self.n | |
| self.n = [v, n[1:]] | |
| def nb_of_points_v(self): | |
| return self.n[1] | |
| def nb_of_points_v(self, v): | |
| n = self.n | |
| self.n = [n[0], v, n[2]] | |
| def __str__(self): | |
| return self._str_helper( | |
| "parametric cartesian surface: (%s, %s, %s) for" | |
| " %s over %s and %s over %s" % ( | |
| str(self.expr_x), str(self.expr_y), str(self.expr_z), | |
| str(self.var_u), str((self.start_u, self.end_u)), | |
| str(self.var_v), str((self.start_v, self.end_v)), | |
| )) | |
| def get_parameter_meshes(self): | |
| return self.get_data()[3:] | |
| def get_meshes(self): | |
| """Return the x,y,z coordinates for plotting the surface. | |
| This function is available for back-compatibility purposes. Consider | |
| using ``get_data()`` instead. | |
| """ | |
| return self.get_data()[:3] | |
| def get_data(self): | |
| """Return arrays of coordinates for plotting. | |
| Returns | |
| ======= | |
| x : np.ndarray [n2 x n1] | |
| x-coordinates. | |
| y : np.ndarray [n2 x n1] | |
| y-coordinates. | |
| z : np.ndarray [n2 x n1] | |
| z-coordinates. | |
| mesh_u : np.ndarray [n2 x n1] | |
| Discretized u range. | |
| mesh_v : np.ndarray [n2 x n1] | |
| Discretized v range. | |
| """ | |
| np = import_module('numpy') | |
| results = self._evaluate() | |
| # mask out complex values | |
| for i, r in enumerate(results): | |
| _re, _im = np.real(r), np.imag(r) | |
| _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan | |
| results[i] = _re | |
| # TODO: remove this | |
| x, y, z = results[2:] | |
| self._xlim = (np.amin(x), np.amax(x)) | |
| self._ylim = (np.amin(y), np.amax(y)) | |
| self._zlim = (np.amin(z), np.amax(z)) | |
| return self._apply_transform(*results[2:], *results[:2]) | |
| ### Contours | |
| class ContourSeries(SurfaceOver2DRangeSeries): | |
| """Representation for a contour plot.""" | |
| is_3Dsurface = False | |
| is_contour = True | |
| def __init__(self, *args, **kwargs): | |
| super().__init__(*args, **kwargs) | |
| self.is_filled = kwargs.get("is_filled", kwargs.get("fill", True)) | |
| self.show_clabels = kwargs.get("clabels", True) | |
| # NOTE: contour plots are used by plot_contour, plot_vector and | |
| # plot_complex_vector. By implementing contour_kw we are able to | |
| # quickly target the contour plot. | |
| self.rendering_kw = kwargs.get("contour_kw", | |
| kwargs.get("rendering_kw", {})) | |
| class GenericDataSeries(BaseSeries): | |
| """Represents generic numerical data. | |
| Notes | |
| ===== | |
| This class serves the purpose of back-compatibility with the "markers, | |
| annotations, fill, rectangles" keyword arguments that represent | |
| user-provided numerical data. In particular, it solves the problem of | |
| combining together two or more plot-objects with the ``extend`` or | |
| ``append`` methods: user-provided numerical data is also taken into | |
| consideration because it is stored in this series class. | |
| Also note that the current implementation is far from optimal, as each | |
| keyword argument is stored into an attribute in the ``Plot`` class, which | |
| requires a hard-coded if-statement in the ``MatplotlibBackend`` class. | |
| The implementation suggests that it is ok to add attributes and | |
| if-statements to provide more and more functionalities for user-provided | |
| numerical data (e.g. adding horizontal lines, or vertical lines, or bar | |
| plots, etc). However, in doing so one would reinvent the wheel: plotting | |
| libraries (like Matplotlib) already implements the necessary API. | |
| Instead of adding more keyword arguments and attributes, users interested | |
| in adding custom numerical data to a plot should retrieve the figure | |
| created by this plotting module. For example, this code: | |
| .. plot:: | |
| :context: close-figs | |
| :include-source: True | |
| from sympy import Symbol, plot, cos | |
| x = Symbol("x") | |
| p = plot(cos(x), markers=[{"args": [[0, 1, 2], [0, 1, -1], "*"]}]) | |
| Becomes: | |
| .. plot:: | |
| :context: close-figs | |
| :include-source: True | |
| p = plot(cos(x), backend="matplotlib") | |
| fig, ax = p._backend.fig, p._backend.ax | |
| ax.plot([0, 1, 2], [0, 1, -1], "*") | |
| fig | |
| Which is far better in terms of readability. Also, it gives access to the | |
| full plotting library capabilities, without the need to reinvent the wheel. | |
| """ | |
| is_generic = True | |
| def __init__(self, tp, *args, **kwargs): | |
| self.type = tp | |
| self.args = args | |
| self.rendering_kw = kwargs | |
| def get_data(self): | |
| return self.args | |
| class ImplicitSeries(BaseSeries): | |
| """Representation for 2D Implicit plot.""" | |
| is_implicit = True | |
| use_cm = False | |
| _N = 100 | |
| def __init__(self, expr, var_start_end_x, var_start_end_y, label="", **kwargs): | |
| super().__init__(**kwargs) | |
| self.adaptive = kwargs.get("adaptive", False) | |
| self.expr = expr | |
| self._label = str(expr) if label is None else label | |
| self._latex_label = latex(expr) if label is None else label | |
| self.ranges = [var_start_end_x, var_start_end_y] | |
| self.var_x, self.start_x, self.end_x = self.ranges[0] | |
| self.var_y, self.start_y, self.end_y = self.ranges[1] | |
| self._color = kwargs.get("color", kwargs.get("line_color", None)) | |
| if self.is_interactive and self.adaptive: | |
| raise NotImplementedError("Interactive plot with `adaptive=True` " | |
| "is not supported.") | |
| # Check whether the depth is greater than 4 or less than 0. | |
| depth = kwargs.get("depth", 0) | |
| if depth > 4: | |
| depth = 4 | |
| elif depth < 0: | |
| depth = 0 | |
| self.depth = 4 + depth | |
| self._post_init() | |
| def expr(self): | |
| if self.adaptive: | |
| return self._adaptive_expr | |
| return self._non_adaptive_expr | |
| def expr(self, expr): | |
| self._block_lambda_functions(expr) | |
| # these are needed for adaptive evaluation | |
| expr, has_equality = self._has_equality(sympify(expr)) | |
| self._adaptive_expr = expr | |
| self.has_equality = has_equality | |
| self._label = str(expr) | |
| self._latex_label = latex(expr) | |
| if isinstance(expr, (BooleanFunction, Ne)) and (not self.adaptive): | |
| self.adaptive = True | |
| msg = "contains Boolean functions. " | |
| if isinstance(expr, Ne): | |
| msg = "is an unequality. " | |
| warnings.warn( | |
| "The provided expression " + msg | |
| + "In order to plot the expression, the algorithm " | |
| + "automatically switched to an adaptive sampling." | |
| ) | |
| if isinstance(expr, BooleanFunction): | |
| self._non_adaptive_expr = None | |
| self._is_equality = False | |
| else: | |
| # these are needed for uniform meshing evaluation | |
| expr, is_equality = self._preprocess_meshgrid_expression(expr, self.adaptive) | |
| self._non_adaptive_expr = expr | |
| self._is_equality = is_equality | |
| def line_color(self): | |
| return self._color | |
| def line_color(self, v): | |
| self._color = v | |
| color = line_color | |
| def _has_equality(self, expr): | |
| # Represents whether the expression contains an Equality, GreaterThan | |
| # or LessThan | |
| has_equality = False | |
| def arg_expand(bool_expr): | |
| """Recursively expands the arguments of an Boolean Function""" | |
| for arg in bool_expr.args: | |
| if isinstance(arg, BooleanFunction): | |
| arg_expand(arg) | |
| elif isinstance(arg, Relational): | |
| arg_list.append(arg) | |
| arg_list = [] | |
| if isinstance(expr, BooleanFunction): | |
| arg_expand(expr) | |
| # Check whether there is an equality in the expression provided. | |
| if any(isinstance(e, (Equality, GreaterThan, LessThan)) for e in arg_list): | |
| has_equality = True | |
| elif not isinstance(expr, Relational): | |
| expr = Equality(expr, 0) | |
| has_equality = True | |
| elif isinstance(expr, (Equality, GreaterThan, LessThan)): | |
| has_equality = True | |
| return expr, has_equality | |
| def __str__(self): | |
| f = lambda t: float(t) if len(t.free_symbols) == 0 else t | |
| return self._str_helper( | |
| "Implicit expression: %s for %s over %s and %s over %s") % ( | |
| str(self._adaptive_expr), | |
| str(self.var_x), | |
| str((f(self.start_x), f(self.end_x))), | |
| str(self.var_y), | |
| str((f(self.start_y), f(self.end_y))), | |
| ) | |
| def get_data(self): | |
| """Returns numerical data. | |
| Returns | |
| ======= | |
| If the series is evaluated with the `adaptive=True` it returns: | |
| interval_list : list | |
| List of bounding rectangular intervals to be postprocessed and | |
| eventually used with Matplotlib's ``fill`` command. | |
| dummy : str | |
| A string containing ``"fill"``. | |
| Otherwise, it returns 2D numpy arrays to be used with Matplotlib's | |
| ``contour`` or ``contourf`` commands: | |
| x_array : np.ndarray | |
| y_array : np.ndarray | |
| z_array : np.ndarray | |
| plot_type : str | |
| A string specifying which plot command to use, ``"contour"`` | |
| or ``"contourf"``. | |
| """ | |
| if self.adaptive: | |
| data = self._adaptive_eval() | |
| if data is not None: | |
| return data | |
| return self._get_meshes_grid() | |
| def _adaptive_eval(self): | |
| """ | |
| References | |
| ========== | |
| .. [1] Jeffrey Allen Tupper. Reliable Two-Dimensional Graphing Methods for | |
| Mathematical Formulae with Two Free Variables. | |
| .. [2] Jeffrey Allen Tupper. Graphing Equations with Generalized Interval | |
| Arithmetic. Master's thesis. University of Toronto, 1996 | |
| """ | |
| import sympy.plotting.intervalmath.lib_interval as li | |
| user_functions = {} | |
| printer = IntervalMathPrinter({ | |
| 'fully_qualified_modules': False, 'inline': True, | |
| 'allow_unknown_functions': True, | |
| 'user_functions': user_functions}) | |
| keys = [t for t in dir(li) if ("__" not in t) and (t not in ["import_module", "interval"])] | |
| vals = [getattr(li, k) for k in keys] | |
| d = dict(zip(keys, vals)) | |
| func = lambdify((self.var_x, self.var_y), self.expr, modules=[d], printer=printer) | |
| data = None | |
| try: | |
| data = self._get_raster_interval(func) | |
| except NameError as err: | |
| warnings.warn( | |
| "Adaptive meshing could not be applied to the" | |
| " expression, as some functions are not yet implemented" | |
| " in the interval math module:\n\n" | |
| "NameError: %s\n\n" % err + | |
| "Proceeding with uniform meshing." | |
| ) | |
| self.adaptive = False | |
| except TypeError: | |
| warnings.warn( | |
| "Adaptive meshing could not be applied to the" | |
| " expression. Using uniform meshing.") | |
| self.adaptive = False | |
| return data | |
| def _get_raster_interval(self, func): | |
| """Uses interval math to adaptively mesh and obtain the plot""" | |
| np = import_module('numpy') | |
| k = self.depth | |
| interval_list = [] | |
| sx, sy = [float(t) for t in [self.start_x, self.start_y]] | |
| ex, ey = [float(t) for t in [self.end_x, self.end_y]] | |
| # Create initial 32 divisions | |
| xsample = np.linspace(sx, ex, 33) | |
| ysample = np.linspace(sy, ey, 33) | |
| # Add a small jitter so that there are no false positives for equality. | |
| # Ex: y==x becomes True for x interval(1, 2) and y interval(1, 2) | |
| # which will draw a rectangle. | |
| jitterx = ( | |
| (np.random.rand(len(xsample)) * 2 - 1) | |
| * (ex - sx) | |
| / 2 ** 20 | |
| ) | |
| jittery = ( | |
| (np.random.rand(len(ysample)) * 2 - 1) | |
| * (ey - sy) | |
| / 2 ** 20 | |
| ) | |
| xsample += jitterx | |
| ysample += jittery | |
| xinter = [interval(x1, x2) for x1, x2 in zip(xsample[:-1], xsample[1:])] | |
| yinter = [interval(y1, y2) for y1, y2 in zip(ysample[:-1], ysample[1:])] | |
| interval_list = [[x, y] for x in xinter for y in yinter] | |
| plot_list = [] | |
| # recursive call refinepixels which subdivides the intervals which are | |
| # neither True nor False according to the expression. | |
| def refine_pixels(interval_list): | |
| """Evaluates the intervals and subdivides the interval if the | |
| expression is partially satisfied.""" | |
| temp_interval_list = [] | |
| plot_list = [] | |
| for intervals in interval_list: | |
| # Convert the array indices to x and y values | |
| intervalx = intervals[0] | |
| intervaly = intervals[1] | |
| func_eval = func(intervalx, intervaly) | |
| # The expression is valid in the interval. Change the contour | |
| # array values to 1. | |
| if func_eval[1] is False or func_eval[0] is False: | |
| pass | |
| elif func_eval == (True, True): | |
| plot_list.append([intervalx, intervaly]) | |
| elif func_eval[1] is None or func_eval[0] is None: | |
| # Subdivide | |
| avgx = intervalx.mid | |
| avgy = intervaly.mid | |
| a = interval(intervalx.start, avgx) | |
| b = interval(avgx, intervalx.end) | |
| c = interval(intervaly.start, avgy) | |
| d = interval(avgy, intervaly.end) | |
| temp_interval_list.append([a, c]) | |
| temp_interval_list.append([a, d]) | |
| temp_interval_list.append([b, c]) | |
| temp_interval_list.append([b, d]) | |
| return temp_interval_list, plot_list | |
| while k >= 0 and len(interval_list): | |
| interval_list, plot_list_temp = refine_pixels(interval_list) | |
| plot_list.extend(plot_list_temp) | |
| k = k - 1 | |
| # Check whether the expression represents an equality | |
| # If it represents an equality, then none of the intervals | |
| # would have satisfied the expression due to floating point | |
| # differences. Add all the undecided values to the plot. | |
| if self.has_equality: | |
| for intervals in interval_list: | |
| intervalx = intervals[0] | |
| intervaly = intervals[1] | |
| func_eval = func(intervalx, intervaly) | |
| if func_eval[1] and func_eval[0] is not False: | |
| plot_list.append([intervalx, intervaly]) | |
| return plot_list, "fill" | |
| def _get_meshes_grid(self): | |
| """Generates the mesh for generating a contour. | |
| In the case of equality, ``contour`` function of matplotlib can | |
| be used. In other cases, matplotlib's ``contourf`` is used. | |
| """ | |
| np = import_module('numpy') | |
| xarray, yarray, z_grid = self._evaluate() | |
| _re, _im = np.real(z_grid), np.imag(z_grid) | |
| _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan | |
| if self._is_equality: | |
| return xarray, yarray, _re, 'contour' | |
| return xarray, yarray, _re, 'contourf' | |
| def _preprocess_meshgrid_expression(expr, adaptive): | |
| """If the expression is a Relational, rewrite it as a single | |
| expression. | |
| Returns | |
| ======= | |
| expr : Expr | |
| The rewritten expression | |
| equality : Boolean | |
| Whether the original expression was an Equality or not. | |
| """ | |
| equality = False | |
| if isinstance(expr, Equality): | |
| expr = expr.lhs - expr.rhs | |
| equality = True | |
| elif isinstance(expr, Relational): | |
| expr = expr.gts - expr.lts | |
| elif not adaptive: | |
| raise NotImplementedError( | |
| "The expression is not supported for " | |
| "plotting in uniform meshed plot." | |
| ) | |
| return expr, equality | |
| def get_label(self, use_latex=False, wrapper="$%s$"): | |
| """Return the label to be used to display the expression. | |
| Parameters | |
| ========== | |
| use_latex : bool | |
| If False, the string representation of the expression is returned. | |
| If True, the latex representation is returned. | |
| wrapper : str | |
| The backend might need the latex representation to be wrapped by | |
| some characters. Default to ``"$%s$"``. | |
| Returns | |
| ======= | |
| label : str | |
| """ | |
| if use_latex is False: | |
| return self._label | |
| if self._label == str(self._adaptive_expr): | |
| return self._get_wrapped_label(self._latex_label, wrapper) | |
| return self._latex_label | |
| ############################################################################## | |
| # Finding the centers of line segments or mesh faces | |
| ############################################################################## | |
| def centers_of_segments(array): | |
| np = import_module('numpy') | |
| return np.mean(np.vstack((array[:-1], array[1:])), 0) | |
| def centers_of_faces(array): | |
| np = import_module('numpy') | |
| return np.mean(np.dstack((array[:-1, :-1], | |
| array[1:, :-1], | |
| array[:-1, 1:], | |
| array[:-1, :-1], | |
| )), 2) | |
| def flat(x, y, z, eps=1e-3): | |
| """Checks whether three points are almost collinear""" | |
| np = import_module('numpy') | |
| # Workaround plotting piecewise (#8577) | |
| vector_a = (x - y).astype(float) | |
| vector_b = (z - y).astype(float) | |
| dot_product = np.dot(vector_a, vector_b) | |
| vector_a_norm = np.linalg.norm(vector_a) | |
| vector_b_norm = np.linalg.norm(vector_b) | |
| cos_theta = dot_product / (vector_a_norm * vector_b_norm) | |
| return abs(cos_theta + 1) < eps | |
| def _set_discretization_points(kwargs, pt): | |
| """Allow the use of the keyword arguments ``n, n1, n2`` to | |
| specify the number of discretization points in one and two | |
| directions, while keeping back-compatibility with older keyword arguments | |
| like, ``nb_of_points, nb_of_points_*, points``. | |
| Parameters | |
| ========== | |
| kwargs : dict | |
| Dictionary of keyword arguments passed into a plotting function. | |
| pt : type | |
| The type of the series, which indicates the kind of plot we are | |
| trying to create. | |
| """ | |
| replace_old_keywords = { | |
| "nb_of_points": "n", | |
| "nb_of_points_x": "n1", | |
| "nb_of_points_y": "n2", | |
| "nb_of_points_u": "n1", | |
| "nb_of_points_v": "n2", | |
| "points": "n" | |
| } | |
| for k, v in replace_old_keywords.items(): | |
| if k in kwargs.keys(): | |
| kwargs[v] = kwargs.pop(k) | |
| if pt in [LineOver1DRangeSeries, Parametric2DLineSeries, | |
| Parametric3DLineSeries]: | |
| if "n" in kwargs.keys(): | |
| kwargs["n1"] = kwargs["n"] | |
| if hasattr(kwargs["n"], "__iter__") and (len(kwargs["n"]) > 0): | |
| kwargs["n1"] = kwargs["n"][0] | |
| elif pt in [SurfaceOver2DRangeSeries, ContourSeries, | |
| ParametricSurfaceSeries, ImplicitSeries]: | |
| if "n" in kwargs.keys(): | |
| if hasattr(kwargs["n"], "__iter__") and (len(kwargs["n"]) > 1): | |
| kwargs["n1"] = kwargs["n"][0] | |
| kwargs["n2"] = kwargs["n"][1] | |
| else: | |
| kwargs["n1"] = kwargs["n2"] = kwargs["n"] | |
| return kwargs | |
Xet Storage Details
- Size:
- 96.6 kB
- Xet hash:
- b9392567176b9a9482f0d1edbff83ff532dedbc183bb18598207b23f3260813a
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.