Coverage for /usr/lib/python3/dist-packages/sympy/plotting/plot.py: 15%

866 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1"""Plotting module for SymPy. 

2 

3A plot is represented by the ``Plot`` class that contains a reference to the 

4backend and a list of the data series to be plotted. The data series are 

5instances of classes meant to simplify getting points and meshes from SymPy 

6expressions. ``plot_backends`` is a dictionary with all the backends. 

7 

8This module gives only the essential. For all the fancy stuff use directly 

9the backend. You can get the backend wrapper for every plot from the 

10``_backend`` attribute. Moreover the data series classes have various useful 

11methods like ``get_points``, ``get_meshes``, etc, that may 

12be useful if you wish to use another plotting library. 

13 

14Especially if you need publication ready graphs and this module is not enough 

15for you - just get the ``_backend`` attribute and add whatever you want 

16directly to it. In the case of matplotlib (the common way to graph data in 

17python) just copy ``_backend.fig`` which is the figure and ``_backend.ax`` 

18which is the axis and work on them as you would on any other matplotlib object. 

19 

20Simplicity of code takes much greater importance than performance. Do not use it 

21if you care at all about performance. A new backend instance is initialized 

22every time you call ``show()`` and the old one is left to the garbage collector. 

23""" 

24 

25 

26from collections.abc import Callable 

27 

28 

29from sympy.core.basic import Basic 

30from sympy.core.containers import Tuple 

31from sympy.core.expr import Expr 

32from sympy.core.function import arity, Function 

33from sympy.core.symbol import (Dummy, Symbol) 

34from sympy.core.sympify import sympify 

35from sympy.external import import_module 

36from sympy.printing.latex import latex 

37from sympy.utilities.exceptions import sympy_deprecation_warning 

38from sympy.utilities.iterables import is_sequence 

39from .experimental_lambdify import (vectorized_lambdify, lambdify) 

40 

41# N.B. 

42# When changing the minimum module version for matplotlib, please change 

43# the same in the `SymPyDocTestFinder`` in `sympy/testing/runtests.py` 

44 

45# Backend specific imports - textplot 

46from sympy.plotting.textplot import textplot 

47 

48# Global variable 

49# Set to False when running tests / doctests so that the plots don't show. 

50_show = True 

51 

52 

53def unset_show(): 

54 """ 

55 Disable show(). For use in the tests. 

56 """ 

57 global _show 

58 _show = False 

59 

60def _str_or_latex(label): 

61 if isinstance(label, Basic): 

62 return latex(label, mode='inline') 

63 return str(label) 

64 

65############################################################################## 

66# The public interface 

67############################################################################## 

68 

69 

70class Plot: 

71 """The central class of the plotting module. 

72 

73 Explanation 

74 =========== 

75 

76 For interactive work the function :func:`plot()` is better suited. 

77 

78 This class permits the plotting of SymPy expressions using numerous 

79 backends (:external:mod:`matplotlib`, textplot, the old pyglet module for SymPy, Google 

80 charts api, etc). 

81 

82 The figure can contain an arbitrary number of plots of SymPy expressions, 

83 lists of coordinates of points, etc. Plot has a private attribute _series that 

84 contains all data series to be plotted (expressions for lines or surfaces, 

85 lists of points, etc (all subclasses of BaseSeries)). Those data series are 

86 instances of classes not imported by ``from sympy import *``. 

87 

88 The customization of the figure is on two levels. Global options that 

89 concern the figure as a whole (e.g. title, xlabel, scale, etc) and 

90 per-data series options (e.g. name) and aesthetics (e.g. color, point shape, 

91 line type, etc.). 

92 

93 The difference between options and aesthetics is that an aesthetic can be 

94 a function of the coordinates (or parameters in a parametric plot). The 

95 supported values for an aesthetic are: 

96 

97 - None (the backend uses default values) 

98 - a constant 

99 - a function of one variable (the first coordinate or parameter) 

100 - a function of two variables (the first and second coordinate or parameters) 

101 - a function of three variables (only in nonparametric 3D plots) 

102 

103 Their implementation depends on the backend so they may not work in some 

104 backends. 

105 

106 If the plot is parametric and the arity of the aesthetic function permits 

107 it the aesthetic is calculated over parameters and not over coordinates. 

108 If the arity does not permit calculation over parameters the calculation is 

109 done over coordinates. 

110 

111 Only cartesian coordinates are supported for the moment, but you can use 

112 the parametric plots to plot in polar, spherical and cylindrical 

113 coordinates. 

114 

115 The arguments for the constructor Plot must be subclasses of BaseSeries. 

116 

117 Any global option can be specified as a keyword argument. 

118 

119 The global options for a figure are: 

120 

121 - title : str 

122 - xlabel : str or Symbol 

123 - ylabel : str or Symbol 

124 - zlabel : str or Symbol 

125 - legend : bool 

126 - xscale : {'linear', 'log'} 

127 - yscale : {'linear', 'log'} 

128 - axis : bool 

129 - axis_center : tuple of two floats or {'center', 'auto'} 

130 - xlim : tuple of two floats 

131 - ylim : tuple of two floats 

132 - aspect_ratio : tuple of two floats or {'auto'} 

133 - autoscale : bool 

134 - margin : float in [0, 1] 

135 - backend : {'default', 'matplotlib', 'text'} or a subclass of BaseBackend 

136 - size : optional tuple of two floats, (width, height); default: None 

137 

138 The per data series options and aesthetics are: 

139 There are none in the base series. See below for options for subclasses. 

140 

141 Some data series support additional aesthetics or options: 

142 

143 :class:`~.LineOver1DRangeSeries`, :class:`~.Parametric2DLineSeries`, and 

144 :class:`~.Parametric3DLineSeries` support the following: 

145 

146 Aesthetics: 

147 

148 - line_color : string, or float, or function, optional 

149 Specifies the color for the plot, which depends on the backend being 

150 used. 

151 

152 For example, if ``MatplotlibBackend`` is being used, then 

153 Matplotlib string colors are acceptable (``"red"``, ``"r"``, 

154 ``"cyan"``, ``"c"``, ...). 

155 Alternatively, we can use a float number, 0 < color < 1, wrapped in a 

156 string (for example, ``line_color="0.5"``) to specify grayscale colors. 

157 Alternatively, We can specify a function returning a single 

158 float value: this will be used to apply a color-loop (for example, 

159 ``line_color=lambda x: math.cos(x)``). 

160 

161 Note that by setting line_color, it would be applied simultaneously 

162 to all the series. 

163 

164 Options: 

165 

166 - label : str 

167 - steps : bool 

168 - integers_only : bool 

169 

170 :class:`~.SurfaceOver2DRangeSeries` and :class:`~.ParametricSurfaceSeries` 

171 support the following: 

172 

173 Aesthetics: 

174 

175 - surface_color : function which returns a float. 

176 """ 

177 

178 def __init__(self, *args, 

179 title=None, xlabel=None, ylabel=None, zlabel=None, aspect_ratio='auto', 

180 xlim=None, ylim=None, axis_center='auto', axis=True, 

181 xscale='linear', yscale='linear', legend=False, autoscale=True, 

182 margin=0, annotations=None, markers=None, rectangles=None, 

183 fill=None, backend='default', size=None, **kwargs): 

184 super().__init__() 

185 

186 # Options for the graph as a whole. 

187 # The possible values for each option are described in the docstring of 

188 # Plot. They are based purely on convention, no checking is done. 

189 self.title = title 

190 self.xlabel = xlabel 

191 self.ylabel = ylabel 

192 self.zlabel = zlabel 

193 self.aspect_ratio = aspect_ratio 

194 self.axis_center = axis_center 

195 self.axis = axis 

196 self.xscale = xscale 

197 self.yscale = yscale 

198 self.legend = legend 

199 self.autoscale = autoscale 

200 self.margin = margin 

201 self.annotations = annotations 

202 self.markers = markers 

203 self.rectangles = rectangles 

204 self.fill = fill 

205 

206 # Contains the data objects to be plotted. The backend should be smart 

207 # enough to iterate over this list. 

208 self._series = [] 

209 self._series.extend(args) 

210 

211 # The backend type. On every show() a new backend instance is created 

212 # in self._backend which is tightly coupled to the Plot instance 

213 # (thanks to the parent attribute of the backend). 

214 if isinstance(backend, str): 

215 self.backend = plot_backends[backend] 

216 elif (type(backend) == type) and issubclass(backend, BaseBackend): 

217 self.backend = backend 

218 else: 

219 raise TypeError( 

220 "backend must be either a string or a subclass of BaseBackend") 

221 

222 is_real = \ 

223 lambda lim: all(getattr(i, 'is_real', True) for i in lim) 

224 is_finite = \ 

225 lambda lim: all(getattr(i, 'is_finite', True) for i in lim) 

226 

227 # reduce code repetition 

228 def check_and_set(t_name, t): 

229 if t: 

230 if not is_real(t): 

231 raise ValueError( 

232 "All numbers from {}={} must be real".format(t_name, t)) 

233 if not is_finite(t): 

234 raise ValueError( 

235 "All numbers from {}={} must be finite".format(t_name, t)) 

236 setattr(self, t_name, (float(t[0]), float(t[1]))) 

237 

238 self.xlim = None 

239 check_and_set("xlim", xlim) 

240 self.ylim = None 

241 check_and_set("ylim", ylim) 

242 self.size = None 

243 check_and_set("size", size) 

244 

245 

246 def show(self): 

247 # TODO move this to the backend (also for save) 

248 if hasattr(self, '_backend'): 

249 self._backend.close() 

250 self._backend = self.backend(self) 

251 self._backend.show() 

252 

253 def save(self, path): 

254 if hasattr(self, '_backend'): 

255 self._backend.close() 

256 self._backend = self.backend(self) 

257 self._backend.save(path) 

258 

259 def __str__(self): 

260 series_strs = [('[%d]: ' % i) + str(s) 

261 for i, s in enumerate(self._series)] 

262 return 'Plot object containing:\n' + '\n'.join(series_strs) 

263 

264 def __getitem__(self, index): 

265 return self._series[index] 

266 

267 def __setitem__(self, index, *args): 

268 if len(args) == 1 and isinstance(args[0], BaseSeries): 

269 self._series[index] = args 

270 

271 def __delitem__(self, index): 

272 del self._series[index] 

273 

274 def append(self, arg): 

275 """Adds an element from a plot's series to an existing plot. 

276 

277 Examples 

278 ======== 

279 

280 Consider two ``Plot`` objects, ``p1`` and ``p2``. To add the 

281 second plot's first series object to the first, use the 

282 ``append`` method, like so: 

283 

284 .. plot:: 

285 :format: doctest 

286 :include-source: True 

287 

288 >>> from sympy import symbols 

289 >>> from sympy.plotting import plot 

290 >>> x = symbols('x') 

291 >>> p1 = plot(x*x, show=False) 

292 >>> p2 = plot(x, show=False) 

293 >>> p1.append(p2[0]) 

294 >>> p1 

295 Plot object containing: 

296 [0]: cartesian line: x**2 for x over (-10.0, 10.0) 

297 [1]: cartesian line: x for x over (-10.0, 10.0) 

298 >>> p1.show() 

299 

300 See Also 

301 ======== 

302 

303 extend 

304 

305 """ 

306 if isinstance(arg, BaseSeries): 

307 self._series.append(arg) 

308 else: 

309 raise TypeError('Must specify element of plot to append.') 

310 

311 def extend(self, arg): 

312 """Adds all series from another plot. 

313 

314 Examples 

315 ======== 

316 

317 Consider two ``Plot`` objects, ``p1`` and ``p2``. To add the 

318 second plot to the first, use the ``extend`` method, like so: 

319 

320 .. plot:: 

321 :format: doctest 

322 :include-source: True 

323 

324 >>> from sympy import symbols 

325 >>> from sympy.plotting import plot 

326 >>> x = symbols('x') 

327 >>> p1 = plot(x**2, show=False) 

328 >>> p2 = plot(x, -x, show=False) 

329 >>> p1.extend(p2) 

330 >>> p1 

331 Plot object containing: 

332 [0]: cartesian line: x**2 for x over (-10.0, 10.0) 

333 [1]: cartesian line: x for x over (-10.0, 10.0) 

334 [2]: cartesian line: -x for x over (-10.0, 10.0) 

335 >>> p1.show() 

336 

337 """ 

338 if isinstance(arg, Plot): 

339 self._series.extend(arg._series) 

340 elif is_sequence(arg): 

341 self._series.extend(arg) 

342 else: 

343 raise TypeError('Expecting Plot or sequence of BaseSeries') 

344 

345 

346class PlotGrid: 

347 """This class helps to plot subplots from already created SymPy plots 

348 in a single figure. 

349 

350 Examples 

351 ======== 

352 

353 .. plot:: 

354 :context: close-figs 

355 :format: doctest 

356 :include-source: True 

357 

358 >>> from sympy import symbols 

359 >>> from sympy.plotting import plot, plot3d, PlotGrid 

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

361 >>> p1 = plot(x, x**2, x**3, (x, -5, 5)) 

362 >>> p2 = plot((x**2, (x, -6, 6)), (x, (x, -5, 5))) 

363 >>> p3 = plot(x**3, (x, -5, 5)) 

364 >>> p4 = plot3d(x*y, (x, -5, 5), (y, -5, 5)) 

365 

366 Plotting vertically in a single line: 

367 

368 .. plot:: 

369 :context: close-figs 

370 :format: doctest 

371 :include-source: True 

372 

373 >>> PlotGrid(2, 1, p1, p2) 

374 PlotGrid object containing: 

375 Plot[0]:Plot object containing: 

376 [0]: cartesian line: x for x over (-5.0, 5.0) 

377 [1]: cartesian line: x**2 for x over (-5.0, 5.0) 

378 [2]: cartesian line: x**3 for x over (-5.0, 5.0) 

379 Plot[1]:Plot object containing: 

380 [0]: cartesian line: x**2 for x over (-6.0, 6.0) 

381 [1]: cartesian line: x for x over (-5.0, 5.0) 

382 

383 Plotting horizontally in a single line: 

384 

385 .. plot:: 

386 :context: close-figs 

387 :format: doctest 

388 :include-source: True 

389 

390 >>> PlotGrid(1, 3, p2, p3, p4) 

391 PlotGrid object containing: 

392 Plot[0]:Plot object containing: 

393 [0]: cartesian line: x**2 for x over (-6.0, 6.0) 

394 [1]: cartesian line: x for x over (-5.0, 5.0) 

395 Plot[1]:Plot object containing: 

396 [0]: cartesian line: x**3 for x over (-5.0, 5.0) 

397 Plot[2]:Plot object containing: 

398 [0]: cartesian surface: x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0) 

399 

400 Plotting in a grid form: 

401 

402 .. plot:: 

403 :context: close-figs 

404 :format: doctest 

405 :include-source: True 

406 

407 >>> PlotGrid(2, 2, p1, p2, p3, p4) 

408 PlotGrid object containing: 

409 Plot[0]:Plot object containing: 

410 [0]: cartesian line: x for x over (-5.0, 5.0) 

411 [1]: cartesian line: x**2 for x over (-5.0, 5.0) 

412 [2]: cartesian line: x**3 for x over (-5.0, 5.0) 

413 Plot[1]:Plot object containing: 

414 [0]: cartesian line: x**2 for x over (-6.0, 6.0) 

415 [1]: cartesian line: x for x over (-5.0, 5.0) 

416 Plot[2]:Plot object containing: 

417 [0]: cartesian line: x**3 for x over (-5.0, 5.0) 

418 Plot[3]:Plot object containing: 

419 [0]: cartesian surface: x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0) 

420 

421 """ 

422 def __init__(self, nrows, ncolumns, *args, show=True, size=None, **kwargs): 

423 """ 

424 Parameters 

425 ========== 

426 

427 nrows : 

428 The number of rows that should be in the grid of the 

429 required subplot. 

430 ncolumns : 

431 The number of columns that should be in the grid 

432 of the required subplot. 

433 

434 nrows and ncolumns together define the required grid. 

435 

436 Arguments 

437 ========= 

438 

439 A list of predefined plot objects entered in a row-wise sequence 

440 i.e. plot objects which are to be in the top row of the required 

441 grid are written first, then the second row objects and so on 

442 

443 Keyword arguments 

444 ================= 

445 

446 show : Boolean 

447 The default value is set to ``True``. Set show to ``False`` and 

448 the function will not display the subplot. The returned instance 

449 of the ``PlotGrid`` class can then be used to save or display the 

450 plot by calling the ``save()`` and ``show()`` methods 

451 respectively. 

452 size : (float, float), optional 

453 A tuple in the form (width, height) in inches to specify the size of 

454 the overall figure. The default value is set to ``None``, meaning 

455 the size will be set by the default backend. 

456 """ 

457 self.nrows = nrows 

458 self.ncolumns = ncolumns 

459 self._series = [] 

460 self.args = args 

461 for arg in args: 

462 self._series.append(arg._series) 

463 self.backend = DefaultBackend 

464 self.size = size 

465 if show: 

466 self.show() 

467 

468 def show(self): 

469 if hasattr(self, '_backend'): 

470 self._backend.close() 

471 self._backend = self.backend(self) 

472 self._backend.show() 

473 

474 def save(self, path): 

475 if hasattr(self, '_backend'): 

476 self._backend.close() 

477 self._backend = self.backend(self) 

478 self._backend.save(path) 

479 

480 def __str__(self): 

481 plot_strs = [('Plot[%d]:' % i) + str(plot) 

482 for i, plot in enumerate(self.args)] 

483 

484 return 'PlotGrid object containing:\n' + '\n'.join(plot_strs) 

485 

486 

487############################################################################## 

488# Data Series 

489############################################################################## 

490#TODO more general way to calculate aesthetics (see get_color_array) 

491 

492### The base class for all series 

493class BaseSeries: 

494 """Base class for the data objects containing stuff to be plotted. 

495 

496 Explanation 

497 =========== 

498 

499 The backend should check if it supports the data series that is given. 

500 (e.g. TextBackend supports only LineOver1DRangeSeries). 

501 It is the backend responsibility to know how to use the class of 

502 data series that is given. 

503 

504 Some data series classes are grouped (using a class attribute like is_2Dline) 

505 according to the api they present (based only on convention). The backend is 

506 not obliged to use that api (e.g. LineOver1DRangeSeries belongs to the 

507 is_2Dline group and presents the get_points method, but the 

508 TextBackend does not use the get_points method). 

509 """ 

510 

511 # Some flags follow. The rationale for using flags instead of checking base 

512 # classes is that setting multiple flags is simpler than multiple 

513 # inheritance. 

514 

515 is_2Dline = False 

516 # Some of the backends expect: 

517 # - get_points returning 1D np.arrays list_x, list_y 

518 # - get_color_array returning 1D np.array (done in Line2DBaseSeries) 

519 # with the colors calculated at the points from get_points 

520 

521 is_3Dline = False 

522 # Some of the backends expect: 

523 # - get_points returning 1D np.arrays list_x, list_y, list_y 

524 # - get_color_array returning 1D np.array (done in Line2DBaseSeries) 

525 # with the colors calculated at the points from get_points 

526 

527 is_3Dsurface = False 

528 # Some of the backends expect: 

529 # - get_meshes returning mesh_x, mesh_y, mesh_z (2D np.arrays) 

530 # - get_points an alias for get_meshes 

531 

532 is_contour = False 

533 # Some of the backends expect: 

534 # - get_meshes returning mesh_x, mesh_y, mesh_z (2D np.arrays) 

535 # - get_points an alias for get_meshes 

536 

537 is_implicit = False 

538 # Some of the backends expect: 

539 # - get_meshes returning mesh_x (1D array), mesh_y(1D array, 

540 # mesh_z (2D np.arrays) 

541 # - get_points an alias for get_meshes 

542 # Different from is_contour as the colormap in backend will be 

543 # different 

544 

545 is_parametric = False 

546 # The calculation of aesthetics expects: 

547 # - get_parameter_points returning one or two np.arrays (1D or 2D) 

548 # used for calculation aesthetics 

549 

550 def __init__(self): 

551 super().__init__() 

552 

553 @property 

554 def is_3D(self): 

555 flags3D = [ 

556 self.is_3Dline, 

557 self.is_3Dsurface 

558 ] 

559 return any(flags3D) 

560 

561 @property 

562 def is_line(self): 

563 flagslines = [ 

564 self.is_2Dline, 

565 self.is_3Dline 

566 ] 

567 return any(flagslines) 

568 

569 

570### 2D lines 

571class Line2DBaseSeries(BaseSeries): 

572 """A base class for 2D lines. 

573 

574 - adding the label, steps and only_integers options 

575 - making is_2Dline true 

576 - defining get_segments and get_color_array 

577 """ 

578 

579 is_2Dline = True 

580 

581 _dim = 2 

582 

583 def __init__(self): 

584 super().__init__() 

585 self.label = None 

586 self.steps = False 

587 self.only_integers = False 

588 self.line_color = None 

589 

590 def get_data(self): 

591 """ Return lists of coordinates for plotting the line. 

592 

593 Returns 

594 ======= 

595 x : list 

596 List of x-coordinates 

597 

598 y : list 

599 List of y-coordinates 

600 

601 z : list 

602 List of z-coordinates in case of Parametric3DLineSeries 

603 """ 

604 np = import_module('numpy') 

605 points = self.get_points() 

606 if self.steps is True: 

607 if len(points) == 2: 

608 x = np.array((points[0], points[0])).T.flatten()[1:] 

609 y = np.array((points[1], points[1])).T.flatten()[:-1] 

610 points = (x, y) 

611 else: 

612 x = np.repeat(points[0], 3)[2:] 

613 y = np.repeat(points[1], 3)[:-2] 

614 z = np.repeat(points[2], 3)[1:-1] 

615 points = (x, y, z) 

616 return points 

617 

618 def get_segments(self): 

619 sympy_deprecation_warning( 

620 """ 

621 The Line2DBaseSeries.get_segments() method is deprecated. 

622 

623 Instead, use the MatplotlibBackend.get_segments() method, or use 

624 The get_points() or get_data() methods. 

625 """, 

626 deprecated_since_version="1.9", 

627 active_deprecations_target="deprecated-get-segments") 

628 

629 np = import_module('numpy') 

630 points = type(self).get_data(self) 

631 points = np.ma.array(points).T.reshape(-1, 1, self._dim) 

632 return np.ma.concatenate([points[:-1], points[1:]], axis=1) 

633 

634 def get_color_array(self): 

635 np = import_module('numpy') 

636 c = self.line_color 

637 if hasattr(c, '__call__'): 

638 f = np.vectorize(c) 

639 nargs = arity(c) 

640 if nargs == 1 and self.is_parametric: 

641 x = self.get_parameter_points() 

642 return f(centers_of_segments(x)) 

643 else: 

644 variables = list(map(centers_of_segments, self.get_points())) 

645 if nargs == 1: 

646 return f(variables[0]) 

647 elif nargs == 2: 

648 return f(*variables[:2]) 

649 else: # only if the line is 3D (otherwise raises an error) 

650 return f(*variables) 

651 else: 

652 return c*np.ones(self.nb_of_points) 

653 

654 

655class List2DSeries(Line2DBaseSeries): 

656 """Representation for a line consisting of list of points.""" 

657 

658 def __init__(self, list_x, list_y): 

659 np = import_module('numpy') 

660 super().__init__() 

661 self.list_x = np.array(list_x) 

662 self.list_y = np.array(list_y) 

663 self.label = 'list' 

664 

665 def __str__(self): 

666 return 'list plot' 

667 

668 def get_points(self): 

669 return (self.list_x, self.list_y) 

670 

671 

672class LineOver1DRangeSeries(Line2DBaseSeries): 

673 """Representation for a line consisting of a SymPy expression over a range.""" 

674 

675 def __init__(self, expr, var_start_end, **kwargs): 

676 super().__init__() 

677 self.expr = sympify(expr) 

678 self.label = kwargs.get('label', None) or self.expr 

679 self.var = sympify(var_start_end[0]) 

680 self.start = float(var_start_end[1]) 

681 self.end = float(var_start_end[2]) 

682 self.nb_of_points = kwargs.get('nb_of_points', 300) 

683 self.adaptive = kwargs.get('adaptive', True) 

684 self.depth = kwargs.get('depth', 12) 

685 self.line_color = kwargs.get('line_color', None) 

686 self.xscale = kwargs.get('xscale', 'linear') 

687 

688 def __str__(self): 

689 return 'cartesian line: %s for %s over %s' % ( 

690 str(self.expr), str(self.var), str((self.start, self.end))) 

691 

692 def get_points(self): 

693 """ Return lists of coordinates for plotting. Depending on the 

694 ``adaptive`` option, this function will either use an adaptive algorithm 

695 or it will uniformly sample the expression over the provided range. 

696 

697 Returns 

698 ======= 

699 x : list 

700 List of x-coordinates 

701 

702 y : list 

703 List of y-coordinates 

704 

705 

706 Explanation 

707 =========== 

708 

709 The adaptive sampling is done by recursively checking if three 

710 points are almost collinear. If they are not collinear, then more 

711 points are added between those points. 

712 

713 References 

714 ========== 

715 

716 .. [1] Adaptive polygonal approximation of parametric curves, 

717 Luiz Henrique de Figueiredo. 

718 

719 """ 

720 if self.only_integers or not self.adaptive: 

721 return self._uniform_sampling() 

722 else: 

723 f = lambdify([self.var], self.expr) 

724 x_coords = [] 

725 y_coords = [] 

726 np = import_module('numpy') 

727 def sample(p, q, depth): 

728 """ Samples recursively if three points are almost collinear. 

729 For depth < 6, points are added irrespective of whether they 

730 satisfy the collinearity condition or not. The maximum depth 

731 allowed is 12. 

732 """ 

733 # Randomly sample to avoid aliasing. 

734 random = 0.45 + np.random.rand() * 0.1 

735 if self.xscale == 'log': 

736 xnew = 10**(np.log10(p[0]) + random * (np.log10(q[0]) - 

737 np.log10(p[0]))) 

738 else: 

739 xnew = p[0] + random * (q[0] - p[0]) 

740 ynew = f(xnew) 

741 new_point = np.array([xnew, ynew]) 

742 

743 # Maximum depth 

744 if depth > self.depth: 

745 x_coords.append(q[0]) 

746 y_coords.append(q[1]) 

747 

748 # Sample irrespective of whether the line is flat till the 

749 # depth of 6. We are not using linspace to avoid aliasing. 

750 elif depth < 6: 

751 sample(p, new_point, depth + 1) 

752 sample(new_point, q, depth + 1) 

753 

754 # Sample ten points if complex values are encountered 

755 # at both ends. If there is a real value in between, then 

756 # sample those points further. 

757 elif p[1] is None and q[1] is None: 

758 if self.xscale == 'log': 

759 xarray = np.logspace(p[0], q[0], 10) 

760 else: 

761 xarray = np.linspace(p[0], q[0], 10) 

762 yarray = list(map(f, xarray)) 

763 if not all(y is None for y in yarray): 

764 for i in range(len(yarray) - 1): 

765 if not (yarray[i] is None and yarray[i + 1] is None): 

766 sample([xarray[i], yarray[i]], 

767 [xarray[i + 1], yarray[i + 1]], depth + 1) 

768 

769 # Sample further if one of the end points in None (i.e. a 

770 # complex value) or the three points are not almost collinear. 

771 elif (p[1] is None or q[1] is None or new_point[1] is None 

772 or not flat(p, new_point, q)): 

773 sample(p, new_point, depth + 1) 

774 sample(new_point, q, depth + 1) 

775 else: 

776 x_coords.append(q[0]) 

777 y_coords.append(q[1]) 

778 

779 f_start = f(self.start) 

780 f_end = f(self.end) 

781 x_coords.append(self.start) 

782 y_coords.append(f_start) 

783 sample(np.array([self.start, f_start]), 

784 np.array([self.end, f_end]), 0) 

785 

786 return (x_coords, y_coords) 

787 

788 def _uniform_sampling(self): 

789 np = import_module('numpy') 

790 if self.only_integers is True: 

791 if self.xscale == 'log': 

792 list_x = np.logspace(int(self.start), int(self.end), 

793 num=int(self.end) - int(self.start) + 1) 

794 else: 

795 list_x = np.linspace(int(self.start), int(self.end), 

796 num=int(self.end) - int(self.start) + 1) 

797 else: 

798 if self.xscale == 'log': 

799 list_x = np.logspace(self.start, self.end, num=self.nb_of_points) 

800 else: 

801 list_x = np.linspace(self.start, self.end, num=self.nb_of_points) 

802 f = vectorized_lambdify([self.var], self.expr) 

803 list_y = f(list_x) 

804 return (list_x, list_y) 

805 

806 

807class Parametric2DLineSeries(Line2DBaseSeries): 

808 """Representation for a line consisting of two parametric SymPy expressions 

809 over a range.""" 

810 

811 is_parametric = True 

812 

813 def __init__(self, expr_x, expr_y, var_start_end, **kwargs): 

814 super().__init__() 

815 self.expr_x = sympify(expr_x) 

816 self.expr_y = sympify(expr_y) 

817 self.label = kwargs.get('label', None) or \ 

818 Tuple(self.expr_x, self.expr_y) 

819 self.var = sympify(var_start_end[0]) 

820 self.start = float(var_start_end[1]) 

821 self.end = float(var_start_end[2]) 

822 self.nb_of_points = kwargs.get('nb_of_points', 300) 

823 self.adaptive = kwargs.get('adaptive', True) 

824 self.depth = kwargs.get('depth', 12) 

825 self.line_color = kwargs.get('line_color', None) 

826 

827 def __str__(self): 

828 return 'parametric cartesian line: (%s, %s) for %s over %s' % ( 

829 str(self.expr_x), str(self.expr_y), str(self.var), 

830 str((self.start, self.end))) 

831 

832 def get_parameter_points(self): 

833 np = import_module('numpy') 

834 return np.linspace(self.start, self.end, num=self.nb_of_points) 

835 

836 def _uniform_sampling(self): 

837 param = self.get_parameter_points() 

838 fx = vectorized_lambdify([self.var], self.expr_x) 

839 fy = vectorized_lambdify([self.var], self.expr_y) 

840 list_x = fx(param) 

841 list_y = fy(param) 

842 return (list_x, list_y) 

843 

844 def get_points(self): 

845 """ Return lists of coordinates for plotting. Depending on the 

846 ``adaptive`` option, this function will either use an adaptive algorithm 

847 or it will uniformly sample the expression over the provided range. 

848 

849 Returns 

850 ======= 

851 x : list 

852 List of x-coordinates 

853 

854 y : list 

855 List of y-coordinates 

856 

857 

858 Explanation 

859 =========== 

860 

861 The adaptive sampling is done by recursively checking if three 

862 points are almost collinear. If they are not collinear, then more 

863 points are added between those points. 

864 

865 References 

866 ========== 

867 

868 .. [1] Adaptive polygonal approximation of parametric curves, 

869 Luiz Henrique de Figueiredo. 

870 

871 """ 

872 if not self.adaptive: 

873 return self._uniform_sampling() 

874 

875 f_x = lambdify([self.var], self.expr_x) 

876 f_y = lambdify([self.var], self.expr_y) 

877 x_coords = [] 

878 y_coords = [] 

879 

880 def sample(param_p, param_q, p, q, depth): 

881 """ Samples recursively if three points are almost collinear. 

882 For depth < 6, points are added irrespective of whether they 

883 satisfy the collinearity condition or not. The maximum depth 

884 allowed is 12. 

885 """ 

886 # Randomly sample to avoid aliasing. 

887 np = import_module('numpy') 

888 random = 0.45 + np.random.rand() * 0.1 

889 param_new = param_p + random * (param_q - param_p) 

890 xnew = f_x(param_new) 

891 ynew = f_y(param_new) 

892 new_point = np.array([xnew, ynew]) 

893 

894 # Maximum depth 

895 if depth > self.depth: 

896 x_coords.append(q[0]) 

897 y_coords.append(q[1]) 

898 

899 # Sample irrespective of whether the line is flat till the 

900 # depth of 6. We are not using linspace to avoid aliasing. 

901 elif depth < 6: 

902 sample(param_p, param_new, p, new_point, depth + 1) 

903 sample(param_new, param_q, new_point, q, depth + 1) 

904 

905 # Sample ten points if complex values are encountered 

906 # at both ends. If there is a real value in between, then 

907 # sample those points further. 

908 elif ((p[0] is None and q[1] is None) or 

909 (p[1] is None and q[1] is None)): 

910 param_array = np.linspace(param_p, param_q, 10) 

911 x_array = list(map(f_x, param_array)) 

912 y_array = list(map(f_y, param_array)) 

913 if not all(x is None and y is None 

914 for x, y in zip(x_array, y_array)): 

915 for i in range(len(y_array) - 1): 

916 if ((x_array[i] is not None and y_array[i] is not None) or 

917 (x_array[i + 1] is not None and y_array[i + 1] is not None)): 

918 point_a = [x_array[i], y_array[i]] 

919 point_b = [x_array[i + 1], y_array[i + 1]] 

920 sample(param_array[i], param_array[i], point_a, 

921 point_b, depth + 1) 

922 

923 # Sample further if one of the end points in None (i.e. a complex 

924 # value) or the three points are not almost collinear. 

925 elif (p[0] is None or p[1] is None 

926 or q[1] is None or q[0] is None 

927 or not flat(p, new_point, q)): 

928 sample(param_p, param_new, p, new_point, depth + 1) 

929 sample(param_new, param_q, new_point, q, depth + 1) 

930 else: 

931 x_coords.append(q[0]) 

932 y_coords.append(q[1]) 

933 

934 f_start_x = f_x(self.start) 

935 f_start_y = f_y(self.start) 

936 start = [f_start_x, f_start_y] 

937 f_end_x = f_x(self.end) 

938 f_end_y = f_y(self.end) 

939 end = [f_end_x, f_end_y] 

940 x_coords.append(f_start_x) 

941 y_coords.append(f_start_y) 

942 sample(self.start, self.end, start, end, 0) 

943 

944 return x_coords, y_coords 

945 

946 

947### 3D lines 

948class Line3DBaseSeries(Line2DBaseSeries): 

949 """A base class for 3D lines. 

950 

951 Most of the stuff is derived from Line2DBaseSeries.""" 

952 

953 is_2Dline = False 

954 is_3Dline = True 

955 _dim = 3 

956 

957 def __init__(self): 

958 super().__init__() 

959 

960 

961class Parametric3DLineSeries(Line3DBaseSeries): 

962 """Representation for a 3D line consisting of three parametric SymPy 

963 expressions and a range.""" 

964 

965 is_parametric = True 

966 

967 def __init__(self, expr_x, expr_y, expr_z, var_start_end, **kwargs): 

968 super().__init__() 

969 self.expr_x = sympify(expr_x) 

970 self.expr_y = sympify(expr_y) 

971 self.expr_z = sympify(expr_z) 

972 self.label = kwargs.get('label', None) or \ 

973 Tuple(self.expr_x, self.expr_y) 

974 self.var = sympify(var_start_end[0]) 

975 self.start = float(var_start_end[1]) 

976 self.end = float(var_start_end[2]) 

977 self.nb_of_points = kwargs.get('nb_of_points', 300) 

978 self.line_color = kwargs.get('line_color', None) 

979 self._xlim = None 

980 self._ylim = None 

981 self._zlim = None 

982 

983 def __str__(self): 

984 return '3D parametric cartesian line: (%s, %s, %s) for %s over %s' % ( 

985 str(self.expr_x), str(self.expr_y), str(self.expr_z), 

986 str(self.var), str((self.start, self.end))) 

987 

988 def get_parameter_points(self): 

989 np = import_module('numpy') 

990 return np.linspace(self.start, self.end, num=self.nb_of_points) 

991 

992 def get_points(self): 

993 np = import_module('numpy') 

994 param = self.get_parameter_points() 

995 fx = vectorized_lambdify([self.var], self.expr_x) 

996 fy = vectorized_lambdify([self.var], self.expr_y) 

997 fz = vectorized_lambdify([self.var], self.expr_z) 

998 

999 list_x = fx(param) 

1000 list_y = fy(param) 

1001 list_z = fz(param) 

1002 

1003 list_x = np.array(list_x, dtype=np.float64) 

1004 list_y = np.array(list_y, dtype=np.float64) 

1005 list_z = np.array(list_z, dtype=np.float64) 

1006 

1007 list_x = np.ma.masked_invalid(list_x) 

1008 list_y = np.ma.masked_invalid(list_y) 

1009 list_z = np.ma.masked_invalid(list_z) 

1010 

1011 self._xlim = (np.amin(list_x), np.amax(list_x)) 

1012 self._ylim = (np.amin(list_y), np.amax(list_y)) 

1013 self._zlim = (np.amin(list_z), np.amax(list_z)) 

1014 return list_x, list_y, list_z 

1015 

1016 

1017### Surfaces 

1018class SurfaceBaseSeries(BaseSeries): 

1019 """A base class for 3D surfaces.""" 

1020 

1021 is_3Dsurface = True 

1022 

1023 def __init__(self): 

1024 super().__init__() 

1025 self.surface_color = None 

1026 

1027 def get_color_array(self): 

1028 np = import_module('numpy') 

1029 c = self.surface_color 

1030 if isinstance(c, Callable): 

1031 f = np.vectorize(c) 

1032 nargs = arity(c) 

1033 if self.is_parametric: 

1034 variables = list(map(centers_of_faces, self.get_parameter_meshes())) 

1035 if nargs == 1: 

1036 return f(variables[0]) 

1037 elif nargs == 2: 

1038 return f(*variables) 

1039 variables = list(map(centers_of_faces, self.get_meshes())) 

1040 if nargs == 1: 

1041 return f(variables[0]) 

1042 elif nargs == 2: 

1043 return f(*variables[:2]) 

1044 else: 

1045 return f(*variables) 

1046 else: 

1047 if isinstance(self, SurfaceOver2DRangeSeries): 

1048 return c*np.ones(min(self.nb_of_points_x, self.nb_of_points_y)) 

1049 else: 

1050 return c*np.ones(min(self.nb_of_points_u, self.nb_of_points_v)) 

1051 

1052 

1053class SurfaceOver2DRangeSeries(SurfaceBaseSeries): 

1054 """Representation for a 3D surface consisting of a SymPy expression and 2D 

1055 range.""" 

1056 def __init__(self, expr, var_start_end_x, var_start_end_y, **kwargs): 

1057 super().__init__() 

1058 self.expr = sympify(expr) 

1059 self.var_x = sympify(var_start_end_x[0]) 

1060 self.start_x = float(var_start_end_x[1]) 

1061 self.end_x = float(var_start_end_x[2]) 

1062 self.var_y = sympify(var_start_end_y[0]) 

1063 self.start_y = float(var_start_end_y[1]) 

1064 self.end_y = float(var_start_end_y[2]) 

1065 self.nb_of_points_x = kwargs.get('nb_of_points_x', 50) 

1066 self.nb_of_points_y = kwargs.get('nb_of_points_y', 50) 

1067 self.surface_color = kwargs.get('surface_color', None) 

1068 

1069 self._xlim = (self.start_x, self.end_x) 

1070 self._ylim = (self.start_y, self.end_y) 

1071 

1072 def __str__(self): 

1073 return ('cartesian surface: %s for' 

1074 ' %s over %s and %s over %s') % ( 

1075 str(self.expr), 

1076 str(self.var_x), 

1077 str((self.start_x, self.end_x)), 

1078 str(self.var_y), 

1079 str((self.start_y, self.end_y))) 

1080 

1081 def get_meshes(self): 

1082 np = import_module('numpy') 

1083 mesh_x, mesh_y = np.meshgrid(np.linspace(self.start_x, self.end_x, 

1084 num=self.nb_of_points_x), 

1085 np.linspace(self.start_y, self.end_y, 

1086 num=self.nb_of_points_y)) 

1087 f = vectorized_lambdify((self.var_x, self.var_y), self.expr) 

1088 mesh_z = f(mesh_x, mesh_y) 

1089 mesh_z = np.array(mesh_z, dtype=np.float64) 

1090 mesh_z = np.ma.masked_invalid(mesh_z) 

1091 self._zlim = (np.amin(mesh_z), np.amax(mesh_z)) 

1092 return mesh_x, mesh_y, mesh_z 

1093 

1094 

1095class ParametricSurfaceSeries(SurfaceBaseSeries): 

1096 """Representation for a 3D surface consisting of three parametric SymPy 

1097 expressions and a range.""" 

1098 

1099 is_parametric = True 

1100 

1101 def __init__( 

1102 self, expr_x, expr_y, expr_z, var_start_end_u, var_start_end_v, 

1103 **kwargs): 

1104 super().__init__() 

1105 self.expr_x = sympify(expr_x) 

1106 self.expr_y = sympify(expr_y) 

1107 self.expr_z = sympify(expr_z) 

1108 self.var_u = sympify(var_start_end_u[0]) 

1109 self.start_u = float(var_start_end_u[1]) 

1110 self.end_u = float(var_start_end_u[2]) 

1111 self.var_v = sympify(var_start_end_v[0]) 

1112 self.start_v = float(var_start_end_v[1]) 

1113 self.end_v = float(var_start_end_v[2]) 

1114 self.nb_of_points_u = kwargs.get('nb_of_points_u', 50) 

1115 self.nb_of_points_v = kwargs.get('nb_of_points_v', 50) 

1116 self.surface_color = kwargs.get('surface_color', None) 

1117 

1118 def __str__(self): 

1119 return ('parametric cartesian surface: (%s, %s, %s) for' 

1120 ' %s over %s and %s over %s') % ( 

1121 str(self.expr_x), 

1122 str(self.expr_y), 

1123 str(self.expr_z), 

1124 str(self.var_u), 

1125 str((self.start_u, self.end_u)), 

1126 str(self.var_v), 

1127 str((self.start_v, self.end_v))) 

1128 

1129 def get_parameter_meshes(self): 

1130 np = import_module('numpy') 

1131 return np.meshgrid(np.linspace(self.start_u, self.end_u, 

1132 num=self.nb_of_points_u), 

1133 np.linspace(self.start_v, self.end_v, 

1134 num=self.nb_of_points_v)) 

1135 

1136 def get_meshes(self): 

1137 np = import_module('numpy') 

1138 

1139 mesh_u, mesh_v = self.get_parameter_meshes() 

1140 fx = vectorized_lambdify((self.var_u, self.var_v), self.expr_x) 

1141 fy = vectorized_lambdify((self.var_u, self.var_v), self.expr_y) 

1142 fz = vectorized_lambdify((self.var_u, self.var_v), self.expr_z) 

1143 

1144 mesh_x = fx(mesh_u, mesh_v) 

1145 mesh_y = fy(mesh_u, mesh_v) 

1146 mesh_z = fz(mesh_u, mesh_v) 

1147 

1148 mesh_x = np.array(mesh_x, dtype=np.float64) 

1149 mesh_y = np.array(mesh_y, dtype=np.float64) 

1150 mesh_z = np.array(mesh_z, dtype=np.float64) 

1151 

1152 mesh_x = np.ma.masked_invalid(mesh_x) 

1153 mesh_y = np.ma.masked_invalid(mesh_y) 

1154 mesh_z = np.ma.masked_invalid(mesh_z) 

1155 

1156 self._xlim = (np.amin(mesh_x), np.amax(mesh_x)) 

1157 self._ylim = (np.amin(mesh_y), np.amax(mesh_y)) 

1158 self._zlim = (np.amin(mesh_z), np.amax(mesh_z)) 

1159 

1160 return mesh_x, mesh_y, mesh_z 

1161 

1162 

1163### Contours 

1164class ContourSeries(BaseSeries): 

1165 """Representation for a contour plot.""" 

1166 # The code is mostly repetition of SurfaceOver2DRange. 

1167 # Presently used in contour_plot function 

1168 

1169 is_contour = True 

1170 

1171 def __init__(self, expr, var_start_end_x, var_start_end_y): 

1172 super().__init__() 

1173 self.nb_of_points_x = 50 

1174 self.nb_of_points_y = 50 

1175 self.expr = sympify(expr) 

1176 self.var_x = sympify(var_start_end_x[0]) 

1177 self.start_x = float(var_start_end_x[1]) 

1178 self.end_x = float(var_start_end_x[2]) 

1179 self.var_y = sympify(var_start_end_y[0]) 

1180 self.start_y = float(var_start_end_y[1]) 

1181 self.end_y = float(var_start_end_y[2]) 

1182 

1183 self.get_points = self.get_meshes 

1184 

1185 self._xlim = (self.start_x, self.end_x) 

1186 self._ylim = (self.start_y, self.end_y) 

1187 

1188 def __str__(self): 

1189 return ('contour: %s for ' 

1190 '%s over %s and %s over %s') % ( 

1191 str(self.expr), 

1192 str(self.var_x), 

1193 str((self.start_x, self.end_x)), 

1194 str(self.var_y), 

1195 str((self.start_y, self.end_y))) 

1196 

1197 def get_meshes(self): 

1198 np = import_module('numpy') 

1199 mesh_x, mesh_y = np.meshgrid(np.linspace(self.start_x, self.end_x, 

1200 num=self.nb_of_points_x), 

1201 np.linspace(self.start_y, self.end_y, 

1202 num=self.nb_of_points_y)) 

1203 f = vectorized_lambdify((self.var_x, self.var_y), self.expr) 

1204 return (mesh_x, mesh_y, f(mesh_x, mesh_y)) 

1205 

1206 

1207############################################################################## 

1208# Backends 

1209############################################################################## 

1210 

1211class BaseBackend: 

1212 """Base class for all backends. A backend represents the plotting library, 

1213 which implements the necessary functionalities in order to use SymPy 

1214 plotting functions. 

1215 

1216 How the plotting module works: 

1217 

1218 1. Whenever a plotting function is called, the provided expressions are 

1219 processed and a list of instances of the :class:`BaseSeries` class is 

1220 created, containing the necessary information to plot the expressions 

1221 (e.g. the expression, ranges, series name, ...). Eventually, these 

1222 objects will generate the numerical data to be plotted. 

1223 2. A :class:`~.Plot` object is instantiated, which stores the list of 

1224 series and the main attributes of the plot (e.g. axis labels, title, ...). 

1225 3. When the ``show`` command is executed, a new backend is instantiated, 

1226 which loops through each series object to generate and plot the 

1227 numerical data. The backend is also going to set the axis labels, title, 

1228 ..., according to the values stored in the Plot instance. 

1229 

1230 The backend should check if it supports the data series that it is given 

1231 (e.g. :class:`TextBackend` supports only :class:`LineOver1DRangeSeries`). 

1232 

1233 It is the backend responsibility to know how to use the class of data series 

1234 that it's given. Note that the current implementation of the ``*Series`` 

1235 classes is "matplotlib-centric": the numerical data returned by the 

1236 ``get_points`` and ``get_meshes`` methods is meant to be used directly by 

1237 Matplotlib. Therefore, the new backend will have to pre-process the 

1238 numerical data to make it compatible with the chosen plotting library. 

1239 Keep in mind that future SymPy versions may improve the ``*Series`` classes 

1240 in order to return numerical data "non-matplotlib-centric", hence if you code 

1241 a new backend you have the responsibility to check if its working on each 

1242 SymPy release. 

1243 

1244 Please explore the :class:`MatplotlibBackend` source code to understand how a 

1245 backend should be coded. 

1246 

1247 Methods 

1248 ======= 

1249 

1250 In order to be used by SymPy plotting functions, a backend must implement 

1251 the following methods: 

1252 

1253 * show(self): used to loop over the data series, generate the numerical 

1254 data, plot it and set the axis labels, title, ... 

1255 * save(self, path): used to save the current plot to the specified file 

1256 path. 

1257 * close(self): used to close the current plot backend (note: some plotting 

1258 library does not support this functionality. In that case, just raise a 

1259 warning). 

1260 

1261 See also 

1262 ======== 

1263 

1264 MatplotlibBackend 

1265 """ 

1266 def __init__(self, parent): 

1267 super().__init__() 

1268 self.parent = parent 

1269 

1270 def show(self): 

1271 raise NotImplementedError 

1272 

1273 def save(self, path): 

1274 raise NotImplementedError 

1275 

1276 def close(self): 

1277 raise NotImplementedError 

1278 

1279 

1280# Don't have to check for the success of importing matplotlib in each case; 

1281# we will only be using this backend if we can successfully import matploblib 

1282class MatplotlibBackend(BaseBackend): 

1283 """ This class implements the functionalities to use Matplotlib with SymPy 

1284 plotting functions. 

1285 """ 

1286 def __init__(self, parent): 

1287 super().__init__(parent) 

1288 self.matplotlib = import_module('matplotlib', 

1289 import_kwargs={'fromlist': ['pyplot', 'cm', 'collections']}, 

1290 min_module_version='1.1.0', catch=(RuntimeError,)) 

1291 self.plt = self.matplotlib.pyplot 

1292 self.cm = self.matplotlib.cm 

1293 self.LineCollection = self.matplotlib.collections.LineCollection 

1294 aspect = getattr(self.parent, 'aspect_ratio', 'auto') 

1295 if aspect != 'auto': 

1296 aspect = float(aspect[1]) / aspect[0] 

1297 

1298 if isinstance(self.parent, Plot): 

1299 nrows, ncolumns = 1, 1 

1300 series_list = [self.parent._series] 

1301 elif isinstance(self.parent, PlotGrid): 

1302 nrows, ncolumns = self.parent.nrows, self.parent.ncolumns 

1303 series_list = self.parent._series 

1304 

1305 self.ax = [] 

1306 self.fig = self.plt.figure(figsize=parent.size) 

1307 

1308 for i, series in enumerate(series_list): 

1309 are_3D = [s.is_3D for s in series] 

1310 

1311 if any(are_3D) and not all(are_3D): 

1312 raise ValueError('The matplotlib backend cannot mix 2D and 3D.') 

1313 elif all(are_3D): 

1314 # mpl_toolkits.mplot3d is necessary for 

1315 # projection='3d' 

1316 mpl_toolkits = import_module('mpl_toolkits', # noqa 

1317 import_kwargs={'fromlist': ['mplot3d']}) 

1318 self.ax.append(self.fig.add_subplot(nrows, ncolumns, i + 1, projection='3d', aspect=aspect)) 

1319 

1320 elif not any(are_3D): 

1321 self.ax.append(self.fig.add_subplot(nrows, ncolumns, i + 1, aspect=aspect)) 

1322 self.ax[i].spines['left'].set_position('zero') 

1323 self.ax[i].spines['right'].set_color('none') 

1324 self.ax[i].spines['bottom'].set_position('zero') 

1325 self.ax[i].spines['top'].set_color('none') 

1326 self.ax[i].xaxis.set_ticks_position('bottom') 

1327 self.ax[i].yaxis.set_ticks_position('left') 

1328 

1329 @staticmethod 

1330 def get_segments(x, y, z=None): 

1331 """ Convert two list of coordinates to a list of segments to be used 

1332 with Matplotlib's :external:class:`~matplotlib.collections.LineCollection`. 

1333 

1334 Parameters 

1335 ========== 

1336 x : list 

1337 List of x-coordinates 

1338 

1339 y : list 

1340 List of y-coordinates 

1341 

1342 z : list 

1343 List of z-coordinates for a 3D line. 

1344 """ 

1345 np = import_module('numpy') 

1346 if z is not None: 

1347 dim = 3 

1348 points = (x, y, z) 

1349 else: 

1350 dim = 2 

1351 points = (x, y) 

1352 points = np.ma.array(points).T.reshape(-1, 1, dim) 

1353 return np.ma.concatenate([points[:-1], points[1:]], axis=1) 

1354 

1355 def _process_series(self, series, ax, parent): 

1356 np = import_module('numpy') 

1357 mpl_toolkits = import_module( 

1358 'mpl_toolkits', import_kwargs={'fromlist': ['mplot3d']}) 

1359 

1360 # XXX Workaround for matplotlib issue 

1361 # https://github.com/matplotlib/matplotlib/issues/17130 

1362 xlims, ylims, zlims = [], [], [] 

1363 

1364 for s in series: 

1365 # Create the collections 

1366 if s.is_2Dline: 

1367 x, y = s.get_data() 

1368 if (isinstance(s.line_color, (int, float)) or 

1369 callable(s.line_color)): 

1370 segments = self.get_segments(x, y) 

1371 collection = self.LineCollection(segments) 

1372 collection.set_array(s.get_color_array()) 

1373 ax.add_collection(collection) 

1374 else: 

1375 lbl = _str_or_latex(s.label) 

1376 line, = ax.plot(x, y, label=lbl, color=s.line_color) 

1377 elif s.is_contour: 

1378 ax.contour(*s.get_meshes()) 

1379 elif s.is_3Dline: 

1380 x, y, z = s.get_data() 

1381 if (isinstance(s.line_color, (int, float)) or 

1382 callable(s.line_color)): 

1383 art3d = mpl_toolkits.mplot3d.art3d 

1384 segments = self.get_segments(x, y, z) 

1385 collection = art3d.Line3DCollection(segments) 

1386 collection.set_array(s.get_color_array()) 

1387 ax.add_collection(collection) 

1388 else: 

1389 lbl = _str_or_latex(s.label) 

1390 ax.plot(x, y, z, label=lbl, color=s.line_color) 

1391 

1392 xlims.append(s._xlim) 

1393 ylims.append(s._ylim) 

1394 zlims.append(s._zlim) 

1395 elif s.is_3Dsurface: 

1396 x, y, z = s.get_meshes() 

1397 collection = ax.plot_surface(x, y, z, 

1398 cmap=getattr(self.cm, 'viridis', self.cm.jet), 

1399 rstride=1, cstride=1, linewidth=0.1) 

1400 if isinstance(s.surface_color, (float, int, Callable)): 

1401 color_array = s.get_color_array() 

1402 color_array = color_array.reshape(color_array.size) 

1403 collection.set_array(color_array) 

1404 else: 

1405 collection.set_color(s.surface_color) 

1406 

1407 xlims.append(s._xlim) 

1408 ylims.append(s._ylim) 

1409 zlims.append(s._zlim) 

1410 elif s.is_implicit: 

1411 points = s.get_raster() 

1412 if len(points) == 2: 

1413 # interval math plotting 

1414 x, y = _matplotlib_list(points[0]) 

1415 ax.fill(x, y, facecolor=s.line_color, edgecolor='None') 

1416 else: 

1417 # use contourf or contour depending on whether it is 

1418 # an inequality or equality. 

1419 # XXX: ``contour`` plots multiple lines. Should be fixed. 

1420 ListedColormap = self.matplotlib.colors.ListedColormap 

1421 colormap = ListedColormap(["white", s.line_color]) 

1422 xarray, yarray, zarray, plot_type = points 

1423 if plot_type == 'contour': 

1424 ax.contour(xarray, yarray, zarray, cmap=colormap) 

1425 else: 

1426 ax.contourf(xarray, yarray, zarray, cmap=colormap) 

1427 else: 

1428 raise NotImplementedError( 

1429 '{} is not supported in the SymPy plotting module ' 

1430 'with matplotlib backend. Please report this issue.' 

1431 .format(ax)) 

1432 

1433 Axes3D = mpl_toolkits.mplot3d.Axes3D 

1434 if not isinstance(ax, Axes3D): 

1435 ax.autoscale_view( 

1436 scalex=ax.get_autoscalex_on(), 

1437 scaley=ax.get_autoscaley_on()) 

1438 else: 

1439 # XXX Workaround for matplotlib issue 

1440 # https://github.com/matplotlib/matplotlib/issues/17130 

1441 if xlims: 

1442 xlims = np.array(xlims) 

1443 xlim = (np.amin(xlims[:, 0]), np.amax(xlims[:, 1])) 

1444 ax.set_xlim(xlim) 

1445 else: 

1446 ax.set_xlim([0, 1]) 

1447 

1448 if ylims: 

1449 ylims = np.array(ylims) 

1450 ylim = (np.amin(ylims[:, 0]), np.amax(ylims[:, 1])) 

1451 ax.set_ylim(ylim) 

1452 else: 

1453 ax.set_ylim([0, 1]) 

1454 

1455 if zlims: 

1456 zlims = np.array(zlims) 

1457 zlim = (np.amin(zlims[:, 0]), np.amax(zlims[:, 1])) 

1458 ax.set_zlim(zlim) 

1459 else: 

1460 ax.set_zlim([0, 1]) 

1461 

1462 # Set global options. 

1463 # TODO The 3D stuff 

1464 # XXX The order of those is important. 

1465 if parent.xscale and not isinstance(ax, Axes3D): 

1466 ax.set_xscale(parent.xscale) 

1467 if parent.yscale and not isinstance(ax, Axes3D): 

1468 ax.set_yscale(parent.yscale) 

1469 if not isinstance(ax, Axes3D) or self.matplotlib.__version__ >= '1.2.0': # XXX in the distant future remove this check 

1470 ax.set_autoscale_on(parent.autoscale) 

1471 if parent.axis_center: 

1472 val = parent.axis_center 

1473 if isinstance(ax, Axes3D): 

1474 pass 

1475 elif val == 'center': 

1476 ax.spines['left'].set_position('center') 

1477 ax.spines['bottom'].set_position('center') 

1478 elif val == 'auto': 

1479 xl, xh = ax.get_xlim() 

1480 yl, yh = ax.get_ylim() 

1481 pos_left = ('data', 0) if xl*xh <= 0 else 'center' 

1482 pos_bottom = ('data', 0) if yl*yh <= 0 else 'center' 

1483 ax.spines['left'].set_position(pos_left) 

1484 ax.spines['bottom'].set_position(pos_bottom) 

1485 else: 

1486 ax.spines['left'].set_position(('data', val[0])) 

1487 ax.spines['bottom'].set_position(('data', val[1])) 

1488 if not parent.axis: 

1489 ax.set_axis_off() 

1490 if parent.legend: 

1491 if ax.legend(): 

1492 ax.legend_.set_visible(parent.legend) 

1493 if parent.margin: 

1494 ax.set_xmargin(parent.margin) 

1495 ax.set_ymargin(parent.margin) 

1496 if parent.title: 

1497 ax.set_title(parent.title) 

1498 if parent.xlabel: 

1499 xlbl = _str_or_latex(parent.xlabel) 

1500 ax.set_xlabel(xlbl, position=(1, 0)) 

1501 if parent.ylabel: 

1502 ylbl = _str_or_latex(parent.ylabel) 

1503 ax.set_ylabel(ylbl, position=(0, 1)) 

1504 if isinstance(ax, Axes3D) and parent.zlabel: 

1505 zlbl = _str_or_latex(parent.zlabel) 

1506 ax.set_zlabel(zlbl, position=(0, 1)) 

1507 if parent.annotations: 

1508 for a in parent.annotations: 

1509 ax.annotate(**a) 

1510 if parent.markers: 

1511 for marker in parent.markers: 

1512 # make a copy of the marker dictionary 

1513 # so that it doesn't get altered 

1514 m = marker.copy() 

1515 args = m.pop('args') 

1516 ax.plot(*args, **m) 

1517 if parent.rectangles: 

1518 for r in parent.rectangles: 

1519 rect = self.matplotlib.patches.Rectangle(**r) 

1520 ax.add_patch(rect) 

1521 if parent.fill: 

1522 ax.fill_between(**parent.fill) 

1523 

1524 # xlim and ylim should always be set at last so that plot limits 

1525 # doesn't get altered during the process. 

1526 if parent.xlim: 

1527 ax.set_xlim(parent.xlim) 

1528 if parent.ylim: 

1529 ax.set_ylim(parent.ylim) 

1530 

1531 

1532 def process_series(self): 

1533 """ 

1534 Iterates over every ``Plot`` object and further calls 

1535 _process_series() 

1536 """ 

1537 parent = self.parent 

1538 if isinstance(parent, Plot): 

1539 series_list = [parent._series] 

1540 else: 

1541 series_list = parent._series 

1542 

1543 for i, (series, ax) in enumerate(zip(series_list, self.ax)): 

1544 if isinstance(self.parent, PlotGrid): 

1545 parent = self.parent.args[i] 

1546 self._process_series(series, ax, parent) 

1547 

1548 def show(self): 

1549 self.process_series() 

1550 #TODO after fixing https://github.com/ipython/ipython/issues/1255 

1551 # you can uncomment the next line and remove the pyplot.show() call 

1552 #self.fig.show() 

1553 if _show: 

1554 self.fig.tight_layout() 

1555 self.plt.show() 

1556 else: 

1557 self.close() 

1558 

1559 def save(self, path): 

1560 self.process_series() 

1561 self.fig.savefig(path) 

1562 

1563 def close(self): 

1564 self.plt.close(self.fig) 

1565 

1566 

1567class TextBackend(BaseBackend): 

1568 def __init__(self, parent): 

1569 super().__init__(parent) 

1570 

1571 def show(self): 

1572 if not _show: 

1573 return 

1574 if len(self.parent._series) != 1: 

1575 raise ValueError( 

1576 'The TextBackend supports only one graph per Plot.') 

1577 elif not isinstance(self.parent._series[0], LineOver1DRangeSeries): 

1578 raise ValueError( 

1579 'The TextBackend supports only expressions over a 1D range') 

1580 else: 

1581 ser = self.parent._series[0] 

1582 textplot(ser.expr, ser.start, ser.end) 

1583 

1584 def close(self): 

1585 pass 

1586 

1587 

1588class DefaultBackend(BaseBackend): 

1589 def __new__(cls, parent): 

1590 matplotlib = import_module('matplotlib', min_module_version='1.1.0', catch=(RuntimeError,)) 

1591 if matplotlib: 

1592 return MatplotlibBackend(parent) 

1593 else: 

1594 return TextBackend(parent) 

1595 

1596 

1597plot_backends = { 

1598 'matplotlib': MatplotlibBackend, 

1599 'text': TextBackend, 

1600 'default': DefaultBackend 

1601} 

1602 

1603 

1604############################################################################## 

1605# Finding the centers of line segments or mesh faces 

1606############################################################################## 

1607 

1608def centers_of_segments(array): 

1609 np = import_module('numpy') 

1610 return np.mean(np.vstack((array[:-1], array[1:])), 0) 

1611 

1612 

1613def centers_of_faces(array): 

1614 np = import_module('numpy') 

1615 return np.mean(np.dstack((array[:-1, :-1], 

1616 array[1:, :-1], 

1617 array[:-1, 1:], 

1618 array[:-1, :-1], 

1619 )), 2) 

1620 

1621 

1622def flat(x, y, z, eps=1e-3): 

1623 """Checks whether three points are almost collinear""" 

1624 np = import_module('numpy') 

1625 # Workaround plotting piecewise (#8577): 

1626 # workaround for `lambdify` in `.experimental_lambdify` fails 

1627 # to return numerical values in some cases. Lower-level fix 

1628 # in `lambdify` is possible. 

1629 vector_a = (x - y).astype(np.float64) 

1630 vector_b = (z - y).astype(np.float64) 

1631 dot_product = np.dot(vector_a, vector_b) 

1632 vector_a_norm = np.linalg.norm(vector_a) 

1633 vector_b_norm = np.linalg.norm(vector_b) 

1634 cos_theta = dot_product / (vector_a_norm * vector_b_norm) 

1635 return abs(cos_theta + 1) < eps 

1636 

1637 

1638def _matplotlib_list(interval_list): 

1639 """ 

1640 Returns lists for matplotlib ``fill`` command from a list of bounding 

1641 rectangular intervals 

1642 """ 

1643 xlist = [] 

1644 ylist = [] 

1645 if len(interval_list): 

1646 for intervals in interval_list: 

1647 intervalx = intervals[0] 

1648 intervaly = intervals[1] 

1649 xlist.extend([intervalx.start, intervalx.start, 

1650 intervalx.end, intervalx.end, None]) 

1651 ylist.extend([intervaly.start, intervaly.end, 

1652 intervaly.end, intervaly.start, None]) 

1653 else: 

1654 #XXX Ugly hack. Matplotlib does not accept empty lists for ``fill`` 

1655 xlist.extend((None, None, None, None)) 

1656 ylist.extend((None, None, None, None)) 

1657 return xlist, ylist 

1658 

1659 

1660####New API for plotting module #### 

1661 

1662# TODO: Add color arrays for plots. 

1663# TODO: Add more plotting options for 3d plots. 

1664# TODO: Adaptive sampling for 3D plots. 

1665 

1666def plot(*args, show=True, **kwargs): 

1667 """Plots a function of a single variable as a curve. 

1668 

1669 Parameters 

1670 ========== 

1671 

1672 args : 

1673 The first argument is the expression representing the function 

1674 of single variable to be plotted. 

1675 

1676 The last argument is a 3-tuple denoting the range of the free 

1677 variable. e.g. ``(x, 0, 5)`` 

1678 

1679 Typical usage examples are in the following: 

1680 

1681 - Plotting a single expression with a single range. 

1682 ``plot(expr, range, **kwargs)`` 

1683 - Plotting a single expression with the default range (-10, 10). 

1684 ``plot(expr, **kwargs)`` 

1685 - Plotting multiple expressions with a single range. 

1686 ``plot(expr1, expr2, ..., range, **kwargs)`` 

1687 - Plotting multiple expressions with multiple ranges. 

1688 ``plot((expr1, range1), (expr2, range2), ..., **kwargs)`` 

1689 

1690 It is best practice to specify range explicitly because default 

1691 range may change in the future if a more advanced default range 

1692 detection algorithm is implemented. 

1693 

1694 show : bool, optional 

1695 The default value is set to ``True``. Set show to ``False`` and 

1696 the function will not display the plot. The returned instance of 

1697 the ``Plot`` class can then be used to save or display the plot 

1698 by calling the ``save()`` and ``show()`` methods respectively. 

1699 

1700 line_color : string, or float, or function, optional 

1701 Specifies the color for the plot. 

1702 See ``Plot`` to see how to set color for the plots. 

1703 Note that by setting ``line_color``, it would be applied simultaneously 

1704 to all the series. 

1705 

1706 title : str, optional 

1707 Title of the plot. It is set to the latex representation of 

1708 the expression, if the plot has only one expression. 

1709 

1710 label : str, optional 

1711 The label of the expression in the plot. It will be used when 

1712 called with ``legend``. Default is the name of the expression. 

1713 e.g. ``sin(x)`` 

1714 

1715 xlabel : str or expression, optional 

1716 Label for the x-axis. 

1717 

1718 ylabel : str or expression, optional 

1719 Label for the y-axis. 

1720 

1721 xscale : 'linear' or 'log', optional 

1722 Sets the scaling of the x-axis. 

1723 

1724 yscale : 'linear' or 'log', optional 

1725 Sets the scaling of the y-axis. 

1726 

1727 axis_center : (float, float), optional 

1728 Tuple of two floats denoting the coordinates of the center or 

1729 {'center', 'auto'} 

1730 

1731 xlim : (float, float), optional 

1732 Denotes the x-axis limits, ``(min, max)```. 

1733 

1734 ylim : (float, float), optional 

1735 Denotes the y-axis limits, ``(min, max)```. 

1736 

1737 annotations : list, optional 

1738 A list of dictionaries specifying the type of annotation 

1739 required. The keys in the dictionary should be equivalent 

1740 to the arguments of the :external:mod:`matplotlib`'s 

1741 :external:meth:`~matplotlib.axes.Axes.annotate` method. 

1742 

1743 markers : list, optional 

1744 A list of dictionaries specifying the type the markers required. 

1745 The keys in the dictionary should be equivalent to the arguments 

1746 of the :external:mod:`matplotlib`'s :external:func:`~matplotlib.pyplot.plot()` function 

1747 along with the marker related keyworded arguments. 

1748 

1749 rectangles : list, optional 

1750 A list of dictionaries specifying the dimensions of the 

1751 rectangles to be plotted. The keys in the dictionary should be 

1752 equivalent to the arguments of the :external:mod:`matplotlib`'s 

1753 :external:class:`~matplotlib.patches.Rectangle` class. 

1754 

1755 fill : dict, optional 

1756 A dictionary specifying the type of color filling required in 

1757 the plot. The keys in the dictionary should be equivalent to the 

1758 arguments of the :external:mod:`matplotlib`'s 

1759 :external:meth:`~matplotlib.axes.Axes.fill_between` method. 

1760 

1761 adaptive : bool, optional 

1762 The default value is set to ``True``. Set adaptive to ``False`` 

1763 and specify ``nb_of_points`` if uniform sampling is required. 

1764 

1765 The plotting uses an adaptive algorithm which samples 

1766 recursively to accurately plot. The adaptive algorithm uses a 

1767 random point near the midpoint of two points that has to be 

1768 further sampled. Hence the same plots can appear slightly 

1769 different. 

1770 

1771 depth : int, optional 

1772 Recursion depth of the adaptive algorithm. A depth of value 

1773 `n` samples a maximum of `2^{n}` points. 

1774 

1775 If the ``adaptive`` flag is set to ``False``, this will be 

1776 ignored. 

1777 

1778 nb_of_points : int, optional 

1779 Used when the ``adaptive`` is set to ``False``. The function 

1780 is uniformly sampled at ``nb_of_points`` number of points. 

1781 

1782 If the ``adaptive`` flag is set to ``True``, this will be 

1783 ignored. 

1784 

1785 size : (float, float), optional 

1786 A tuple in the form (width, height) in inches to specify the size of 

1787 the overall figure. The default value is set to ``None``, meaning 

1788 the size will be set by the default backend. 

1789 

1790 Examples 

1791 ======== 

1792 

1793 .. plot:: 

1794 :context: close-figs 

1795 :format: doctest 

1796 :include-source: True 

1797 

1798 >>> from sympy import symbols 

1799 >>> from sympy.plotting import plot 

1800 >>> x = symbols('x') 

1801 

1802 Single Plot 

1803 

1804 .. plot:: 

1805 :context: close-figs 

1806 :format: doctest 

1807 :include-source: True 

1808 

1809 >>> plot(x**2, (x, -5, 5)) 

1810 Plot object containing: 

1811 [0]: cartesian line: x**2 for x over (-5.0, 5.0) 

1812 

1813 Multiple plots with single range. 

1814 

1815 .. plot:: 

1816 :context: close-figs 

1817 :format: doctest 

1818 :include-source: True 

1819 

1820 >>> plot(x, x**2, x**3, (x, -5, 5)) 

1821 Plot object containing: 

1822 [0]: cartesian line: x for x over (-5.0, 5.0) 

1823 [1]: cartesian line: x**2 for x over (-5.0, 5.0) 

1824 [2]: cartesian line: x**3 for x over (-5.0, 5.0) 

1825 

1826 Multiple plots with different ranges. 

1827 

1828 .. plot:: 

1829 :context: close-figs 

1830 :format: doctest 

1831 :include-source: True 

1832 

1833 >>> plot((x**2, (x, -6, 6)), (x, (x, -5, 5))) 

1834 Plot object containing: 

1835 [0]: cartesian line: x**2 for x over (-6.0, 6.0) 

1836 [1]: cartesian line: x for x over (-5.0, 5.0) 

1837 

1838 No adaptive sampling. 

1839 

1840 .. plot:: 

1841 :context: close-figs 

1842 :format: doctest 

1843 :include-source: True 

1844 

1845 >>> plot(x**2, adaptive=False, nb_of_points=400) 

1846 Plot object containing: 

1847 [0]: cartesian line: x**2 for x over (-10.0, 10.0) 

1848 

1849 See Also 

1850 ======== 

1851 

1852 Plot, LineOver1DRangeSeries 

1853 

1854 """ 

1855 args = list(map(sympify, args)) 

1856 free = set() 

1857 for a in args: 

1858 if isinstance(a, Expr): 

1859 free |= a.free_symbols 

1860 if len(free) > 1: 

1861 raise ValueError( 

1862 'The same variable should be used in all ' 

1863 'univariate expressions being plotted.') 

1864 x = free.pop() if free else Symbol('x') 

1865 kwargs.setdefault('xlabel', x) 

1866 kwargs.setdefault('ylabel', Function('f')(x)) 

1867 series = [] 

1868 plot_expr = check_arguments(args, 1, 1) 

1869 series = [LineOver1DRangeSeries(*arg, **kwargs) for arg in plot_expr] 

1870 

1871 plots = Plot(*series, **kwargs) 

1872 if show: 

1873 plots.show() 

1874 return plots 

1875 

1876 

1877def plot_parametric(*args, show=True, **kwargs): 

1878 """ 

1879 Plots a 2D parametric curve. 

1880 

1881 Parameters 

1882 ========== 

1883 

1884 args 

1885 Common specifications are: 

1886 

1887 - Plotting a single parametric curve with a range 

1888 ``plot_parametric((expr_x, expr_y), range)`` 

1889 - Plotting multiple parametric curves with the same range 

1890 ``plot_parametric((expr_x, expr_y), ..., range)`` 

1891 - Plotting multiple parametric curves with different ranges 

1892 ``plot_parametric((expr_x, expr_y, range), ...)`` 

1893 

1894 ``expr_x`` is the expression representing $x$ component of the 

1895 parametric function. 

1896 

1897 ``expr_y`` is the expression representing $y$ component of the 

1898 parametric function. 

1899 

1900 ``range`` is a 3-tuple denoting the parameter symbol, start and 

1901 stop. For example, ``(u, 0, 5)``. 

1902 

1903 If the range is not specified, then a default range of (-10, 10) 

1904 is used. 

1905 

1906 However, if the arguments are specified as 

1907 ``(expr_x, expr_y, range), ...``, you must specify the ranges 

1908 for each expressions manually. 

1909 

1910 Default range may change in the future if a more advanced 

1911 algorithm is implemented. 

1912 

1913 adaptive : bool, optional 

1914 Specifies whether to use the adaptive sampling or not. 

1915 

1916 The default value is set to ``True``. Set adaptive to ``False`` 

1917 and specify ``nb_of_points`` if uniform sampling is required. 

1918 

1919 depth : int, optional 

1920 The recursion depth of the adaptive algorithm. A depth of 

1921 value $n$ samples a maximum of $2^n$ points. 

1922 

1923 nb_of_points : int, optional 

1924 Used when the ``adaptive`` flag is set to ``False``. 

1925 

1926 Specifies the number of the points used for the uniform 

1927 sampling. 

1928 

1929 line_color : string, or float, or function, optional 

1930 Specifies the color for the plot. 

1931 See ``Plot`` to see how to set color for the plots. 

1932 Note that by setting ``line_color``, it would be applied simultaneously 

1933 to all the series. 

1934 

1935 label : str, optional 

1936 The label of the expression in the plot. It will be used when 

1937 called with ``legend``. Default is the name of the expression. 

1938 e.g. ``sin(x)`` 

1939 

1940 xlabel : str, optional 

1941 Label for the x-axis. 

1942 

1943 ylabel : str, optional 

1944 Label for the y-axis. 

1945 

1946 xscale : 'linear' or 'log', optional 

1947 Sets the scaling of the x-axis. 

1948 

1949 yscale : 'linear' or 'log', optional 

1950 Sets the scaling of the y-axis. 

1951 

1952 axis_center : (float, float), optional 

1953 Tuple of two floats denoting the coordinates of the center or 

1954 {'center', 'auto'} 

1955 

1956 xlim : (float, float), optional 

1957 Denotes the x-axis limits, ``(min, max)```. 

1958 

1959 ylim : (float, float), optional 

1960 Denotes the y-axis limits, ``(min, max)```. 

1961 

1962 size : (float, float), optional 

1963 A tuple in the form (width, height) in inches to specify the size of 

1964 the overall figure. The default value is set to ``None``, meaning 

1965 the size will be set by the default backend. 

1966 

1967 Examples 

1968 ======== 

1969 

1970 .. plot:: 

1971 :context: reset 

1972 :format: doctest 

1973 :include-source: True 

1974 

1975 >>> from sympy import plot_parametric, symbols, cos, sin 

1976 >>> u = symbols('u') 

1977 

1978 A parametric plot with a single expression: 

1979 

1980 .. plot:: 

1981 :context: close-figs 

1982 :format: doctest 

1983 :include-source: True 

1984 

1985 >>> plot_parametric((cos(u), sin(u)), (u, -5, 5)) 

1986 Plot object containing: 

1987 [0]: parametric cartesian line: (cos(u), sin(u)) for u over (-5.0, 5.0) 

1988 

1989 A parametric plot with multiple expressions with the same range: 

1990 

1991 .. plot:: 

1992 :context: close-figs 

1993 :format: doctest 

1994 :include-source: True 

1995 

1996 >>> plot_parametric((cos(u), sin(u)), (u, cos(u)), (u, -10, 10)) 

1997 Plot object containing: 

1998 [0]: parametric cartesian line: (cos(u), sin(u)) for u over (-10.0, 10.0) 

1999 [1]: parametric cartesian line: (u, cos(u)) for u over (-10.0, 10.0) 

2000 

2001 A parametric plot with multiple expressions with different ranges 

2002 for each curve: 

2003 

2004 .. plot:: 

2005 :context: close-figs 

2006 :format: doctest 

2007 :include-source: True 

2008 

2009 >>> plot_parametric((cos(u), sin(u), (u, -5, 5)), 

2010 ... (cos(u), u, (u, -5, 5))) 

2011 Plot object containing: 

2012 [0]: parametric cartesian line: (cos(u), sin(u)) for u over (-5.0, 5.0) 

2013 [1]: parametric cartesian line: (cos(u), u) for u over (-5.0, 5.0) 

2014 

2015 Notes 

2016 ===== 

2017 

2018 The plotting uses an adaptive algorithm which samples recursively to 

2019 accurately plot the curve. The adaptive algorithm uses a random point 

2020 near the midpoint of two points that has to be further sampled. 

2021 Hence, repeating the same plot command can give slightly different 

2022 results because of the random sampling. 

2023 

2024 If there are multiple plots, then the same optional arguments are 

2025 applied to all the plots drawn in the same canvas. If you want to 

2026 set these options separately, you can index the returned ``Plot`` 

2027 object and set it. 

2028 

2029 For example, when you specify ``line_color`` once, it would be 

2030 applied simultaneously to both series. 

2031 

2032 .. plot:: 

2033 :context: close-figs 

2034 :format: doctest 

2035 :include-source: True 

2036 

2037 >>> from sympy import pi 

2038 >>> expr1 = (u, cos(2*pi*u)/2 + 1/2) 

2039 >>> expr2 = (u, sin(2*pi*u)/2 + 1/2) 

2040 >>> p = plot_parametric(expr1, expr2, (u, 0, 1), line_color='blue') 

2041 

2042 If you want to specify the line color for the specific series, you 

2043 should index each item and apply the property manually. 

2044 

2045 .. plot:: 

2046 :context: close-figs 

2047 :format: doctest 

2048 :include-source: True 

2049 

2050 >>> p[0].line_color = 'red' 

2051 >>> p.show() 

2052 

2053 See Also 

2054 ======== 

2055 

2056 Plot, Parametric2DLineSeries 

2057 """ 

2058 args = list(map(sympify, args)) 

2059 series = [] 

2060 plot_expr = check_arguments(args, 2, 1) 

2061 series = [Parametric2DLineSeries(*arg, **kwargs) for arg in plot_expr] 

2062 plots = Plot(*series, **kwargs) 

2063 if show: 

2064 plots.show() 

2065 return plots 

2066 

2067 

2068def plot3d_parametric_line(*args, show=True, **kwargs): 

2069 """ 

2070 Plots a 3D parametric line plot. 

2071 

2072 Usage 

2073 ===== 

2074 

2075 Single plot: 

2076 

2077 ``plot3d_parametric_line(expr_x, expr_y, expr_z, range, **kwargs)`` 

2078 

2079 If the range is not specified, then a default range of (-10, 10) is used. 

2080 

2081 Multiple plots. 

2082 

2083 ``plot3d_parametric_line((expr_x, expr_y, expr_z, range), ..., **kwargs)`` 

2084 

2085 Ranges have to be specified for every expression. 

2086 

2087 Default range may change in the future if a more advanced default range 

2088 detection algorithm is implemented. 

2089 

2090 Arguments 

2091 ========= 

2092 

2093 expr_x : Expression representing the function along x. 

2094 

2095 expr_y : Expression representing the function along y. 

2096 

2097 expr_z : Expression representing the function along z. 

2098 

2099 range : (:class:`~.Symbol`, float, float) 

2100 A 3-tuple denoting the range of the parameter variable, e.g., (u, 0, 5). 

2101 

2102 Keyword Arguments 

2103 ================= 

2104 

2105 Arguments for ``Parametric3DLineSeries`` class. 

2106 

2107 nb_of_points : The range is uniformly sampled at ``nb_of_points`` 

2108 number of points. 

2109 

2110 Aesthetics: 

2111 

2112 line_color : string, or float, or function, optional 

2113 Specifies the color for the plot. 

2114 See ``Plot`` to see how to set color for the plots. 

2115 Note that by setting ``line_color``, it would be applied simultaneously 

2116 to all the series. 

2117 

2118 label : str 

2119 The label to the plot. It will be used when called with ``legend=True`` 

2120 to denote the function with the given label in the plot. 

2121 

2122 If there are multiple plots, then the same series arguments are applied to 

2123 all the plots. If you want to set these options separately, you can index 

2124 the returned ``Plot`` object and set it. 

2125 

2126 Arguments for ``Plot`` class. 

2127 

2128 title : str 

2129 Title of the plot. 

2130 

2131 size : (float, float), optional 

2132 A tuple in the form (width, height) in inches to specify the size of 

2133 the overall figure. The default value is set to ``None``, meaning 

2134 the size will be set by the default backend. 

2135 

2136 Examples 

2137 ======== 

2138 

2139 .. plot:: 

2140 :context: reset 

2141 :format: doctest 

2142 :include-source: True 

2143 

2144 >>> from sympy import symbols, cos, sin 

2145 >>> from sympy.plotting import plot3d_parametric_line 

2146 >>> u = symbols('u') 

2147 

2148 Single plot. 

2149 

2150 .. plot:: 

2151 :context: close-figs 

2152 :format: doctest 

2153 :include-source: True 

2154 

2155 >>> plot3d_parametric_line(cos(u), sin(u), u, (u, -5, 5)) 

2156 Plot object containing: 

2157 [0]: 3D parametric cartesian line: (cos(u), sin(u), u) for u over (-5.0, 5.0) 

2158 

2159 

2160 Multiple plots. 

2161 

2162 .. plot:: 

2163 :context: close-figs 

2164 :format: doctest 

2165 :include-source: True 

2166 

2167 >>> plot3d_parametric_line((cos(u), sin(u), u, (u, -5, 5)), 

2168 ... (sin(u), u**2, u, (u, -5, 5))) 

2169 Plot object containing: 

2170 [0]: 3D parametric cartesian line: (cos(u), sin(u), u) for u over (-5.0, 5.0) 

2171 [1]: 3D parametric cartesian line: (sin(u), u**2, u) for u over (-5.0, 5.0) 

2172 

2173 

2174 See Also 

2175 ======== 

2176 

2177 Plot, Parametric3DLineSeries 

2178 

2179 """ 

2180 args = list(map(sympify, args)) 

2181 series = [] 

2182 plot_expr = check_arguments(args, 3, 1) 

2183 series = [Parametric3DLineSeries(*arg, **kwargs) for arg in plot_expr] 

2184 kwargs.setdefault("xlabel", "x") 

2185 kwargs.setdefault("ylabel", "y") 

2186 kwargs.setdefault("zlabel", "z") 

2187 plots = Plot(*series, **kwargs) 

2188 if show: 

2189 plots.show() 

2190 return plots 

2191 

2192 

2193def plot3d(*args, show=True, **kwargs): 

2194 """ 

2195 Plots a 3D surface plot. 

2196 

2197 Usage 

2198 ===== 

2199 

2200 Single plot 

2201 

2202 ``plot3d(expr, range_x, range_y, **kwargs)`` 

2203 

2204 If the ranges are not specified, then a default range of (-10, 10) is used. 

2205 

2206 Multiple plot with the same range. 

2207 

2208 ``plot3d(expr1, expr2, range_x, range_y, **kwargs)`` 

2209 

2210 If the ranges are not specified, then a default range of (-10, 10) is used. 

2211 

2212 Multiple plots with different ranges. 

2213 

2214 ``plot3d((expr1, range_x, range_y), (expr2, range_x, range_y), ..., **kwargs)`` 

2215 

2216 Ranges have to be specified for every expression. 

2217 

2218 Default range may change in the future if a more advanced default range 

2219 detection algorithm is implemented. 

2220 

2221 Arguments 

2222 ========= 

2223 

2224 expr : Expression representing the function along x. 

2225 

2226 range_x : (:class:`~.Symbol`, float, float) 

2227 A 3-tuple denoting the range of the x variable, e.g. (x, 0, 5). 

2228 

2229 range_y : (:class:`~.Symbol`, float, float) 

2230 A 3-tuple denoting the range of the y variable, e.g. (y, 0, 5). 

2231 

2232 Keyword Arguments 

2233 ================= 

2234 

2235 Arguments for ``SurfaceOver2DRangeSeries`` class: 

2236 

2237 nb_of_points_x : int 

2238 The x range is sampled uniformly at ``nb_of_points_x`` of points. 

2239 

2240 nb_of_points_y : int 

2241 The y range is sampled uniformly at ``nb_of_points_y`` of points. 

2242 

2243 Aesthetics: 

2244 

2245 surface_color : Function which returns a float 

2246 Specifies the color for the surface of the plot. 

2247 See :class:`~.Plot` for more details. 

2248 

2249 If there are multiple plots, then the same series arguments are applied to 

2250 all the plots. If you want to set these options separately, you can index 

2251 the returned ``Plot`` object and set it. 

2252 

2253 Arguments for ``Plot`` class: 

2254 

2255 title : str 

2256 Title of the plot. 

2257 

2258 size : (float, float), optional 

2259 A tuple in the form (width, height) in inches to specify the size of the 

2260 overall figure. The default value is set to ``None``, meaning the size will 

2261 be set by the default backend. 

2262 

2263 Examples 

2264 ======== 

2265 

2266 .. plot:: 

2267 :context: reset 

2268 :format: doctest 

2269 :include-source: True 

2270 

2271 >>> from sympy import symbols 

2272 >>> from sympy.plotting import plot3d 

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

2274 

2275 Single plot 

2276 

2277 .. plot:: 

2278 :context: close-figs 

2279 :format: doctest 

2280 :include-source: True 

2281 

2282 >>> plot3d(x*y, (x, -5, 5), (y, -5, 5)) 

2283 Plot object containing: 

2284 [0]: cartesian surface: x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0) 

2285 

2286 

2287 Multiple plots with same range 

2288 

2289 .. plot:: 

2290 :context: close-figs 

2291 :format: doctest 

2292 :include-source: True 

2293 

2294 >>> plot3d(x*y, -x*y, (x, -5, 5), (y, -5, 5)) 

2295 Plot object containing: 

2296 [0]: cartesian surface: x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0) 

2297 [1]: cartesian surface: -x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0) 

2298 

2299 

2300 Multiple plots with different ranges. 

2301 

2302 .. plot:: 

2303 :context: close-figs 

2304 :format: doctest 

2305 :include-source: True 

2306 

2307 >>> plot3d((x**2 + y**2, (x, -5, 5), (y, -5, 5)), 

2308 ... (x*y, (x, -3, 3), (y, -3, 3))) 

2309 Plot object containing: 

2310 [0]: cartesian surface: x**2 + y**2 for x over (-5.0, 5.0) and y over (-5.0, 5.0) 

2311 [1]: cartesian surface: x*y for x over (-3.0, 3.0) and y over (-3.0, 3.0) 

2312 

2313 

2314 See Also 

2315 ======== 

2316 

2317 Plot, SurfaceOver2DRangeSeries 

2318 

2319 """ 

2320 

2321 args = list(map(sympify, args)) 

2322 series = [] 

2323 plot_expr = check_arguments(args, 1, 2) 

2324 series = [SurfaceOver2DRangeSeries(*arg, **kwargs) for arg in plot_expr] 

2325 kwargs.setdefault("xlabel", series[0].var_x) 

2326 kwargs.setdefault("ylabel", series[0].var_y) 

2327 kwargs.setdefault("zlabel", Function('f')(series[0].var_x, series[0].var_y)) 

2328 plots = Plot(*series, **kwargs) 

2329 if show: 

2330 plots.show() 

2331 return plots 

2332 

2333 

2334def plot3d_parametric_surface(*args, show=True, **kwargs): 

2335 """ 

2336 Plots a 3D parametric surface plot. 

2337 

2338 Explanation 

2339 =========== 

2340 

2341 Single plot. 

2342 

2343 ``plot3d_parametric_surface(expr_x, expr_y, expr_z, range_u, range_v, **kwargs)`` 

2344 

2345 If the ranges is not specified, then a default range of (-10, 10) is used. 

2346 

2347 Multiple plots. 

2348 

2349 ``plot3d_parametric_surface((expr_x, expr_y, expr_z, range_u, range_v), ..., **kwargs)`` 

2350 

2351 Ranges have to be specified for every expression. 

2352 

2353 Default range may change in the future if a more advanced default range 

2354 detection algorithm is implemented. 

2355 

2356 Arguments 

2357 ========= 

2358 

2359 expr_x : Expression representing the function along ``x``. 

2360 

2361 expr_y : Expression representing the function along ``y``. 

2362 

2363 expr_z : Expression representing the function along ``z``. 

2364 

2365 range_u : (:class:`~.Symbol`, float, float) 

2366 A 3-tuple denoting the range of the u variable, e.g. (u, 0, 5). 

2367 

2368 range_v : (:class:`~.Symbol`, float, float) 

2369 A 3-tuple denoting the range of the v variable, e.g. (v, 0, 5). 

2370 

2371 Keyword Arguments 

2372 ================= 

2373 

2374 Arguments for ``ParametricSurfaceSeries`` class: 

2375 

2376 nb_of_points_u : int 

2377 The ``u`` range is sampled uniformly at ``nb_of_points_v`` of points 

2378 

2379 nb_of_points_y : int 

2380 The ``v`` range is sampled uniformly at ``nb_of_points_y`` of points 

2381 

2382 Aesthetics: 

2383 

2384 surface_color : Function which returns a float 

2385 Specifies the color for the surface of the plot. See 

2386 :class:`~Plot` for more details. 

2387 

2388 If there are multiple plots, then the same series arguments are applied for 

2389 all the plots. If you want to set these options separately, you can index 

2390 the returned ``Plot`` object and set it. 

2391 

2392 

2393 Arguments for ``Plot`` class: 

2394 

2395 title : str 

2396 Title of the plot. 

2397 

2398 size : (float, float), optional 

2399 A tuple in the form (width, height) in inches to specify the size of the 

2400 overall figure. The default value is set to ``None``, meaning the size will 

2401 be set by the default backend. 

2402 

2403 Examples 

2404 ======== 

2405 

2406 .. plot:: 

2407 :context: reset 

2408 :format: doctest 

2409 :include-source: True 

2410 

2411 >>> from sympy import symbols, cos, sin 

2412 >>> from sympy.plotting import plot3d_parametric_surface 

2413 >>> u, v = symbols('u v') 

2414 

2415 Single plot. 

2416 

2417 .. plot:: 

2418 :context: close-figs 

2419 :format: doctest 

2420 :include-source: True 

2421 

2422 >>> plot3d_parametric_surface(cos(u + v), sin(u - v), u - v, 

2423 ... (u, -5, 5), (v, -5, 5)) 

2424 Plot object containing: 

2425 [0]: parametric cartesian surface: (cos(u + v), sin(u - v), u - v) for u over (-5.0, 5.0) and v over (-5.0, 5.0) 

2426 

2427 

2428 See Also 

2429 ======== 

2430 

2431 Plot, ParametricSurfaceSeries 

2432 

2433 """ 

2434 

2435 args = list(map(sympify, args)) 

2436 series = [] 

2437 plot_expr = check_arguments(args, 3, 2) 

2438 series = [ParametricSurfaceSeries(*arg, **kwargs) for arg in plot_expr] 

2439 kwargs.setdefault("xlabel", "x") 

2440 kwargs.setdefault("ylabel", "y") 

2441 kwargs.setdefault("zlabel", "z") 

2442 plots = Plot(*series, **kwargs) 

2443 if show: 

2444 plots.show() 

2445 return plots 

2446 

2447def plot_contour(*args, show=True, **kwargs): 

2448 """ 

2449 Draws contour plot of a function 

2450 

2451 Usage 

2452 ===== 

2453 

2454 Single plot 

2455 

2456 ``plot_contour(expr, range_x, range_y, **kwargs)`` 

2457 

2458 If the ranges are not specified, then a default range of (-10, 10) is used. 

2459 

2460 Multiple plot with the same range. 

2461 

2462 ``plot_contour(expr1, expr2, range_x, range_y, **kwargs)`` 

2463 

2464 If the ranges are not specified, then a default range of (-10, 10) is used. 

2465 

2466 Multiple plots with different ranges. 

2467 

2468 ``plot_contour((expr1, range_x, range_y), (expr2, range_x, range_y), ..., **kwargs)`` 

2469 

2470 Ranges have to be specified for every expression. 

2471 

2472 Default range may change in the future if a more advanced default range 

2473 detection algorithm is implemented. 

2474 

2475 Arguments 

2476 ========= 

2477 

2478 expr : Expression representing the function along x. 

2479 

2480 range_x : (:class:`Symbol`, float, float) 

2481 A 3-tuple denoting the range of the x variable, e.g. (x, 0, 5). 

2482 

2483 range_y : (:class:`Symbol`, float, float) 

2484 A 3-tuple denoting the range of the y variable, e.g. (y, 0, 5). 

2485 

2486 Keyword Arguments 

2487 ================= 

2488 

2489 Arguments for ``ContourSeries`` class: 

2490 

2491 nb_of_points_x : int 

2492 The x range is sampled uniformly at ``nb_of_points_x`` of points. 

2493 

2494 nb_of_points_y : int 

2495 The y range is sampled uniformly at ``nb_of_points_y`` of points. 

2496 

2497 Aesthetics: 

2498 

2499 surface_color : Function which returns a float 

2500 Specifies the color for the surface of the plot. See 

2501 :class:`sympy.plotting.Plot` for more details. 

2502 

2503 If there are multiple plots, then the same series arguments are applied to 

2504 all the plots. If you want to set these options separately, you can index 

2505 the returned ``Plot`` object and set it. 

2506 

2507 Arguments for ``Plot`` class: 

2508 

2509 title : str 

2510 Title of the plot. 

2511 

2512 size : (float, float), optional 

2513 A tuple in the form (width, height) in inches to specify the size of 

2514 the overall figure. The default value is set to ``None``, meaning 

2515 the size will be set by the default backend. 

2516 

2517 See Also 

2518 ======== 

2519 

2520 Plot, ContourSeries 

2521 

2522 """ 

2523 

2524 args = list(map(sympify, args)) 

2525 plot_expr = check_arguments(args, 1, 2) 

2526 series = [ContourSeries(*arg) for arg in plot_expr] 

2527 plot_contours = Plot(*series, **kwargs) 

2528 if len(plot_expr[0].free_symbols) > 2: 

2529 raise ValueError('Contour Plot cannot Plot for more than two variables.') 

2530 if show: 

2531 plot_contours.show() 

2532 return plot_contours 

2533 

2534def check_arguments(args, expr_len, nb_of_free_symbols): 

2535 """ 

2536 Checks the arguments and converts into tuples of the 

2537 form (exprs, ranges). 

2538 

2539 Examples 

2540 ======== 

2541 

2542 .. plot:: 

2543 :context: reset 

2544 :format: doctest 

2545 :include-source: True 

2546 

2547 >>> from sympy import cos, sin, symbols 

2548 >>> from sympy.plotting.plot import check_arguments 

2549 >>> x = symbols('x') 

2550 >>> check_arguments([cos(x), sin(x)], 2, 1) 

2551 [(cos(x), sin(x), (x, -10, 10))] 

2552 

2553 >>> check_arguments([x, x**2], 1, 1) 

2554 [(x, (x, -10, 10)), (x**2, (x, -10, 10))] 

2555 """ 

2556 if not args: 

2557 return [] 

2558 if expr_len > 1 and isinstance(args[0], Expr): 

2559 # Multiple expressions same range. 

2560 # The arguments are tuples when the expression length is 

2561 # greater than 1. 

2562 if len(args) < expr_len: 

2563 raise ValueError("len(args) should not be less than expr_len") 

2564 for i in range(len(args)): 

2565 if isinstance(args[i], Tuple): 

2566 break 

2567 else: 

2568 i = len(args) + 1 

2569 

2570 exprs = Tuple(*args[:i]) 

2571 free_symbols = list(set().union(*[e.free_symbols for e in exprs])) 

2572 if len(args) == expr_len + nb_of_free_symbols: 

2573 #Ranges given 

2574 plots = [exprs + Tuple(*args[expr_len:])] 

2575 else: 

2576 default_range = Tuple(-10, 10) 

2577 ranges = [] 

2578 for symbol in free_symbols: 

2579 ranges.append(Tuple(symbol) + default_range) 

2580 

2581 for i in range(len(free_symbols) - nb_of_free_symbols): 

2582 ranges.append(Tuple(Dummy()) + default_range) 

2583 plots = [exprs + Tuple(*ranges)] 

2584 return plots 

2585 

2586 if isinstance(args[0], Expr) or (isinstance(args[0], Tuple) and 

2587 len(args[0]) == expr_len and 

2588 expr_len != 3): 

2589 # Cannot handle expressions with number of expression = 3. It is 

2590 # not possible to differentiate between expressions and ranges. 

2591 #Series of plots with same range 

2592 for i in range(len(args)): 

2593 if isinstance(args[i], Tuple) and len(args[i]) != expr_len: 

2594 break 

2595 if not isinstance(args[i], Tuple): 

2596 args[i] = Tuple(args[i]) 

2597 else: 

2598 i = len(args) + 1 

2599 

2600 exprs = args[:i] 

2601 assert all(isinstance(e, Expr) for expr in exprs for e in expr) 

2602 free_symbols = list(set().union(*[e.free_symbols for expr in exprs 

2603 for e in expr])) 

2604 

2605 if len(free_symbols) > nb_of_free_symbols: 

2606 raise ValueError("The number of free_symbols in the expression " 

2607 "is greater than %d" % nb_of_free_symbols) 

2608 if len(args) == i + nb_of_free_symbols and isinstance(args[i], Tuple): 

2609 ranges = Tuple(*list(args[ 

2610 i:i + nb_of_free_symbols])) 

2611 plots = [expr + ranges for expr in exprs] 

2612 return plots 

2613 else: 

2614 # Use default ranges. 

2615 default_range = Tuple(-10, 10) 

2616 ranges = [] 

2617 for symbol in free_symbols: 

2618 ranges.append(Tuple(symbol) + default_range) 

2619 

2620 for i in range(nb_of_free_symbols - len(free_symbols)): 

2621 ranges.append(Tuple(Dummy()) + default_range) 

2622 ranges = Tuple(*ranges) 

2623 plots = [expr + ranges for expr in exprs] 

2624 return plots 

2625 

2626 elif isinstance(args[0], Tuple) and len(args[0]) == expr_len + nb_of_free_symbols: 

2627 # Multiple plots with different ranges. 

2628 for arg in args: 

2629 for i in range(expr_len): 

2630 if not isinstance(arg[i], Expr): 

2631 raise ValueError("Expected an expression, given %s" % 

2632 str(arg[i])) 

2633 for i in range(nb_of_free_symbols): 

2634 if not len(arg[i + expr_len]) == 3: 

2635 raise ValueError("The ranges should be a tuple of " 

2636 "length 3, got %s" % str(arg[i + expr_len])) 

2637 return args