Slow update with a parameterized class and dynamic number of parameters

I have a parameterized class that creates a dynamic number of parameters. The aim of this class is to create interactive matplotlib plots inside Jupyter Notebook.

The final user should be able to use it without any knowledge of panel. So, to create an interactive plot, the user instantiate the class providing a numerical function and one or more parameters. Then, the show method should be called.

This is what I have so far, which I took inspiration from this thread.

It works, but it is terribly slow: I don’t understand what is causing this slowdown, after all I’m evaluating the function over 30 points, so it must be panel-related. I’m not willing to set throttled=True to evaluate just 30 points.

What can be done to improve it?

import param
import panel as pn
pn.extension()
import numpy as np
from matplotlib.figure import Figure

class MPL(param.Parameterized):
    check_val = param.Integer(default=0)
    
    def __init__(self, func, *parameters):
        super().__init__()
        
        # remove the previous class attributes added by the previous instances
        cls_name = type(self).__name__
        setattr(type(self), "_" + cls_name + "__params", dict())
        prev_params = [k for k in type(self).__dict__.keys() if "dyn_param_" in k]
        for p in prev_params:
            delattr(type(self), p)
        
        self.mapping = {}
        # create and attach the params to the class
        for i, p in enumerate(parameters):
            pname = "dyn_param_{}".format(i)
            # TODO: using a private method: not the smartest thing to do
            self.param._add_parameter(pname, p)
            self.param.watch(self._increment_val, pname)
            self.mapping[i] = pname
        
        self.fig = Figure()
        self.ax = self.fig.add_subplot(1, 1, 1)
        self.ax.plot([], [])
        self.ax.set_xlim(-10, 10)
        self.ax.set_ylim(-5, 5)
        self.x = np.linspace(-10, 10, 30)
        self.func = func
        self.pane = pn.pane.Matplotlib(self.fig, dpi=96)
    
    def _increment_val(self, *depends):
        self.check_val += 1
    
    def _read_parameters(self):
        readout = []
        for k, v in self.mapping.items():
            readout.append(getattr(self, v))
        return readout
    
    @param.depends("check_val")
    def _view(self):
        y = self.func(self.x, *self._read_parameters())
        self.ax.lines[0].set_data(self.x, y)
        self.pane.param.trigger("object")
        return pn.Column(self.param, self.pane)
    
    def show(self):
        return pn.Row(self._view)

f = lambda x, t: np.cos(x * t)
c = MPL(f, param.Number(default=1, softbounds=(0, 2)))
c.show()

The problem with your code is that the way it is written every time a widget is changed you are causing it to re-evaluate the _view and thereby re-render the entire UI. What I think you want is simply for it to update the pane. This can be achieved by using param.depends(..., watch=True) which will trigger an event when the parameter changes. By swapping in the _view and show method below I think you will find your app is much better behaved:

    @param.depends("check_val", watch=True)
    def _view(self):
        y = self.func(self.x, *self._read_parameters())
        self.ax.lines[0].set_data(self.x, y)
        self.pane.param.trigger("object")

    def show(self):
        return pn.Column(self.param, self.pane)
1 Like