123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637 |
- """Plotting module for SymPy.
- A plot is represented by the ``Plot`` class that contains a reference to the
- backend and a list of the data series to be plotted. The data series are
- instances of classes meant to simplify getting points and meshes from SymPy
- expressions. ``plot_backends`` is a dictionary with all the backends.
- This module gives only the essential. For all the fancy stuff use directly
- the backend. You can get the backend wrapper for every plot from the
- ``_backend`` attribute. Moreover the data series classes have various useful
- methods like ``get_points``, ``get_meshes``, etc, that may
- be useful if you wish to use another plotting library.
- Especially if you need publication ready graphs and this module is not enough
- for you - just get the ``_backend`` attribute and add whatever you want
- directly to it. In the case of matplotlib (the common way to graph data in
- python) just copy ``_backend.fig`` which is the figure and ``_backend.ax``
- which is the axis and work on them as you would on any other matplotlib object.
- Simplicity of code takes much greater importance than performance. Do not use it
- if you care at all about performance. A new backend instance is initialized
- every time you call ``show()`` and the old one is left to the garbage collector.
- """
- from collections.abc import Callable
- from sympy.core.basic import Basic
- from sympy.core.containers import Tuple
- from sympy.core.expr import Expr
- from sympy.core.function import arity, Function
- from sympy.core.symbol import (Dummy, Symbol)
- from sympy.core.sympify import sympify
- from sympy.external import import_module
- from sympy.printing.latex import latex
- from sympy.utilities.exceptions import sympy_deprecation_warning
- from sympy.utilities.iterables import is_sequence
- from .experimental_lambdify import (vectorized_lambdify, lambdify)
- # N.B.
- # When changing the minimum module version for matplotlib, please change
- # the same in the `SymPyDocTestFinder`` in `sympy/testing/runtests.py`
- # Backend specific imports - textplot
- from sympy.plotting.textplot import textplot
- # 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 _str_or_latex(label):
- if isinstance(label, Basic):
- return latex(label, mode='inline')
- return str(label)
- ##############################################################################
- # The public interface
- ##############################################################################
- class Plot:
- """The central class of the plotting module.
- Explanation
- ===========
- 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.
- """
- 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):
- super().__init__()
- # 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)
- # The backend type. On every show() a new backend instance is created
- # in self._backend which is tightly coupled to the Plot instance
- # (thanks to the parent attribute of the backend).
- if isinstance(backend, str):
- self.backend = plot_backends[backend]
- elif (type(backend) == type) and issubclass(backend, BaseBackend):
- self.backend = backend
- else:
- raise TypeError(
- "backend must be either a string or a subclass of BaseBackend")
- 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)
- def show(self):
- # TODO move this to the backend (also for save)
- if hasattr(self, '_backend'):
- self._backend.close()
- self._backend = self.backend(self)
- self._backend.show()
- def save(self, path):
- if hasattr(self, '_backend'):
- self._backend.close()
- self._backend = self.backend(self)
- self._backend.save(path)
- 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')
- class PlotGrid:
- """This class helps to plot subplots from already created SymPy plots
- in a single figure.
- Examples
- ========
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> from sympy import symbols
- >>> from sympy.plotting import plot, plot3d, PlotGrid
- >>> x, y = symbols('x, y')
- >>> p1 = plot(x, x**2, x**3, (x, -5, 5))
- >>> p2 = plot((x**2, (x, -6, 6)), (x, (x, -5, 5)))
- >>> p3 = plot(x**3, (x, -5, 5))
- >>> p4 = plot3d(x*y, (x, -5, 5), (y, -5, 5))
- Plotting vertically in a single line:
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> PlotGrid(2, 1, p1, p2)
- PlotGrid object containing:
- Plot[0]:Plot object containing:
- [0]: cartesian line: x for x over (-5.0, 5.0)
- [1]: cartesian line: x**2 for x over (-5.0, 5.0)
- [2]: cartesian line: x**3 for x over (-5.0, 5.0)
- Plot[1]:Plot object containing:
- [0]: cartesian line: x**2 for x over (-6.0, 6.0)
- [1]: cartesian line: x for x over (-5.0, 5.0)
- Plotting horizontally in a single line:
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> PlotGrid(1, 3, p2, p3, p4)
- PlotGrid object containing:
- Plot[0]:Plot object containing:
- [0]: cartesian line: x**2 for x over (-6.0, 6.0)
- [1]: cartesian line: x for x over (-5.0, 5.0)
- Plot[1]:Plot object containing:
- [0]: cartesian line: x**3 for x over (-5.0, 5.0)
- Plot[2]:Plot object containing:
- [0]: cartesian surface: x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0)
- Plotting in a grid form:
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> PlotGrid(2, 2, p1, p2, p3, p4)
- PlotGrid object containing:
- Plot[0]:Plot object containing:
- [0]: cartesian line: x for x over (-5.0, 5.0)
- [1]: cartesian line: x**2 for x over (-5.0, 5.0)
- [2]: cartesian line: x**3 for x over (-5.0, 5.0)
- Plot[1]:Plot object containing:
- [0]: cartesian line: x**2 for x over (-6.0, 6.0)
- [1]: cartesian line: x for x over (-5.0, 5.0)
- Plot[2]:Plot object containing:
- [0]: cartesian line: x**3 for x over (-5.0, 5.0)
- Plot[3]:Plot object containing:
- [0]: cartesian surface: x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0)
- """
- def __init__(self, nrows, ncolumns, *args, show=True, size=None, **kwargs):
- """
- Parameters
- ==========
- nrows :
- The number of rows that should be in the grid of the
- required subplot.
- ncolumns :
- The number of columns that should be in the grid
- of the required subplot.
- nrows and ncolumns together define the required grid.
- Arguments
- =========
- A list of predefined plot objects entered in a row-wise sequence
- i.e. plot objects which are to be in the top row of the required
- grid are written first, then the second row objects and so on
- Keyword arguments
- =================
- show : Boolean
- The default value is set to ``True``. Set show to ``False`` and
- the function will not display the subplot. The returned instance
- of the ``PlotGrid`` class can then be used to save or display the
- plot by calling the ``save()`` and ``show()`` methods
- respectively.
- size : (float, float), optional
- A tuple in the form (width, height) in inches to specify the size of
- the overall figure. The default value is set to ``None``, meaning
- the size will be set by the default backend.
- """
- self.nrows = nrows
- self.ncolumns = ncolumns
- self._series = []
- self.args = args
- for arg in args:
- self._series.append(arg._series)
- self.backend = DefaultBackend
- self.size = size
- if show:
- self.show()
- def show(self):
- if hasattr(self, '_backend'):
- self._backend.close()
- self._backend = self.backend(self)
- self._backend.show()
- def save(self, path):
- if hasattr(self, '_backend'):
- self._backend.close()
- self._backend = self.backend(self)
- self._backend.save(path)
- def __str__(self):
- plot_strs = [('Plot[%d]:' % i) + str(plot)
- for i, plot in enumerate(self.args)]
- return 'PlotGrid object containing:\n' + '\n'.join(plot_strs)
- ##############################################################################
- # Data Series
- ##############################################################################
- #TODO more general way to calculate aesthetics (see get_color_array)
- ### The base class for all series
- class BaseSeries:
- """Base class for the data objects containing stuff to be plotted.
- Explanation
- ===========
- The backend should check if it supports the data series that is given.
- (e.g. TextBackend supports only LineOver1DRangeSeries).
- It is the backend responsibility to know how to use the class of
- data series that is given.
- Some data series classes are grouped (using a class attribute like is_2Dline)
- according to the api they present (based only on convention). The backend is
- not obliged to use that api (e.g. LineOver1DRangeSeries belongs to the
- is_2Dline group and presents the get_points method, but the
- TextBackend does not use the get_points method).
- """
- # Some flags follow. The rationale for using flags instead of checking base
- # classes is that setting multiple flags is simpler than multiple
- # inheritance.
- is_2Dline = False
- # Some of the backends expect:
- # - get_points returning 1D np.arrays list_x, list_y
- # - get_color_array returning 1D np.array (done in Line2DBaseSeries)
- # with the colors calculated at the points from get_points
- is_3Dline = False
- # Some of the backends expect:
- # - get_points returning 1D np.arrays list_x, list_y, list_y
- # - get_color_array returning 1D np.array (done in Line2DBaseSeries)
- # with the colors calculated at the points from get_points
- is_3Dsurface = False
- # Some of the backends expect:
- # - get_meshes returning mesh_x, mesh_y, mesh_z (2D np.arrays)
- # - get_points an alias for get_meshes
- is_contour = False
- # Some of the backends expect:
- # - get_meshes returning mesh_x, mesh_y, mesh_z (2D np.arrays)
- # - get_points an alias for get_meshes
- is_implicit = False
- # Some of the backends expect:
- # - get_meshes returning mesh_x (1D array), mesh_y(1D array,
- # mesh_z (2D np.arrays)
- # - get_points an alias for get_meshes
- # Different from is_contour as the colormap in backend will be
- # different
- is_parametric = False
- # The calculation of aesthetics expects:
- # - get_parameter_points returning one or two np.arrays (1D or 2D)
- # used for calculation aesthetics
- def __init__(self):
- super().__init__()
- @property
- def is_3D(self):
- flags3D = [
- self.is_3Dline,
- self.is_3Dsurface
- ]
- return any(flags3D)
- @property
- def is_line(self):
- flagslines = [
- self.is_2Dline,
- self.is_3Dline
- ]
- return any(flagslines)
- ### 2D lines
- class Line2DBaseSeries(BaseSeries):
- """A base class for 2D lines.
- - adding the label, steps and only_integers options
- - making is_2Dline true
- - defining get_segments and get_color_array
- """
- is_2Dline = True
- _dim = 2
- def __init__(self):
- super().__init__()
- self.label = None
- self.steps = False
- self.only_integers = False
- self.line_color = None
- def get_data(self):
- """ Return lists of coordinates for plotting the line.
- Returns
- =======
- x : list
- List of x-coordinates
- y : list
- List of y-coordinates
- z : list
- List of z-coordinates in case of Parametric3DLineSeries
- """
- np = import_module('numpy')
- points = self.get_points()
- if self.steps is True:
- if len(points) == 2:
- x = np.array((points[0], points[0])).T.flatten()[1:]
- y = np.array((points[1], points[1])).T.flatten()[:-1]
- points = (x, y)
- else:
- x = np.repeat(points[0], 3)[2:]
- y = np.repeat(points[1], 3)[:-2]
- z = np.repeat(points[2], 3)[1:-1]
- points = (x, y, z)
- return points
- def get_segments(self):
- sympy_deprecation_warning(
- """
- The Line2DBaseSeries.get_segments() method is deprecated.
- Instead, use the MatplotlibBackend.get_segments() method, or use
- The get_points() or get_data() methods.
- """,
- deprecated_since_version="1.9",
- active_deprecations_target="deprecated-get-segments")
- np = import_module('numpy')
- points = type(self).get_data(self)
- points = np.ma.array(points).T.reshape(-1, 1, self._dim)
- return np.ma.concatenate([points[:-1], points[1:]], axis=1)
- def get_color_array(self):
- np = import_module('numpy')
- c = self.line_color
- if hasattr(c, '__call__'):
- f = np.vectorize(c)
- nargs = arity(c)
- if nargs == 1 and self.is_parametric:
- x = self.get_parameter_points()
- return f(centers_of_segments(x))
- else:
- variables = list(map(centers_of_segments, self.get_points()))
- if nargs == 1:
- return f(variables[0])
- elif nargs == 2:
- return f(*variables[:2])
- else: # only if the line is 3D (otherwise raises an error)
- return f(*variables)
- else:
- return c*np.ones(self.nb_of_points)
- class List2DSeries(Line2DBaseSeries):
- """Representation for a line consisting of list of points."""
- def __init__(self, list_x, list_y):
- np = import_module('numpy')
- super().__init__()
- self.list_x = np.array(list_x)
- self.list_y = np.array(list_y)
- self.label = 'list'
- def __str__(self):
- return 'list plot'
- def get_points(self):
- return (self.list_x, self.list_y)
- class LineOver1DRangeSeries(Line2DBaseSeries):
- """Representation for a line consisting of a SymPy expression over a range."""
- def __init__(self, expr, var_start_end, **kwargs):
- super().__init__()
- self.expr = sympify(expr)
- self.label = kwargs.get('label', None) or self.expr
- self.var = sympify(var_start_end[0])
- self.start = float(var_start_end[1])
- self.end = float(var_start_end[2])
- self.nb_of_points = kwargs.get('nb_of_points', 300)
- self.adaptive = kwargs.get('adaptive', True)
- self.depth = kwargs.get('depth', 12)
- self.line_color = kwargs.get('line_color', None)
- self.xscale = kwargs.get('xscale', 'linear')
- def __str__(self):
- return 'cartesian line: %s for %s over %s' % (
- str(self.expr), str(self.var), str((self.start, self.end)))
- def get_points(self):
- """ Return lists of coordinates for plotting. Depending on the
- ``adaptive`` option, this function will either use an adaptive algorithm
- or it will uniformly sample the expression over the provided range.
- Returns
- =======
- x : list
- List of x-coordinates
- y : list
- List of y-coordinates
- Explanation
- ===========
- The adaptive sampling is done by recursively checking if three
- points are almost collinear. If they are not collinear, then more
- points are added between those points.
- References
- ==========
- .. [1] Adaptive polygonal approximation of parametric curves,
- Luiz Henrique de Figueiredo.
- """
- if self.only_integers or not self.adaptive:
- return self._uniform_sampling()
- else:
- f = lambdify([self.var], self.expr)
- x_coords = []
- y_coords = []
- np = import_module('numpy')
- def sample(p, q, depth):
- """ Samples recursively if three points are almost collinear.
- For depth < 6, points are added irrespective of whether they
- satisfy the collinearity condition or not. The maximum depth
- allowed is 12.
- """
- # Randomly sample to avoid aliasing.
- random = 0.45 + np.random.rand() * 0.1
- if self.xscale == 'log':
- xnew = 10**(np.log10(p[0]) + random * (np.log10(q[0]) -
- np.log10(p[0])))
- else:
- xnew = p[0] + random * (q[0] - p[0])
- ynew = f(xnew)
- new_point = np.array([xnew, ynew])
- # Maximum depth
- if depth > self.depth:
- x_coords.append(q[0])
- y_coords.append(q[1])
- # Sample irrespective of whether the line is flat till the
- # depth of 6. We are not using linspace to avoid aliasing.
- elif depth < 6:
- sample(p, new_point, depth + 1)
- sample(new_point, q, depth + 1)
- # Sample ten points if complex values are encountered
- # at both ends. If there is a real value in between, then
- # sample those points further.
- elif p[1] is None and q[1] is None:
- if self.xscale == 'log':
- xarray = np.logspace(p[0], q[0], 10)
- else:
- xarray = np.linspace(p[0], q[0], 10)
- yarray = list(map(f, xarray))
- if not all(y is None for y in yarray):
- for i in range(len(yarray) - 1):
- if not (yarray[i] is None and yarray[i + 1] is None):
- sample([xarray[i], yarray[i]],
- [xarray[i + 1], yarray[i + 1]], depth + 1)
- # Sample further if one of the end points in None (i.e. a
- # complex value) or the three points are not almost collinear.
- elif (p[1] is None or q[1] is None or new_point[1] is None
- or not flat(p, new_point, q)):
- sample(p, new_point, depth + 1)
- sample(new_point, q, depth + 1)
- else:
- x_coords.append(q[0])
- y_coords.append(q[1])
- f_start = f(self.start)
- f_end = f(self.end)
- x_coords.append(self.start)
- y_coords.append(f_start)
- sample(np.array([self.start, f_start]),
- np.array([self.end, f_end]), 0)
- return (x_coords, y_coords)
- def _uniform_sampling(self):
- np = import_module('numpy')
- if self.only_integers is True:
- if self.xscale == 'log':
- list_x = np.logspace(int(self.start), int(self.end),
- num=int(self.end) - int(self.start) + 1)
- else:
- list_x = np.linspace(int(self.start), int(self.end),
- num=int(self.end) - int(self.start) + 1)
- else:
- if self.xscale == 'log':
- list_x = np.logspace(self.start, self.end, num=self.nb_of_points)
- else:
- list_x = np.linspace(self.start, self.end, num=self.nb_of_points)
- f = vectorized_lambdify([self.var], self.expr)
- list_y = f(list_x)
- return (list_x, list_y)
- class Parametric2DLineSeries(Line2DBaseSeries):
- """Representation for a line consisting of two parametric SymPy expressions
- over a range."""
- is_parametric = True
- def __init__(self, expr_x, expr_y, var_start_end, **kwargs):
- super().__init__()
- self.expr_x = sympify(expr_x)
- self.expr_y = sympify(expr_y)
- self.label = kwargs.get('label', None) or \
- Tuple(self.expr_x, self.expr_y)
- self.var = sympify(var_start_end[0])
- self.start = float(var_start_end[1])
- self.end = float(var_start_end[2])
- self.nb_of_points = kwargs.get('nb_of_points', 300)
- self.adaptive = kwargs.get('adaptive', True)
- self.depth = kwargs.get('depth', 12)
- self.line_color = kwargs.get('line_color', None)
- def __str__(self):
- return 'parametric cartesian line: (%s, %s) for %s over %s' % (
- str(self.expr_x), str(self.expr_y), str(self.var),
- str((self.start, self.end)))
- def get_parameter_points(self):
- np = import_module('numpy')
- return np.linspace(self.start, self.end, num=self.nb_of_points)
- def _uniform_sampling(self):
- param = self.get_parameter_points()
- fx = vectorized_lambdify([self.var], self.expr_x)
- fy = vectorized_lambdify([self.var], self.expr_y)
- list_x = fx(param)
- list_y = fy(param)
- return (list_x, list_y)
- def get_points(self):
- """ Return lists of coordinates for plotting. Depending on the
- ``adaptive`` option, this function will either use an adaptive algorithm
- or it will uniformly sample the expression over the provided range.
- Returns
- =======
- x : list
- List of x-coordinates
- y : list
- List of y-coordinates
- Explanation
- ===========
- The adaptive sampling is done by recursively checking if three
- points are almost collinear. If they are not collinear, then more
- points are added between those points.
- References
- ==========
- .. [1] Adaptive polygonal approximation of parametric curves,
- Luiz Henrique de Figueiredo.
- """
- if not self.adaptive:
- return self._uniform_sampling()
- f_x = lambdify([self.var], self.expr_x)
- f_y = lambdify([self.var], self.expr_y)
- x_coords = []
- y_coords = []
- def sample(param_p, param_q, p, q, depth):
- """ Samples recursively if three points are almost collinear.
- For depth < 6, points are added irrespective of whether they
- satisfy the collinearity condition or not. The maximum depth
- allowed is 12.
- """
- # Randomly sample to avoid aliasing.
- np = import_module('numpy')
- random = 0.45 + np.random.rand() * 0.1
- param_new = param_p + random * (param_q - param_p)
- xnew = f_x(param_new)
- ynew = f_y(param_new)
- new_point = np.array([xnew, ynew])
- # Maximum depth
- if depth > self.depth:
- x_coords.append(q[0])
- y_coords.append(q[1])
- # Sample irrespective of whether the line is flat till the
- # depth of 6. We are not using linspace to avoid aliasing.
- elif depth < 6:
- sample(param_p, param_new, p, new_point, depth + 1)
- sample(param_new, param_q, new_point, q, depth + 1)
- # Sample ten points if complex values are encountered
- # at both ends. If there is a real value in between, then
- # sample those points further.
- elif ((p[0] is None and q[1] is None) or
- (p[1] is None and q[1] is None)):
- param_array = np.linspace(param_p, param_q, 10)
- x_array = list(map(f_x, param_array))
- y_array = list(map(f_y, param_array))
- if not all(x is None and y is None
- for x, y in zip(x_array, y_array)):
- for i in range(len(y_array) - 1):
- if ((x_array[i] is not None and y_array[i] is not None) or
- (x_array[i + 1] is not None and y_array[i + 1] is not None)):
- point_a = [x_array[i], y_array[i]]
- point_b = [x_array[i + 1], y_array[i + 1]]
- sample(param_array[i], param_array[i], point_a,
- point_b, depth + 1)
- # Sample further if one of the end points in None (i.e. a complex
- # value) or the three points are not almost collinear.
- elif (p[0] is None or p[1] is None
- or q[1] is None or q[0] is None
- or not flat(p, new_point, q)):
- sample(param_p, param_new, p, new_point, depth + 1)
- sample(param_new, param_q, new_point, q, depth + 1)
- else:
- x_coords.append(q[0])
- y_coords.append(q[1])
- f_start_x = f_x(self.start)
- f_start_y = f_y(self.start)
- start = [f_start_x, f_start_y]
- f_end_x = f_x(self.end)
- f_end_y = f_y(self.end)
- end = [f_end_x, f_end_y]
- x_coords.append(f_start_x)
- y_coords.append(f_start_y)
- sample(self.start, self.end, start, end, 0)
- return x_coords, y_coords
- ### 3D lines
- class Line3DBaseSeries(Line2DBaseSeries):
- """A base class for 3D lines.
- Most of the stuff is derived from Line2DBaseSeries."""
- is_2Dline = False
- is_3Dline = True
- _dim = 3
- def __init__(self):
- super().__init__()
- class Parametric3DLineSeries(Line3DBaseSeries):
- """Representation for a 3D line consisting of three parametric SymPy
- expressions and a range."""
- is_parametric = True
- def __init__(self, expr_x, expr_y, expr_z, var_start_end, **kwargs):
- super().__init__()
- self.expr_x = sympify(expr_x)
- self.expr_y = sympify(expr_y)
- self.expr_z = sympify(expr_z)
- self.label = kwargs.get('label', None) or \
- Tuple(self.expr_x, self.expr_y)
- self.var = sympify(var_start_end[0])
- self.start = float(var_start_end[1])
- self.end = float(var_start_end[2])
- self.nb_of_points = kwargs.get('nb_of_points', 300)
- self.line_color = kwargs.get('line_color', None)
- self._xlim = None
- self._ylim = None
- self._zlim = None
- def __str__(self):
- return '3D parametric cartesian line: (%s, %s, %s) for %s over %s' % (
- str(self.expr_x), str(self.expr_y), str(self.expr_z),
- str(self.var), str((self.start, self.end)))
- def get_parameter_points(self):
- np = import_module('numpy')
- return np.linspace(self.start, self.end, num=self.nb_of_points)
- def get_points(self):
- np = import_module('numpy')
- param = self.get_parameter_points()
- fx = vectorized_lambdify([self.var], self.expr_x)
- fy = vectorized_lambdify([self.var], self.expr_y)
- fz = vectorized_lambdify([self.var], self.expr_z)
- list_x = fx(param)
- list_y = fy(param)
- list_z = fz(param)
- list_x = np.array(list_x, dtype=np.float64)
- list_y = np.array(list_y, dtype=np.float64)
- list_z = np.array(list_z, dtype=np.float64)
- list_x = np.ma.masked_invalid(list_x)
- list_y = np.ma.masked_invalid(list_y)
- list_z = np.ma.masked_invalid(list_z)
- self._xlim = (np.amin(list_x), np.amax(list_x))
- self._ylim = (np.amin(list_y), np.amax(list_y))
- self._zlim = (np.amin(list_z), np.amax(list_z))
- return list_x, list_y, list_z
- ### Surfaces
- class SurfaceBaseSeries(BaseSeries):
- """A base class for 3D surfaces."""
- is_3Dsurface = True
- def __init__(self):
- super().__init__()
- self.surface_color = None
- def get_color_array(self):
- np = import_module('numpy')
- c = self.surface_color
- if isinstance(c, Callable):
- f = np.vectorize(c)
- nargs = arity(c)
- if self.is_parametric:
- variables = list(map(centers_of_faces, self.get_parameter_meshes()))
- if nargs == 1:
- return f(variables[0])
- elif nargs == 2:
- return f(*variables)
- variables = list(map(centers_of_faces, self.get_meshes()))
- if nargs == 1:
- return f(variables[0])
- elif nargs == 2:
- return f(*variables[:2])
- else:
- return f(*variables)
- else:
- if isinstance(self, SurfaceOver2DRangeSeries):
- return c*np.ones(min(self.nb_of_points_x, self.nb_of_points_y))
- else:
- return c*np.ones(min(self.nb_of_points_u, self.nb_of_points_v))
- class SurfaceOver2DRangeSeries(SurfaceBaseSeries):
- """Representation for a 3D surface consisting of a SymPy expression and 2D
- range."""
- def __init__(self, expr, var_start_end_x, var_start_end_y, **kwargs):
- super().__init__()
- self.expr = sympify(expr)
- self.var_x = sympify(var_start_end_x[0])
- self.start_x = float(var_start_end_x[1])
- self.end_x = float(var_start_end_x[2])
- self.var_y = sympify(var_start_end_y[0])
- self.start_y = float(var_start_end_y[1])
- self.end_y = float(var_start_end_y[2])
- self.nb_of_points_x = kwargs.get('nb_of_points_x', 50)
- self.nb_of_points_y = kwargs.get('nb_of_points_y', 50)
- self.surface_color = kwargs.get('surface_color', None)
- self._xlim = (self.start_x, self.end_x)
- self._ylim = (self.start_y, self.end_y)
- def __str__(self):
- return ('cartesian surface: %s for'
- ' %s over %s and %s over %s') % (
- str(self.expr),
- str(self.var_x),
- str((self.start_x, self.end_x)),
- str(self.var_y),
- str((self.start_y, self.end_y)))
- def get_meshes(self):
- np = import_module('numpy')
- mesh_x, mesh_y = np.meshgrid(np.linspace(self.start_x, self.end_x,
- num=self.nb_of_points_x),
- np.linspace(self.start_y, self.end_y,
- num=self.nb_of_points_y))
- f = vectorized_lambdify((self.var_x, self.var_y), self.expr)
- mesh_z = f(mesh_x, mesh_y)
- mesh_z = np.array(mesh_z, dtype=np.float64)
- mesh_z = np.ma.masked_invalid(mesh_z)
- self._zlim = (np.amin(mesh_z), np.amax(mesh_z))
- return mesh_x, mesh_y, mesh_z
- class ParametricSurfaceSeries(SurfaceBaseSeries):
- """Representation for a 3D surface consisting of three parametric SymPy
- expressions and a range."""
- is_parametric = True
- def __init__(
- self, expr_x, expr_y, expr_z, var_start_end_u, var_start_end_v,
- **kwargs):
- super().__init__()
- self.expr_x = sympify(expr_x)
- self.expr_y = sympify(expr_y)
- self.expr_z = sympify(expr_z)
- self.var_u = sympify(var_start_end_u[0])
- self.start_u = float(var_start_end_u[1])
- self.end_u = float(var_start_end_u[2])
- self.var_v = sympify(var_start_end_v[0])
- self.start_v = float(var_start_end_v[1])
- self.end_v = float(var_start_end_v[2])
- self.nb_of_points_u = kwargs.get('nb_of_points_u', 50)
- self.nb_of_points_v = kwargs.get('nb_of_points_v', 50)
- self.surface_color = kwargs.get('surface_color', None)
- def __str__(self):
- return ('parametric cartesian surface: (%s, %s, %s) for'
- ' %s over %s and %s over %s') % (
- str(self.expr_x),
- str(self.expr_y),
- str(self.expr_z),
- str(self.var_u),
- str((self.start_u, self.end_u)),
- str(self.var_v),
- str((self.start_v, self.end_v)))
- def get_parameter_meshes(self):
- np = import_module('numpy')
- return np.meshgrid(np.linspace(self.start_u, self.end_u,
- num=self.nb_of_points_u),
- np.linspace(self.start_v, self.end_v,
- num=self.nb_of_points_v))
- def get_meshes(self):
- np = import_module('numpy')
- mesh_u, mesh_v = self.get_parameter_meshes()
- fx = vectorized_lambdify((self.var_u, self.var_v), self.expr_x)
- fy = vectorized_lambdify((self.var_u, self.var_v), self.expr_y)
- fz = vectorized_lambdify((self.var_u, self.var_v), self.expr_z)
- mesh_x = fx(mesh_u, mesh_v)
- mesh_y = fy(mesh_u, mesh_v)
- mesh_z = fz(mesh_u, mesh_v)
- mesh_x = np.array(mesh_x, dtype=np.float64)
- mesh_y = np.array(mesh_y, dtype=np.float64)
- mesh_z = np.array(mesh_z, dtype=np.float64)
- mesh_x = np.ma.masked_invalid(mesh_x)
- mesh_y = np.ma.masked_invalid(mesh_y)
- mesh_z = np.ma.masked_invalid(mesh_z)
- self._xlim = (np.amin(mesh_x), np.amax(mesh_x))
- self._ylim = (np.amin(mesh_y), np.amax(mesh_y))
- self._zlim = (np.amin(mesh_z), np.amax(mesh_z))
- return mesh_x, mesh_y, mesh_z
- ### Contours
- class ContourSeries(BaseSeries):
- """Representation for a contour plot."""
- # The code is mostly repetition of SurfaceOver2DRange.
- # Presently used in contour_plot function
- is_contour = True
- def __init__(self, expr, var_start_end_x, var_start_end_y):
- super().__init__()
- self.nb_of_points_x = 50
- self.nb_of_points_y = 50
- self.expr = sympify(expr)
- self.var_x = sympify(var_start_end_x[0])
- self.start_x = float(var_start_end_x[1])
- self.end_x = float(var_start_end_x[2])
- self.var_y = sympify(var_start_end_y[0])
- self.start_y = float(var_start_end_y[1])
- self.end_y = float(var_start_end_y[2])
- self.get_points = self.get_meshes
- self._xlim = (self.start_x, self.end_x)
- self._ylim = (self.start_y, self.end_y)
- def __str__(self):
- return ('contour: %s for '
- '%s over %s and %s over %s') % (
- str(self.expr),
- str(self.var_x),
- str((self.start_x, self.end_x)),
- str(self.var_y),
- str((self.start_y, self.end_y)))
- def get_meshes(self):
- np = import_module('numpy')
- mesh_x, mesh_y = np.meshgrid(np.linspace(self.start_x, self.end_x,
- num=self.nb_of_points_x),
- np.linspace(self.start_y, self.end_y,
- num=self.nb_of_points_y))
- f = vectorized_lambdify((self.var_x, self.var_y), self.expr)
- return (mesh_x, mesh_y, f(mesh_x, mesh_y))
- ##############################################################################
- # Backends
- ##############################################################################
- class BaseBackend:
- """Base class for all backends. A backend represents the plotting library,
- which implements the necessary functionalities in order to use SymPy
- plotting functions.
- 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:`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 :class:`~.Plot` object is instantiated, which stores the list of
- series and the main attributes of the plot (e.g. axis labels, title, ...).
- 3. When the ``show`` command is executed, a new backend is instantiated,
- which loops through each series object to generate and plot the
- numerical data. 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:`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.
- Methods
- =======
- 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).
- See also
- ========
- MatplotlibBackend
- """
- def __init__(self, parent):
- super().__init__()
- self.parent = parent
- def show(self):
- raise NotImplementedError
- def save(self, path):
- raise NotImplementedError
- def close(self):
- raise NotImplementedError
- # 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(BaseBackend):
- """ This class implements the functionalities to use Matplotlib with SymPy
- plotting functions.
- """
- def __init__(self, parent):
- super().__init__(parent)
- 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
- aspect = getattr(self.parent, 'aspect_ratio', 'auto')
- if aspect != 'auto':
- aspect = float(aspect[1]) / aspect[0]
- if isinstance(self.parent, Plot):
- nrows, ncolumns = 1, 1
- series_list = [self.parent._series]
- elif isinstance(self.parent, PlotGrid):
- nrows, ncolumns = self.parent.nrows, self.parent.ncolumns
- series_list = self.parent._series
- self.ax = []
- self.fig = self.plt.figure(figsize=parent.size)
- for i, series in enumerate(series_list):
- are_3D = [s.is_3D for s in series]
- if any(are_3D) and not all(are_3D):
- raise ValueError('The matplotlib backend cannot mix 2D and 3D.')
- elif all(are_3D):
- # mpl_toolkits.mplot3d is necessary for
- # projection='3d'
- mpl_toolkits = import_module('mpl_toolkits', # noqa
- import_kwargs={'fromlist': ['mplot3d']})
- self.ax.append(self.fig.add_subplot(nrows, ncolumns, i + 1, projection='3d', aspect=aspect))
- elif not any(are_3D):
- self.ax.append(self.fig.add_subplot(nrows, ncolumns, i + 1, aspect=aspect))
- self.ax[i].spines['left'].set_position('zero')
- self.ax[i].spines['right'].set_color('none')
- self.ax[i].spines['bottom'].set_position('zero')
- self.ax[i].spines['top'].set_color('none')
- self.ax[i].xaxis.set_ticks_position('bottom')
- self.ax[i].yaxis.set_ticks_position('left')
- @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, parent):
- 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:
- 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_meshes())
- elif s.is_3Dline:
- x, y, z = 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:
- x, y, z = s.get_meshes()
- 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_raster()
- 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)
- 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 parent.xscale and not isinstance(ax, Axes3D):
- ax.set_xscale(parent.xscale)
- if parent.yscale and not isinstance(ax, Axes3D):
- ax.set_yscale(parent.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(parent.autoscale)
- if parent.axis_center:
- val = parent.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 parent.axis:
- ax.set_axis_off()
- if parent.legend:
- if ax.legend():
- ax.legend_.set_visible(parent.legend)
- if parent.margin:
- ax.set_xmargin(parent.margin)
- ax.set_ymargin(parent.margin)
- if parent.title:
- ax.set_title(parent.title)
- if parent.xlabel:
- xlbl = _str_or_latex(parent.xlabel)
- ax.set_xlabel(xlbl, position=(1, 0))
- if parent.ylabel:
- ylbl = _str_or_latex(parent.ylabel)
- ax.set_ylabel(ylbl, position=(0, 1))
- if isinstance(ax, Axes3D) and parent.zlabel:
- zlbl = _str_or_latex(parent.zlabel)
- ax.set_zlabel(zlbl, position=(0, 1))
- if parent.annotations:
- for a in parent.annotations:
- ax.annotate(**a)
- if parent.markers:
- for marker in parent.markers:
- # make a copy of the marker dictionary
- # so that it doesn't get altered
- m = marker.copy()
- args = m.pop('args')
- ax.plot(*args, **m)
- if parent.rectangles:
- for r in parent.rectangles:
- rect = self.matplotlib.patches.Rectangle(**r)
- ax.add_patch(rect)
- if parent.fill:
- ax.fill_between(**parent.fill)
- # xlim and ylim should always be set at last so that plot limits
- # doesn't get altered during the process.
- if parent.xlim:
- ax.set_xlim(parent.xlim)
- if parent.ylim:
- ax.set_ylim(parent.ylim)
- def process_series(self):
- """
- Iterates over every ``Plot`` object and further calls
- _process_series()
- """
- parent = self.parent
- if isinstance(parent, Plot):
- series_list = [parent._series]
- else:
- series_list = parent._series
- for i, (series, ax) in enumerate(zip(series_list, self.ax)):
- if isinstance(self.parent, PlotGrid):
- parent = self.parent.args[i]
- self._process_series(series, ax, parent)
- 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 _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)
- class TextBackend(BaseBackend):
- def __init__(self, parent):
- super().__init__(parent)
- def show(self):
- if not _show:
- return
- if len(self.parent._series) != 1:
- raise ValueError(
- 'The TextBackend supports only one graph per Plot.')
- elif not isinstance(self.parent._series[0], LineOver1DRangeSeries):
- raise ValueError(
- 'The TextBackend supports only expressions over a 1D range')
- else:
- ser = self.parent._series[0]
- textplot(ser.expr, ser.start, ser.end)
- def close(self):
- pass
- class DefaultBackend(BaseBackend):
- def __new__(cls, parent):
- matplotlib = import_module('matplotlib', min_module_version='1.1.0', catch=(RuntimeError,))
- if matplotlib:
- return MatplotlibBackend(parent)
- else:
- return TextBackend(parent)
- plot_backends = {
- 'matplotlib': MatplotlibBackend,
- 'text': TextBackend,
- 'default': DefaultBackend
- }
- ##############################################################################
- # Finding the centers of line segments or mesh faces
- ##############################################################################
- def centers_of_segments(array):
- np = import_module('numpy')
- return np.mean(np.vstack((array[:-1], array[1:])), 0)
- def centers_of_faces(array):
- np = import_module('numpy')
- return np.mean(np.dstack((array[:-1, :-1],
- array[1:, :-1],
- array[:-1, 1:],
- array[:-1, :-1],
- )), 2)
- def flat(x, y, z, eps=1e-3):
- """Checks whether three points are almost collinear"""
- np = import_module('numpy')
- # Workaround plotting piecewise (#8577):
- # workaround for `lambdify` in `.experimental_lambdify` fails
- # to return numerical values in some cases. Lower-level fix
- # in `lambdify` is possible.
- vector_a = (x - y).astype(np.float64)
- vector_b = (z - y).astype(np.float64)
- dot_product = np.dot(vector_a, vector_b)
- vector_a_norm = np.linalg.norm(vector_a)
- vector_b_norm = np.linalg.norm(vector_b)
- cos_theta = dot_product / (vector_a_norm * vector_b_norm)
- return abs(cos_theta + 1) < eps
- 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
- ####New API for plotting module ####
- # TODO: Add color arrays for plots.
- # TODO: Add more plotting options for 3d plots.
- # TODO: Adaptive sampling for 3D plots.
- def plot(*args, show=True, **kwargs):
- """Plots a function of a single variable as a curve.
- Parameters
- ==========
- args :
- The first argument is the expression representing the function
- of single variable to be plotted.
- The last argument is a 3-tuple denoting the range of the free
- variable. e.g. ``(x, 0, 5)``
- Typical usage examples are in the following:
- - Plotting a single expression with a single range.
- ``plot(expr, range, **kwargs)``
- - Plotting a single expression with the default range (-10, 10).
- ``plot(expr, **kwargs)``
- - Plotting multiple expressions with a single range.
- ``plot(expr1, expr2, ..., range, **kwargs)``
- - Plotting multiple expressions with multiple ranges.
- ``plot((expr1, range1), (expr2, range2), ..., **kwargs)``
- It is best practice to specify range explicitly because default
- range may change in the future if a more advanced default range
- detection algorithm is implemented.
- show : bool, optional
- The default value is set to ``True``. Set show to ``False`` and
- the function will not display the plot. The returned instance of
- the ``Plot`` class can then be used to save or display the plot
- by calling the ``save()`` and ``show()`` methods respectively.
- line_color : string, or float, or function, optional
- Specifies the color for the plot.
- See ``Plot`` to see how to set color for the plots.
- Note that by setting ``line_color``, it would be applied simultaneously
- to all the series.
- title : str, optional
- Title of the plot. It is set to the latex representation of
- the expression, if the plot has only one expression.
- label : str, optional
- The label of the expression in the plot. It will be used when
- called with ``legend``. Default is the name of the expression.
- e.g. ``sin(x)``
- xlabel : str or expression, optional
- Label for the x-axis.
- ylabel : str or expression, optional
- Label for the y-axis.
- xscale : 'linear' or 'log', optional
- Sets the scaling of the x-axis.
- yscale : 'linear' or 'log', optional
- Sets the scaling of the y-axis.
- axis_center : (float, float), optional
- Tuple of two floats denoting the coordinates of the center or
- {'center', 'auto'}
- xlim : (float, float), optional
- Denotes the x-axis limits, ``(min, max)```.
- ylim : (float, float), optional
- Denotes the y-axis limits, ``(min, max)```.
- annotations : list, optional
- A list of dictionaries specifying the type of annotation
- required. The keys in the dictionary should be equivalent
- to the arguments of the :external:mod:`matplotlib`'s
- :external:meth:`~matplotlib.axes.Axes.annotate` method.
- markers : list, optional
- A list of dictionaries specifying the type the markers required.
- The keys in the dictionary should be equivalent to the arguments
- of the :external:mod:`matplotlib`'s :external:func:`~matplotlib.pyplot.plot()` function
- along with the marker related keyworded arguments.
- rectangles : list, optional
- A list of dictionaries specifying the dimensions of the
- rectangles to be plotted. The keys in the dictionary should be
- equivalent to the arguments of the :external:mod:`matplotlib`'s
- :external:class:`~matplotlib.patches.Rectangle` class.
- fill : dict, optional
- A dictionary specifying the type of color filling required in
- the plot. The keys in the dictionary should be equivalent to the
- arguments of the :external:mod:`matplotlib`'s
- :external:meth:`~matplotlib.axes.Axes.fill_between` method.
- adaptive : bool, optional
- The default value is set to ``True``. Set adaptive to ``False``
- and specify ``nb_of_points`` if uniform sampling is required.
- The plotting uses an adaptive algorithm which samples
- recursively to accurately plot. The adaptive algorithm uses a
- random point near the midpoint of two points that has to be
- further sampled. Hence the same plots can appear slightly
- different.
- depth : int, optional
- Recursion depth of the adaptive algorithm. A depth of value
- `n` samples a maximum of `2^{n}` points.
- If the ``adaptive`` flag is set to ``False``, this will be
- ignored.
- nb_of_points : int, optional
- Used when the ``adaptive`` is set to ``False``. The function
- is uniformly sampled at ``nb_of_points`` number of points.
- If the ``adaptive`` flag is set to ``True``, this will be
- ignored.
- size : (float, float), optional
- A tuple in the form (width, height) in inches to specify the size of
- the overall figure. The default value is set to ``None``, meaning
- the size will be set by the default backend.
- Examples
- ========
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> from sympy import symbols
- >>> from sympy.plotting import plot
- >>> x = symbols('x')
- Single Plot
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot(x**2, (x, -5, 5))
- Plot object containing:
- [0]: cartesian line: x**2 for x over (-5.0, 5.0)
- Multiple plots with single range.
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot(x, x**2, x**3, (x, -5, 5))
- Plot object containing:
- [0]: cartesian line: x for x over (-5.0, 5.0)
- [1]: cartesian line: x**2 for x over (-5.0, 5.0)
- [2]: cartesian line: x**3 for x over (-5.0, 5.0)
- Multiple plots with different ranges.
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot((x**2, (x, -6, 6)), (x, (x, -5, 5)))
- Plot object containing:
- [0]: cartesian line: x**2 for x over (-6.0, 6.0)
- [1]: cartesian line: x for x over (-5.0, 5.0)
- No adaptive sampling.
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot(x**2, adaptive=False, nb_of_points=400)
- Plot object containing:
- [0]: cartesian line: x**2 for x over (-10.0, 10.0)
- See Also
- ========
- Plot, LineOver1DRangeSeries
- """
- args = list(map(sympify, args))
- free = set()
- for a in args:
- if isinstance(a, Expr):
- free |= a.free_symbols
- if len(free) > 1:
- raise ValueError(
- 'The same variable should be used in all '
- 'univariate expressions being plotted.')
- x = free.pop() if free else Symbol('x')
- kwargs.setdefault('xlabel', x)
- kwargs.setdefault('ylabel', Function('f')(x))
- series = []
- plot_expr = check_arguments(args, 1, 1)
- series = [LineOver1DRangeSeries(*arg, **kwargs) for arg in plot_expr]
- plots = Plot(*series, **kwargs)
- if show:
- plots.show()
- return plots
- def plot_parametric(*args, show=True, **kwargs):
- """
- Plots a 2D parametric curve.
- Parameters
- ==========
- args
- Common specifications are:
- - Plotting a single parametric curve with a range
- ``plot_parametric((expr_x, expr_y), range)``
- - Plotting multiple parametric curves with the same range
- ``plot_parametric((expr_x, expr_y), ..., range)``
- - Plotting multiple parametric curves with different ranges
- ``plot_parametric((expr_x, expr_y, range), ...)``
- ``expr_x`` is the expression representing $x$ component of the
- parametric function.
- ``expr_y`` is the expression representing $y$ component of the
- parametric function.
- ``range`` is a 3-tuple denoting the parameter symbol, start and
- stop. For example, ``(u, 0, 5)``.
- If the range is not specified, then a default range of (-10, 10)
- is used.
- However, if the arguments are specified as
- ``(expr_x, expr_y, range), ...``, you must specify the ranges
- for each expressions manually.
- Default range may change in the future if a more advanced
- algorithm is implemented.
- adaptive : bool, optional
- Specifies whether to use the adaptive sampling or not.
- The default value is set to ``True``. Set adaptive to ``False``
- and specify ``nb_of_points`` if uniform sampling is required.
- depth : int, optional
- The recursion depth of the adaptive algorithm. A depth of
- value $n$ samples a maximum of $2^n$ points.
- nb_of_points : int, optional
- Used when the ``adaptive`` flag is set to ``False``.
- Specifies the number of the points used for the uniform
- sampling.
- line_color : string, or float, or function, optional
- Specifies the color for the plot.
- See ``Plot`` to see how to set color for the plots.
- Note that by setting ``line_color``, it would be applied simultaneously
- to all the series.
- label : str, optional
- The label of the expression in the plot. It will be used when
- called with ``legend``. Default is the name of the expression.
- e.g. ``sin(x)``
- xlabel : str, optional
- Label for the x-axis.
- ylabel : str, optional
- Label for the y-axis.
- xscale : 'linear' or 'log', optional
- Sets the scaling of the x-axis.
- yscale : 'linear' or 'log', optional
- Sets the scaling of the y-axis.
- axis_center : (float, float), optional
- Tuple of two floats denoting the coordinates of the center or
- {'center', 'auto'}
- xlim : (float, float), optional
- Denotes the x-axis limits, ``(min, max)```.
- ylim : (float, float), optional
- Denotes the y-axis limits, ``(min, max)```.
- size : (float, float), optional
- A tuple in the form (width, height) in inches to specify the size of
- the overall figure. The default value is set to ``None``, meaning
- the size will be set by the default backend.
- Examples
- ========
- .. plot::
- :context: reset
- :format: doctest
- :include-source: True
- >>> from sympy import plot_parametric, symbols, cos, sin
- >>> u = symbols('u')
- A parametric plot with a single expression:
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot_parametric((cos(u), sin(u)), (u, -5, 5))
- Plot object containing:
- [0]: parametric cartesian line: (cos(u), sin(u)) for u over (-5.0, 5.0)
- A parametric plot with multiple expressions with the same range:
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot_parametric((cos(u), sin(u)), (u, cos(u)), (u, -10, 10))
- Plot object containing:
- [0]: parametric cartesian line: (cos(u), sin(u)) for u over (-10.0, 10.0)
- [1]: parametric cartesian line: (u, cos(u)) for u over (-10.0, 10.0)
- A parametric plot with multiple expressions with different ranges
- for each curve:
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot_parametric((cos(u), sin(u), (u, -5, 5)),
- ... (cos(u), u, (u, -5, 5)))
- Plot object containing:
- [0]: parametric cartesian line: (cos(u), sin(u)) for u over (-5.0, 5.0)
- [1]: parametric cartesian line: (cos(u), u) for u over (-5.0, 5.0)
- Notes
- =====
- The plotting uses an adaptive algorithm which samples recursively to
- accurately plot the curve. The adaptive algorithm uses a random point
- near the midpoint of two points that has to be further sampled.
- Hence, repeating the same plot command can give slightly different
- results because of the random sampling.
- If there are multiple plots, then the same optional arguments are
- applied to all the plots drawn in the same canvas. If you want to
- set these options separately, you can index the returned ``Plot``
- object and set it.
- For example, when you specify ``line_color`` once, it would be
- applied simultaneously to both series.
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> from sympy import pi
- >>> expr1 = (u, cos(2*pi*u)/2 + 1/2)
- >>> expr2 = (u, sin(2*pi*u)/2 + 1/2)
- >>> p = plot_parametric(expr1, expr2, (u, 0, 1), line_color='blue')
- If you want to specify the line color for the specific series, you
- should index each item and apply the property manually.
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> p[0].line_color = 'red'
- >>> p.show()
- See Also
- ========
- Plot, Parametric2DLineSeries
- """
- args = list(map(sympify, args))
- series = []
- plot_expr = check_arguments(args, 2, 1)
- series = [Parametric2DLineSeries(*arg, **kwargs) for arg in plot_expr]
- plots = Plot(*series, **kwargs)
- if show:
- plots.show()
- return plots
- def plot3d_parametric_line(*args, show=True, **kwargs):
- """
- Plots a 3D parametric line plot.
- Usage
- =====
- Single plot:
- ``plot3d_parametric_line(expr_x, expr_y, expr_z, range, **kwargs)``
- If the range is not specified, then a default range of (-10, 10) is used.
- Multiple plots.
- ``plot3d_parametric_line((expr_x, expr_y, expr_z, range), ..., **kwargs)``
- Ranges have to be specified for every expression.
- Default range may change in the future if a more advanced default range
- detection algorithm is implemented.
- Arguments
- =========
- expr_x : Expression representing the function along x.
- expr_y : Expression representing the function along y.
- expr_z : Expression representing the function along z.
- range : (:class:`~.Symbol`, float, float)
- A 3-tuple denoting the range of the parameter variable, e.g., (u, 0, 5).
- Keyword Arguments
- =================
- Arguments for ``Parametric3DLineSeries`` class.
- nb_of_points : The range is uniformly sampled at ``nb_of_points``
- number of points.
- Aesthetics:
- line_color : string, or float, or function, optional
- Specifies the color for the plot.
- See ``Plot`` to see how to set color for the plots.
- Note that by setting ``line_color``, it would be applied simultaneously
- to all the series.
- label : str
- The label to the plot. It will be used when called with ``legend=True``
- to denote the function with the given label in the plot.
- If there are multiple plots, then the same series arguments are applied to
- all the plots. If you want to set these options separately, you can index
- the returned ``Plot`` object and set it.
- Arguments for ``Plot`` class.
- title : str
- Title of the plot.
- size : (float, float), optional
- A tuple in the form (width, height) in inches to specify the size of
- the overall figure. The default value is set to ``None``, meaning
- the size will be set by the default backend.
- Examples
- ========
- .. plot::
- :context: reset
- :format: doctest
- :include-source: True
- >>> from sympy import symbols, cos, sin
- >>> from sympy.plotting import plot3d_parametric_line
- >>> u = symbols('u')
- Single plot.
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot3d_parametric_line(cos(u), sin(u), u, (u, -5, 5))
- Plot object containing:
- [0]: 3D parametric cartesian line: (cos(u), sin(u), u) for u over (-5.0, 5.0)
- Multiple plots.
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot3d_parametric_line((cos(u), sin(u), u, (u, -5, 5)),
- ... (sin(u), u**2, u, (u, -5, 5)))
- Plot object containing:
- [0]: 3D parametric cartesian line: (cos(u), sin(u), u) for u over (-5.0, 5.0)
- [1]: 3D parametric cartesian line: (sin(u), u**2, u) for u over (-5.0, 5.0)
- See Also
- ========
- Plot, Parametric3DLineSeries
- """
- args = list(map(sympify, args))
- series = []
- plot_expr = check_arguments(args, 3, 1)
- series = [Parametric3DLineSeries(*arg, **kwargs) for arg in plot_expr]
- kwargs.setdefault("xlabel", "x")
- kwargs.setdefault("ylabel", "y")
- kwargs.setdefault("zlabel", "z")
- plots = Plot(*series, **kwargs)
- if show:
- plots.show()
- return plots
- def plot3d(*args, show=True, **kwargs):
- """
- Plots a 3D surface plot.
- Usage
- =====
- Single plot
- ``plot3d(expr, range_x, range_y, **kwargs)``
- If the ranges are not specified, then a default range of (-10, 10) is used.
- Multiple plot with the same range.
- ``plot3d(expr1, expr2, range_x, range_y, **kwargs)``
- If the ranges are not specified, then a default range of (-10, 10) is used.
- Multiple plots with different ranges.
- ``plot3d((expr1, range_x, range_y), (expr2, range_x, range_y), ..., **kwargs)``
- Ranges have to be specified for every expression.
- Default range may change in the future if a more advanced default range
- detection algorithm is implemented.
- Arguments
- =========
- expr : Expression representing the function along x.
- range_x : (:class:`~.Symbol`, float, float)
- A 3-tuple denoting the range of the x variable, e.g. (x, 0, 5).
- range_y : (:class:`~.Symbol`, float, float)
- A 3-tuple denoting the range of the y variable, e.g. (y, 0, 5).
- Keyword Arguments
- =================
- Arguments for ``SurfaceOver2DRangeSeries`` class:
- nb_of_points_x : int
- The x range is sampled uniformly at ``nb_of_points_x`` of points.
- nb_of_points_y : int
- The y range is sampled uniformly at ``nb_of_points_y`` of points.
- Aesthetics:
- surface_color : Function which returns a float
- Specifies the color for the surface of the plot.
- See :class:`~.Plot` for more details.
- If there are multiple plots, then the same series arguments are applied to
- all the plots. If you want to set these options separately, you can index
- the returned ``Plot`` object and set it.
- Arguments for ``Plot`` class:
- title : str
- Title of the plot.
- size : (float, float), optional
- A tuple in the form (width, height) in inches to specify the size of the
- overall figure. The default value is set to ``None``, meaning the size will
- be set by the default backend.
- Examples
- ========
- .. plot::
- :context: reset
- :format: doctest
- :include-source: True
- >>> from sympy import symbols
- >>> from sympy.plotting import plot3d
- >>> x, y = symbols('x y')
- Single plot
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot3d(x*y, (x, -5, 5), (y, -5, 5))
- Plot object containing:
- [0]: cartesian surface: x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0)
- Multiple plots with same range
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot3d(x*y, -x*y, (x, -5, 5), (y, -5, 5))
- Plot object containing:
- [0]: cartesian surface: x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0)
- [1]: cartesian surface: -x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0)
- Multiple plots with different ranges.
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot3d((x**2 + y**2, (x, -5, 5), (y, -5, 5)),
- ... (x*y, (x, -3, 3), (y, -3, 3)))
- Plot object containing:
- [0]: cartesian surface: x**2 + y**2 for x over (-5.0, 5.0) and y over (-5.0, 5.0)
- [1]: cartesian surface: x*y for x over (-3.0, 3.0) and y over (-3.0, 3.0)
- See Also
- ========
- Plot, SurfaceOver2DRangeSeries
- """
- args = list(map(sympify, args))
- series = []
- plot_expr = check_arguments(args, 1, 2)
- series = [SurfaceOver2DRangeSeries(*arg, **kwargs) for arg in plot_expr]
- kwargs.setdefault("xlabel", series[0].var_x)
- kwargs.setdefault("ylabel", series[0].var_y)
- kwargs.setdefault("zlabel", Function('f')(series[0].var_x, series[0].var_y))
- plots = Plot(*series, **kwargs)
- if show:
- plots.show()
- return plots
- def plot3d_parametric_surface(*args, show=True, **kwargs):
- """
- Plots a 3D parametric surface plot.
- Explanation
- ===========
- Single plot.
- ``plot3d_parametric_surface(expr_x, expr_y, expr_z, range_u, range_v, **kwargs)``
- If the ranges is not specified, then a default range of (-10, 10) is used.
- Multiple plots.
- ``plot3d_parametric_surface((expr_x, expr_y, expr_z, range_u, range_v), ..., **kwargs)``
- Ranges have to be specified for every expression.
- Default range may change in the future if a more advanced default range
- detection algorithm is implemented.
- Arguments
- =========
- expr_x : Expression representing the function along ``x``.
- expr_y : Expression representing the function along ``y``.
- expr_z : Expression representing the function along ``z``.
- range_u : (:class:`~.Symbol`, float, float)
- A 3-tuple denoting the range of the u variable, e.g. (u, 0, 5).
- range_v : (:class:`~.Symbol`, float, float)
- A 3-tuple denoting the range of the v variable, e.g. (v, 0, 5).
- Keyword Arguments
- =================
- Arguments for ``ParametricSurfaceSeries`` class:
- nb_of_points_u : int
- The ``u`` range is sampled uniformly at ``nb_of_points_v`` of points
- nb_of_points_y : int
- The ``v`` range is sampled uniformly at ``nb_of_points_y`` of points
- Aesthetics:
- surface_color : Function which returns a float
- Specifies the color for the surface of the plot. See
- :class:`~Plot` for more details.
- If there are multiple plots, then the same series arguments are applied for
- all the plots. If you want to set these options separately, you can index
- the returned ``Plot`` object and set it.
- Arguments for ``Plot`` class:
- title : str
- Title of the plot.
- size : (float, float), optional
- A tuple in the form (width, height) in inches to specify the size of the
- overall figure. The default value is set to ``None``, meaning the size will
- be set by the default backend.
- Examples
- ========
- .. plot::
- :context: reset
- :format: doctest
- :include-source: True
- >>> from sympy import symbols, cos, sin
- >>> from sympy.plotting import plot3d_parametric_surface
- >>> u, v = symbols('u v')
- Single plot.
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> plot3d_parametric_surface(cos(u + v), sin(u - v), u - v,
- ... (u, -5, 5), (v, -5, 5))
- Plot object containing:
- [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)
- See Also
- ========
- Plot, ParametricSurfaceSeries
- """
- args = list(map(sympify, args))
- series = []
- plot_expr = check_arguments(args, 3, 2)
- series = [ParametricSurfaceSeries(*arg, **kwargs) for arg in plot_expr]
- kwargs.setdefault("xlabel", "x")
- kwargs.setdefault("ylabel", "y")
- kwargs.setdefault("zlabel", "z")
- plots = Plot(*series, **kwargs)
- if show:
- plots.show()
- return plots
- def plot_contour(*args, show=True, **kwargs):
- """
- Draws contour plot of a function
- Usage
- =====
- Single plot
- ``plot_contour(expr, range_x, range_y, **kwargs)``
- If the ranges are not specified, then a default range of (-10, 10) is used.
- Multiple plot with the same range.
- ``plot_contour(expr1, expr2, range_x, range_y, **kwargs)``
- If the ranges are not specified, then a default range of (-10, 10) is used.
- Multiple plots with different ranges.
- ``plot_contour((expr1, range_x, range_y), (expr2, range_x, range_y), ..., **kwargs)``
- Ranges have to be specified for every expression.
- Default range may change in the future if a more advanced default range
- detection algorithm is implemented.
- Arguments
- =========
- expr : Expression representing the function along x.
- range_x : (:class:`Symbol`, float, float)
- A 3-tuple denoting the range of the x variable, e.g. (x, 0, 5).
- range_y : (:class:`Symbol`, float, float)
- A 3-tuple denoting the range of the y variable, e.g. (y, 0, 5).
- Keyword Arguments
- =================
- Arguments for ``ContourSeries`` class:
- nb_of_points_x : int
- The x range is sampled uniformly at ``nb_of_points_x`` of points.
- nb_of_points_y : int
- The y range is sampled uniformly at ``nb_of_points_y`` of points.
- Aesthetics:
- surface_color : Function which returns a float
- Specifies the color for the surface of the plot. See
- :class:`sympy.plotting.Plot` for more details.
- If there are multiple plots, then the same series arguments are applied to
- all the plots. If you want to set these options separately, you can index
- the returned ``Plot`` object and set it.
- Arguments for ``Plot`` class:
- title : str
- Title of the plot.
- size : (float, float), optional
- A tuple in the form (width, height) in inches to specify the size of
- the overall figure. The default value is set to ``None``, meaning
- the size will be set by the default backend.
- See Also
- ========
- Plot, ContourSeries
- """
- args = list(map(sympify, args))
- plot_expr = check_arguments(args, 1, 2)
- series = [ContourSeries(*arg) for arg in plot_expr]
- plot_contours = Plot(*series, **kwargs)
- if len(plot_expr[0].free_symbols) > 2:
- raise ValueError('Contour Plot cannot Plot for more than two variables.')
- if show:
- plot_contours.show()
- return plot_contours
- def check_arguments(args, expr_len, nb_of_free_symbols):
- """
- Checks the arguments and converts into tuples of the
- form (exprs, ranges).
- Examples
- ========
- .. plot::
- :context: reset
- :format: doctest
- :include-source: True
- >>> from sympy import cos, sin, symbols
- >>> from sympy.plotting.plot import check_arguments
- >>> x = symbols('x')
- >>> check_arguments([cos(x), sin(x)], 2, 1)
- [(cos(x), sin(x), (x, -10, 10))]
- >>> check_arguments([x, x**2], 1, 1)
- [(x, (x, -10, 10)), (x**2, (x, -10, 10))]
- """
- if not args:
- return []
- if expr_len > 1 and isinstance(args[0], Expr):
- # Multiple expressions same range.
- # The arguments are tuples when the expression length is
- # greater than 1.
- if len(args) < expr_len:
- raise ValueError("len(args) should not be less than expr_len")
- for i in range(len(args)):
- if isinstance(args[i], Tuple):
- break
- else:
- i = len(args) + 1
- exprs = Tuple(*args[:i])
- free_symbols = list(set().union(*[e.free_symbols for e in exprs]))
- if len(args) == expr_len + nb_of_free_symbols:
- #Ranges given
- plots = [exprs + Tuple(*args[expr_len:])]
- else:
- default_range = Tuple(-10, 10)
- ranges = []
- for symbol in free_symbols:
- ranges.append(Tuple(symbol) + default_range)
- for i in range(len(free_symbols) - nb_of_free_symbols):
- ranges.append(Tuple(Dummy()) + default_range)
- plots = [exprs + Tuple(*ranges)]
- return plots
- if isinstance(args[0], Expr) or (isinstance(args[0], Tuple) and
- len(args[0]) == expr_len and
- expr_len != 3):
- # Cannot handle expressions with number of expression = 3. It is
- # not possible to differentiate between expressions and ranges.
- #Series of plots with same range
- for i in range(len(args)):
- if isinstance(args[i], Tuple) and len(args[i]) != expr_len:
- break
- if not isinstance(args[i], Tuple):
- args[i] = Tuple(args[i])
- else:
- i = len(args) + 1
- exprs = args[:i]
- assert all(isinstance(e, Expr) for expr in exprs for e in expr)
- free_symbols = list(set().union(*[e.free_symbols for expr in exprs
- for e in expr]))
- if len(free_symbols) > nb_of_free_symbols:
- raise ValueError("The number of free_symbols in the expression "
- "is greater than %d" % nb_of_free_symbols)
- if len(args) == i + nb_of_free_symbols and isinstance(args[i], Tuple):
- ranges = Tuple(*list(args[
- i:i + nb_of_free_symbols]))
- plots = [expr + ranges for expr in exprs]
- return plots
- else:
- # Use default ranges.
- default_range = Tuple(-10, 10)
- ranges = []
- for symbol in free_symbols:
- ranges.append(Tuple(symbol) + default_range)
- for i in range(nb_of_free_symbols - len(free_symbols)):
- ranges.append(Tuple(Dummy()) + default_range)
- ranges = Tuple(*ranges)
- plots = [expr + ranges for expr in exprs]
- return plots
- elif isinstance(args[0], Tuple) and len(args[0]) == expr_len + nb_of_free_symbols:
- # Multiple plots with different ranges.
- for arg in args:
- for i in range(expr_len):
- if not isinstance(arg[i], Expr):
- raise ValueError("Expected an expression, given %s" %
- str(arg[i]))
- for i in range(nb_of_free_symbols):
- if not len(arg[i + expr_len]) == 3:
- raise ValueError("The ranges should be a tuple of "
- "length 3, got %s" % str(arg[i + expr_len]))
- return args
|