I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,419 @@
from sympy.plotting.series import BaseSeries, GenericDataSeries
from sympy.utilities.exceptions import sympy_deprecation_warning
from sympy.utilities.iterables import is_sequence
__doctest_requires__ = {
('Plot.append', 'Plot.extend'): ['matplotlib'],
}
# Global variable
# Set to False when running tests / doctests so that the plots don't show.
_show = True
def unset_show():
"""
Disable show(). For use in the tests.
"""
global _show
_show = False
def _deprecation_msg_m_a_r_f(attr):
sympy_deprecation_warning(
f"The `{attr}` property is deprecated. The `{attr}` keyword "
"argument should be passed to a plotting function, which generates "
"the appropriate data series. If needed, index the plot object to "
"retrieve a specific data series.",
deprecated_since_version="1.13",
active_deprecations_target="deprecated-markers-annotations-fill-rectangles",
stacklevel=4)
def _create_generic_data_series(**kwargs):
keywords = ["annotations", "markers", "fill", "rectangles"]
series = []
for kw in keywords:
dictionaries = kwargs.pop(kw, [])
if dictionaries is None:
dictionaries = []
if isinstance(dictionaries, dict):
dictionaries = [dictionaries]
for d in dictionaries:
args = d.pop("args", [])
series.append(GenericDataSeries(kw, *args, **d))
return series
class Plot:
"""Base class for all backends. A backend represents the plotting library,
which implements the necessary functionalities in order to use SymPy
plotting functions.
For interactive work the function :func:`plot()` is better suited.
This class permits the plotting of SymPy expressions using numerous
backends (:external:mod:`matplotlib`, textplot, the old pyglet module for SymPy, Google
charts api, etc).
The figure can contain an arbitrary number of plots of SymPy expressions,
lists of coordinates of points, etc. Plot has a private attribute _series that
contains all data series to be plotted (expressions for lines or surfaces,
lists of points, etc (all subclasses of BaseSeries)). Those data series are
instances of classes not imported by ``from sympy import *``.
The customization of the figure is on two levels. Global options that
concern the figure as a whole (e.g. title, xlabel, scale, etc) and
per-data series options (e.g. name) and aesthetics (e.g. color, point shape,
line type, etc.).
The difference between options and aesthetics is that an aesthetic can be
a function of the coordinates (or parameters in a parametric plot). The
supported values for an aesthetic are:
- None (the backend uses default values)
- a constant
- a function of one variable (the first coordinate or parameter)
- a function of two variables (the first and second coordinate or parameters)
- a function of three variables (only in nonparametric 3D plots)
Their implementation depends on the backend so they may not work in some
backends.
If the plot is parametric and the arity of the aesthetic function permits
it the aesthetic is calculated over parameters and not over coordinates.
If the arity does not permit calculation over parameters the calculation is
done over coordinates.
Only cartesian coordinates are supported for the moment, but you can use
the parametric plots to plot in polar, spherical and cylindrical
coordinates.
The arguments for the constructor Plot must be subclasses of BaseSeries.
Any global option can be specified as a keyword argument.
The global options for a figure are:
- title : str
- xlabel : str or Symbol
- ylabel : str or Symbol
- zlabel : str or Symbol
- legend : bool
- xscale : {'linear', 'log'}
- yscale : {'linear', 'log'}
- axis : bool
- axis_center : tuple of two floats or {'center', 'auto'}
- xlim : tuple of two floats
- ylim : tuple of two floats
- aspect_ratio : tuple of two floats or {'auto'}
- autoscale : bool
- margin : float in [0, 1]
- backend : {'default', 'matplotlib', 'text'} or a subclass of BaseBackend
- size : optional tuple of two floats, (width, height); default: None
The per data series options and aesthetics are:
There are none in the base series. See below for options for subclasses.
Some data series support additional aesthetics or options:
:class:`~.LineOver1DRangeSeries`, :class:`~.Parametric2DLineSeries`, and
:class:`~.Parametric3DLineSeries` support the following:
Aesthetics:
- line_color : string, or float, or function, optional
Specifies the color for the plot, which depends on the backend being
used.
For example, if ``MatplotlibBackend`` is being used, then
Matplotlib string colors are acceptable (``"red"``, ``"r"``,
``"cyan"``, ``"c"``, ...).
Alternatively, we can use a float number, 0 < color < 1, wrapped in a
string (for example, ``line_color="0.5"``) to specify grayscale colors.
Alternatively, We can specify a function returning a single
float value: this will be used to apply a color-loop (for example,
``line_color=lambda x: math.cos(x)``).
Note that by setting line_color, it would be applied simultaneously
to all the series.
Options:
- label : str
- steps : bool
- integers_only : bool
:class:`~.SurfaceOver2DRangeSeries` and :class:`~.ParametricSurfaceSeries`
support the following:
Aesthetics:
- surface_color : function which returns a float.
Notes
=====
How the plotting module works:
1. Whenever a plotting function is called, the provided expressions are
processed and a list of instances of the
:class:`~sympy.plotting.series.BaseSeries` class is created, containing
the necessary information to plot the expressions
(e.g. the expression, ranges, series name, ...). Eventually, these
objects will generate the numerical data to be plotted.
2. A subclass of :class:`~.Plot` class is instantiaed (referred to as
backend, from now on), which stores the list of series and the main
attributes of the plot (e.g. axis labels, title, ...).
The backend implements the logic to generate the actual figure with
some plotting library.
3. When the ``show`` command is executed, series are processed one by one
to generate numerical data and add it to the figure. The backend is also
going to set the axis labels, title, ..., according to the values stored
in the Plot instance.
The backend should check if it supports the data series that it is given
(e.g. :class:`TextBackend` supports only
:class:`~sympy.plotting.series.LineOver1DRangeSeries`).
It is the backend responsibility to know how to use the class of data series
that it's given. Note that the current implementation of the ``*Series``
classes is "matplotlib-centric": the numerical data returned by the
``get_points`` and ``get_meshes`` methods is meant to be used directly by
Matplotlib. Therefore, the new backend will have to pre-process the
numerical data to make it compatible with the chosen plotting library.
Keep in mind that future SymPy versions may improve the ``*Series`` classes
in order to return numerical data "non-matplotlib-centric", hence if you code
a new backend you have the responsibility to check if its working on each
SymPy release.
Please explore the :class:`MatplotlibBackend` source code to understand
how a backend should be coded.
In order to be used by SymPy plotting functions, a backend must implement
the following methods:
* show(self): used to loop over the data series, generate the numerical
data, plot it and set the axis labels, title, ...
* save(self, path): used to save the current plot to the specified file
path.
* close(self): used to close the current plot backend (note: some plotting
library does not support this functionality. In that case, just raise a
warning).
"""
def __init__(self, *args,
title=None, xlabel=None, ylabel=None, zlabel=None, aspect_ratio='auto',
xlim=None, ylim=None, axis_center='auto', axis=True,
xscale='linear', yscale='linear', legend=False, autoscale=True,
margin=0, annotations=None, markers=None, rectangles=None,
fill=None, backend='default', size=None, **kwargs):
# Options for the graph as a whole.
# The possible values for each option are described in the docstring of
# Plot. They are based purely on convention, no checking is done.
self.title = title
self.xlabel = xlabel
self.ylabel = ylabel
self.zlabel = zlabel
self.aspect_ratio = aspect_ratio
self.axis_center = axis_center
self.axis = axis
self.xscale = xscale
self.yscale = yscale
self.legend = legend
self.autoscale = autoscale
self.margin = margin
self._annotations = annotations
self._markers = markers
self._rectangles = rectangles
self._fill = fill
# Contains the data objects to be plotted. The backend should be smart
# enough to iterate over this list.
self._series = []
self._series.extend(args)
self._series.extend(_create_generic_data_series(
annotations=annotations, markers=markers, rectangles=rectangles,
fill=fill))
is_real = \
lambda lim: all(getattr(i, 'is_real', True) for i in lim)
is_finite = \
lambda lim: all(getattr(i, 'is_finite', True) for i in lim)
# reduce code repetition
def check_and_set(t_name, t):
if t:
if not is_real(t):
raise ValueError(
"All numbers from {}={} must be real".format(t_name, t))
if not is_finite(t):
raise ValueError(
"All numbers from {}={} must be finite".format(t_name, t))
setattr(self, t_name, (float(t[0]), float(t[1])))
self.xlim = None
check_and_set("xlim", xlim)
self.ylim = None
check_and_set("ylim", ylim)
self.size = None
check_and_set("size", size)
@property
def _backend(self):
return self
@property
def backend(self):
return type(self)
def __str__(self):
series_strs = [('[%d]: ' % i) + str(s)
for i, s in enumerate(self._series)]
return 'Plot object containing:\n' + '\n'.join(series_strs)
def __getitem__(self, index):
return self._series[index]
def __setitem__(self, index, *args):
if len(args) == 1 and isinstance(args[0], BaseSeries):
self._series[index] = args
def __delitem__(self, index):
del self._series[index]
def append(self, arg):
"""Adds an element from a plot's series to an existing plot.
Examples
========
Consider two ``Plot`` objects, ``p1`` and ``p2``. To add the
second plot's first series object to the first, use the
``append`` method, like so:
.. plot::
:format: doctest
:include-source: True
>>> from sympy import symbols
>>> from sympy.plotting import plot
>>> x = symbols('x')
>>> p1 = plot(x*x, show=False)
>>> p2 = plot(x, show=False)
>>> p1.append(p2[0])
>>> p1
Plot object containing:
[0]: cartesian line: x**2 for x over (-10.0, 10.0)
[1]: cartesian line: x for x over (-10.0, 10.0)
>>> p1.show()
See Also
========
extend
"""
if isinstance(arg, BaseSeries):
self._series.append(arg)
else:
raise TypeError('Must specify element of plot to append.')
def extend(self, arg):
"""Adds all series from another plot.
Examples
========
Consider two ``Plot`` objects, ``p1`` and ``p2``. To add the
second plot to the first, use the ``extend`` method, like so:
.. plot::
:format: doctest
:include-source: True
>>> from sympy import symbols
>>> from sympy.plotting import plot
>>> x = symbols('x')
>>> p1 = plot(x**2, show=False)
>>> p2 = plot(x, -x, show=False)
>>> p1.extend(p2)
>>> p1
Plot object containing:
[0]: cartesian line: x**2 for x over (-10.0, 10.0)
[1]: cartesian line: x for x over (-10.0, 10.0)
[2]: cartesian line: -x for x over (-10.0, 10.0)
>>> p1.show()
"""
if isinstance(arg, Plot):
self._series.extend(arg._series)
elif is_sequence(arg):
self._series.extend(arg)
else:
raise TypeError('Expecting Plot or sequence of BaseSeries')
def show(self):
raise NotImplementedError
def save(self, path):
raise NotImplementedError
def close(self):
raise NotImplementedError
# deprecations
@property
def markers(self):
""".. deprecated:: 1.13"""
_deprecation_msg_m_a_r_f("markers")
return self._markers
@markers.setter
def markers(self, v):
""".. deprecated:: 1.13"""
_deprecation_msg_m_a_r_f("markers")
self._series.extend(_create_generic_data_series(markers=v))
self._markers = v
@property
def annotations(self):
""".. deprecated:: 1.13"""
_deprecation_msg_m_a_r_f("annotations")
return self._annotations
@annotations.setter
def annotations(self, v):
""".. deprecated:: 1.13"""
_deprecation_msg_m_a_r_f("annotations")
self._series.extend(_create_generic_data_series(annotations=v))
self._annotations = v
@property
def rectangles(self):
""".. deprecated:: 1.13"""
_deprecation_msg_m_a_r_f("rectangles")
return self._rectangles
@rectangles.setter
def rectangles(self, v):
""".. deprecated:: 1.13"""
_deprecation_msg_m_a_r_f("rectangles")
self._series.extend(_create_generic_data_series(rectangles=v))
self._rectangles = v
@property
def fill(self):
""".. deprecated:: 1.13"""
_deprecation_msg_m_a_r_f("fill")
return self._fill
@fill.setter
def fill(self, v):
""".. deprecated:: 1.13"""
_deprecation_msg_m_a_r_f("fill")
self._series.extend(_create_generic_data_series(fill=v))
self._fill = v

View File

@ -0,0 +1,5 @@
from sympy.plotting.backends.matplotlibbackend.matplotlib import (
MatplotlibBackend, _matplotlib_list
)
__all__ = ["MatplotlibBackend", "_matplotlib_list"]

View File

@ -0,0 +1,318 @@
from collections.abc import Callable
from sympy.core.basic import Basic
from sympy.external import import_module
import sympy.plotting.backends.base_backend as base_backend
from sympy.printing.latex import latex
# N.B.
# When changing the minimum module version for matplotlib, please change
# the same in the `SymPyDocTestFinder`` in `sympy/testing/runtests.py`
def _str_or_latex(label):
if isinstance(label, Basic):
return latex(label, mode='inline')
return str(label)
def _matplotlib_list(interval_list):
"""
Returns lists for matplotlib ``fill`` command from a list of bounding
rectangular intervals
"""
xlist = []
ylist = []
if len(interval_list):
for intervals in interval_list:
intervalx = intervals[0]
intervaly = intervals[1]
xlist.extend([intervalx.start, intervalx.start,
intervalx.end, intervalx.end, None])
ylist.extend([intervaly.start, intervaly.end,
intervaly.end, intervaly.start, None])
else:
#XXX Ugly hack. Matplotlib does not accept empty lists for ``fill``
xlist.extend((None, None, None, None))
ylist.extend((None, None, None, None))
return xlist, ylist
# Don't have to check for the success of importing matplotlib in each case;
# we will only be using this backend if we can successfully import matploblib
class MatplotlibBackend(base_backend.Plot):
""" This class implements the functionalities to use Matplotlib with SymPy
plotting functions.
"""
def __init__(self, *series, **kwargs):
super().__init__(*series, **kwargs)
self.matplotlib = import_module('matplotlib',
import_kwargs={'fromlist': ['pyplot', 'cm', 'collections']},
min_module_version='1.1.0', catch=(RuntimeError,))
self.plt = self.matplotlib.pyplot
self.cm = self.matplotlib.cm
self.LineCollection = self.matplotlib.collections.LineCollection
self.aspect = kwargs.get('aspect_ratio', 'auto')
if self.aspect != 'auto':
self.aspect = float(self.aspect[1]) / self.aspect[0]
# PlotGrid can provide its figure and axes to be populated with
# the data from the series.
self._plotgrid_fig = kwargs.pop("fig", None)
self._plotgrid_ax = kwargs.pop("ax", None)
def _create_figure(self):
def set_spines(ax):
ax.spines['left'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
if self._plotgrid_fig is not None:
self.fig = self._plotgrid_fig
self.ax = self._plotgrid_ax
if not any(s.is_3D for s in self._series):
set_spines(self.ax)
else:
self.fig = self.plt.figure(figsize=self.size)
if any(s.is_3D for s in self._series):
self.ax = self.fig.add_subplot(1, 1, 1, projection="3d")
else:
self.ax = self.fig.add_subplot(1, 1, 1)
set_spines(self.ax)
@staticmethod
def get_segments(x, y, z=None):
""" Convert two list of coordinates to a list of segments to be used
with Matplotlib's :external:class:`~matplotlib.collections.LineCollection`.
Parameters
==========
x : list
List of x-coordinates
y : list
List of y-coordinates
z : list
List of z-coordinates for a 3D line.
"""
np = import_module('numpy')
if z is not None:
dim = 3
points = (x, y, z)
else:
dim = 2
points = (x, y)
points = np.ma.array(points).T.reshape(-1, 1, dim)
return np.ma.concatenate([points[:-1], points[1:]], axis=1)
def _process_series(self, series, ax):
np = import_module('numpy')
mpl_toolkits = import_module(
'mpl_toolkits', import_kwargs={'fromlist': ['mplot3d']})
# XXX Workaround for matplotlib issue
# https://github.com/matplotlib/matplotlib/issues/17130
xlims, ylims, zlims = [], [], []
for s in series:
# Create the collections
if s.is_2Dline:
if s.is_parametric:
x, y, param = s.get_data()
else:
x, y = s.get_data()
if (isinstance(s.line_color, (int, float)) or
callable(s.line_color)):
segments = self.get_segments(x, y)
collection = self.LineCollection(segments)
collection.set_array(s.get_color_array())
ax.add_collection(collection)
else:
lbl = _str_or_latex(s.label)
line, = ax.plot(x, y, label=lbl, color=s.line_color)
elif s.is_contour:
ax.contour(*s.get_data())
elif s.is_3Dline:
x, y, z, param = s.get_data()
if (isinstance(s.line_color, (int, float)) or
callable(s.line_color)):
art3d = mpl_toolkits.mplot3d.art3d
segments = self.get_segments(x, y, z)
collection = art3d.Line3DCollection(segments)
collection.set_array(s.get_color_array())
ax.add_collection(collection)
else:
lbl = _str_or_latex(s.label)
ax.plot(x, y, z, label=lbl, color=s.line_color)
xlims.append(s._xlim)
ylims.append(s._ylim)
zlims.append(s._zlim)
elif s.is_3Dsurface:
if s.is_parametric:
x, y, z, u, v = s.get_data()
else:
x, y, z = s.get_data()
collection = ax.plot_surface(x, y, z,
cmap=getattr(self.cm, 'viridis', self.cm.jet),
rstride=1, cstride=1, linewidth=0.1)
if isinstance(s.surface_color, (float, int, Callable)):
color_array = s.get_color_array()
color_array = color_array.reshape(color_array.size)
collection.set_array(color_array)
else:
collection.set_color(s.surface_color)
xlims.append(s._xlim)
ylims.append(s._ylim)
zlims.append(s._zlim)
elif s.is_implicit:
points = s.get_data()
if len(points) == 2:
# interval math plotting
x, y = _matplotlib_list(points[0])
ax.fill(x, y, facecolor=s.line_color, edgecolor='None')
else:
# use contourf or contour depending on whether it is
# an inequality or equality.
# XXX: ``contour`` plots multiple lines. Should be fixed.
ListedColormap = self.matplotlib.colors.ListedColormap
colormap = ListedColormap(["white", s.line_color])
xarray, yarray, zarray, plot_type = points
if plot_type == 'contour':
ax.contour(xarray, yarray, zarray, cmap=colormap)
else:
ax.contourf(xarray, yarray, zarray, cmap=colormap)
elif s.is_generic:
if s.type == "markers":
# s.rendering_kw["color"] = s.line_color
ax.plot(*s.args, **s.rendering_kw)
elif s.type == "annotations":
ax.annotate(*s.args, **s.rendering_kw)
elif s.type == "fill":
# s.rendering_kw["color"] = s.line_color
ax.fill_between(*s.args, **s.rendering_kw)
elif s.type == "rectangles":
# s.rendering_kw["color"] = s.line_color
ax.add_patch(
self.matplotlib.patches.Rectangle(
*s.args, **s.rendering_kw))
else:
raise NotImplementedError(
'{} is not supported in the SymPy plotting module '
'with matplotlib backend. Please report this issue.'
.format(ax))
Axes3D = mpl_toolkits.mplot3d.Axes3D
if not isinstance(ax, Axes3D):
ax.autoscale_view(
scalex=ax.get_autoscalex_on(),
scaley=ax.get_autoscaley_on())
else:
# XXX Workaround for matplotlib issue
# https://github.com/matplotlib/matplotlib/issues/17130
if xlims:
xlims = np.array(xlims)
xlim = (np.amin(xlims[:, 0]), np.amax(xlims[:, 1]))
ax.set_xlim(xlim)
else:
ax.set_xlim([0, 1])
if ylims:
ylims = np.array(ylims)
ylim = (np.amin(ylims[:, 0]), np.amax(ylims[:, 1]))
ax.set_ylim(ylim)
else:
ax.set_ylim([0, 1])
if zlims:
zlims = np.array(zlims)
zlim = (np.amin(zlims[:, 0]), np.amax(zlims[:, 1]))
ax.set_zlim(zlim)
else:
ax.set_zlim([0, 1])
# Set global options.
# TODO The 3D stuff
# XXX The order of those is important.
if self.xscale and not isinstance(ax, Axes3D):
ax.set_xscale(self.xscale)
if self.yscale and not isinstance(ax, Axes3D):
ax.set_yscale(self.yscale)
if not isinstance(ax, Axes3D) or self.matplotlib.__version__ >= '1.2.0': # XXX in the distant future remove this check
ax.set_autoscale_on(self.autoscale)
if self.axis_center:
val = self.axis_center
if isinstance(ax, Axes3D):
pass
elif val == 'center':
ax.spines['left'].set_position('center')
ax.spines['bottom'].set_position('center')
elif val == 'auto':
xl, xh = ax.get_xlim()
yl, yh = ax.get_ylim()
pos_left = ('data', 0) if xl*xh <= 0 else 'center'
pos_bottom = ('data', 0) if yl*yh <= 0 else 'center'
ax.spines['left'].set_position(pos_left)
ax.spines['bottom'].set_position(pos_bottom)
else:
ax.spines['left'].set_position(('data', val[0]))
ax.spines['bottom'].set_position(('data', val[1]))
if not self.axis:
ax.set_axis_off()
if self.legend:
if ax.legend():
ax.legend_.set_visible(self.legend)
if self.margin:
ax.set_xmargin(self.margin)
ax.set_ymargin(self.margin)
if self.title:
ax.set_title(self.title)
if self.xlabel:
xlbl = _str_or_latex(self.xlabel)
ax.set_xlabel(xlbl, position=(1, 0))
if self.ylabel:
ylbl = _str_or_latex(self.ylabel)
ax.set_ylabel(ylbl, position=(0, 1))
if isinstance(ax, Axes3D) and self.zlabel:
zlbl = _str_or_latex(self.zlabel)
ax.set_zlabel(zlbl, position=(0, 1))
# xlim and ylim should always be set at last so that plot limits
# doesn't get altered during the process.
if self.xlim:
ax.set_xlim(self.xlim)
if self.ylim:
ax.set_ylim(self.ylim)
self.ax.set_aspect(self.aspect)
def process_series(self):
"""
Iterates over every ``Plot`` object and further calls
_process_series()
"""
self._create_figure()
self._process_series(self._series, self.ax)
def show(self):
self.process_series()
#TODO after fixing https://github.com/ipython/ipython/issues/1255
# you can uncomment the next line and remove the pyplot.show() call
#self.fig.show()
if base_backend._show:
self.fig.tight_layout()
self.plt.show()
else:
self.close()
def save(self, path):
self.process_series()
self.fig.savefig(path)
def close(self):
self.plt.close(self.fig)

View File

@ -0,0 +1,3 @@
from sympy.plotting.backends.textbackend.text import TextBackend
__all__ = ["TextBackend"]

View File

@ -0,0 +1,24 @@
import sympy.plotting.backends.base_backend as base_backend
from sympy.plotting.series import LineOver1DRangeSeries
from sympy.plotting.textplot import textplot
class TextBackend(base_backend.Plot):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def show(self):
if not base_backend._show:
return
if len(self._series) != 1:
raise ValueError(
'The TextBackend supports only one graph per Plot.')
elif not isinstance(self._series[0], LineOver1DRangeSeries):
raise ValueError(
'The TextBackend supports only expressions over a 1D range')
else:
ser = self._series[0]
textplot(ser.expr, ser.start, ser.end)
def close(self):
pass