sciapp / gr

GR framework: a graphics library for visualisation applications
Other
329 stars 54 forks source link

When used as backend for matplotlib, GR can't export to svg #178

Open JackLilhammers opened 1 year ago

JackLilhammers commented 1 year ago

I am developing a desktop application that utilizes Matplotlib for plotting purposes. To improve performance, I have chosen the GR framework as the backend for Matplotlib, since it's 100 times faster. (nice!)

However, I am currently facing an issue where exporting plots to SVG:

While researching a solution, I found this old closed issue: gr as backend for matplotlib: Can I save images as png? Interestingly enough, a response suggested that exporting to SVG format would be an easier alternative, indicating the potential existence of SVG export capability with GR as a backend. Did I misunderstand something?

OTOH, if this feature is missing, would it be hard to add it? What would it take to implement it? I'm willing to help, but I know nothing about matplotlib's backends...

Thank you for your attention to this matter

Update: I patched the backend to save SVGs, but it does it as slow as mpl, so I'm clearly doing something wrong...

jheinen commented 1 year ago

As mentioned in this issue, the only way to redirect the GR output is to use the GKSwstype environment variable, e.g.

GKSwstype=svg python ...

Could you provide an example how you use GR as a backend for MPL? How did you patch the backend?

JackLilhammers commented 1 year ago

I'm sorry, here some code :)

I made a test script using this example as base: https://gr-framework.org/examples/figanim.html It's quite long, so I attached it last.

You can try GR as a backend for matplotlib and/or as backend for savefig() If you run the script with -s gr or with -b gr -o it'll crash. It'll crash with the Agg backend too, because it doesn't support SVGs.

You'll notice that if you select GR as backend it's not actually used, because FigureCanvasGR doesn't have a print_svg() method, and matplotlib falls back to its SVG backend. Here's my dumb print_svg() for FigureCanvasGR

    def print_svg(self, filename, *args, **kwargs):
        gr.beginprint(filename)
        self.draw()
        gr.endprint()

This however is as slow as the default one, and I don't know why. Maybe because the slow part runs in matplotlib

To see what's the actual backend used, I added a couple of debug prints at the beginning of _switch_canvas_and_return_print_method() in backend_bases.py inside matplotlib

    def _switch_canvas_and_return_print_method(self, fmt, backend=None):
        """..."""
        canvas = None
        if backend is not None:
            print(f'using the selected backend: {backend}')
            # Return a specific canvas class, if requested.
            canvas_class = (
                importlib.import_module(cbook._backend_module_name(backend))
                .FigureCanvas)
            if not hasattr(canvas_class, f"print_{fmt}"):
                raise ValueError(
                    f"The {backend!r} backend does not support {fmt} output")
        elif hasattr(self, f"print_{fmt}"):
            print(f'using current backend: {self.__class__}')
            # Return the current canvas if it supports the requested format.
            canvas = self
            canvas_class = None  # Skip call to switch_backends.
        else:
            # Return a default canvas for the requested format, if it exists.
            canvas_class = get_registered_canvas_class(fmt)
            print(f'selected the default backend: {canvas_class}')

Here's your modified example

import sys
import os
from timeit import default_timer as timer
import numpy as np

import os
import shutil
import argparse

# env
os.environ["GKS_WSTYPE"] = 'svg'

# utility function because gr is easier to write
def backend_name(name: str|None) -> str|None:
    if name is not None:
        return name.lower() if name.lower() != 'gr' else 'module://gr.matplotlib.backend_gr'

#-------------------------------------------------------------------------------

# args
parser = argparse.ArgumentParser()

parser.add_argument(
    "-b", "--backend",
    help="Selects a backend for matplotlib",
)
parser.add_argument(
    "-s", "--savefig",
    help="Selects a backend for savefig(), can be overridden by `--override`",
)
parser.add_argument(
    "-o", "--override",
    help="The backend selected with `--backend`, if any, will be used for savefig(), "
        "otherwise it'll use the default one. "
        "If `--savefig` is passed it'll be ignored",
    action="store_true",
)

args = parser.parse_args()

backend = backend_name(args.backend)
if backend is not None:
    os.environ["MPLBACKEND"] = backend

savefig = backend_name(args.savefig)

override = bool(args.override)

#-------------------------------------------------------------------------------

# output folder
PLOTS_PATH = 'plots'
shutil.rmtree(PLOTS_PATH, ignore_errors=True)
os.mkdir(PLOTS_PATH)
os.chdir(PLOTS_PATH)

#-------------------------------------------------------------------------------

x = np.arange(0, 2 * np.pi, 0.01)

# create an animation using GR

from gr.pygr import plot

tstart = timer()
for i in range(1, 100):
    plot(x, np.sin(x + i / 10.0))
    if i % 2 == 0:
        print('.', end="")
        sys.stdout.flush()

fps_gr = int(100 / (timer() - tstart))
print('fps  (GR): %4d' % fps_gr)

# create the same animation using matplotlib

import matplotlib
import matplotlib.pyplot as plt

# the current backend, just to be sure
print(f'matplotlib is using: {matplotlib.get_backend()}')

# choose a backend for savefig
# if is overridden uses backend
if not override:
    if savefig is not None:
        # user selected
        backend = savefig
    else:
        # default
        backend = None

tstart = timer()
for i in range(1, 100):
    plt.clf()
    plt.plot(np.sin(x + i / 10.0))
    plt.savefig(f'mpl{i:04d}.svg', backend=backend)
    if i % 2 == 0:
        print('.', end="")
        sys.stdout.flush()

fps_mpl = int(100 / (timer() - tstart))
print('fps (mpl): %4d' % fps_mpl)

print('  speedup: %6.1f' % (float(fps_gr) / fps_mpl))
jheinen commented 1 year ago

The plain GR example works fine:

from timeit import default_timer as timer
import numpy as np
from gr.pygr.mlab import plot, savefig

x = np.arange(0, 2 * np.pi, 0.01)

tstart = timer()
for i in range(100):
    plot(x, np.sin(x + i / 10.0))
    savefig(f'gr{i:04d}.svg')

fps_gr = int(100 / (timer() - tstart))
print('fps (gr): %4d' % fps_gr)

Therefore I have to assume that something is suboptimally implemented in Matplotlib. I'll try to find out tomorrow what the problem is.

JackLilhammers commented 1 year ago

Did you find anything? Can I help you in some way? I profiled the execution and indeed it's matplotlib's drawing functions that are slow, but I don't know enough to understand what happens

image

This is the full log of cProfile gr_backend.log

jheinen commented 1 year ago

After tests with cProfile I can unfortunately only confirm that most of the time is spent in the Python savefig part. Optimizing the GR wrapper would not help until the root cause is found.

JackLilhammers commented 1 year ago

We'll try something else. Anyway, thank you for your time!