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

1"""Implicit plotting module for SymPy. 

2 

3Explanation 

4=========== 

5 

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. 

11 

12Boolean combinations of expressions cannot be plotted by the fall back 

13algorithm. 

14 

15See Also 

16======== 

17 

18sympy.plotting.plot 

19 

20References 

21========== 

22 

23.. [1] Jeffrey Allen Tupper. Reliable Two-Dimensional Graphing Methods for 

24Mathematical Formulae with Two Free Variables. 

25 

26.. [2] Jeffrey Allen Tupper. Graphing Equations with Generalized Interval 

27Arithmetic. Master's thesis. University of Toronto, 1996 

28 

29""" 

30 

31 

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 

47 

48 

49class ImplicitSeries(BaseSeries): 

50 """ Representation for Implicit plot """ 

51 is_implicit = True 

52 

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 

72 

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))) 

81 

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 

97 

98 if self.use_interval_math: 

99 return self._get_raster_interval(func) 

100 else: 

101 return self._get_meshes_grid() 

102 

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) 

111 

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 

121 

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 = [] 

128 

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: 

137 

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 

161 

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' 

178 

179 def _get_meshes_grid(self): 

180 """Generates the mesh for generating a contour. 

181 

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 

189 

190 elif isinstance(self.expr, (GreaterThan, StrictGreaterThan)): 

191 expr = self.expr.lhs - self.expr.rhs 

192 

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) 

202 

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' 

211 

212 

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. 

217 

218 Arguments 

219 ========= 

220 

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)`` 

226 

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. 

229 

230 The following keyword arguments can also be used: 

231 

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. 

234 

235 - ``depth`` integer. The depth of recursion for adaptive mesh grid. 

236 Default value is 0. Takes value in the range (0, 4). 

237 

238 - ``points`` integer. The number of points if adaptive mesh grid is not 

239 used. Default value is 300. 

240 

241 - ``show`` Boolean. Default value is True. If set to False, the plot will 

242 not be shown. See ``Plot`` for further information. 

243 

244 - ``title`` string. The title for the plot. 

245 

246 - ``xlabel`` string. The label for the x-axis 

247 

248 - ``ylabel`` string. The label for the y-axis 

249 

250 Aesthetics options: 

251 

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" 

255 

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. 

262 

263 Examples 

264 ======== 

265 

266 Plot expressions: 

267 

268 .. plot:: 

269 :context: reset 

270 :format: doctest 

271 :include-source: True 

272 

273 >>> from sympy import plot_implicit, symbols, Eq, And 

274 >>> x, y = symbols('x y') 

275 

276 Without any ranges for the symbols in the expression: 

277 

278 .. plot:: 

279 :context: close-figs 

280 :format: doctest 

281 :include-source: True 

282 

283 >>> p1 = plot_implicit(Eq(x**2 + y**2, 5)) 

284 

285 With the range for the symbols: 

286 

287 .. plot:: 

288 :context: close-figs 

289 :format: doctest 

290 :include-source: True 

291 

292 >>> p2 = plot_implicit( 

293 ... Eq(x**2 + y**2, 3), (x, -3, 3), (y, -3, 3)) 

294 

295 With depth of recursion as argument: 

296 

297 .. plot:: 

298 :context: close-figs 

299 :format: doctest 

300 :include-source: True 

301 

302 >>> p3 = plot_implicit( 

303 ... Eq(x**2 + y**2, 5), (x, -4, 4), (y, -4, 4), depth = 2) 

304 

305 Using mesh grid and not using adaptive meshing: 

306 

307 .. plot:: 

308 :context: close-figs 

309 :format: doctest 

310 :include-source: True 

311 

312 >>> p4 = plot_implicit( 

313 ... Eq(x**2 + y**2, 5), (x, -5, 5), (y, -2, 2), 

314 ... adaptive=False) 

315 

316 Using mesh grid without using adaptive meshing with number of points 

317 specified: 

318 

319 .. plot:: 

320 :context: close-figs 

321 :format: doctest 

322 :include-source: True 

323 

324 >>> p5 = plot_implicit( 

325 ... Eq(x**2 + y**2, 5), (x, -5, 5), (y, -2, 2), 

326 ... adaptive=False, points=400) 

327 

328 Plotting regions: 

329 

330 .. plot:: 

331 :context: close-figs 

332 :format: doctest 

333 :include-source: True 

334 

335 >>> p6 = plot_implicit(y > x**2) 

336 

337 Plotting Using boolean conjunctions: 

338 

339 .. plot:: 

340 :context: close-figs 

341 :format: doctest 

342 :include-source: True 

343 

344 >>> p7 = plot_implicit(And(y > x, y > -x)) 

345 

346 When plotting an expression with a single variable (y - 1, for example), 

347 specify the x or the y variable explicitly: 

348 

349 .. plot:: 

350 :context: close-figs 

351 :format: doctest 

352 :include-source: True 

353 

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 

359 

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) 

369 

370 arg_list = [] 

371 if isinstance(expr, BooleanFunction): 

372 arg_expand(expr) 

373 

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 

378 

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 

384 

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") 

392 

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) 

401 

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]) 

412 

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 

418 

419 series_argument = ImplicitSeries(expr, var_start_end_x, var_start_end_y, 

420 has_equality, adaptive, depth, 

421 points, line_color) 

422 

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