Coverage for /usr/lib/python3/dist-packages/sympy/plotting/plot_implicit.py: 13%
171 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""Implicit plotting module for SymPy.
3Explanation
4===========
6The module implements a data series called ImplicitSeries which is used by
7``Plot`` class to plot implicit plots for different backends. The module,
8by default, implements plotting using interval arithmetic. It switches to a
9fall back algorithm if the expression cannot be plotted using interval arithmetic.
10It is also possible to specify to use the fall back algorithm for all plots.
12Boolean combinations of expressions cannot be plotted by the fall back
13algorithm.
15See Also
16========
18sympy.plotting.plot
20References
21==========
23.. [1] Jeffrey Allen Tupper. Reliable Two-Dimensional Graphing Methods for
24Mathematical Formulae with Two Free Variables.
26.. [2] Jeffrey Allen Tupper. Graphing Equations with Generalized Interval
27Arithmetic. Master's thesis. University of Toronto, 1996
29"""
32from .plot import BaseSeries, Plot
33from .experimental_lambdify import experimental_lambdify, vectorized_lambdify
34from .intervalmath import interval
35from sympy.core.relational import (Equality, GreaterThan, LessThan,
36 Relational, StrictLessThan, StrictGreaterThan)
37from sympy.core.containers import Tuple
38from sympy.core.relational import Eq
39from sympy.core.symbol import (Dummy, Symbol)
40from sympy.core.sympify import sympify
41from sympy.external import import_module
42from sympy.logic.boolalg import BooleanFunction
43from sympy.polys.polyutils import _sort_gens
44from sympy.utilities.decorator import doctest_depends_on
45from sympy.utilities.iterables import flatten
46import warnings
49class ImplicitSeries(BaseSeries):
50 """ Representation for Implicit plot """
51 is_implicit = True
53 def __init__(self, expr, var_start_end_x, var_start_end_y,
54 has_equality, use_interval_math, depth, nb_of_points,
55 line_color):
56 super().__init__()
57 self.expr = sympify(expr)
58 self.label = self.expr
59 self.var_x = sympify(var_start_end_x[0])
60 self.start_x = float(var_start_end_x[1])
61 self.end_x = float(var_start_end_x[2])
62 self.var_y = sympify(var_start_end_y[0])
63 self.start_y = float(var_start_end_y[1])
64 self.end_y = float(var_start_end_y[2])
65 self.get_points = self.get_raster
66 self.has_equality = has_equality # If the expression has equality, i.e.
67 #Eq, Greaterthan, LessThan.
68 self.nb_of_points = nb_of_points
69 self.use_interval_math = use_interval_math
70 self.depth = 4 + depth
71 self.line_color = line_color
73 def __str__(self):
74 return ('Implicit equation: %s for '
75 '%s over %s and %s over %s') % (
76 str(self.expr),
77 str(self.var_x),
78 str((self.start_x, self.end_x)),
79 str(self.var_y),
80 str((self.start_y, self.end_y)))
82 def get_raster(self):
83 func = experimental_lambdify((self.var_x, self.var_y), self.expr,
84 use_interval=True)
85 xinterval = interval(self.start_x, self.end_x)
86 yinterval = interval(self.start_y, self.end_y)
87 try:
88 func(xinterval, yinterval)
89 except AttributeError:
90 # XXX: AttributeError("'list' object has no attribute 'is_real'")
91 # That needs fixing somehow - we shouldn't be catching
92 # AttributeError here.
93 if self.use_interval_math:
94 warnings.warn("Adaptive meshing could not be applied to the"
95 " expression. Using uniform meshing.", stacklevel=7)
96 self.use_interval_math = False
98 if self.use_interval_math:
99 return self._get_raster_interval(func)
100 else:
101 return self._get_meshes_grid()
103 def _get_raster_interval(self, func):
104 """ Uses interval math to adaptively mesh and obtain the plot"""
105 k = self.depth
106 interval_list = []
107 #Create initial 32 divisions
108 np = import_module('numpy')
109 xsample = np.linspace(self.start_x, self.end_x, 33)
110 ysample = np.linspace(self.start_y, self.end_y, 33)
112 #Add a small jitter so that there are no false positives for equality.
113 # Ex: y==x becomes True for x interval(1, 2) and y interval(1, 2)
114 #which will draw a rectangle.
115 jitterx = (np.random.rand(
116 len(xsample)) * 2 - 1) * (self.end_x - self.start_x) / 2**20
117 jittery = (np.random.rand(
118 len(ysample)) * 2 - 1) * (self.end_y - self.start_y) / 2**20
119 xsample += jitterx
120 ysample += jittery
122 xinter = [interval(x1, x2) for x1, x2 in zip(xsample[:-1],
123 xsample[1:])]
124 yinter = [interval(y1, y2) for y1, y2 in zip(ysample[:-1],
125 ysample[1:])]
126 interval_list = [[x, y] for x in xinter for y in yinter]
127 plot_list = []
129 #recursive call refinepixels which subdivides the intervals which are
130 #neither True nor False according to the expression.
131 def refine_pixels(interval_list):
132 """ Evaluates the intervals and subdivides the interval if the
133 expression is partially satisfied."""
134 temp_interval_list = []
135 plot_list = []
136 for intervals in interval_list:
138 #Convert the array indices to x and y values
139 intervalx = intervals[0]
140 intervaly = intervals[1]
141 func_eval = func(intervalx, intervaly)
142 #The expression is valid in the interval. Change the contour
143 #array values to 1.
144 if func_eval[1] is False or func_eval[0] is False:
145 pass
146 elif func_eval == (True, True):
147 plot_list.append([intervalx, intervaly])
148 elif func_eval[1] is None or func_eval[0] is None:
149 #Subdivide
150 avgx = intervalx.mid
151 avgy = intervaly.mid
152 a = interval(intervalx.start, avgx)
153 b = interval(avgx, intervalx.end)
154 c = interval(intervaly.start, avgy)
155 d = interval(avgy, intervaly.end)
156 temp_interval_list.append([a, c])
157 temp_interval_list.append([a, d])
158 temp_interval_list.append([b, c])
159 temp_interval_list.append([b, d])
160 return temp_interval_list, plot_list
162 while k >= 0 and len(interval_list):
163 interval_list, plot_list_temp = refine_pixels(interval_list)
164 plot_list.extend(plot_list_temp)
165 k = k - 1
166 #Check whether the expression represents an equality
167 #If it represents an equality, then none of the intervals
168 #would have satisfied the expression due to floating point
169 #differences. Add all the undecided values to the plot.
170 if self.has_equality:
171 for intervals in interval_list:
172 intervalx = intervals[0]
173 intervaly = intervals[1]
174 func_eval = func(intervalx, intervaly)
175 if func_eval[1] and func_eval[0] is not False:
176 plot_list.append([intervalx, intervaly])
177 return plot_list, 'fill'
179 def _get_meshes_grid(self):
180 """Generates the mesh for generating a contour.
182 In the case of equality, ``contour`` function of matplotlib can
183 be used. In other cases, matplotlib's ``contourf`` is used.
184 """
185 equal = False
186 if isinstance(self.expr, Equality):
187 expr = self.expr.lhs - self.expr.rhs
188 equal = True
190 elif isinstance(self.expr, (GreaterThan, StrictGreaterThan)):
191 expr = self.expr.lhs - self.expr.rhs
193 elif isinstance(self.expr, (LessThan, StrictLessThan)):
194 expr = self.expr.rhs - self.expr.lhs
195 else:
196 raise NotImplementedError("The expression is not supported for "
197 "plotting in uniform meshed plot.")
198 np = import_module('numpy')
199 xarray = np.linspace(self.start_x, self.end_x, self.nb_of_points)
200 yarray = np.linspace(self.start_y, self.end_y, self.nb_of_points)
201 x_grid, y_grid = np.meshgrid(xarray, yarray)
203 func = vectorized_lambdify((self.var_x, self.var_y), expr)
204 z_grid = func(x_grid, y_grid)
205 z_grid[np.ma.where(z_grid < 0)] = -1
206 z_grid[np.ma.where(z_grid > 0)] = 1
207 if equal:
208 return xarray, yarray, z_grid, 'contour'
209 else:
210 return xarray, yarray, z_grid, 'contourf'
213@doctest_depends_on(modules=('matplotlib',))
214def plot_implicit(expr, x_var=None, y_var=None, adaptive=True, depth=0,
215 points=300, line_color="blue", show=True, **kwargs):
216 """A plot function to plot implicit equations / inequalities.
218 Arguments
219 =========
221 - expr : The equation / inequality that is to be plotted.
222 - x_var (optional) : symbol to plot on x-axis or tuple giving symbol
223 and range as ``(symbol, xmin, xmax)``
224 - y_var (optional) : symbol to plot on y-axis or tuple giving symbol
225 and range as ``(symbol, ymin, ymax)``
227 If neither ``x_var`` nor ``y_var`` are given then the free symbols in the
228 expression will be assigned in the order they are sorted.
230 The following keyword arguments can also be used:
232 - ``adaptive`` Boolean. The default value is set to True. It has to be
233 set to False if you want to use a mesh grid.
235 - ``depth`` integer. The depth of recursion for adaptive mesh grid.
236 Default value is 0. Takes value in the range (0, 4).
238 - ``points`` integer. The number of points if adaptive mesh grid is not
239 used. Default value is 300.
241 - ``show`` Boolean. Default value is True. If set to False, the plot will
242 not be shown. See ``Plot`` for further information.
244 - ``title`` string. The title for the plot.
246 - ``xlabel`` string. The label for the x-axis
248 - ``ylabel`` string. The label for the y-axis
250 Aesthetics options:
252 - ``line_color``: float or string. Specifies the color for the plot.
253 See ``Plot`` to see how to set color for the plots.
254 Default value is "Blue"
256 plot_implicit, by default, uses interval arithmetic to plot functions. If
257 the expression cannot be plotted using interval arithmetic, it defaults to
258 a generating a contour using a mesh grid of fixed number of points. By
259 setting adaptive to False, you can force plot_implicit to use the mesh
260 grid. The mesh grid method can be effective when adaptive plotting using
261 interval arithmetic, fails to plot with small line width.
263 Examples
264 ========
266 Plot expressions:
268 .. plot::
269 :context: reset
270 :format: doctest
271 :include-source: True
273 >>> from sympy import plot_implicit, symbols, Eq, And
274 >>> x, y = symbols('x y')
276 Without any ranges for the symbols in the expression:
278 .. plot::
279 :context: close-figs
280 :format: doctest
281 :include-source: True
283 >>> p1 = plot_implicit(Eq(x**2 + y**2, 5))
285 With the range for the symbols:
287 .. plot::
288 :context: close-figs
289 :format: doctest
290 :include-source: True
292 >>> p2 = plot_implicit(
293 ... Eq(x**2 + y**2, 3), (x, -3, 3), (y, -3, 3))
295 With depth of recursion as argument:
297 .. plot::
298 :context: close-figs
299 :format: doctest
300 :include-source: True
302 >>> p3 = plot_implicit(
303 ... Eq(x**2 + y**2, 5), (x, -4, 4), (y, -4, 4), depth = 2)
305 Using mesh grid and not using adaptive meshing:
307 .. plot::
308 :context: close-figs
309 :format: doctest
310 :include-source: True
312 >>> p4 = plot_implicit(
313 ... Eq(x**2 + y**2, 5), (x, -5, 5), (y, -2, 2),
314 ... adaptive=False)
316 Using mesh grid without using adaptive meshing with number of points
317 specified:
319 .. plot::
320 :context: close-figs
321 :format: doctest
322 :include-source: True
324 >>> p5 = plot_implicit(
325 ... Eq(x**2 + y**2, 5), (x, -5, 5), (y, -2, 2),
326 ... adaptive=False, points=400)
328 Plotting regions:
330 .. plot::
331 :context: close-figs
332 :format: doctest
333 :include-source: True
335 >>> p6 = plot_implicit(y > x**2)
337 Plotting Using boolean conjunctions:
339 .. plot::
340 :context: close-figs
341 :format: doctest
342 :include-source: True
344 >>> p7 = plot_implicit(And(y > x, y > -x))
346 When plotting an expression with a single variable (y - 1, for example),
347 specify the x or the y variable explicitly:
349 .. plot::
350 :context: close-figs
351 :format: doctest
352 :include-source: True
354 >>> p8 = plot_implicit(y - 1, y_var=y)
355 >>> p9 = plot_implicit(x - 1, x_var=x)
356 """
357 has_equality = False # Represents whether the expression contains an Equality,
358 #GreaterThan or LessThan
360 def arg_expand(bool_expr):
361 """
362 Recursively expands the arguments of an Boolean Function
363 """
364 for arg in bool_expr.args:
365 if isinstance(arg, BooleanFunction):
366 arg_expand(arg)
367 elif isinstance(arg, Relational):
368 arg_list.append(arg)
370 arg_list = []
371 if isinstance(expr, BooleanFunction):
372 arg_expand(expr)
374 #Check whether there is an equality in the expression provided.
375 if any(isinstance(e, (Equality, GreaterThan, LessThan))
376 for e in arg_list):
377 has_equality = True
379 elif not isinstance(expr, Relational):
380 expr = Eq(expr, 0)
381 has_equality = True
382 elif isinstance(expr, (Equality, GreaterThan, LessThan)):
383 has_equality = True
385 xyvar = [i for i in (x_var, y_var) if i is not None]
386 free_symbols = expr.free_symbols
387 range_symbols = Tuple(*flatten(xyvar)).free_symbols
388 undeclared = free_symbols - range_symbols
389 if len(free_symbols & range_symbols) > 2:
390 raise NotImplementedError("Implicit plotting is not implemented for "
391 "more than 2 variables")
393 #Create default ranges if the range is not provided.
394 default_range = Tuple(-5, 5)
395 def _range_tuple(s):
396 if isinstance(s, Symbol):
397 return Tuple(s) + default_range
398 if len(s) == 3:
399 return Tuple(*s)
400 raise ValueError('symbol or `(symbol, min, max)` expected but got %s' % s)
402 if len(xyvar) == 0:
403 xyvar = list(_sort_gens(free_symbols))
404 var_start_end_x = _range_tuple(xyvar[0])
405 x = var_start_end_x[0]
406 if len(xyvar) != 2:
407 if x in undeclared or not undeclared:
408 xyvar.append(Dummy('f(%s)' % x.name))
409 else:
410 xyvar.append(undeclared.pop())
411 var_start_end_y = _range_tuple(xyvar[1])
413 #Check whether the depth is greater than 4 or less than 0.
414 if depth > 4:
415 depth = 4
416 elif depth < 0:
417 depth = 0
419 series_argument = ImplicitSeries(expr, var_start_end_x, var_start_end_y,
420 has_equality, adaptive, depth,
421 points, line_color)
423 #set the x and y limits
424 kwargs['xlim'] = tuple(float(x) for x in var_start_end_x[1:])
425 kwargs['ylim'] = tuple(float(y) for y in var_start_end_y[1:])
426 # set the x and y labels
427 kwargs.setdefault('xlabel', var_start_end_x[0])
428 kwargs.setdefault('ylabel', var_start_end_y[0])
429 p = Plot(series_argument, **kwargs)
430 if show:
431 p.show()
432 return p