Davide-sd / sympy-plot-backends

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

missing line compared to 464 #30

Closed Chrillebon closed 10 months ago

Chrillebon commented 11 months ago

In the matplotlib backend, one line is missing when updating the ylims (compared to the xlims right above). Updated by matching the second half of the xlims "if-statement" (line 464 in backends/matplotlib.py) to the second half of the ylims "if-statement" (line 471 in backends/matplotlib.py)

Result of this missing line: 2D vector-plots with specified xlims and ylims (or inferred from data) only have their xlims updated.

Davide-sd commented 11 months ago

Hello @Chrillebon ,

thanks for creating this PR. Would you be able to share an example of the behavior that has been fixed by it?

Chrillebon commented 11 months ago

Because of the "_sal" variable in the 2D vector renderer, this line is (as far as I can tell) never relevant for commands "entirely contained within spb". However, as I am trying to build on top of your functionality (and thank you for that), I found this to be the easiest way to automatically let the xlim and ylim adjust. I will come with an example as soon as I have time again.

Chrillebon commented 10 months ago

Sorry that I never got back to you, but thanks for completing the PR. I did promise an example, so here is one. It is a bit lengthy, so I will start with a summary to give an overview of the goal:

I wanted to create a 2D vector class (a single vector, not a field) utilizing much of the code already written for the vector fields. This worked quite well, but I had trouble getting the arrow within view (adjusting the xlim and ylim). By adjusting the class to internally use the starting and end point, it would then automatically extract the right limits and have the vector within frame. All worked fine until the one missing line caused only the xlim to be updated, why I created this PR.

The first piece of code inherits from the VectorBase, but giving start and direction creates a vector. The get_data function adjusts to get start and end point instead of start and direction.

from sympy import latex
from spb.series import VectorBase
import numpy as np

class Arrow2DSeries(VectorBase):
    #Represent a vector field.

    is_vector = True
    is_slice = False
    is_streamlines = False
    is_2Dvector = True

    def __init__(self, start, direction, label=None, **kwargs):
        # Ranges must be given for VectorBase, even though they are None
        super().__init__(
            [start, direction], ranges=start.shape[-1] * [None], label=label, **kwargs
        )

        self.start = start
        self.direction = direction

        self._label = f"{start}->{direction}" if label is None else label
        self._latex_label = latex(f"{start}->{direction}") if label is None else label

        # Standard for 'use_cm' should be False
        self.use_cm = kwargs.get("use_cm", False)
        # Linked colormap using vector2d renderer
        self.use_quiver_solid_color = not self.use_cm
        # Line color needed for Mayavi
        self._line_color = kwargs.get("line_color", None)

    def __str__(self):
        # Overwrite the VectorBase __str__ as it assumes things
        # about variables that does not hold for this class.
        return self._str_helper(
            f"Arrow Series with start point {self.start}, and direction {self.direction}"
        )

    def get_data(self):
        # This format works for both MB and PB
        # Has to translate to start/end and transpose
        # such that the x,y,z lims match the arrow
        # compensation for this in arrow done in
        # quiverplot_helpers.
        start = np.array(self.start)
        end = start + np.array(self.direction)
        return np.array(
            [start, end]
        ).T

The second piece of code compensates for the get_data by updating the vector2d_helper functions, and updates the _renderersmap.

from spb import MB
from spb.backends.matplotlib.renderers.renderer import MatplotlibRenderer
from spb.backends.matplotlib.renderers.vector2d import (
    _draw_vector2d_helper as MB_draw_vector2d_helper,
    _update_vector2d_helper as MB_update_vector2d_helper,
)

def MB_draw_quiver2d_helper(renderer, data):
    start, end = data.T
    direction = end - start
    xx, yy = start
    uu, vv = direction
    return MB_draw_vector2d_helper(renderer, (xx, yy, uu, vv))

def MB_update_quiver2d_helper(renderer, data, handle):
    start, end = data.T
    direction = end - start
    xx, yy = start
    uu, vv = direction
    return MB_update_vector2d_helper(renderer, (xx, yy, uu, vv), handle)

class MBQuiver2DRenderer(MatplotlibRenderer):
    _sal = True
    draw_update_map = {MB_draw_quiver2d_helper: MB_update_quiver2d_helper}

MB.renderers_map.update({
    Arrow2DSeries: MBQuiver2DRenderer,
})

The final piece of code creates two series and adjusts the rendering_kw (for MB) such that the arrows start and end in the correct places.

from spb.utils import _instantiate_backend
from spb.functions import _set_labels

start = np.array([1,2])
dir1 = np.array([3,4])
dir2 = np.array([5,-6])

s1 = Arrow2DSeries(start, dir1, normalize=False)
s2 = Arrow2DSeries(start, dir2, normalize=False)

rendering_kw = {
    "angles": "xy",
    "scale_units": "xy",
    "scale": 1,
}
_set_labels([s1,s2], [], rendering_kw)

Here comes the problems, because as can be seen with the following line, only the xlims are updated:

B1 = _instantiate_backend(MB, s1, show=True)

This also extends to multiple series, where it is seen that the xlims are automatically adjusted as intended in the following, but the ylims are not:

B1 = _instantiate_backend(MB, s1, show=False)
B2 = _instantiate_backend(MB, s2, show=False)
(B1+B2).show()

The instance with a single series can be fixed using xlims and ylims for the kwargs, but this does not extend to multiple series, as running the following does not give the wanted result. This example also the reason why the internal start/end points are used, and why the PR (and not a workaround) was needed for this instance.

B1 = _instantiate_backend(MB, s1, show=False, xlim=[1,4], ylim=[2,6])
B2 = _instantiate_backend(MB, s2, show=False, xlim=[1,6], ylim=[-5,1])
(B1+B2).show()

So to conclude, I am not sure if it is the best way of doing this, and this was definitely not the simplest example one could have thought of, but I hope that you at least can see the problem that I was trying to resolve.

Davide-sd commented 9 months ago

Hello @Chrillebon ,

I just released a new version. There is a lot of new stuff intended to make it easier to use the module.

But I've also implemented the Arrow2DSeries and an associated plotting function. I implemented two renderers: