Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
JackLilhammers opened this issue Jun 8, 2023 · 6 comments
Open

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

JackLilhammers opened this issue Jun 8, 2023 · 6 comments

Comments

@JackLilhammers
Copy link

JackLilhammers commented Jun 8, 2023

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:

  • GR, when used directly, supports exporting plots to SVG
  • When used as backend for mpl, I get the infamous: The GR backend does not support svg output error

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
Copy link
Collaborator

jheinen commented Jun 8, 2023

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
Copy link
Author

JackLilhammers commented Jun 9, 2023

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
Copy link
Collaborator

jheinen commented Jun 11, 2023

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
Copy link
Author

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
Copy link
Collaborator

jheinen commented Jun 13, 2023

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
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants