Davide-sd / sympy-plot-backends

An improved plotting module for SymPy
BSD 3-Clause "New" or "Revised" License
42 stars 9 forks source link

Plot multiple function with graphics() #34

Closed L2uke closed 5 months ago

L2uke commented 5 months ago

Hello, I have tried to improve my figures by using spb and it doing an amazing job! Only recently the following problem occurred. While plotting multiple standard functions like cos(x) and sin(x) worked well, I'm running into a value error issue when plotting multiple hypergeometric functions or when combining them with other functions, e.g.

from sympy import hyper
from spb import *
c = Symbol('c')
graphics(
    line(hyper((1, 2, 3), [3, 4], c), (c,0,1)),
    line(c**4, (c,0,1)))

I get a couple of value errors (see below). Any help how to overcome this issue is very much appreciated.

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[7], line 5
      2 from sympy import hyper
      4 c = Symbol('c')
----> 5 graphics(
      6     line(hyper((1, 2, 3), [3, 4], c), (c,0,1)),
      7     line(c**4, (c,0,1)))

File /opt/conda/lib/python3.11/site-packages/spb/graphics/graphics.py:226, in graphics(aspect, axis_center, is_polar, legend, show, size, title, xlabel, ylabel, zlabel, xlim, ylim, zlim, *args, **kwargs)
    224 is_3D = any(s.is_3D for s in series)
    225 Backend = kwargs.pop("backend", TWO_D_B if is_3D else THREE_D_B)
--> 226 return _instantiate_backend(
    227     Backend, *series,
    228     aspect=aspect, axis_center=axis_center,
    229     is_polar=is_polar, legend=legend, show=show, size=size,
    230     title=title, xlim=xlim, ylim=ylim, zlim=zlim, **kwargs)

File /opt/conda/lib/python3.11/site-packages/spb/utils.py:465, in _instantiate_backend(Backend, *series, **kwargs)
    462 _validate_kwargs(p, **kwargs)
    464 if kwargs.get("show", True):
--> 465     p.show()
    466 return p

File /opt/conda/lib/python3.11/site-packages/spb/backends/matplotlib/matplotlib.py:637, in MatplotlibBackend.show(self, **kwargs)
    635 self.draw()
    636 if _show:
--> 637     self._fig.tight_layout()
    638     self.plt.show(**kwargs)
    639 else:

File /opt/conda/lib/python3.11/site-packages/matplotlib/figure.py:3544, in Figure.tight_layout(self, pad, h_pad, w_pad, rect)
   3542 previous_engine = self.get_layout_engine()
   3543 self.set_layout_engine(engine)
-> 3544 engine.execute(self)
   3545 if not isinstance(previous_engine, TightLayoutEngine) \
   3546         and previous_engine is not None:
   3547     _api.warn_external('The figure layout has changed to tight')

File /opt/conda/lib/python3.11/site-packages/matplotlib/layout_engine.py:184, in TightLayoutEngine.execute(self, fig)
    182 renderer = fig._get_renderer()
    183 with getattr(renderer, "_draw_disabled", nullcontext)():
--> 184     kwargs = get_tight_layout_figure(
    185         fig, fig.axes, get_subplotspec_list(fig.axes), renderer,
    186         pad=info['pad'], h_pad=info['h_pad'], w_pad=info['w_pad'],
    187         rect=info['rect'])
    188 if kwargs:
    189     fig.subplots_adjust(**kwargs)

File /opt/conda/lib/python3.11/site-packages/matplotlib/_tight_layout.py:266, in get_tight_layout_figure(fig, axes_list, subplotspec_list, renderer, pad, h_pad, w_pad, rect)
    261         return {}
    262     span_pairs.append((
    263         slice(ss.rowspan.start * div_row, ss.rowspan.stop * div_row),
    264         slice(ss.colspan.start * div_col, ss.colspan.stop * div_col)))
--> 266 kwargs = _auto_adjust_subplotpars(fig, renderer,
    267                                   shape=(max_nrows, max_ncols),
    268                                   span_pairs=span_pairs,
    269                                   subplot_list=subplot_list,
    270                                   ax_bbox_list=ax_bbox_list,
    271                                   pad=pad, h_pad=h_pad, w_pad=w_pad)
    273 # kwargs can be none if tight_layout fails...
    274 if rect is not None and kwargs is not None:
    275     # if rect is given, the whole subplots area (including
    276     # labels) will fit into the rect instead of the
   (...)
    280     # auto_adjust_subplotpars twice, where the second run
    281     # with adjusted rect parameters.

File /opt/conda/lib/python3.11/site-packages/matplotlib/_tight_layout.py:82, in _auto_adjust_subplotpars(fig, renderer, shape, span_pairs, subplot_list, ax_bbox_list, pad, h_pad, w_pad, rect)
     80 for ax in subplots:
     81     if ax.get_visible():
---> 82         bb += [martist._get_tightbbox_for_layout_only(ax, renderer)]
     84 tight_bbox_raw = Bbox.union(bb)
     85 tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw)

File /opt/conda/lib/python3.11/site-packages/matplotlib/artist.py:1415, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1409 """
   1410 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1411 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1412 when encountering third-party subclasses that do not support it.
   1413 """
   1414 try:
-> 1415     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1416 except TypeError:
   1417     return obj.get_tightbbox(*args, **kwargs)

File /opt/conda/lib/python3.11/site-packages/matplotlib/axes/_base.py:4408, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4405     bbox_artists = self.get_default_bbox_extra_artists()
   4407 for a in bbox_artists:
-> 4408     bbox = a.get_tightbbox(renderer)
   4409     if (bbox is not None
   4410             and 0 < bbox.width < np.inf
   4411             and 0 < bbox.height < np.inf):
   4412         bb.append(bbox)

File /opt/conda/lib/python3.11/site-packages/matplotlib/legend.py:1026, in Legend.get_tightbbox(self, renderer)
   1024 def get_tightbbox(self, renderer=None):
   1025     # docstring inherited
-> 1026     return self._legend_box.get_window_extent(renderer)

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:400, in OffsetBox.get_window_extent(self, renderer)
    398 if renderer is None:
    399     renderer = self.figure._get_renderer()
--> 400 bbox = self.get_bbox(renderer)
    401 try:  # Some subclasses redefine get_offset to take no args.
    402     px, py = self.get_offset(bbox, renderer)

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:367, in OffsetBox.get_bbox(self, renderer)
    365 def get_bbox(self, renderer):
    366     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 367     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    368     return bbox

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:485, in VPacker._get_bbox_and_child_offsets(self, renderer)
    482         if isinstance(c, PackerBase) and c.mode == "expand":
    483             c.set_width(self.width)
--> 485 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    486 (x0, x1), xoffsets = _get_aligned_offsets(
    487     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    488 height, yoffsets = _get_packed_offsets(
    489     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:485, in <listcomp>(.0)
    482         if isinstance(c, PackerBase) and c.mode == "expand":
    483             c.set_width(self.width)
--> 485 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    486 (x0, x1), xoffsets = _get_aligned_offsets(
    487     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    488 height, yoffsets = _get_packed_offsets(
    489     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:367, in OffsetBox.get_bbox(self, renderer)
    365 def get_bbox(self, renderer):
    366     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 367     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    368     return bbox

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:512, in HPacker._get_bbox_and_child_offsets(self, renderer)
    509 pad = self.pad * dpicor
    510 sep = self.sep * dpicor
--> 512 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    513 if not bboxes:
    514     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:512, in <listcomp>(.0)
    509 pad = self.pad * dpicor
    510 sep = self.sep * dpicor
--> 512 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    513 if not bboxes:
    514     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:367, in OffsetBox.get_bbox(self, renderer)
    365 def get_bbox(self, renderer):
    366     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 367     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    368     return bbox

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:485, in VPacker._get_bbox_and_child_offsets(self, renderer)
    482         if isinstance(c, PackerBase) and c.mode == "expand":
    483             c.set_width(self.width)
--> 485 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    486 (x0, x1), xoffsets = _get_aligned_offsets(
    487     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    488 height, yoffsets = _get_packed_offsets(
    489     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:485, in <listcomp>(.0)
    482         if isinstance(c, PackerBase) and c.mode == "expand":
    483             c.set_width(self.width)
--> 485 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    486 (x0, x1), xoffsets = _get_aligned_offsets(
    487     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    488 height, yoffsets = _get_packed_offsets(
    489     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:367, in OffsetBox.get_bbox(self, renderer)
    365 def get_bbox(self, renderer):
    366     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 367     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    368     return bbox

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:512, in HPacker._get_bbox_and_child_offsets(self, renderer)
    509 pad = self.pad * dpicor
    510 sep = self.sep * dpicor
--> 512 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    513 if not bboxes:
    514     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:512, in <listcomp>(.0)
    509 pad = self.pad * dpicor
    510 sep = self.sep * dpicor
--> 512 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    513 if not bboxes:
    514     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File /opt/conda/lib/python3.11/site-packages/matplotlib/offsetbox.py:801, in TextArea.get_bbox(self, renderer)
    796 def get_bbox(self, renderer):
    797     _, h_, d_ = renderer.get_text_width_height_descent(
    798         "lp", self._text._fontproperties,
    799         ismath="TeX" if self._text.get_usetex() else False)
--> 801     bbox, info, yd = self._text._get_layout(renderer)
    802     w, h = bbox.size
    804     self._baseline_transform.clear()

File /opt/conda/lib/python3.11/site-packages/matplotlib/text.py:386, in Text._get_layout(self, renderer)
    384 clean_line, ismath = self._preprocess_math(line)
    385 if clean_line:
--> 386     w, h, d = _get_text_metrics_with_cache(
    387         renderer, clean_line, self._fontproperties,
    388         ismath=ismath, dpi=self.figure.dpi)
    389 else:
    390     w = h = d = 0

File /opt/conda/lib/python3.11/site-packages/matplotlib/text.py:97, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     94 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     95 # Cached based on a copy of fontprop so that later in-place mutations of
     96 # the passed-in argument do not mess up the cache.
---> 97 return _get_text_metrics_with_cache_impl(
     98     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File /opt/conda/lib/python3.11/site-packages/matplotlib/text.py:105, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
    101 @functools.lru_cache(4096)
    102 def _get_text_metrics_with_cache_impl(
    103         renderer_ref, text, fontprop, ismath, dpi):
    104     # dpi is unused, but participates in cache invalidation (via the renderer).
--> 105     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File /opt/conda/lib/python3.11/site-packages/matplotlib/backends/backend_agg.py:230, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    226     return super().get_text_width_height_descent(s, prop, ismath)
    228 if ismath:
    229     ox, oy, width, height, descent, font_image = \
--> 230         self.mathtext_parser.parse(s, self.dpi, prop)
    231     return width, height, descent
    233 font = self._prepare_font(prop)

File /opt/conda/lib/python3.11/site-packages/matplotlib/mathtext.py:226, in MathTextParser.parse(self, s, dpi, prop)
    222 # lru_cache can't decorate parse() directly because prop
    223 # is mutable; key the cache using an internal copy (see
    224 # text._get_text_metrics_with_cache for a similar case).
    225 prop = prop.copy() if prop is not None else None
--> 226 return self._parse_cached(s, dpi, prop)

File /opt/conda/lib/python3.11/site-packages/matplotlib/mathtext.py:247, in MathTextParser._parse_cached(self, s, dpi, prop)
    244 if self._parser is None:  # Cache the parser globally.
    245     self.__class__._parser = _mathtext.Parser()
--> 247 box = self._parser.parse(s, fontset, fontsize, dpi)
    248 output = _mathtext.ship(box)
    249 if self._output_type == "vector":

File /opt/conda/lib/python3.11/site-packages/matplotlib/_mathtext.py:1995, in Parser.parse(self, s, fonts_object, fontsize, dpi)
   1992     result = self._expression.parseString(s)
   1993 except ParseBaseException as err:
   1994     # explain becomes a plain method on pyparsing 3 (err.explain(0)).
-> 1995     raise ValueError("\n" + ParseException.explain(err, 0)) from None
   1996 self._state_stack = None
   1997 self._in_subscript_or_superscript = False

ValueError: 
{{}_{3}F_{2}\left(\begin{matrix} 1, 2, 3 \\ 3, 4 \end{matrix}\middle| {c} \right)}
                  ^
ParseSyntaxException: Expected '\\right', found '\'  (at char 18), (line:1, col:19)
Error in callback <function _draw_all_if_interactive at 0x7fb3a020ea20> (for post_execute):
Davide-sd commented 5 months ago

Hello @L2uke ,

I will run the code tomorrow, but looking at the error stack I believe it is caused by matplotlib not being able to render the latex on the legend of some of its entries.

You can try 'legend=False' on the graphics command, or you can manually set the 'label' for each of the line...

I definitely need to implement a fix to this behavior!

L2uke commented 5 months ago

Dear David, thank you so much for the reply!

line(hyper((1, 2, 3), [3, 4], c), (c,0,1), label="Hyper")

worked! Have a nice weekend. Luke

L2uke commented 5 months ago

Dear Davide,

maybe this is related. If I want to plot a piecewise defined hyper-function I also get an error. (Please note, if I call that with the sympy-plot then it works. Code provide after the error-message). This time, however, adding label="abs" shows no effect. Do you have an idea?

from sympy import *
from spb import *
from sympy.abc import x

g2= graphics(
    line(Piecewise((hyper((1.0331469998003, 9.67916472867169), (10.0,), x), x<0.6),
        (7,True)),(x,0,1)), label="abs")

Gives the following error:

/opt/conda/lib/python3.11/site-packages/spb/series.py:259: UserWarning: The evaluation with NumPy/SciPy failed.
NameError: name 'hyper' is not defined
Trying to evaluate the expression with Sympy, but it might be a slow operation.
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
File /opt/conda/lib/python3.11/site-packages/spb/series.py:253, in _uniform_eval(f1, f2, modules, force_real_eval, has_sum, *args)
    251 try:
    252     # attempt to use numpy/numexpr native vectorized operation
--> 253     return f1(*args)
    254 except ValueError:
    255     # attempt to use numpy/numexpr with numpy.vectorize

File <lambdifygenerated-1>:2, in _lambdifygenerated(x)
      1 def _lambdifygenerated(x):
----> 2     return select([less(x, 0.6),True], [hyper((1.0331469998003, 9.67916472867169), (10.0,), x),7], default=nan)

NameError: name 'hyper' is not defined

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
Cell In[2], line 1
----> 1 plot(Piecewise(
      2         (hyper((1.0331469998003, 9.67916472867169), (10.0,), x), x<0.6),
      3         (7,True)),(x,0,1))

File /opt/conda/lib/python3.11/site-packages/spb/plot_functions/functions_2d.py:345, in plot(*args, **kwargs)
    343 _set_labels(lines, global_labels, global_rendering_kw)
    344 gs = _create_generic_data_series(**kwargs)
--> 345 return graphics(*lines, gs, **kwargs)

File /opt/conda/lib/python3.11/site-packages/spb/graphics/graphics.py:226, in graphics(aspect, axis_center, is_polar, legend, show, size, title, xlabel, ylabel, zlabel, xlim, ylim, zlim, *args, **kwargs)
    224 is_3D = any(s.is_3D for s in series)
    225 Backend = kwargs.pop("backend", TWO_D_B if is_3D else THREE_D_B)
--> 226 return _instantiate_backend(
    227     Backend, *series,
    228     aspect=aspect, axis_center=axis_center,
    229     is_polar=is_polar, legend=legend, show=show, size=size,
    230     title=title, xlim=xlim, ylim=ylim, zlim=zlim, **kwargs)

File /opt/conda/lib/python3.11/site-packages/spb/utils.py:465, in _instantiate_backend(Backend, *series, **kwargs)
    462 _validate_kwargs(p, **kwargs)
    464 if kwargs.get("show", True):
--> 465     p.show()
    466 return p

File /opt/conda/lib/python3.11/site-packages/spb/backends/matplotlib/matplotlib.py:635, in MatplotlibBackend.show(self, **kwargs)
    626 def show(self, **kwargs):
    627     """Display the current plot.
    628 
    629     Parameters
   (...)
    633         Keyword arguments to be passed to plt.show().
    634     """
--> 635     self.draw()
    636     if _show:
    637         self._fig.tight_layout()

File /opt/conda/lib/python3.11/site-packages/spb/backends/matplotlib/matplotlib.py:622, in MatplotlibBackend.draw(self)
    618 # create the figure from scratch every time, otherwise if the plot was
    619 # previously shown, it would not be possible to show it again. This
    620 # behaviour is specific to Matplotlib
    621 self._create_figure()
--> 622 self._process_renderers()

File /opt/conda/lib/python3.11/site-packages/spb/backends/matplotlib/matplotlib.py:340, in MatplotlibBackend._process_renderers(self)
    338 for r, s in zip(self.renderers, self.series):
    339     self._check_supported_series(r, s)
--> 340     r.draw()
    341     xlims.extend(r._xlims)
    342     ylims.extend(r._ylims)

File /opt/conda/lib/python3.11/site-packages/spb/backends/matplotlib/renderers/line2d.py:113, in Line2DRenderer.draw(self)
    112 def draw(self):
--> 113     data = self.series.get_data()
    115     # NOTE: matplotlib's is able to deal with axis limits when
    116     # LineCollection is present, so no need to compute them here.
    117     if not self.series.is_2Dline:

File /opt/conda/lib/python3.11/site-packages/spb/series.py:1161, in Line2DBaseSeries.get_data(self)
   1140 """Return coordinates for plotting the line.
   1141 
   1142 Returns
   (...)
   1158     corresponding interactive series).
   1159 """
   1160 np = import_module('numpy')
-> 1161 points = self._get_data_helper()
   1163 if (
   1164     isinstance(self, LineOver1DRangeSeries) and
   1165     (self.detect_poles == "symbolic")
   1166 ):
   1167     poles = _detect_poles_symbolic_helper(
   1168         self.expr.subs(self.params), *self.ranges[0])

File /opt/conda/lib/python3.11/site-packages/spb/series.py:1520, in LineOver1DRangeSeries._get_data_helper(self)
   1516 """Returns coordinates that needs to be postprocessed.
   1517 """
   1518 np = import_module('numpy')
-> 1520 x, _re, _im = self._get_real_imag()
   1522 if self._return is None:
   1523     # The evaluation could produce complex numbers. Set real elements
   1524     # to NaN where there are non-zero imaginary elements
   1525     _re[np.invert(np.isclose(_im, np.zeros_like(_im)))] = np.nan

File /opt/conda/lib/python3.11/site-packages/spb/series.py:1513, in LineOver1DRangeSeries._get_real_imag(self)
   1511 if self.adaptive:
   1512     return self._adaptive_sampling()
-> 1513 return self._uniform_sampling()

File /opt/conda/lib/python3.11/site-packages/spb/series.py:1500, in LineOver1DRangeSeries._uniform_sampling(self)
   1497 def _uniform_sampling(self):
   1498     np = import_module('numpy')
-> 1500     x, result = self._evaluate()
   1501     _re, _im = np.real(result), np.imag(result)
   1502     _re = self._correct_shape(_re, x)

File /opt/conda/lib/python3.11/site-packages/spb/series.py:591, in BaseSeries._evaluate(self, cast_to_real)
    589 results = []
    590 for f in self._functions:
--> 591     r = _uniform_eval(*f, *args, modules=self.modules)
    592     # the evaluation might produce an int/float. Need this correction.
    593     r = self._correct_shape(np.array(r), discr[0])

File /opt/conda/lib/python3.11/site-packages/spb/series.py:260, in _uniform_eval(f1, f2, modules, force_real_eval, has_sum, *args)
    257     except Exception as err:
    258         # fall back to sympy
    259         _msg(err)
--> 260         return _eval_with_sympy()
    262 try:
    263     # any other module attempts to use numpy.vectorize
    264     return wrapper_func(f1, *args)

File /opt/conda/lib/python3.11/site-packages/spb/series.py:235, in _uniform_eval.<locals>._eval_with_sympy(err)
    230 if f2 is None:
    231     raise RuntimeError(
    232         "Impossible to evaluate the provided numerical function "
    233         "because there is no fall-back numerical function to "
    234         "be evaluated with SymPy.")
--> 235 return wrapper_func(f2, *args)

File /opt/conda/lib/python3.11/site-packages/numpy/lib/function_base.py:2329, in vectorize.__call__(self, *args, **kwargs)
   2326     vargs = [args[_i] for _i in inds]
   2327     vargs.extend([kwargs[_n] for _n in names])
-> 2329 return self._vectorize_call(func=func, args=vargs)

File /opt/conda/lib/python3.11/site-packages/numpy/lib/function_base.py:2412, in vectorize._vectorize_call(self, func, args)
   2409 # Convert args to object arrays first
   2410 inputs = [asanyarray(a, dtype=object) for a in args]
-> 2412 outputs = ufunc(*inputs)
   2414 if ufunc.nout == 1:
   2415     res = asanyarray(outputs, dtype=otypes[0])

File /opt/conda/lib/python3.11/site-packages/spb/series.py:220, in _uniform_eval.<locals>.wrapper_func(func, *args)
    218 def wrapper_func(func, *args):
    219     try:
--> 220         return complex(func(*args))
    221     except (ZeroDivisionError, OverflowError):
    222         return complex(np.nan, np.nan)

File <lambdifygenerated-2>:2, in _lambdifygenerated(Dummy_36)
      1 def _lambdifygenerated(Dummy_36):
----> 2     return ((hyper((1.0331469998003, 9.67916472867169), (10.0,), Dummy_36)) if (Dummy_36 < 0.6) else (7))

TypeError: '<' not supported between instances of 'complex' and 'float'

The same happened when I switch to

plot(Piecewise(
        (hyper((1.0331469998003, 9.67916472867169), (10.0,), x), x<0.6),
        (7,True)),(x,0,1))

Here is the code for the simply-plot which works

from sympy import *
from sympy.plotting import plot
from sympy.abc import x
plot(Piecewise(
        (hyper((1.0331469998003, 9.67916472867169), (10.0,), x), x<0.6),
        (7,True)),(x,0,1))
Davide-sd commented 5 months ago

By default, spb is going to evaluate the lambdified expression with complex numbers. In this case, the lambdified expression contains comparisons with <: for example, the left hand side evaluates to a complex number, while the right hand side is a float. This comparison doesn't make mathematical sense, hence it raises the error.

In these cases we have to force the evaluation to use real numbers with force_real_eval=True. Here is how:

from sympy import *
from spb import *
from sympy.abc import x

g2= graphics(
    line(Piecewise((hyper((1.0331469998003, 9.67916472867169), (10.0,), x), x<0.6),
        (7,True)),(x,0,1), force_real_eval=True, detect_poles=True), label="abs")

image

I'll save this example into my todo list. I think it's possible to make the module detect these cases and automatically switch to real evaluation... Thanks for pointing this out :D

Davide-sd commented 5 months ago

New version is out, fixing both problems highlighted in this issue.

from sympy import *
from spb import *
c = Symbol('c')
graphics(
    line(hyper((1, 2, 3), [3, 4], c), (c,0,1)),
    line(c**4, (c,0,1)))

This will show a warning on the screen:

/home/davide/Documents/Development/sympy_plot_backends/spb/series.py:648: UserWarning: NumPy is unable to evaluate with complex numbers some of the functions included in this symbolic expression: [hyper]. 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.
/home/davide/Documents/Development/sympy_plot_backends/spb/series.py:259: UserWarning: The evaluation with NumPy/SciPy failed.
NameError: name 'hyper' is not defined
Trying to evaluate the expression with Sympy, but it might be a slow operation.
/home/davide/Documents/Development/sympy_plot_backends/spb/backends/matplotlib/matplotlib.py:647: UserWarning: The picture could not be shown. The following error was raised:
ValueError: 
{{}_{3}F_{2}\left(\begin{matrix} 1, 2, 3 \\ 3, 4 \end{matrix}\middle| {c} \right)}
                  ^
ParseSyntaxException: Expected '\\right', found '\'  (at char 18), (line:1, col:19)
This is probably caused by Matplotlib's inability to render a legend entry. Hence, the legend has been turned off in order to visualize the plot. If you need a legend you have to manually provide labels for each symbolic expression.

Essentially, it says the legend can't be shown, and suggests to provide appropriate labels for each symbolic expression. Nonetheless, the plot will be visible:

image

The second example automatically detects the hyper function and switches to an evaluation strategy which uses real numbers.

from sympy import *
from spb import *
from sympy.abc import x

g2= graphics(
    line(Piecewise((hyper((1.0331469998003, 9.67916472867169), (10.0,), x), x<0.6),
        (7,True)),(x,0,1)), label="abs")

This is the warning to inform users about the evaluation strategy.

/home/davide/Documents/Development/sympy_plot_backends/spb/series.py:648: UserWarning: NumPy is unable to evaluate with complex numbers some of the functions included in this symbolic expression: [hyper]. 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.

And this is the plot:

image

I'm closing this issue.