base_backend.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. from sympy.plotting.series import BaseSeries, GenericDataSeries
  2. from sympy.utilities.exceptions import sympy_deprecation_warning
  3. from sympy.utilities.iterables import is_sequence
  4. __doctest_requires__ = {
  5. ('Plot.append', 'Plot.extend'): ['matplotlib'],
  6. }
  7. # Global variable
  8. # Set to False when running tests / doctests so that the plots don't show.
  9. _show = True
  10. def unset_show():
  11. """
  12. Disable show(). For use in the tests.
  13. """
  14. global _show
  15. _show = False
  16. def _deprecation_msg_m_a_r_f(attr):
  17. sympy_deprecation_warning(
  18. f"The `{attr}` property is deprecated. The `{attr}` keyword "
  19. "argument should be passed to a plotting function, which generates "
  20. "the appropriate data series. If needed, index the plot object to "
  21. "retrieve a specific data series.",
  22. deprecated_since_version="1.13",
  23. active_deprecations_target="deprecated-markers-annotations-fill-rectangles",
  24. stacklevel=4)
  25. def _create_generic_data_series(**kwargs):
  26. keywords = ["annotations", "markers", "fill", "rectangles"]
  27. series = []
  28. for kw in keywords:
  29. dictionaries = kwargs.pop(kw, [])
  30. if dictionaries is None:
  31. dictionaries = []
  32. if isinstance(dictionaries, dict):
  33. dictionaries = [dictionaries]
  34. for d in dictionaries:
  35. args = d.pop("args", [])
  36. series.append(GenericDataSeries(kw, *args, **d))
  37. return series
  38. class Plot:
  39. """Base class for all backends. A backend represents the plotting library,
  40. which implements the necessary functionalities in order to use SymPy
  41. plotting functions.
  42. For interactive work the function :func:`plot` is better suited.
  43. This class permits the plotting of SymPy expressions using numerous
  44. backends (:external:mod:`matplotlib`, textplot, the old pyglet module for SymPy, Google
  45. charts api, etc).
  46. The figure can contain an arbitrary number of plots of SymPy expressions,
  47. lists of coordinates of points, etc. Plot has a private attribute _series that
  48. contains all data series to be plotted (expressions for lines or surfaces,
  49. lists of points, etc (all subclasses of BaseSeries)). Those data series are
  50. instances of classes not imported by ``from sympy import *``.
  51. The customization of the figure is on two levels. Global options that
  52. concern the figure as a whole (e.g. title, xlabel, scale, etc) and
  53. per-data series options (e.g. name) and aesthetics (e.g. color, point shape,
  54. line type, etc.).
  55. The difference between options and aesthetics is that an aesthetic can be
  56. a function of the coordinates (or parameters in a parametric plot). The
  57. supported values for an aesthetic are:
  58. - None (the backend uses default values)
  59. - a constant
  60. - a function of one variable (the first coordinate or parameter)
  61. - a function of two variables (the first and second coordinate or parameters)
  62. - a function of three variables (only in nonparametric 3D plots)
  63. Their implementation depends on the backend so they may not work in some
  64. backends.
  65. If the plot is parametric and the arity of the aesthetic function permits
  66. it the aesthetic is calculated over parameters and not over coordinates.
  67. If the arity does not permit calculation over parameters the calculation is
  68. done over coordinates.
  69. Only cartesian coordinates are supported for the moment, but you can use
  70. the parametric plots to plot in polar, spherical and cylindrical
  71. coordinates.
  72. The arguments for the constructor Plot must be subclasses of BaseSeries.
  73. Any global option can be specified as a keyword argument.
  74. The global options for a figure are:
  75. - title : str
  76. - xlabel : str or Symbol
  77. - ylabel : str or Symbol
  78. - zlabel : str or Symbol
  79. - legend : bool
  80. - xscale : {'linear', 'log'}
  81. - yscale : {'linear', 'log'}
  82. - axis : bool
  83. - axis_center : tuple of two floats or {'center', 'auto'}
  84. - xlim : tuple of two floats
  85. - ylim : tuple of two floats
  86. - aspect_ratio : tuple of two floats or {'auto'}
  87. - autoscale : bool
  88. - margin : float in [0, 1]
  89. - backend : {'default', 'matplotlib', 'text'} or a subclass of BaseBackend
  90. - size : optional tuple of two floats, (width, height); default: None
  91. The per data series options and aesthetics are:
  92. There are none in the base series. See below for options for subclasses.
  93. Some data series support additional aesthetics or options:
  94. :class:`~.LineOver1DRangeSeries`, :class:`~.Parametric2DLineSeries`, and
  95. :class:`~.Parametric3DLineSeries` support the following:
  96. Aesthetics:
  97. - line_color : string, or float, or function, optional
  98. Specifies the color for the plot, which depends on the backend being
  99. used.
  100. For example, if ``MatplotlibBackend`` is being used, then
  101. Matplotlib string colors are acceptable (``"red"``, ``"r"``,
  102. ``"cyan"``, ``"c"``, ...).
  103. Alternatively, we can use a float number, 0 < color < 1, wrapped in a
  104. string (for example, ``line_color="0.5"``) to specify grayscale colors.
  105. Alternatively, We can specify a function returning a single
  106. float value: this will be used to apply a color-loop (for example,
  107. ``line_color=lambda x: math.cos(x)``).
  108. Note that by setting line_color, it would be applied simultaneously
  109. to all the series.
  110. Options:
  111. - label : str
  112. - steps : bool
  113. - integers_only : bool
  114. :class:`~.SurfaceOver2DRangeSeries` and :class:`~.ParametricSurfaceSeries`
  115. support the following:
  116. Aesthetics:
  117. - surface_color : function which returns a float.
  118. Notes
  119. =====
  120. How the plotting module works:
  121. 1. Whenever a plotting function is called, the provided expressions are
  122. processed and a list of instances of the
  123. :class:`~sympy.plotting.series.BaseSeries` class is created, containing
  124. the necessary information to plot the expressions
  125. (e.g. the expression, ranges, series name, ...). Eventually, these
  126. objects will generate the numerical data to be plotted.
  127. 2. A subclass of :class:`~.Plot` class is instantiaed (referred to as
  128. backend, from now on), which stores the list of series and the main
  129. attributes of the plot (e.g. axis labels, title, ...).
  130. The backend implements the logic to generate the actual figure with
  131. some plotting library.
  132. 3. When the ``show`` command is executed, series are processed one by one
  133. to generate numerical data and add it to the figure. The backend is also
  134. going to set the axis labels, title, ..., according to the values stored
  135. in the Plot instance.
  136. The backend should check if it supports the data series that it is given
  137. (e.g. :class:`TextBackend` supports only
  138. :class:`~sympy.plotting.series.LineOver1DRangeSeries`).
  139. It is the backend responsibility to know how to use the class of data series
  140. that it's given. Note that the current implementation of the ``*Series``
  141. classes is "matplotlib-centric": the numerical data returned by the
  142. ``get_points`` and ``get_meshes`` methods is meant to be used directly by
  143. Matplotlib. Therefore, the new backend will have to pre-process the
  144. numerical data to make it compatible with the chosen plotting library.
  145. Keep in mind that future SymPy versions may improve the ``*Series`` classes
  146. in order to return numerical data "non-matplotlib-centric", hence if you code
  147. a new backend you have the responsibility to check if its working on each
  148. SymPy release.
  149. Please explore the :class:`MatplotlibBackend` source code to understand
  150. how a backend should be coded.
  151. In order to be used by SymPy plotting functions, a backend must implement
  152. the following methods:
  153. * show(self): used to loop over the data series, generate the numerical
  154. data, plot it and set the axis labels, title, ...
  155. * save(self, path): used to save the current plot to the specified file
  156. path.
  157. * close(self): used to close the current plot backend (note: some plotting
  158. library does not support this functionality. In that case, just raise a
  159. warning).
  160. """
  161. def __init__(self, *args,
  162. title=None, xlabel=None, ylabel=None, zlabel=None, aspect_ratio='auto',
  163. xlim=None, ylim=None, axis_center='auto', axis=True,
  164. xscale='linear', yscale='linear', legend=False, autoscale=True,
  165. margin=0, annotations=None, markers=None, rectangles=None,
  166. fill=None, backend='default', size=None, **kwargs):
  167. # Options for the graph as a whole.
  168. # The possible values for each option are described in the docstring of
  169. # Plot. They are based purely on convention, no checking is done.
  170. self.title = title
  171. self.xlabel = xlabel
  172. self.ylabel = ylabel
  173. self.zlabel = zlabel
  174. self.aspect_ratio = aspect_ratio
  175. self.axis_center = axis_center
  176. self.axis = axis
  177. self.xscale = xscale
  178. self.yscale = yscale
  179. self.legend = legend
  180. self.autoscale = autoscale
  181. self.margin = margin
  182. self._annotations = annotations
  183. self._markers = markers
  184. self._rectangles = rectangles
  185. self._fill = fill
  186. # Contains the data objects to be plotted. The backend should be smart
  187. # enough to iterate over this list.
  188. self._series = []
  189. self._series.extend(args)
  190. self._series.extend(_create_generic_data_series(
  191. annotations=annotations, markers=markers, rectangles=rectangles,
  192. fill=fill))
  193. is_real = \
  194. lambda lim: all(getattr(i, 'is_real', True) for i in lim)
  195. is_finite = \
  196. lambda lim: all(getattr(i, 'is_finite', True) for i in lim)
  197. # reduce code repetition
  198. def check_and_set(t_name, t):
  199. if t:
  200. if not is_real(t):
  201. raise ValueError(
  202. "All numbers from {}={} must be real".format(t_name, t))
  203. if not is_finite(t):
  204. raise ValueError(
  205. "All numbers from {}={} must be finite".format(t_name, t))
  206. setattr(self, t_name, (float(t[0]), float(t[1])))
  207. self.xlim = None
  208. check_and_set("xlim", xlim)
  209. self.ylim = None
  210. check_and_set("ylim", ylim)
  211. self.size = None
  212. check_and_set("size", size)
  213. @property
  214. def _backend(self):
  215. return self
  216. @property
  217. def backend(self):
  218. return type(self)
  219. def __str__(self):
  220. series_strs = [('[%d]: ' % i) + str(s)
  221. for i, s in enumerate(self._series)]
  222. return 'Plot object containing:\n' + '\n'.join(series_strs)
  223. def __getitem__(self, index):
  224. return self._series[index]
  225. def __setitem__(self, index, *args):
  226. if len(args) == 1 and isinstance(args[0], BaseSeries):
  227. self._series[index] = args
  228. def __delitem__(self, index):
  229. del self._series[index]
  230. def append(self, arg):
  231. """Adds an element from a plot's series to an existing plot.
  232. Examples
  233. ========
  234. Consider two ``Plot`` objects, ``p1`` and ``p2``. To add the
  235. second plot's first series object to the first, use the
  236. ``append`` method, like so:
  237. .. plot::
  238. :format: doctest
  239. :include-source: True
  240. >>> from sympy import symbols
  241. >>> from sympy.plotting import plot
  242. >>> x = symbols('x')
  243. >>> p1 = plot(x*x, show=False)
  244. >>> p2 = plot(x, show=False)
  245. >>> p1.append(p2[0])
  246. >>> p1
  247. Plot object containing:
  248. [0]: cartesian line: x**2 for x over (-10.0, 10.0)
  249. [1]: cartesian line: x for x over (-10.0, 10.0)
  250. >>> p1.show()
  251. See Also
  252. ========
  253. extend
  254. """
  255. if isinstance(arg, BaseSeries):
  256. self._series.append(arg)
  257. else:
  258. raise TypeError('Must specify element of plot to append.')
  259. def extend(self, arg):
  260. """Adds all series from another plot.
  261. Examples
  262. ========
  263. Consider two ``Plot`` objects, ``p1`` and ``p2``. To add the
  264. second plot to the first, use the ``extend`` method, like so:
  265. .. plot::
  266. :format: doctest
  267. :include-source: True
  268. >>> from sympy import symbols
  269. >>> from sympy.plotting import plot
  270. >>> x = symbols('x')
  271. >>> p1 = plot(x**2, show=False)
  272. >>> p2 = plot(x, -x, show=False)
  273. >>> p1.extend(p2)
  274. >>> p1
  275. Plot object containing:
  276. [0]: cartesian line: x**2 for x over (-10.0, 10.0)
  277. [1]: cartesian line: x for x over (-10.0, 10.0)
  278. [2]: cartesian line: -x for x over (-10.0, 10.0)
  279. >>> p1.show()
  280. """
  281. if isinstance(arg, Plot):
  282. self._series.extend(arg._series)
  283. elif is_sequence(arg):
  284. self._series.extend(arg)
  285. else:
  286. raise TypeError('Expecting Plot or sequence of BaseSeries')
  287. def show(self):
  288. raise NotImplementedError
  289. def save(self, path):
  290. raise NotImplementedError
  291. def close(self):
  292. raise NotImplementedError
  293. # deprecations
  294. @property
  295. def markers(self):
  296. """.. deprecated:: 1.13"""
  297. _deprecation_msg_m_a_r_f("markers")
  298. return self._markers
  299. @markers.setter
  300. def markers(self, v):
  301. """.. deprecated:: 1.13"""
  302. _deprecation_msg_m_a_r_f("markers")
  303. self._series.extend(_create_generic_data_series(markers=v))
  304. self._markers = v
  305. @property
  306. def annotations(self):
  307. """.. deprecated:: 1.13"""
  308. _deprecation_msg_m_a_r_f("annotations")
  309. return self._annotations
  310. @annotations.setter
  311. def annotations(self, v):
  312. """.. deprecated:: 1.13"""
  313. _deprecation_msg_m_a_r_f("annotations")
  314. self._series.extend(_create_generic_data_series(annotations=v))
  315. self._annotations = v
  316. @property
  317. def rectangles(self):
  318. """.. deprecated:: 1.13"""
  319. _deprecation_msg_m_a_r_f("rectangles")
  320. return self._rectangles
  321. @rectangles.setter
  322. def rectangles(self, v):
  323. """.. deprecated:: 1.13"""
  324. _deprecation_msg_m_a_r_f("rectangles")
  325. self._series.extend(_create_generic_data_series(rectangles=v))
  326. self._rectangles = v
  327. @property
  328. def fill(self):
  329. """.. deprecated:: 1.13"""
  330. _deprecation_msg_m_a_r_f("fill")
  331. return self._fill
  332. @fill.setter
  333. def fill(self, v):
  334. """.. deprecated:: 1.13"""
  335. _deprecation_msg_m_a_r_f("fill")
  336. self._series.extend(_create_generic_data_series(fill=v))
  337. self._fill = v