| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591 |
- ### The base class for all series
- from collections.abc import Callable
- from sympy.calculus.util import continuous_domain
- from sympy.concrete import Sum, Product
- from sympy.core.containers import Tuple
- from sympy.core.expr import Expr
- from sympy.core.function import arity
- from sympy.core.sorting import default_sort_key
- from sympy.core.symbol import Symbol
- from sympy.functions import atan2, zeta, frac, ceiling, floor, im
- from sympy.core.relational import (Equality, GreaterThan,
- LessThan, Relational, Ne)
- from sympy.core.sympify import sympify
- from sympy.external import import_module
- from sympy.logic.boolalg import BooleanFunction
- from sympy.plotting.utils import _get_free_symbols, extract_solution
- from sympy.printing.latex import latex
- from sympy.printing.pycode import PythonCodePrinter
- from sympy.printing.precedence import precedence
- from sympy.sets.sets import Set, Interval, Union
- from sympy.simplify.simplify import nsimplify
- from sympy.utilities.exceptions import sympy_deprecation_warning
- from sympy.utilities.lambdify import lambdify
- from .intervalmath import interval
- import warnings
- class IntervalMathPrinter(PythonCodePrinter):
- """A printer to be used inside `plot_implicit` when `adaptive=True`,
- in which case the interval arithmetic module is going to be used, which
- requires the following edits.
- """
- def _print_And(self, expr):
- PREC = precedence(expr)
- return " & ".join(self.parenthesize(a, PREC)
- for a in sorted(expr.args, key=default_sort_key))
- def _print_Or(self, expr):
- PREC = precedence(expr)
- return " | ".join(self.parenthesize(a, PREC)
- for a in sorted(expr.args, key=default_sort_key))
- def _uniform_eval(f1, f2, *args, modules=None,
- force_real_eval=False, has_sum=False):
- """
- Note: this is an experimental function, as such it is prone to changes.
- Please, do not use it in your code.
- """
- np = import_module('numpy')
- def wrapper_func(func, *args):
- try:
- return complex(func(*args))
- except (ZeroDivisionError, OverflowError):
- return complex(np.nan, np.nan)
- # NOTE: np.vectorize is much slower than numpy vectorized operations.
- # However, this modules must be able to evaluate functions also with
- # mpmath or sympy.
- wrapper_func = np.vectorize(wrapper_func, otypes=[complex])
- def _eval_with_sympy(err=None):
- if f2 is None:
- msg = "Impossible to evaluate the provided numerical function"
- if err is None:
- msg += "."
- else:
- msg += "because the following exception was raised:\n"
- "{}: {}".format(type(err).__name__, err)
- raise RuntimeError(msg)
- if err:
- warnings.warn(
- "The evaluation with %s failed.\n" % (
- "NumPy/SciPy" if not modules else modules) +
- "{}: {}\n".format(type(err).__name__, err) +
- "Trying to evaluate the expression with Sympy, but it might "
- "be a slow operation."
- )
- return wrapper_func(f2, *args)
- if modules == "sympy":
- return _eval_with_sympy()
- try:
- return wrapper_func(f1, *args)
- except Exception as err:
- return _eval_with_sympy(err)
- def _adaptive_eval(f, x):
- """Evaluate f(x) with an adaptive algorithm. Post-process the result.
- If a symbolic expression is evaluated with SymPy, it might returns
- another symbolic expression, containing additions, ...
- Force evaluation to a float.
- Parameters
- ==========
- f : callable
- x : float
- """
- np = import_module('numpy')
- y = f(x)
- if isinstance(y, Expr) and (not y.is_Number):
- y = y.evalf()
- y = complex(y)
- if y.imag > 1e-08:
- return np.nan
- return y.real
- def _get_wrapper_for_expr(ret):
- wrapper = "%s"
- if ret == "real":
- wrapper = "re(%s)"
- elif ret == "imag":
- wrapper = "im(%s)"
- elif ret == "abs":
- wrapper = "abs(%s)"
- elif ret == "arg":
- wrapper = "arg(%s)"
- return wrapper
- class BaseSeries:
- """Base class for the data objects containing stuff to be plotted.
- Notes
- =====
- 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).
- BaseSeries
- """
- # 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_interactive = False
- # An interactive series can update its data.
- is_parametric = False
- # The calculation of aesthetics expects:
- # - get_parameter_points returning one or two np.arrays (1D or 2D)
- # used for calculation aesthetics
- is_generic = False
- # Represent generic user-provided numerical data
- is_vector = False
- is_2Dvector = False
- is_3Dvector = False
- # Represents a 2D or 3D vector data series
- _N = 100
- # default number of discretization points for uniform sampling. Each
- # subclass can set its number.
- def __init__(self, *args, **kwargs):
- kwargs = _set_discretization_points(kwargs.copy(), type(self))
- # discretize the domain using only integer numbers
- self.only_integers = kwargs.get("only_integers", False)
- # represents the evaluation modules to be used by lambdify
- self.modules = kwargs.get("modules", None)
- # plot functions might create data series that might not be useful to
- # be shown on the legend, for example wireframe lines on 3D plots.
- self.show_in_legend = kwargs.get("show_in_legend", True)
- # line and surface series can show data with a colormap, hence a
- # colorbar is essential to understand the data. However, sometime it
- # is useful to hide it on series-by-series base. The following keyword
- # controls whether the series should show a colorbar or not.
- self.colorbar = kwargs.get("colorbar", True)
- # Some series might use a colormap as default coloring. Setting this
- # attribute to False will inform the backends to use solid color.
- self.use_cm = kwargs.get("use_cm", False)
- # If True, the backend will attempt to render it on a polar-projection
- # axis, or using a polar discretization if a 3D plot is requested
- self.is_polar = kwargs.get("is_polar", kwargs.get("polar", False))
- # If True, the rendering will use points, not lines.
- self.is_point = kwargs.get("is_point", kwargs.get("point", False))
- # some backend is able to render latex, other needs standard text
- self._label = self._latex_label = ""
- self._ranges = []
- self._n = [
- int(kwargs.get("n1", self._N)),
- int(kwargs.get("n2", self._N)),
- int(kwargs.get("n3", self._N))
- ]
- self._scales = [
- kwargs.get("xscale", "linear"),
- kwargs.get("yscale", "linear"),
- kwargs.get("zscale", "linear")
- ]
- # enable interactive widget plots
- self._params = kwargs.get("params", {})
- if not isinstance(self._params, dict):
- raise TypeError("`params` must be a dictionary mapping symbols "
- "to numeric values.")
- if len(self._params) > 0:
- self.is_interactive = True
- # contains keyword arguments that will be passed to the rendering
- # function of the chosen plotting library
- self.rendering_kw = kwargs.get("rendering_kw", {})
- # numerical transformation functions to be applied to the output data:
- # x, y, z (coordinates), p (parameter on parametric plots)
- self._tx = kwargs.get("tx", None)
- self._ty = kwargs.get("ty", None)
- self._tz = kwargs.get("tz", None)
- self._tp = kwargs.get("tp", None)
- if not all(callable(t) or (t is None) for t in
- [self._tx, self._ty, self._tz, self._tp]):
- raise TypeError("`tx`, `ty`, `tz`, `tp` must be functions.")
- # list of numerical functions representing the expressions to evaluate
- self._functions = []
- # signature for the numerical functions
- self._signature = []
- # some expressions don't like to be evaluated over complex data.
- # if that's the case, set this to True
- self._force_real_eval = kwargs.get("force_real_eval", None)
- # this attribute will eventually contain a dictionary with the
- # discretized ranges
- self._discretized_domain = None
- # whether the series contains any interactive range, which is a range
- # where the minimum and maximum values can be changed with an
- # interactive widget
- self._interactive_ranges = False
- # NOTE: consider a generic summation, for example:
- # s = Sum(cos(pi * x), (x, 1, y))
- # This gets lambdified to something:
- # sum(cos(pi*x) for x in range(1, y+1))
- # Hence, y needs to be an integer, otherwise it raises:
- # TypeError: 'complex' object cannot be interpreted as an integer
- # This list will contains symbols that are upper bound to summations
- # or products
- self._needs_to_be_int = []
- # a color function will be responsible to set the line/surface color
- # according to some logic. Each data series will et an appropriate
- # default value.
- self.color_func = None
- # NOTE: color_func usually receives numerical functions that are going
- # to be evaluated over the coordinates of the computed points (or the
- # discretized meshes).
- # However, if an expression is given to color_func, then it will be
- # lambdified with symbols in self._signature, and it will be evaluated
- # with the same data used to evaluate the plotted expression.
- self._eval_color_func_with_signature = False
- def _block_lambda_functions(self, *exprs):
- """Some data series can be used to plot numerical functions, others
- cannot. Execute this method inside the `__init__` to prevent the
- processing of numerical functions.
- """
- if any(callable(e) for e in exprs):
- raise TypeError(type(self).__name__ + " requires a symbolic "
- "expression.")
- def _check_fs(self):
- """ Checks if there are enough parameters and free symbols.
- """
- exprs, ranges = self.expr, self.ranges
- params, label = self.params, self.label
- exprs = exprs if hasattr(exprs, "__iter__") else [exprs]
- if any(callable(e) for e in exprs):
- return
- # from the expression's free symbols, remove the ones used in
- # the parameters and the ranges
- fs = _get_free_symbols(exprs)
- fs = fs.difference(params.keys())
- if ranges is not None:
- fs = fs.difference([r[0] for r in ranges])
- if len(fs) > 0:
- raise ValueError(
- "Incompatible expression and parameters.\n"
- + "Expression: {}\n".format(
- (exprs, ranges, label) if ranges is not None else (exprs, label))
- + "params: {}\n".format(params)
- + "Specify what these symbols represent: {}\n".format(fs)
- + "Are they ranges or parameters?"
- )
- # verify that all symbols are known (they either represent plotting
- # ranges or parameters)
- range_symbols = [r[0] for r in ranges]
- for r in ranges:
- fs = set().union(*[e.free_symbols for e in r[1:]])
- if any(t in fs for t in range_symbols):
- # ranges can't depend on each other, for example this are
- # not allowed:
- # (x, 0, y), (y, 0, 3)
- # (x, 0, y), (y, x + 2, 3)
- raise ValueError("Range symbols can't be included into "
- "minimum and maximum of a range. "
- "Received range: %s" % str(r))
- if len(fs) > 0:
- self._interactive_ranges = True
- remaining_fs = fs.difference(params.keys())
- if len(remaining_fs) > 0:
- raise ValueError(
- "Unknown symbols found in plotting range: %s. " % (r,) +
- "Are the following parameters? %s" % remaining_fs)
- def _create_lambda_func(self):
- """Create the lambda functions to be used by the uniform meshing
- strategy.
- Notes
- =====
- The old sympy.plotting used experimental_lambdify. It created one
- lambda function each time an evaluation was requested. If that failed,
- it went on to create a different lambda function and evaluated it,
- and so on.
- This new module changes strategy: it creates right away the default
- lambda function as well as the backup one. The reason is that the
- series could be interactive, hence the numerical function will be
- evaluated multiple times. So, let's create the functions just once.
- This approach works fine for the majority of cases, in which the
- symbolic expression is relatively short, hence the lambdification
- is fast. If the expression is very long, this approach takes twice
- the time to create the lambda functions. Be aware of that!
- """
- exprs = self.expr if hasattr(self.expr, "__iter__") else [self.expr]
- if not any(callable(e) for e in exprs):
- fs = _get_free_symbols(exprs)
- self._signature = sorted(fs, key=lambda t: t.name)
- # Generate a list of lambda functions, two for each expression:
- # 1. the default one.
- # 2. the backup one, in case of failures with the default one.
- self._functions = []
- for e in exprs:
- # TODO: set cse=True once this issue is solved:
- # https://github.com/sympy/sympy/issues/24246
- self._functions.append([
- lambdify(self._signature, e, modules=self.modules),
- lambdify(self._signature, e, modules="sympy", dummify=True),
- ])
- else:
- self._signature = sorted([r[0] for r in self.ranges], key=lambda t: t.name)
- self._functions = [(e, None) for e in exprs]
- # deal with symbolic color_func
- if isinstance(self.color_func, Expr):
- self.color_func = lambdify(self._signature, self.color_func)
- self._eval_color_func_with_signature = True
- def _update_range_value(self, t):
- """If the value of a plotting range is a symbolic expression,
- substitute the parameters in order to get a numerical value.
- """
- if not self._interactive_ranges:
- return complex(t)
- return complex(t.subs(self.params))
- def _create_discretized_domain(self):
- """Discretize the ranges for uniform meshing strategy.
- """
- # NOTE: the goal is to create a dictionary stored in
- # self._discretized_domain, mapping symbols to a numpy array
- # representing the discretization
- discr_symbols = []
- discretizations = []
- # create a 1D discretization
- for i, r in enumerate(self.ranges):
- discr_symbols.append(r[0])
- c_start = self._update_range_value(r[1])
- c_end = self._update_range_value(r[2])
- start = c_start.real if c_start.imag == c_end.imag == 0 else c_start
- end = c_end.real if c_start.imag == c_end.imag == 0 else c_end
- needs_integer_discr = self.only_integers or (r[0] in self._needs_to_be_int)
- d = BaseSeries._discretize(start, end, self.n[i],
- scale=self.scales[i],
- only_integers=needs_integer_discr)
- if ((not self._force_real_eval) and (not needs_integer_discr) and
- (d.dtype != "complex")):
- d = d + 1j * c_start.imag
- if needs_integer_discr:
- d = d.astype(int)
- discretizations.append(d)
- # create 2D or 3D
- self._create_discretized_domain_helper(discr_symbols, discretizations)
- def _create_discretized_domain_helper(self, discr_symbols, discretizations):
- """Create 2D or 3D discretized grids.
- Subclasses should override this method in order to implement a
- different behaviour.
- """
- np = import_module('numpy')
- # discretization suitable for 2D line plots, 3D surface plots,
- # contours plots, vector plots
- # NOTE: why indexing='ij'? Because it produces consistent results with
- # np.mgrid. This is important as Mayavi requires this indexing
- # to correctly compute 3D streamlines. While VTK is able to compute
- # streamlines regardless of the indexing, with indexing='xy' it
- # produces "strange" results with "voids" into the
- # discretization volume. indexing='ij' solves the problem.
- # Also note that matplotlib 2D streamlines requires indexing='xy'.
- indexing = "xy"
- if self.is_3Dvector or (self.is_3Dsurface and self.is_implicit):
- indexing = "ij"
- meshes = np.meshgrid(*discretizations, indexing=indexing)
- self._discretized_domain = dict(zip(discr_symbols, meshes))
- def _evaluate(self, cast_to_real=True):
- """Evaluation of the symbolic expression (or expressions) with the
- uniform meshing strategy, based on current values of the parameters.
- """
- np = import_module('numpy')
- # create lambda functions
- if not self._functions:
- self._create_lambda_func()
- # create (or update) the discretized domain
- if (not self._discretized_domain) or self._interactive_ranges:
- self._create_discretized_domain()
- # ensure that discretized domains are returned with the proper order
- discr = [self._discretized_domain[s[0]] for s in self.ranges]
- args = self._aggregate_args()
- results = []
- for f in self._functions:
- r = _uniform_eval(*f, *args)
- # the evaluation might produce an int/float. Need this correction.
- r = self._correct_shape(np.array(r), discr[0])
- # sometime the evaluation is performed over arrays of type object.
- # hence, `result` might be of type object, which don't work well
- # with numpy real and imag functions.
- r = r.astype(complex)
- results.append(r)
- if cast_to_real:
- discr = [np.real(d.astype(complex)) for d in discr]
- return [*discr, *results]
- def _aggregate_args(self):
- """Create a list of arguments to be passed to the lambda function,
- sorted according to self._signature.
- """
- args = []
- for s in self._signature:
- if s in self._params.keys():
- args.append(
- int(self._params[s]) if s in self._needs_to_be_int else
- self._params[s] if self._force_real_eval
- else complex(self._params[s]))
- else:
- args.append(self._discretized_domain[s])
- return args
- @property
- def expr(self):
- """Return the expression (or expressions) of the series."""
- return self._expr
- @expr.setter
- def expr(self, e):
- """Set the expression (or expressions) of the series."""
- is_iter = hasattr(e, "__iter__")
- is_callable = callable(e) if not is_iter else any(callable(t) for t in e)
- if is_callable:
- self._expr = e
- else:
- self._expr = sympify(e) if not is_iter else Tuple(*e)
- # look for the upper bound of summations and products
- s = set()
- for e in self._expr.atoms(Sum, Product):
- for a in e.args[1:]:
- if isinstance(a[-1], Symbol):
- s.add(a[-1])
- self._needs_to_be_int = list(s)
- # list of sympy functions that when lambdified, the corresponding
- # numpy functions don't like complex-type arguments
- pf = [ceiling, floor, atan2, frac, zeta]
- if self._force_real_eval is not True:
- check_res = [self._expr.has(f) for f in pf]
- self._force_real_eval = any(check_res)
- if self._force_real_eval and ((self.modules is None) or
- (isinstance(self.modules, str) and "numpy" in self.modules)):
- funcs = [f for f, c in zip(pf, check_res) if c]
- warnings.warn("NumPy is unable to evaluate with complex "
- "numbers some of the functions included in this "
- "symbolic expression: %s. " % funcs +
- "Hence, the evaluation will use real numbers. "
- "If you believe the resulting plot is incorrect, "
- "change the evaluation module by setting the "
- "`modules` keyword argument.")
- if self._functions:
- # update lambda functions
- self._create_lambda_func()
- @property
- def is_3D(self):
- flags3D = [self.is_3Dline, self.is_3Dsurface, self.is_3Dvector]
- return any(flags3D)
- @property
- def is_line(self):
- flagslines = [self.is_2Dline, self.is_3Dline]
- return any(flagslines)
- def _line_surface_color(self, prop, val):
- """This method enables back-compatibility with old sympy.plotting"""
- # NOTE: color_func is set inside the init method of the series.
- # If line_color/surface_color is not a callable, then color_func will
- # be set to None.
- setattr(self, prop, val)
- if callable(val) or isinstance(val, Expr):
- self.color_func = val
- setattr(self, prop, None)
- elif val is not None:
- self.color_func = None
- @property
- def line_color(self):
- return self._line_color
- @line_color.setter
- def line_color(self, val):
- self._line_surface_color("_line_color", val)
- @property
- def n(self):
- """Returns a list [n1, n2, n3] of numbers of discratization points.
- """
- return self._n
- @n.setter
- def n(self, v):
- """Set the numbers of discretization points. ``v`` must be an int or
- a list.
- Let ``s`` be a series. Then:
- * to set the number of discretization points along the x direction (or
- first parameter): ``s.n = 10``
- * to set the number of discretization points along the x and y
- directions (or first and second parameters): ``s.n = [10, 15]``
- * to set the number of discretization points along the x, y and z
- directions: ``s.n = [10, 15, 20]``
- The following is highly unreccomended, because it prevents
- the execution of necessary code in order to keep updated data:
- ``s.n[1] = 15``
- """
- if not hasattr(v, "__iter__"):
- self._n[0] = v
- else:
- self._n[:len(v)] = v
- if self._discretized_domain:
- # update the discretized domain
- self._create_discretized_domain()
- @property
- def params(self):
- """Get or set the current parameters dictionary.
- Parameters
- ==========
- p : dict
- * key: symbol associated to the parameter
- * val: the numeric value
- """
- return self._params
- @params.setter
- def params(self, p):
- self._params = p
- def _post_init(self):
- exprs = self.expr if hasattr(self.expr, "__iter__") else [self.expr]
- if any(callable(e) for e in exprs) and self.params:
- raise TypeError("`params` was provided, hence an interactive plot "
- "is expected. However, interactive plots do not support "
- "user-provided numerical functions.")
- # if the expressions is a lambda function and no label has been
- # provided, then its better to do the following in order to avoid
- # surprises on the backend
- if any(callable(e) for e in exprs):
- if self._label == str(self.expr):
- self.label = ""
- self._check_fs()
- if hasattr(self, "adaptive") and self.adaptive and self.params:
- warnings.warn("`params` was provided, hence an interactive plot "
- "is expected. However, interactive plots do not support "
- "adaptive evaluation. Automatically switched to "
- "adaptive=False.")
- self.adaptive = False
- @property
- def scales(self):
- return self._scales
- @scales.setter
- def scales(self, v):
- if isinstance(v, str):
- self._scales[0] = v
- else:
- self._scales[:len(v)] = v
- @property
- def surface_color(self):
- return self._surface_color
- @surface_color.setter
- def surface_color(self, val):
- self._line_surface_color("_surface_color", val)
- @property
- def rendering_kw(self):
- return self._rendering_kw
- @rendering_kw.setter
- def rendering_kw(self, kwargs):
- if isinstance(kwargs, dict):
- self._rendering_kw = kwargs
- else:
- self._rendering_kw = {}
- if kwargs is not None:
- warnings.warn(
- "`rendering_kw` must be a dictionary, instead an "
- "object of type %s was received. " % type(kwargs) +
- "Automatically setting `rendering_kw` to an empty "
- "dictionary")
- @staticmethod
- def _discretize(start, end, N, scale="linear", only_integers=False):
- """Discretize a 1D domain.
- Returns
- =======
- domain : np.ndarray with dtype=float or complex
- The domain's dtype will be float or complex (depending on the
- type of start/end) even if only_integers=True. It is left for
- the downstream code to perform further casting, if necessary.
- """
- np = import_module('numpy')
- if only_integers is True:
- start, end = int(start), int(end)
- N = end - start + 1
- if scale == "linear":
- return np.linspace(start, end, N)
- return np.geomspace(start, end, N)
- @staticmethod
- def _correct_shape(a, b):
- """Convert ``a`` to a np.ndarray of the same shape of ``b``.
- Parameters
- ==========
- a : int, float, complex, np.ndarray
- Usually, this is the result of a numerical evaluation of a
- symbolic expression. Even if a discretized domain was used to
- evaluate the function, the result can be a scalar (int, float,
- complex). Think for example to ``expr = Float(2)`` and
- ``f = lambdify(x, expr)``. No matter the shape of the numerical
- array representing x, the result of the evaluation will be
- a single value.
- b : np.ndarray
- It represents the correct shape that ``a`` should have.
- Returns
- =======
- new_a : np.ndarray
- An array with the correct shape.
- """
- np = import_module('numpy')
- if not isinstance(a, np.ndarray):
- a = np.array(a)
- if a.shape != b.shape:
- if a.shape == ():
- a = a * np.ones_like(b)
- else:
- a = a.reshape(b.shape)
- return a
- def eval_color_func(self, *args):
- """Evaluate the color function.
- Parameters
- ==========
- args : tuple
- Arguments to be passed to the coloring function. Can be coordinates
- or parameters or both.
- Notes
- =====
- The backend will request the data series to generate the numerical
- data. Depending on the data series, either the data series itself or
- the backend will eventually execute this function to generate the
- appropriate coloring value.
- """
- np = import_module('numpy')
- if self.color_func is None:
- # NOTE: with the line_color and surface_color attributes
- # (back-compatibility with the old sympy.plotting module) it is
- # possible to create a plot with a callable line_color (or
- # surface_color). For example:
- # p = plot(sin(x), line_color=lambda x, y: -y)
- # This creates a ColoredLineOver1DRangeSeries with line_color=None
- # and color_func=lambda x, y: -y, which effectively is a
- # parametric series. Later we could change it to a string value:
- # p[0].line_color = "red"
- # However, this sets ine_color="red" and color_func=None, but the
- # series is still ColoredLineOver1DRangeSeries (a parametric
- # series), which will render using a color_func...
- warnings.warn("This is likely not the result you were "
- "looking for. Please, re-execute the plot command, this time "
- "with the appropriate an appropriate value to line_color "
- "or surface_color.")
- return np.ones_like(args[0])
- if self._eval_color_func_with_signature:
- args = self._aggregate_args()
- color = self.color_func(*args)
- _re, _im = np.real(color), np.imag(color)
- _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan
- return _re
- nargs = arity(self.color_func)
- if nargs == 1:
- if self.is_2Dline and self.is_parametric:
- if len(args) == 2:
- # ColoredLineOver1DRangeSeries
- return self._correct_shape(self.color_func(args[0]), args[0])
- # Parametric2DLineSeries
- return self._correct_shape(self.color_func(args[2]), args[2])
- elif self.is_3Dline and self.is_parametric:
- return self._correct_shape(self.color_func(args[3]), args[3])
- elif self.is_3Dsurface and self.is_parametric:
- return self._correct_shape(self.color_func(args[3]), args[3])
- return self._correct_shape(self.color_func(args[0]), args[0])
- elif nargs == 2:
- if self.is_3Dsurface and self.is_parametric:
- return self._correct_shape(self.color_func(*args[3:]), args[3])
- return self._correct_shape(self.color_func(*args[:2]), args[0])
- return self._correct_shape(self.color_func(*args[:nargs]), args[0])
- def get_data(self):
- """Compute and returns the numerical data.
- The number of parameters returned by this method depends on the
- specific instance. If ``s`` is the series, make sure to read
- ``help(s.get_data)`` to understand what it returns.
- """
- raise NotImplementedError
- def _get_wrapped_label(self, label, wrapper):
- """Given a latex representation of an expression, wrap it inside
- some characters. Matplotlib needs "$%s%$", K3D-Jupyter needs "%s".
- """
- return wrapper % label
- def get_label(self, use_latex=False, wrapper="$%s$"):
- """Return the label to be used to display the expression.
- Parameters
- ==========
- use_latex : bool
- If False, the string representation of the expression is returned.
- If True, the latex representation is returned.
- wrapper : str
- The backend might need the latex representation to be wrapped by
- some characters. Default to ``"$%s$"``.
- Returns
- =======
- label : str
- """
- if use_latex is False:
- return self._label
- if self._label == str(self.expr):
- # when the backend requests a latex label and user didn't provide
- # any label
- return self._get_wrapped_label(self._latex_label, wrapper)
- return self._latex_label
- @property
- def label(self):
- return self.get_label()
- @label.setter
- def label(self, val):
- """Set the labels associated to this series."""
- # NOTE: the init method of any series requires a label. If the user do
- # not provide it, the preprocessing function will set label=None, which
- # informs the series to initialize two attributes:
- # _label contains the string representation of the expression.
- # _latex_label contains the latex representation of the expression.
- self._label = self._latex_label = val
- @property
- def ranges(self):
- return self._ranges
- @ranges.setter
- def ranges(self, val):
- new_vals = []
- for v in val:
- if v is not None:
- new_vals.append(tuple([sympify(t) for t in v]))
- self._ranges = new_vals
- def _apply_transform(self, *args):
- """Apply transformations to the results of numerical evaluation.
- Parameters
- ==========
- args : tuple
- Results of numerical evaluation.
- Returns
- =======
- transformed_args : tuple
- Tuple containing the transformed results.
- """
- t = lambda x, transform: x if transform is None else transform(x)
- x, y, z = None, None, None
- if len(args) == 2:
- x, y = args
- return t(x, self._tx), t(y, self._ty)
- elif (len(args) == 3) and isinstance(self, Parametric2DLineSeries):
- x, y, u = args
- return (t(x, self._tx), t(y, self._ty), t(u, self._tp))
- elif len(args) == 3:
- x, y, z = args
- return t(x, self._tx), t(y, self._ty), t(z, self._tz)
- elif (len(args) == 4) and isinstance(self, Parametric3DLineSeries):
- x, y, z, u = args
- return (t(x, self._tx), t(y, self._ty), t(z, self._tz), t(u, self._tp))
- elif len(args) == 4: # 2D vector plot
- x, y, u, v = args
- return (
- t(x, self._tx), t(y, self._ty),
- t(u, self._tx), t(v, self._ty)
- )
- elif (len(args) == 5) and isinstance(self, ParametricSurfaceSeries):
- x, y, z, u, v = args
- return (t(x, self._tx), t(y, self._ty), t(z, self._tz), u, v)
- elif (len(args) == 6) and self.is_3Dvector: # 3D vector plot
- x, y, z, u, v, w = args
- return (
- t(x, self._tx), t(y, self._ty), t(z, self._tz),
- t(u, self._tx), t(v, self._ty), t(w, self._tz)
- )
- elif len(args) == 6: # complex plot
- x, y, _abs, _arg, img, colors = args
- return (
- x, y, t(_abs, self._tz), _arg, img, colors)
- return args
- def _str_helper(self, s):
- pre, post = "", ""
- if self.is_interactive:
- pre = "interactive "
- post = " and parameters " + str(tuple(self.params.keys()))
- return pre + s + post
- def _detect_poles_numerical_helper(x, y, eps=0.01, expr=None, symb=None, symbolic=False):
- """Compute the steepness of each segment. If it's greater than a
- threshold, set the right-point y-value non NaN and record the
- corresponding x-location for further processing.
- Returns
- =======
- x : np.ndarray
- Unchanged x-data.
- yy : np.ndarray
- Modified y-data with NaN values.
- """
- np = import_module('numpy')
- yy = y.copy()
- threshold = np.pi / 2 - eps
- for i in range(len(x) - 1):
- dx = x[i + 1] - x[i]
- dy = abs(y[i + 1] - y[i])
- angle = np.arctan(dy / dx)
- if abs(angle) >= threshold:
- yy[i + 1] = np.nan
- return x, yy
- def _detect_poles_symbolic_helper(expr, symb, start, end):
- """Attempts to compute symbolic discontinuities.
- Returns
- =======
- pole : list
- List of symbolic poles, possibly empty.
- """
- poles = []
- interval = Interval(nsimplify(start), nsimplify(end))
- res = continuous_domain(expr, symb, interval)
- res = res.simplify()
- if res == interval:
- pass
- elif (isinstance(res, Union) and
- all(isinstance(t, Interval) for t in res.args)):
- poles = []
- for s in res.args:
- if s.left_open:
- poles.append(s.left)
- if s.right_open:
- poles.append(s.right)
- poles = list(set(poles))
- else:
- raise ValueError(
- f"Could not parse the following object: {res} .\n"
- "Please, submit this as a bug. Consider also to set "
- "`detect_poles=True`."
- )
- return poles
- ### 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
- _N = 1000
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.steps = kwargs.get("steps", False)
- self.is_point = kwargs.get("is_point", kwargs.get("point", False))
- self.is_filled = kwargs.get("is_filled", kwargs.get("fill", True))
- self.adaptive = kwargs.get("adaptive", False)
- self.depth = kwargs.get('depth', 12)
- self.use_cm = kwargs.get("use_cm", False)
- self.color_func = kwargs.get("color_func", None)
- self.line_color = kwargs.get("line_color", None)
- self.detect_poles = kwargs.get("detect_poles", False)
- self.eps = kwargs.get("eps", 0.01)
- self.is_polar = kwargs.get("is_polar", kwargs.get("polar", False))
- self.unwrap = kwargs.get("unwrap", False)
- # when detect_poles="symbolic", stores the location of poles so that
- # they can be appropriately rendered
- self.poles_locations = []
- exclude = kwargs.get("exclude", [])
- if isinstance(exclude, Set):
- exclude = list(extract_solution(exclude, n=100))
- if not hasattr(exclude, "__iter__"):
- exclude = [exclude]
- exclude = [float(e) for e in exclude]
- self.exclude = sorted(exclude)
- def get_data(self):
- """Return coordinates for plotting the line.
- Returns
- =======
- x: np.ndarray
- x-coordinates
- y: np.ndarray
- y-coordinates
- z: np.ndarray (optional)
- z-coordinates in case of Parametric3DLineSeries,
- Parametric3DLineInteractiveSeries
- param : np.ndarray (optional)
- The parameter in case of Parametric2DLineSeries,
- Parametric3DLineSeries or AbsArgLineSeries (and their
- corresponding interactive series).
- """
- np = import_module('numpy')
- points = self._get_data_helper()
- if (isinstance(self, LineOver1DRangeSeries) and
- (self.detect_poles == "symbolic")):
- poles = _detect_poles_symbolic_helper(
- self.expr.subs(self.params), *self.ranges[0])
- poles = np.array([float(t) for t in poles])
- t = lambda x, transform: x if transform is None else transform(x)
- self.poles_locations = t(np.array(poles), self._tx)
- # postprocessing
- points = self._apply_transform(*points)
- if self.is_2Dline and self.detect_poles:
- if len(points) == 2:
- x, y = points
- x, y = _detect_poles_numerical_helper(
- x, y, self.eps)
- points = (x, y)
- else:
- x, y, p = points
- x, y = _detect_poles_numerical_helper(x, y, self.eps)
- points = (x, y, p)
- if self.unwrap:
- kw = {}
- if self.unwrap is not True:
- kw = self.unwrap
- if self.is_2Dline:
- if len(points) == 2:
- x, y = points
- y = np.unwrap(y, **kw)
- points = (x, y)
- else:
- x, y, p = points
- y = np.unwrap(y, **kw)
- points = (x, y, p)
- if self.steps is True:
- if self.is_2Dline:
- x, y = points[0], points[1]
- x = np.array((x, x)).T.flatten()[1:]
- y = np.array((y, y)).T.flatten()[:-1]
- if self.is_parametric:
- points = (x, y, points[2])
- else:
- points = (x, y)
- elif self.is_3Dline:
- x = np.repeat(points[0], 3)[2:]
- y = np.repeat(points[1], 3)[:-2]
- z = np.repeat(points[2], 3)[1:-1]
- if len(points) > 3:
- points = (x, y, z, points[3])
- else:
- points = (x, y, z)
- if len(self.exclude) > 0:
- points = self._insert_exclusions(points)
- 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 _insert_exclusions(self, points):
- """Add NaN to each of the exclusion point. Practically, this adds a
- NaN to the exclusion point, plus two other nearby points evaluated with
- the numerical functions associated to this data series.
- These nearby points are important when the number of discretization
- points is low, or the scale is logarithm.
- NOTE: it would be easier to just add exclusion points to the
- discretized domain before evaluation, then after evaluation add NaN
- to the exclusion points. But that's only work with adaptive=False.
- The following approach work even with adaptive=True.
- """
- np = import_module("numpy")
- points = list(points)
- n = len(points)
- # index of the x-coordinate (for 2d plots) or parameter (for 2d/3d
- # parametric plots)
- k = n - 1
- if n == 2:
- k = 0
- # indices of the other coordinates
- j_indeces = sorted(set(range(n)).difference([k]))
- # TODO: for now, I assume that numpy functions are going to succeed
- funcs = [f[0] for f in self._functions]
- for e in self.exclude:
- res = points[k] - e >= 0
- # if res contains both True and False, ie, if e is found
- if any(res) and any(~res):
- idx = np.nanargmax(res)
- # select the previous point with respect to e
- idx -= 1
- # TODO: what if points[k][idx]==e or points[k][idx+1]==e?
- if idx > 0 and idx < len(points[k]) - 1:
- delta_prev = abs(e - points[k][idx])
- delta_post = abs(e - points[k][idx + 1])
- delta = min(delta_prev, delta_post) / 100
- prev = e - delta
- post = e + delta
- # add points to the x-coord or the parameter
- points[k] = np.concatenate(
- (points[k][:idx], [prev, e, post], points[k][idx+1:]))
- # add points to the other coordinates
- c = 0
- for j in j_indeces:
- values = funcs[c](np.array([prev, post]))
- c += 1
- points[j] = np.concatenate(
- (points[j][:idx], [values[0], np.nan, values[1]], points[j][idx+1:]))
- return points
- @property
- def var(self):
- return None if not self.ranges else self.ranges[0][0]
- @property
- def start(self):
- if not self.ranges:
- return None
- try:
- return self._cast(self.ranges[0][1])
- except TypeError:
- return self.ranges[0][1]
- @property
- def end(self):
- if not self.ranges:
- return None
- try:
- return self._cast(self.ranges[0][2])
- except TypeError:
- return self.ranges[0][2]
- @property
- def xscale(self):
- return self._scales[0]
- @xscale.setter
- def xscale(self, v):
- self.scales = v
- 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, label="", **kwargs):
- super().__init__(**kwargs)
- np = import_module('numpy')
- if len(list_x) != len(list_y):
- raise ValueError(
- "The two lists of coordinates must have the same "
- "number of elements.\n"
- "Received: len(list_x) = {} ".format(len(list_x)) +
- "and len(list_y) = {}".format(len(list_y))
- )
- self._block_lambda_functions(list_x, list_y)
- check = lambda l: [isinstance(t, Expr) and (not t.is_number) for t in l]
- if any(check(list_x) + check(list_y)) or self.params:
- if not self.params:
- raise ValueError("Some or all elements of the provided lists "
- "are symbolic expressions, but the ``params`` dictionary "
- "was not provided: those elements can't be evaluated.")
- self.list_x = Tuple(*list_x)
- self.list_y = Tuple(*list_y)
- else:
- self.list_x = np.array(list_x, dtype=np.float64)
- self.list_y = np.array(list_y, dtype=np.float64)
- self._expr = (self.list_x, self.list_y)
- if not any(isinstance(t, np.ndarray) for t in [self.list_x, self.list_y]):
- self._check_fs()
- self.is_polar = kwargs.get("is_polar", kwargs.get("polar", False))
- self.label = label
- self.rendering_kw = kwargs.get("rendering_kw", {})
- if self.use_cm and self.color_func:
- self.is_parametric = True
- if isinstance(self.color_func, Expr):
- raise TypeError(
- "%s don't support symbolic " % self.__class__.__name__ +
- "expression for `color_func`.")
- def __str__(self):
- return "2D list plot"
- def _get_data_helper(self):
- """Returns coordinates that needs to be postprocessed."""
- lx, ly = self.list_x, self.list_y
- if not self.is_interactive:
- return self._eval_color_func_and_return(lx, ly)
- np = import_module('numpy')
- lx = np.array([t.evalf(subs=self.params) for t in lx], dtype=float)
- ly = np.array([t.evalf(subs=self.params) for t in ly], dtype=float)
- return self._eval_color_func_and_return(lx, ly)
- def _eval_color_func_and_return(self, *data):
- if self.use_cm and callable(self.color_func):
- return [*data, self.eval_color_func(*data)]
- return data
- class LineOver1DRangeSeries(Line2DBaseSeries):
- """Representation for a line consisting of a SymPy expression over a range."""
- def __init__(self, expr, var_start_end, label="", **kwargs):
- super().__init__(**kwargs)
- self.expr = expr if callable(expr) else sympify(expr)
- self._label = str(self.expr) if label is None else label
- self._latex_label = latex(self.expr) if label is None else label
- self.ranges = [var_start_end]
- self._cast = complex
- # for complex-related data series, this determines what data to return
- # on the y-axis
- self._return = kwargs.get("return", None)
- self._post_init()
- if not self._interactive_ranges:
- # NOTE: the following check is only possible when the minimum and
- # maximum values of a plotting range are numeric
- start, end = [complex(t) for t in self.ranges[0][1:]]
- if im(start) != im(end):
- raise ValueError(
- "%s requires the imaginary " % self.__class__.__name__ +
- "part of the start and end values of the range "
- "to be the same.")
- if self.adaptive and self._return:
- warnings.warn("The adaptive algorithm is unable to deal with "
- "complex numbers. Automatically switching to uniform meshing.")
- self.adaptive = False
- @property
- def nb_of_points(self):
- return self.n[0]
- @nb_of_points.setter
- def nb_of_points(self, v):
- self.n = v
- def __str__(self):
- def f(t):
- if isinstance(t, complex):
- if t.imag != 0:
- return t
- return t.real
- return t
- pre = "interactive " if self.is_interactive else ""
- post = ""
- if self.is_interactive:
- post = " and parameters " + str(tuple(self.params.keys()))
- wrapper = _get_wrapper_for_expr(self._return)
- return pre + "cartesian line: %s for %s over %s" % (
- wrapper % self.expr,
- str(self.var),
- str((f(self.start), f(self.end))),
- ) + post
- 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.
- This function is available for back-compatibility purposes. Consider
- using ``get_data()`` instead.
- Returns
- =======
- x : list
- List of x-coordinates
- y : list
- List of y-coordinates
- """
- return self._get_data_helper()
- def _adaptive_sampling(self):
- try:
- if callable(self.expr):
- f = self.expr
- else:
- f = lambdify([self.var], self.expr, self.modules)
- x, y = self._adaptive_sampling_helper(f)
- except Exception as err:
- warnings.warn(
- "The evaluation with %s failed.\n" % (
- "NumPy/SciPy" if not self.modules else self.modules) +
- "{}: {}\n".format(type(err).__name__, err) +
- "Trying to evaluate the expression with Sympy, but it might "
- "be a slow operation."
- )
- f = lambdify([self.var], self.expr, "sympy")
- x, y = self._adaptive_sampling_helper(f)
- return x, y
- def _adaptive_sampling_helper(self, f):
- """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.
- """
- np = import_module('numpy')
- x_coords = []
- y_coords = []
- 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 = _adaptive_eval(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 to depth of 6 (whether the line is flat or not)
- # without 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 = _adaptive_eval(f, self.start.real)
- f_end = _adaptive_eval(f, self.end.real)
- x_coords.append(self.start.real)
- y_coords.append(f_start)
- sample(np.array([self.start.real, f_start]),
- np.array([self.end.real, f_end]), 0)
- return (x_coords, y_coords)
- def _uniform_sampling(self):
- np = import_module('numpy')
- x, result = self._evaluate()
- _re, _im = np.real(result), np.imag(result)
- _re = self._correct_shape(_re, x)
- _im = self._correct_shape(_im, x)
- return x, _re, _im
- def _get_data_helper(self):
- """Returns coordinates that needs to be postprocessed.
- """
- np = import_module('numpy')
- if self.adaptive and (not self.only_integers):
- x, y = self._adaptive_sampling()
- return [np.array(t) for t in [x, y]]
- x, _re, _im = self._uniform_sampling()
- if self._return is None:
- # The evaluation could produce complex numbers. Set real elements
- # to NaN where there are non-zero imaginary elements
- _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan
- elif self._return == "real":
- pass
- elif self._return == "imag":
- _re = _im
- elif self._return == "abs":
- _re = np.sqrt(_re**2 + _im**2)
- elif self._return == "arg":
- _re = np.arctan2(_im, _re)
- else:
- raise ValueError("`_return` not recognized. "
- "Received: %s" % self._return)
- return x, _re
- class ParametricLineBaseSeries(Line2DBaseSeries):
- is_parametric = True
- def _set_parametric_line_label(self, label):
- """Logic to set the correct label to be shown on the plot.
- If `use_cm=True` there will be a colorbar, so we show the parameter.
- If `use_cm=False`, there might be a legend, so we show the expressions.
- Parameters
- ==========
- label : str
- label passed in by the pre-processor or the user
- """
- self._label = str(self.var) if label is None else label
- self._latex_label = latex(self.var) if label is None else label
- if (self.use_cm is False) and (self._label == str(self.var)):
- self._label = str(self.expr)
- self._latex_label = latex(self.expr)
- # if the expressions is a lambda function and use_cm=False and no label
- # has been provided, then its better to do the following in order to
- # avoid surprises on the backend
- if any(callable(e) for e in self.expr) and (not self.use_cm):
- if self._label == str(self.expr):
- self._label = ""
- def get_label(self, use_latex=False, wrapper="$%s$"):
- # parametric lines returns the representation of the parameter to be
- # shown on the colorbar if `use_cm=True`, otherwise it returns the
- # representation of the expression to be placed on the legend.
- if self.use_cm:
- if str(self.var) == self._label:
- if use_latex:
- return self._get_wrapped_label(latex(self.var), wrapper)
- return str(self.var)
- # here the user has provided a custom label
- return self._label
- if use_latex:
- if self._label != str(self.expr):
- return self._latex_label
- return self._get_wrapped_label(self._latex_label, wrapper)
- return self._label
- def _get_data_helper(self):
- """Returns coordinates that needs to be postprocessed.
- Depending on the `adaptive` option, this function will either use an
- adaptive algorithm or it will uniformly sample the expression over the
- provided range.
- """
- if self.adaptive:
- np = import_module("numpy")
- coords = self._adaptive_sampling()
- coords = [np.array(t) for t in coords]
- else:
- coords = self._uniform_sampling()
- if self.is_2Dline and self.is_polar:
- # when plot_polar is executed with polar_axis=True
- np = import_module('numpy')
- x, y, _ = coords
- r = np.sqrt(x**2 + y**2)
- t = np.arctan2(y, x)
- coords = [t, r, coords[-1]]
- if callable(self.color_func):
- coords = list(coords)
- coords[-1] = self.eval_color_func(*coords)
- return coords
- def _uniform_sampling(self):
- """Returns coordinates that needs to be postprocessed."""
- np = import_module('numpy')
- results = self._evaluate()
- for i, r in enumerate(results):
- _re, _im = np.real(r), np.imag(r)
- _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan
- results[i] = _re
- return [*results[1:], results[0]]
- def get_parameter_points(self):
- return self.get_data()[-1]
- 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.
- This function is available for back-compatibility purposes. Consider
- using ``get_data()`` instead.
- Returns
- =======
- x : list
- List of x-coordinates
- y : list
- List of y-coordinates
- z : list
- List of z-coordinates, only for 3D parametric line plot.
- """
- return self._get_data_helper()[:-1]
- @property
- def nb_of_points(self):
- return self.n[0]
- @nb_of_points.setter
- def nb_of_points(self, v):
- self.n = v
- class Parametric2DLineSeries(ParametricLineBaseSeries):
- """Representation for a line consisting of two parametric SymPy expressions
- over a range."""
- is_2Dline = True
- def __init__(self, expr_x, expr_y, var_start_end, label="", **kwargs):
- super().__init__(**kwargs)
- self.expr_x = expr_x if callable(expr_x) else sympify(expr_x)
- self.expr_y = expr_y if callable(expr_y) else sympify(expr_y)
- self.expr = (self.expr_x, self.expr_y)
- self.ranges = [var_start_end]
- self._cast = float
- self.use_cm = kwargs.get("use_cm", True)
- self._set_parametric_line_label(label)
- self._post_init()
- def __str__(self):
- return self._str_helper(
- "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 _adaptive_sampling(self):
- try:
- if callable(self.expr_x) and callable(self.expr_y):
- f_x = self.expr_x
- f_y = self.expr_y
- else:
- f_x = lambdify([self.var], self.expr_x)
- f_y = lambdify([self.var], self.expr_y)
- x, y, p = self._adaptive_sampling_helper(f_x, f_y)
- except Exception as err:
- warnings.warn(
- "The evaluation with %s failed.\n" % (
- "NumPy/SciPy" if not self.modules else self.modules) +
- "{}: {}\n".format(type(err).__name__, err) +
- "Trying to evaluate the expression with Sympy, but it might "
- "be a slow operation."
- )
- f_x = lambdify([self.var], self.expr_x, "sympy")
- f_y = lambdify([self.var], self.expr_y, "sympy")
- x, y, p = self._adaptive_sampling_helper(f_x, f_y)
- return x, y, p
- def _adaptive_sampling_helper(self, f_x, f_y):
- """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.
- """
- x_coords = []
- y_coords = []
- param = []
- 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 = _adaptive_eval(f_x, param_new)
- ynew = _adaptive_eval(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])
- param.append(param_p)
- # 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 = [_adaptive_eval(f_x, t) for t in param_array]
- y_array = [_adaptive_eval(f_y, t) for t in 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])
- param.append(param_p)
- f_start_x = _adaptive_eval(f_x, self.start)
- f_start_y = _adaptive_eval(f_y, self.start)
- start = [f_start_x, f_start_y]
- f_end_x = _adaptive_eval(f_x, self.end)
- f_end_y = _adaptive_eval(f_y, self.end)
- end = [f_end_x, f_end_y]
- x_coords.append(f_start_x)
- y_coords.append(f_start_y)
- param.append(self.start)
- sample(self.start, self.end, start, end, 0)
- return x_coords, y_coords, param
- ### 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(ParametricLineBaseSeries):
- """Representation for a 3D line consisting of three parametric SymPy
- expressions and a range."""
- is_2Dline = False
- is_3Dline = True
- def __init__(self, expr_x, expr_y, expr_z, var_start_end, label="", **kwargs):
- super().__init__(**kwargs)
- self.expr_x = expr_x if callable(expr_x) else sympify(expr_x)
- self.expr_y = expr_y if callable(expr_y) else sympify(expr_y)
- self.expr_z = expr_z if callable(expr_z) else sympify(expr_z)
- self.expr = (self.expr_x, self.expr_y, self.expr_z)
- self.ranges = [var_start_end]
- self._cast = float
- self.adaptive = False
- self.use_cm = kwargs.get("use_cm", True)
- self._set_parametric_line_label(label)
- self._post_init()
- # TODO: remove this
- self._xlim = None
- self._ylim = None
- self._zlim = None
- def __str__(self):
- return self._str_helper(
- "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_data(self):
- # TODO: remove this
- np = import_module("numpy")
- x, y, z, p = super().get_data()
- self._xlim = (np.amin(x), np.amax(x))
- self._ylim = (np.amin(y), np.amax(y))
- self._zlim = (np.amin(z), np.amax(z))
- return x, y, z, p
- ### Surfaces
- class SurfaceBaseSeries(BaseSeries):
- """A base class for 3D surfaces."""
- is_3Dsurface = True
- def __init__(self, *args, **kwargs):
- super().__init__(**kwargs)
- self.use_cm = kwargs.get("use_cm", False)
- # NOTE: why should SurfaceOver2DRangeSeries support is polar?
- # After all, the same result can be achieve with
- # ParametricSurfaceSeries. For example:
- # sin(r) for (r, 0, 2 * pi) and (theta, 0, pi/2) can be parameterized
- # as (r * cos(theta), r * sin(theta), sin(t)) for (r, 0, 2 * pi) and
- # (theta, 0, pi/2).
- # Because it is faster to evaluate (important for interactive plots).
- self.is_polar = kwargs.get("is_polar", kwargs.get("polar", False))
- self.surface_color = kwargs.get("surface_color", None)
- self.color_func = kwargs.get("color_func", lambda x, y, z: z)
- if callable(self.surface_color):
- self.color_func = self.surface_color
- self.surface_color = None
- def _set_surface_label(self, label):
- exprs = self.expr
- self._label = str(exprs) if label is None else label
- self._latex_label = latex(exprs) if label is None else label
- # if the expressions is a lambda function and no label
- # has been provided, then its better to do the following to avoid
- # surprises on the backend
- is_lambda = (callable(exprs) if not hasattr(exprs, "__iter__")
- else any(callable(e) for e in exprs))
- if is_lambda and (self._label == str(exprs)):
- self._label = ""
- self._latex_label = ""
- 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, label="", **kwargs):
- super().__init__(**kwargs)
- self.expr = expr if callable(expr) else sympify(expr)
- self.ranges = [var_start_end_x, var_start_end_y]
- self._set_surface_label(label)
- self._post_init()
- # TODO: remove this
- self._xlim = (self.start_x, self.end_x)
- self._ylim = (self.start_y, self.end_y)
- @property
- def var_x(self):
- return self.ranges[0][0]
- @property
- def var_y(self):
- return self.ranges[1][0]
- @property
- def start_x(self):
- try:
- return float(self.ranges[0][1])
- except TypeError:
- return self.ranges[0][1]
- @property
- def end_x(self):
- try:
- return float(self.ranges[0][2])
- except TypeError:
- return self.ranges[0][2]
- @property
- def start_y(self):
- try:
- return float(self.ranges[1][1])
- except TypeError:
- return self.ranges[1][1]
- @property
- def end_y(self):
- try:
- return float(self.ranges[1][2])
- except TypeError:
- return self.ranges[1][2]
- @property
- def nb_of_points_x(self):
- return self.n[0]
- @nb_of_points_x.setter
- def nb_of_points_x(self, v):
- n = self.n
- self.n = [v, n[1:]]
- @property
- def nb_of_points_y(self):
- return self.n[1]
- @nb_of_points_y.setter
- def nb_of_points_y(self, v):
- n = self.n
- self.n = [n[0], v, n[2]]
- def __str__(self):
- series_type = "cartesian surface" if self.is_3Dsurface else "contour"
- return self._str_helper(
- series_type + ": %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):
- """Return the x,y,z coordinates for plotting the surface.
- This function is available for back-compatibility purposes. Consider
- using ``get_data()`` instead.
- """
- return self.get_data()
- def get_data(self):
- """Return arrays of coordinates for plotting.
- Returns
- =======
- mesh_x : np.ndarray
- Discretized x-domain.
- mesh_y : np.ndarray
- Discretized y-domain.
- mesh_z : np.ndarray
- Results of the evaluation.
- """
- np = import_module('numpy')
- results = self._evaluate()
- # mask out complex values
- for i, r in enumerate(results):
- _re, _im = np.real(r), np.imag(r)
- _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan
- results[i] = _re
- x, y, z = results
- if self.is_polar and self.is_3Dsurface:
- r = x.copy()
- x = r * np.cos(y)
- y = r * np.sin(y)
- # TODO: remove this
- self._zlim = (np.amin(z), np.amax(z))
- return self._apply_transform(x, y, 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, label="", **kwargs):
- super().__init__(**kwargs)
- self.expr_x = expr_x if callable(expr_x) else sympify(expr_x)
- self.expr_y = expr_y if callable(expr_y) else sympify(expr_y)
- self.expr_z = expr_z if callable(expr_z) else sympify(expr_z)
- self.expr = (self.expr_x, self.expr_y, self.expr_z)
- self.ranges = [var_start_end_u, var_start_end_v]
- self.color_func = kwargs.get("color_func", lambda x, y, z, u, v: z)
- self._set_surface_label(label)
- self._post_init()
- @property
- def var_u(self):
- return self.ranges[0][0]
- @property
- def var_v(self):
- return self.ranges[1][0]
- @property
- def start_u(self):
- try:
- return float(self.ranges[0][1])
- except TypeError:
- return self.ranges[0][1]
- @property
- def end_u(self):
- try:
- return float(self.ranges[0][2])
- except TypeError:
- return self.ranges[0][2]
- @property
- def start_v(self):
- try:
- return float(self.ranges[1][1])
- except TypeError:
- return self.ranges[1][1]
- @property
- def end_v(self):
- try:
- return float(self.ranges[1][2])
- except TypeError:
- return self.ranges[1][2]
- @property
- def nb_of_points_u(self):
- return self.n[0]
- @nb_of_points_u.setter
- def nb_of_points_u(self, v):
- n = self.n
- self.n = [v, n[1:]]
- @property
- def nb_of_points_v(self):
- return self.n[1]
- @nb_of_points_v.setter
- def nb_of_points_v(self, v):
- n = self.n
- self.n = [n[0], v, n[2]]
- def __str__(self):
- return self._str_helper(
- "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):
- return self.get_data()[3:]
- def get_meshes(self):
- """Return the x,y,z coordinates for plotting the surface.
- This function is available for back-compatibility purposes. Consider
- using ``get_data()`` instead.
- """
- return self.get_data()[:3]
- def get_data(self):
- """Return arrays of coordinates for plotting.
- Returns
- =======
- x : np.ndarray [n2 x n1]
- x-coordinates.
- y : np.ndarray [n2 x n1]
- y-coordinates.
- z : np.ndarray [n2 x n1]
- z-coordinates.
- mesh_u : np.ndarray [n2 x n1]
- Discretized u range.
- mesh_v : np.ndarray [n2 x n1]
- Discretized v range.
- """
- np = import_module('numpy')
- results = self._evaluate()
- # mask out complex values
- for i, r in enumerate(results):
- _re, _im = np.real(r), np.imag(r)
- _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan
- results[i] = _re
- # TODO: remove this
- x, y, z = results[2:]
- self._xlim = (np.amin(x), np.amax(x))
- self._ylim = (np.amin(y), np.amax(y))
- self._zlim = (np.amin(z), np.amax(z))
- return self._apply_transform(*results[2:], *results[:2])
- ### Contours
- class ContourSeries(SurfaceOver2DRangeSeries):
- """Representation for a contour plot."""
- is_3Dsurface = False
- is_contour = True
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.is_filled = kwargs.get("is_filled", kwargs.get("fill", True))
- self.show_clabels = kwargs.get("clabels", True)
- # NOTE: contour plots are used by plot_contour, plot_vector and
- # plot_complex_vector. By implementing contour_kw we are able to
- # quickly target the contour plot.
- self.rendering_kw = kwargs.get("contour_kw",
- kwargs.get("rendering_kw", {}))
- class GenericDataSeries(BaseSeries):
- """Represents generic numerical data.
- Notes
- =====
- This class serves the purpose of back-compatibility with the "markers,
- annotations, fill, rectangles" keyword arguments that represent
- user-provided numerical data. In particular, it solves the problem of
- combining together two or more plot-objects with the ``extend`` or
- ``append`` methods: user-provided numerical data is also taken into
- consideration because it is stored in this series class.
- Also note that the current implementation is far from optimal, as each
- keyword argument is stored into an attribute in the ``Plot`` class, which
- requires a hard-coded if-statement in the ``MatplotlibBackend`` class.
- The implementation suggests that it is ok to add attributes and
- if-statements to provide more and more functionalities for user-provided
- numerical data (e.g. adding horizontal lines, or vertical lines, or bar
- plots, etc). However, in doing so one would reinvent the wheel: plotting
- libraries (like Matplotlib) already implements the necessary API.
- Instead of adding more keyword arguments and attributes, users interested
- in adding custom numerical data to a plot should retrieve the figure
- created by this plotting module. For example, this code:
- .. plot::
- :context: close-figs
- :include-source: True
- from sympy import Symbol, plot, cos
- x = Symbol("x")
- p = plot(cos(x), markers=[{"args": [[0, 1, 2], [0, 1, -1], "*"]}])
- Becomes:
- .. plot::
- :context: close-figs
- :include-source: True
- p = plot(cos(x), backend="matplotlib")
- fig, ax = p._backend.fig, p._backend.ax
- ax.plot([0, 1, 2], [0, 1, -1], "*")
- fig
- Which is far better in terms of readability. Also, it gives access to the
- full plotting library capabilities, without the need to reinvent the wheel.
- """
- is_generic = True
- def __init__(self, tp, *args, **kwargs):
- self.type = tp
- self.args = args
- self.rendering_kw = kwargs
- def get_data(self):
- return self.args
- class ImplicitSeries(BaseSeries):
- """Representation for 2D Implicit plot."""
- is_implicit = True
- use_cm = False
- _N = 100
- def __init__(self, expr, var_start_end_x, var_start_end_y, label="", **kwargs):
- super().__init__(**kwargs)
- self.adaptive = kwargs.get("adaptive", False)
- self.expr = expr
- self._label = str(expr) if label is None else label
- self._latex_label = latex(expr) if label is None else label
- self.ranges = [var_start_end_x, var_start_end_y]
- self.var_x, self.start_x, self.end_x = self.ranges[0]
- self.var_y, self.start_y, self.end_y = self.ranges[1]
- self._color = kwargs.get("color", kwargs.get("line_color", None))
- if self.is_interactive and self.adaptive:
- raise NotImplementedError("Interactive plot with `adaptive=True` "
- "is not supported.")
- # Check whether the depth is greater than 4 or less than 0.
- depth = kwargs.get("depth", 0)
- if depth > 4:
- depth = 4
- elif depth < 0:
- depth = 0
- self.depth = 4 + depth
- self._post_init()
- @property
- def expr(self):
- if self.adaptive:
- return self._adaptive_expr
- return self._non_adaptive_expr
- @expr.setter
- def expr(self, expr):
- self._block_lambda_functions(expr)
- # these are needed for adaptive evaluation
- expr, has_equality = self._has_equality(sympify(expr))
- self._adaptive_expr = expr
- self.has_equality = has_equality
- self._label = str(expr)
- self._latex_label = latex(expr)
- if isinstance(expr, (BooleanFunction, Ne)) and (not self.adaptive):
- self.adaptive = True
- msg = "contains Boolean functions. "
- if isinstance(expr, Ne):
- msg = "is an unequality. "
- warnings.warn(
- "The provided expression " + msg
- + "In order to plot the expression, the algorithm "
- + "automatically switched to an adaptive sampling."
- )
- if isinstance(expr, BooleanFunction):
- self._non_adaptive_expr = None
- self._is_equality = False
- else:
- # these are needed for uniform meshing evaluation
- expr, is_equality = self._preprocess_meshgrid_expression(expr, self.adaptive)
- self._non_adaptive_expr = expr
- self._is_equality = is_equality
- @property
- def line_color(self):
- return self._color
- @line_color.setter
- def line_color(self, v):
- self._color = v
- color = line_color
- def _has_equality(self, expr):
- # Represents whether the expression contains an Equality, GreaterThan
- # or LessThan
- has_equality = False
- def arg_expand(bool_expr):
- """Recursively expands the arguments of an Boolean Function"""
- for arg in bool_expr.args:
- if isinstance(arg, BooleanFunction):
- arg_expand(arg)
- elif isinstance(arg, Relational):
- arg_list.append(arg)
- arg_list = []
- if isinstance(expr, BooleanFunction):
- arg_expand(expr)
- # Check whether there is an equality in the expression provided.
- if any(isinstance(e, (Equality, GreaterThan, LessThan)) for e in arg_list):
- has_equality = True
- elif not isinstance(expr, Relational):
- expr = Equality(expr, 0)
- has_equality = True
- elif isinstance(expr, (Equality, GreaterThan, LessThan)):
- has_equality = True
- return expr, has_equality
- def __str__(self):
- f = lambda t: float(t) if len(t.free_symbols) == 0 else t
- return self._str_helper(
- "Implicit expression: %s for %s over %s and %s over %s") % (
- str(self._adaptive_expr),
- str(self.var_x),
- str((f(self.start_x), f(self.end_x))),
- str(self.var_y),
- str((f(self.start_y), f(self.end_y))),
- )
- def get_data(self):
- """Returns numerical data.
- Returns
- =======
- If the series is evaluated with the `adaptive=True` it returns:
- interval_list : list
- List of bounding rectangular intervals to be postprocessed and
- eventually used with Matplotlib's ``fill`` command.
- dummy : str
- A string containing ``"fill"``.
- Otherwise, it returns 2D numpy arrays to be used with Matplotlib's
- ``contour`` or ``contourf`` commands:
- x_array : np.ndarray
- y_array : np.ndarray
- z_array : np.ndarray
- plot_type : str
- A string specifying which plot command to use, ``"contour"``
- or ``"contourf"``.
- """
- if self.adaptive:
- data = self._adaptive_eval()
- if data is not None:
- return data
- return self._get_meshes_grid()
- def _adaptive_eval(self):
- """
- References
- ==========
- .. [1] Jeffrey Allen Tupper. Reliable Two-Dimensional Graphing Methods for
- Mathematical Formulae with Two Free Variables.
- .. [2] Jeffrey Allen Tupper. Graphing Equations with Generalized Interval
- Arithmetic. Master's thesis. University of Toronto, 1996
- """
- import sympy.plotting.intervalmath.lib_interval as li
- user_functions = {}
- printer = IntervalMathPrinter({
- 'fully_qualified_modules': False, 'inline': True,
- 'allow_unknown_functions': True,
- 'user_functions': user_functions})
- keys = [t for t in dir(li) if ("__" not in t) and (t not in ["import_module", "interval"])]
- vals = [getattr(li, k) for k in keys]
- d = dict(zip(keys, vals))
- func = lambdify((self.var_x, self.var_y), self.expr, modules=[d], printer=printer)
- data = None
- try:
- data = self._get_raster_interval(func)
- except NameError as err:
- warnings.warn(
- "Adaptive meshing could not be applied to the"
- " expression, as some functions are not yet implemented"
- " in the interval math module:\n\n"
- "NameError: %s\n\n" % err +
- "Proceeding with uniform meshing."
- )
- self.adaptive = False
- except TypeError:
- warnings.warn(
- "Adaptive meshing could not be applied to the"
- " expression. Using uniform meshing.")
- self.adaptive = False
- return data
- def _get_raster_interval(self, func):
- """Uses interval math to adaptively mesh and obtain the plot"""
- np = import_module('numpy')
- k = self.depth
- interval_list = []
- sx, sy = [float(t) for t in [self.start_x, self.start_y]]
- ex, ey = [float(t) for t in [self.end_x, self.end_y]]
- # Create initial 32 divisions
- xsample = np.linspace(sx, ex, 33)
- ysample = np.linspace(sy, ey, 33)
- # Add a small jitter so that there are no false positives for equality.
- # Ex: y==x becomes True for x interval(1, 2) and y interval(1, 2)
- # which will draw a rectangle.
- jitterx = (
- (np.random.rand(len(xsample)) * 2 - 1)
- * (ex - sx)
- / 2 ** 20
- )
- jittery = (
- (np.random.rand(len(ysample)) * 2 - 1)
- * (ey - sy)
- / 2 ** 20
- )
- xsample += jitterx
- ysample += jittery
- xinter = [interval(x1, x2) for x1, x2 in zip(xsample[:-1], xsample[1:])]
- yinter = [interval(y1, y2) for y1, y2 in zip(ysample[:-1], ysample[1:])]
- interval_list = [[x, y] for x in xinter for y in yinter]
- plot_list = []
- # recursive call refinepixels which subdivides the intervals which are
- # neither True nor False according to the expression.
- def refine_pixels(interval_list):
- """Evaluates the intervals and subdivides the interval if the
- expression is partially satisfied."""
- temp_interval_list = []
- plot_list = []
- for intervals in interval_list:
- # Convert the array indices to x and y values
- intervalx = intervals[0]
- intervaly = intervals[1]
- func_eval = func(intervalx, intervaly)
- # The expression is valid in the interval. Change the contour
- # array values to 1.
- if func_eval[1] is False or func_eval[0] is False:
- pass
- elif func_eval == (True, True):
- plot_list.append([intervalx, intervaly])
- elif func_eval[1] is None or func_eval[0] is None:
- # Subdivide
- avgx = intervalx.mid
- avgy = intervaly.mid
- a = interval(intervalx.start, avgx)
- b = interval(avgx, intervalx.end)
- c = interval(intervaly.start, avgy)
- d = interval(avgy, intervaly.end)
- temp_interval_list.append([a, c])
- temp_interval_list.append([a, d])
- temp_interval_list.append([b, c])
- temp_interval_list.append([b, d])
- return temp_interval_list, plot_list
- while k >= 0 and len(interval_list):
- interval_list, plot_list_temp = refine_pixels(interval_list)
- plot_list.extend(plot_list_temp)
- k = k - 1
- # Check whether the expression represents an equality
- # If it represents an equality, then none of the intervals
- # would have satisfied the expression due to floating point
- # differences. Add all the undecided values to the plot.
- if self.has_equality:
- for intervals in interval_list:
- intervalx = intervals[0]
- intervaly = intervals[1]
- func_eval = func(intervalx, intervaly)
- if func_eval[1] and func_eval[0] is not False:
- plot_list.append([intervalx, intervaly])
- return plot_list, "fill"
- def _get_meshes_grid(self):
- """Generates the mesh for generating a contour.
- In the case of equality, ``contour`` function of matplotlib can
- be used. In other cases, matplotlib's ``contourf`` is used.
- """
- np = import_module('numpy')
- xarray, yarray, z_grid = self._evaluate()
- _re, _im = np.real(z_grid), np.imag(z_grid)
- _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan
- if self._is_equality:
- return xarray, yarray, _re, 'contour'
- return xarray, yarray, _re, 'contourf'
- @staticmethod
- def _preprocess_meshgrid_expression(expr, adaptive):
- """If the expression is a Relational, rewrite it as a single
- expression.
- Returns
- =======
- expr : Expr
- The rewritten expression
- equality : Boolean
- Whether the original expression was an Equality or not.
- """
- equality = False
- if isinstance(expr, Equality):
- expr = expr.lhs - expr.rhs
- equality = True
- elif isinstance(expr, Relational):
- expr = expr.gts - expr.lts
- elif not adaptive:
- raise NotImplementedError(
- "The expression is not supported for "
- "plotting in uniform meshed plot."
- )
- return expr, equality
- def get_label(self, use_latex=False, wrapper="$%s$"):
- """Return the label to be used to display the expression.
- Parameters
- ==========
- use_latex : bool
- If False, the string representation of the expression is returned.
- If True, the latex representation is returned.
- wrapper : str
- The backend might need the latex representation to be wrapped by
- some characters. Default to ``"$%s$"``.
- Returns
- =======
- label : str
- """
- if use_latex is False:
- return self._label
- if self._label == str(self._adaptive_expr):
- return self._get_wrapped_label(self._latex_label, wrapper)
- return self._latex_label
- ##############################################################################
- # 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)
- vector_a = (x - y).astype(float)
- vector_b = (z - y).astype(float)
- 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 _set_discretization_points(kwargs, pt):
- """Allow the use of the keyword arguments ``n, n1, n2`` to
- specify the number of discretization points in one and two
- directions, while keeping back-compatibility with older keyword arguments
- like, ``nb_of_points, nb_of_points_*, points``.
- Parameters
- ==========
- kwargs : dict
- Dictionary of keyword arguments passed into a plotting function.
- pt : type
- The type of the series, which indicates the kind of plot we are
- trying to create.
- """
- replace_old_keywords = {
- "nb_of_points": "n",
- "nb_of_points_x": "n1",
- "nb_of_points_y": "n2",
- "nb_of_points_u": "n1",
- "nb_of_points_v": "n2",
- "points": "n"
- }
- for k, v in replace_old_keywords.items():
- if k in kwargs.keys():
- kwargs[v] = kwargs.pop(k)
- if pt in [LineOver1DRangeSeries, Parametric2DLineSeries,
- Parametric3DLineSeries]:
- if "n" in kwargs.keys():
- kwargs["n1"] = kwargs["n"]
- if hasattr(kwargs["n"], "__iter__") and (len(kwargs["n"]) > 0):
- kwargs["n1"] = kwargs["n"][0]
- elif pt in [SurfaceOver2DRangeSeries, ContourSeries,
- ParametricSurfaceSeries, ImplicitSeries]:
- if "n" in kwargs.keys():
- if hasattr(kwargs["n"], "__iter__") and (len(kwargs["n"]) > 1):
- kwargs["n1"] = kwargs["n"][0]
- kwargs["n2"] = kwargs["n"][1]
- else:
- kwargs["n1"] = kwargs["n2"] = kwargs["n"]
- return kwargs
|