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

Refactor TwoPhotonSeries #241

Draft
wants to merge 24 commits into
base: master
Choose a base branch
from

Conversation

CodyCBakerPhD
Copy link
Collaborator

Inspired to test the proposal structure of #239, and replacing #230, I refactored the ophys submodule to follow the proposed workflow to show how much more readable the code is, as well as how well it can generalize to allow easy creation of subwidgets that differ only slightly in how they setup different parts of the total visualization.

The TwoPhotonSeries visualizations are a good case to use for this because they use essentially the same types of plots for planar view of both single-plane instances (3D data) and volumetric cases (4D data). Thus if any improvements are ever made to either plot, if they didn't have some type of inheritance structure those improvements would have to be duplicated in code. Also we have several project going on right now and coming up (looking at MICrONS) that use these modalities, so further enhancements are no doubt to come.

Several such improvements have been made here...

  • the addition of a rotation feature allows the user to choose a different axis of orientation
  • contrast levels can be sensitive to different source data; imshow allows either automated methods (these are just 'OK' I'd say) or manual specification of ranges
  • the contrast options were starting to add clutter to the window, so for users that aren't looking to control that (and of course, by default) these are all hidden under a 'Simplified' view toggle, which can then be expanded. This is especially useful if we decide to eventually add a faceting, or possibly downsampling, filtering, or coloration controllers to allow maximum user control over the configuration of the call to plotly.express.imshow

Both the SinglePlaneVisualization and the PlaneSliceVisualization share these new features - the only difference between them is the latter has a single extra controller to allow specification of the plane index. As such, the inherited class does as little as possible to produce the key differences.

I also tried to keep all controllers here as general as possible; only the final assembly-level ones are kept in the ophys_controllers.py as being specifically designed for use the other ophys classes. This includes the controllers which have multiple components and their own internally interacting observers, which is one of the design proposals from @h-mayorquin that would up helping a lot to reduce complexity.

Another really helpful suggestion from @h-mayorquin was the MultiController class for assembling these components and allowing an outer exposure of the attributes so we don't have to meticulously (and painfully, sometimes) recurse down through the levels of ipywidgets.Box.children to find an object we want to reference. It also makes these references more verbose, and thus the code more readable.

I also re-organized the file structure to be more readable/navigable instead of everything being in a single file. When you start piling on more visualization for neurodata types from a single modality like that, it gets hard to scroll through looking for items you might want to find; even ctrl-F or GitHub link navigation doesn't always help much.

If this is too much to review all at once, I'd be happy to break the individual controllers into modular PRs accompanied by their own tests and even documentation. Figured it might be nice to see an explicit example of proposal structure #239 in practice and how it can help us keep this project more easily manageable going forward.

Video demonstration:

video2227929494.mp4

TODO:

  • need to add tests for various modular components

Comment on lines +87 to +89
# TOD: Figure out formula for calculating by in one-shot
by_width = 2
by_height = 2
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still a TODO item

Comment on lines +21 to +50
path_ext_file = indexed_timeseries.external_file[0]
# Get Frames dimensions
tif = TiffFile(path_ext_file)
n_samples = len(tif.pages)
page = tif.pages[0]

def _add_fig_trace(img_fig: go.Figure, index):
if self.figure is None:
self.figure = go.FigureWidget(img_fig)
else:
self.figure.for_each_trace(lambda trace: trace.update(img_fig.data[0]))

def update_figure(index=0):
# Read first frame
img_fig = px.imshow(imread(path_ext_file, key=int(index)), binary_string=True)
_add_fig_trace(img_fig, index)

self.slider = widgets.IntSlider(
value=0, min=0, max=n_samples - 1, orientation="horizontal", description="TIFF index: "
)
self.controls = dict(slider=self.slider)
self.slider.observe(lambda change: update_figure(index=change.new), names="value")

update_figure()

series_name = indexed_timeseries.name
base_title = f"TwoPhotonSeries: {series_name}"
self.figure.layout.title = f"{base_title} - read from first external file"

self.children = [self.figure, self.slider]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH this entire section could also be absorbed into a minimal child class (the only thing different is where/how the data is fetched...

Might prefer to do in separate PR though, this is more than enough as is.

"""The rotation attribute of the SinglePlaneDataController cannot be attached in a modifiable state."""
if not hasattr(self, "Controller"): # First time this is called
return 0
return self.Controller.components[self.data_controller_name].components["RotationController"].rotation
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only time a reference to a nested child is necessary (due to attribute mutability relative to the button observer events)

Technically I could override those observers and remap it all to an attribute specific to these classes, but that ends up taking many more lines of code than this simple router

That said, I built the MutliController to at least have dictionary mapping capability, so you can navigate it entirely based on name references to the components (much easier than children indexes)

@CodyCBakerPhD CodyCBakerPhD marked this pull request as ready for review December 7, 2022 16:35
Comment on lines +6 to +32
class MultiController(widgets.VBox):
controller_fields: Tuple[str] = tuple()
components: Dict[str, widgets.VBox] = dict()

def __init__(self, components: list):
super().__init__()

children = list()
controller_fields = list()
self.components = {component.__class__.__name__: component for component in components}
for component in self.components.values():
# Set attributes at outermost level
for field in component.controller_fields:
controller_fields.append(field)
setattr(self, field, getattr(component, field))

# Default layout of children
if isinstance(component, widgets.Widget) and not isinstance(component, MultiController):
children.append(component)

self.children = tuple(children)
self.controller_fields = tuple(controller_fields)

self.setup_observers()

def setup_observers(self):
pass
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the distinction between Controllers and MultiControllers. I think a widget that contains multiple controllers should just be another controller.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine to have a MultiController class that automates combination of Controllers. But this class should also be a Controller.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I just need to utilize a base Controller class, which a MultiController itself is as well in every respect

from ..controllers import RotationController, ImShowController, ViewTypeController, MultiController


class FrameController(widgets.VBox):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a Controller base class.

self.children = (self.frame_slider,)


class PlaneController(widgets.VBox):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think defining controllers that contain a single widget is a bit overkill. In my mind, the point of controllers is collections of widgets (or collections of collections of widgets) in a system that is reusable.

import ipywidgets as widgets


class MultiController(widgets.VBox):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make this inherit from widgets.Box so components can be either horizontal or vertical? See

https://ipywidgets.readthedocs.io/en/7.x/examples/Widget%20Styling.html#The-VBox-and-HBox-helpers

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I started running into this on another PR as well

@CodyCBakerPhD CodyCBakerPhD marked this pull request as draft December 16, 2022 11:00
@CodyCBakerPhD
Copy link
Collaborator Author

Going to modularize certain aspects of the PR, like the controllers, into separate sub-PRs. Moving to draft for now

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

Successfully merging this pull request may close these issues.

None yet

2 participants