holoviz-topics / EarthSim

Tools for working with and visualizing environmental simulations.
https://earthsim.holoviz.org
BSD 3-Clause "New" or "Revised" License
65 stars 21 forks source link

Applying @param.depends from param.ClassSelector #271

Closed kcpevey closed 5 years ago

kcpevey commented 5 years ago

I'm working on applying @param.depends in my classes. I have the following bits of working code:

class adhViz2(param.Parameterized):
    """ AdH visualization class"""
    time = param.ObjectSelector()
    result_label = param.ObjectSelector()
    projection = param.ClassSelector(class_=ccrs.Projection)
   ...
    @param.depends('time','result_label')
    def time_mesh(self):
        data_points = self.mesh_points.add_dimension(self.result_label, 0, self.model[self.result_label].sel(times=self.time).data,
                                                     vdim=True)
        return gv.TriMesh((self.tris[['v0', 'v1', 'v2']], data_points), label=self.result_label, crs=ccrs.GOOGLE_MERCATOR)

where

class projections(param.Parameterized):
    projection = param.ObjectSelector(default="UTM", objects=["Geographic", "Mercator", "UTM"], precedence=1)
    UTM_zone_hemi = param.ObjectSelector(default='North', objects=['North', 'South'], precedence=2)
    UTM_zone_num = param.Integer(52, bounds=(1, 60), precedence=3)

I can use param.depends with time and result_label because they are parameters in this class. But how would I go about adding projections.UTM_zone_num to that param.depends call since I only have the class instance? (note: this is just an example concept)

I imagine I'll need to access the parameters within the class, but given that param.depends wants a string, it doesn't seem like that's possible? @philippjfr Any advice?

philippjfr commented 5 years ago

Here is a little example that demonstrates how to do this:

class B(param.Parameterized):

    c = param.Number(default=10)

class A(param.Parameterized):

    b = param.ClassSelector(class_=B)

    @param.depends('b.c', watch=True)
    def example(self):
        print('A.example was called because b.c changed to', self.b.c)

b = B()
a = A(b=b)

b.c = 12

If you make projections a parameter of your adhViz2 class, you can listen to changes on that subobject using a special string syntax, which is basically the name of the parameter, then '.' and then the name of the parameter on the subobject. So in your example that might look like this:

class adhViz2(param.Parameterized):
    """ AdH visualization class"""
    time = param.ObjectSelector()
    result_label = param.ObjectSelector()
    projection = param.ClassSelector(class_=projections)

    @param.depends('time', 'result_label', 'projection.projection')
    def time_mesh(self):
        current_projection = self.projection.projection
        custom_crs = ...
        data_points = self.mesh_points.add_dimension(self.result_label, 0, 
        self.model[self.result_label].sel(times=self.time).data,
                                                     vdim=True)
        return gv.TriMesh((self.tris[['v0', 'v1', 'v2']], data_points), label=self.result_label, crs=custom_crs)

Your time_mesh method will now depend on the projection parameter of the projection instance that you set on adhViz2 instance's projection parameter. If you want to listen to all parameter changes on the subobject you can change that to:

    @param.depends('time', 'result_label', 'projection')

Hope that makes sense.

kcpevey commented 5 years ago

Makes sense. Those are all things I tried, but it wasn't working for me. However, I just realized that my player widget is not longer interacting with my map. I need to figure out how I managed to break that and it will (hopefully) make your suggestions work ('projection.projection')

kcpevey commented 5 years ago

I fixed the time widget. My param.depends call is successfully seeing changes on the widget and causing my method to execute. However, sometime after the construction of the panel pieces but before (or during?) visualization l'm getting this error:

Python failed with the following traceback: 
/Users/rdchlkcp/miniconda3/envs/earthsim/lib/python3.6/site-packages/pyviz_comms/__init__.py _handle_msg L264
/Users/rdchlkcp/ers/gitHub/panel/master/panel/panel/viewable.py _comm_change L410
/Users/rdchlkcp/ers/gitHub/panel/master/panel/panel/viewable.py _change_event L420
/Users/rdchlkcp/ers/gitHub/param/master/param/param/parameterized.py inner L939
/Users/rdchlkcp/ers/gitHub/param/master/param/param/parameterized.py set_param L2224
/Users/rdchlkcp/ers/gitHub/param/master/param/param/parameterized.py set_param L1077
    ValueError: 'end' is not a parameter of Time

However, I don't have Time capitilized in my code anywhere and I don't think I have and end variable anywhere either. I suspect this is coming from:
time = pn.panel(self.adh_viz, parameters=['time'], widgets={'time': pn.widgets.DiscretePlayer}, show_name=False)

philippjfr commented 5 years ago

May be an issue with the DiscretePlayer widget, I only merged that yesterday so haven't done extensive testing. Does it work if you remove the widgets={...} bit?

kcpevey commented 5 years ago

removing widgets={..} produces different errors.

  1. When I change the widget that I'm watching, it reruns the def but its not visualizing my data on the map - likely an issue with my code
  2. I'm not getting the ValueError from before
  3. If I move the time slider widget I get this for every change:
    VM4011:221 Uncaught TypeError: Cannot read property 'style' of null
    at d (<anonymous>:221:5350)
    at Object.<anonymous> (<anonymous>:221:5435)
    at <anonymous>:222:14925
    at Array.forEach (<anonymous>)
    at <anonymous>:222:14903
    at Array.forEach (<anonymous>)
    at L (<anonymous>:222:14849)
    at <anonymous>:222:16380
    at Array.forEach (<anonymous>)
    at B (<anonymous>:222:16360)
    d @ VM4011:221
    (anonymous) @ VM4011:221
    (anonymous) @ VM4011:222
    (anonymous) @ VM4011:222
    L @ VM4011:222
    (anonymous) @ VM4011:222
    B @ VM4011:222
    e @ VM4011:222
    VM4011:221 Uncaught TypeError: Cannot read property 'style' of null
    at d (<anonymous>:221:5350)
    at Object.<anonymous> (<anonymous>:221:5500)
    at <anonymous>:222:14925
    at Array.forEach (<anonymous>)
    at <anonymous>:222:14903
    at Array.forEach (<anonymous>)
    at L (<anonymous>:222:14849)
    at <anonymous>:222:15601
    at Array.forEach (<anonymous>)
    at W (<anonymous>:222:15556)

Both of the new issues feel like they are originating with my code so I'll dig into that.

kcpevey commented 5 years ago

I now suspect that I have fundamental misunderstanding of how param.depends/panel/parameters all work together. This is my class:

class viewResults(param.Parameterized):
    # objects inherited from the previous page
    load_sim_widget = param.ClassSelector(class_=model.load_adh_simulation)
    att_widget = param.ClassSelector(class_=model.attributes)
    proj_widget = param.ClassSelector(class_=projections.projections)
    select_widget = param.ClassSelector(class_=existingSimulation)

    # objects passed in from the previous page output
    adh_viz = param.ClassSelector(class_=model.adhViz2)
    adh_model = param.ClassSelector(class_=adhModel)

    # objects carried in
    wmts_widget = param.ClassSelector(class_=display_opts.wmts)

    def __init__(self, wmts_widget, **params):
        super(viewResults, self).__init__(wmts_widget=wmts_widget, **params)

    # what to pass out of this page
    @param.output()
    def output(self):
        pass

    # how to build this page
    def panel(self):
        return self.run()

    # what to do with the stuff in this page    
    @param.depends('select_widget.reservoir_level', watch=True)
    def run(self):
        # get the coorinate system 
        coord_sys = self.proj_widget.get_crs()

        directory = 'filepath/' ## hard coded!!

        proj_name = self.select_widget.project_name + str(self.select_widget.reservoir_level)

        self.adh_viz = model.adhViz2(projection=coord_sys)
        self.adh_viz.read_model(directory, project_name=proj_name, netcdf=True)
        meshes = self.adh_viz.create_animation()
        print(self.adh_viz.model.Depth.data)

        # Define function which applies colormap and color_range
        def apply_opts(obj, colormap, color_range):
            return obj.options(cmap=colormap).redim.range(**{obj.vdims[0].name: color_range})

        viz = PolyAndPointAnnotator(path_type=gv.Path, 
                    crs=ccrs.GOOGLE_MERCATOR,
                    point_columns=['depth_elevation'],
                    poly_columns=['name']
                    )

        # Apply the colormap and color range dynamically
        dynamic = hv.util.Dynamic(rasterize(meshes), operation=apply_opts, streams=[Params(cmap_opts), Params(display_range)]
                                 ).options(colorbar=True, height=600, width=600) * self.wmts_widget.view() * viz.polys * viz.points

#         time = pn.panel(self.adh_viz, parameters=['time'], widgets={'time': pn.widgets.DiscretePlayer}, show_name=False)
        time = pn.panel(self.adh_viz, parameters=['time'], show_name=False)

        hv_panel = pn.panel(dynamic)

        map_pane = pn.Column(hv_panel[0], time)

        result = pn.panel(self.adh_viz, parameters=['result_label'], show_name=False)

        disp_tab = pn.Column(self.wmts_widget, 
                      pn.Pane(cmap_opts, show_name=False),
                      pn.Pane(display_range, show_name=False),
                      result,
                      name=proj_name)  # todo removed mesh_opts temporarily

        tool_pane = pn.Tabs(("Simulation", select_widget),
                            ("unused", disp_tab))

        return pn.Row(map_pane, pn.Spacer(width=100), tool_pane)

@param.depends('select_widget.reservoir_level', watch=True) is working like it should, causing run() to execute when reservoir_level is changed. I can see from print(self.adh_viz.model.Depth.data) that it did indeed reload a different model. So the underlying data in self.adh_viz has changed properly.

A couple of things are happening that I don't understand. Once I change the reservoir_level widget (and touch the time slider to make the map refresh), the map no longer shows data (only tiles). However, if run() executed and adh_viz has the new data, why doesn't it visualize?

I felt like maybe the displayed stuff just wasn't reloading when I execute run() so I added name=proj_name to the disp_tab and sure enough, when I change reservoir_level, the displayed name does NOT change. I expected ALL of the widgets constructed inside of run() to be reconstructed, but that doesn't appear to be happening.

Can you give me any insight into all of this?

philippjfr commented 5 years ago

I suspect this is indeed a misunderstanding, by declaring watch=True you are telling it to rerun the method when the parameter changes, but since it just returns the object that change has no effect at all, it simply calls the method and then discards the output.

What you have to do instead is create another panel, that watches the changes to parameters on the run method. In your case that would look something like this:

    def panel(self):
        return pn.panel(self.run)

The panel created from self.run will now watch the parameters it depends on for changes and update the panel when it changes. You should then also remove the explicit watch=True declaration.

watch=True is meant for methods that have some side-effect, e.g. if you want the change in one parameter to trigger a change another parameter.

kcpevey commented 5 years ago

Now I can see how I was just discarding the output.

I changed it to:

    @param.depends('select_widget.reservoir_level')
    def panel(self):
        return pn.panel(self.run())

    @param.depends('select_widget.reservoir_level')
    def run(self):
         ...

And now my map has data when I change the reservoir_level, but my disp_widget still isn't updating with the proj_name which makes me think this still isn't working correctly. Do I have my param.depends correct?

philippjfr commented 5 years ago

No, two things wrong there, first of all the panel method should not have any dependencies, secondly you should not call self.run because once you call it panel cannot re-execute it when the dependencies change. So it should be:

    def panel(self):
        return pn.panel(self.run)
kcpevey commented 5 years ago

It works!!! Whew. That was painful. Thanks for your help!!