1###############################################################################
2# Copyright (c) Lawrence Livermore National Security, LLC and other Ascent
3# Project developers. See top-level LICENSE AND COPYRIGHT files for dates and
4# other details. No copyright assignment is required to contribute to Ascent.
5###############################################################################
6
7###############################################################################
8# file: jupyter.py
9# Purpose: Jupyter Ascent Widgets
10#
11###############################################################################
12
13import os
14import conduit
15
16try:
17    # we might not always have ipywidgets module (jupyter widgets)
18    # allow import to work even if not installed
19    import ipywidgets
20    ipywidgets_support = True
21except:
22    ipywidgets_support = False
23
24try:
25    # we might not always have the cinemasci module
26    # allow import to work even if not installed
27    import cinemasci.pynb
28    cinemasci_support = True
29except:
30    cinemasci_support = False
31
32class AscentImageSequenceViewer(object):
33    """
34    Widget that shows a sequence of images with play controls.
35    """
36    def __init__(self, image_fnames):
37        self.display_w = 400
38        self.display_h = 400
39        self.fnames = image_fnames
40        self.index = 0
41        self.data = []
42        for fname in self.fnames:
43            with open(fname, "rb") as f:
44                self.data.append(f.read())
45        self.label = ipywidgets.Label(value=self.fnames[self.index])
46        self.image = ipywidgets.Image(value=self.data[self.index],
47                                      width=self.display_w,
48                                      height=self.display_h,
49                                      format="png")
50        self.check = ipywidgets.Checkbox(value=False,
51                                      description='Show absolute file path',
52                                      disabled=False,
53                                      indent=False)
54        self.slider = ipywidgets.IntSlider()
55        self.play   = ipywidgets.Play(value=0,
56                                 min=0,
57                                 max=len(self.data)-1,
58                                 step=1,
59                                 interval=500)
60        ipywidgets.jslink((self.play, "min"), (self.slider, "min"))
61        ipywidgets.jslink((self.play, "max"), (self.slider, "max"))
62        ipywidgets.jslink((self.play, "value"), (self.slider, "value"))
63
64    def update_index(self,change):
65        self.index = change.owner.value
66        self.image.value = self.data[self.index]
67        self.label.value = self.image_path_txt()
68
69    def update_checkbox(self,change):
70        self.label.value = self.image_path_txt()
71
72    def image_path_txt(self):
73        fname = self.fnames[self.index]
74        if self.check.value is False:
75            return "File: " + fname
76        else:
77            return "File: " + os.path.abspath(fname)
78
79    def show(self):
80        if len(self.data) > 1:
81            # if we have more than one image
82            # we enabled the slider + play controls
83            v = ipywidgets.VBox([self.image,
84                                 self.label,
85                                 self.check,
86                                 self.slider,self.play])
87            # setup connections for our controls
88            self.slider.observe(self.update_index)
89            self.play.observe(self.update_index)
90        else:
91            # if we have one image, we only show
92            # the label and abs checkbox
93            v = ipywidgets.VBox([self.image,
94                                 self.check,
95                                 self.label])
96        # setup connections the check box (always in use)
97        self.check.observe(self.update_checkbox)
98        return v
99
100class AscentStatusWidget(object):
101    """
102    Simple display of info["status"]
103    """
104    def __init__(self, info):
105        style = self.detect_style(info)
106        status_title = ipywidgets.Button(description=str(info["status/message"]),
107                                         disabled=True,
108                                         layout=ipywidgets.Layout(width='100%'),
109                                         button_style=style)
110        self.main = status_title
111
112    def show(self):
113        return self.main
114
115    def detect_style(self,info):
116        msg = info["status/message"]
117        if "failed" in msg:
118            return 'danger'
119        elif  "Ascent::close" in msg:
120            return 'warning'
121        else:
122            return 'success'
123
124class AscentNodeViewer(object):
125    """
126    Shows YAML repr of a Conduit Node
127    """
128    def __init__(self, node):
129        v = "<pre>" + node.to_yaml() + "</pre>"
130        self.main = ipywidgets.HTML(value=v,
131                                    placeholder='',
132                                    description='')
133
134    def show(self):
135        return self.main
136
137
138class AscentResultsViewer(object):
139    """
140    Widget that display Ascent results from info.
141    """
142    def __init__(self, info):
143        status = AscentStatusWidget(info)
144        widget_titles  = []
145        widgets = []
146
147        # cinema databases
148        # (skipping until we have a handle on embedding cinema widgets)
149        # if cinema_support and info.has_child("extracts"):
150        #     # collect any cinema extracts
151        #     cinema_exts = conduit.Node()
152        #     cinema_paths = []
153        #     for c in info["extracts"].children():
154        #         if c.node()["type"] == "cinema":
155        #             cinema_exts.append().set(c.node())
156        #     if cinema_exts.number_of_children() > 0:
157        #         w = AscentNodeViewer(cinema_exts)
158        #         widget_titles.append("Cinema Databases")
159        #         widgets.append(w.show())
160
161        # rendered images
162        if info.has_child("images"):
163            # get fnames from images section of info
164            renders_fnames = []
165            for c in info["images"].children():
166                renders_fnames.append(c.node()["image_name"])
167            # view using image viewer widget
168            w = AscentImageSequenceViewer(renders_fnames)
169            widget_titles.append("Renders")
170            widgets.append(w.show())
171
172        # extracts
173        if info.has_child("extracts"):
174            # view results info as yaml repr
175            w = AscentNodeViewer(info["extracts"])
176            widget_titles.append("Extracts")
177            widgets.append(w.show())
178
179        # expressions
180        if info.has_child("expressions"):
181            # view results info as yaml repr
182            w = AscentNodeViewer(info["expressions"])
183            widget_titles.append("Expressions")
184            widgets.append(w.show())
185
186        # actions
187        if info.has_child("actions"):
188            # view results info as yaml repr
189            w = AscentNodeViewer(info["actions"])
190            widget_titles.append("Actions")
191            widgets.append(w.show())
192
193        # layout active widgets
194        if len(widgets) > 0:
195            # multiple widgets, group into tabs
196            tab = ipywidgets.Tab()
197            tab.children = widgets
198            for i,v in enumerate(widget_titles):
199                tab.set_title(i,v)
200            self.main = ipywidgets.VBox([tab,
201                                         status.show()],
202                                         layout=ipywidgets.Layout(overflow_x="hidden"))
203        else:
204            # only the status button
205            self.main = ipywidgets.VBox([status.show()],
206                                        layout=ipywidgets.Layout(overflow_x="hidden"))
207
208    def show(self):
209        return self.main
210
211
212class AscentViewer(object):
213    """
214    A jupyter widget ui for Ascent results.
215
216    Thanks to Tom Stitt @ LLNL who provided a great starting
217    point with his image sequence viewer widget.
218    """
219    def __init__(self,ascent):
220        self.ascent = ascent
221
222    def show(self):
223        info  = conduit.Node()
224        self.ascent.info(info)
225        if not info.has_child("status"):
226            raise Exception("ERROR: AscentViewer.show() failed: info lacks `status`")
227        msg = info["status/message"]
228        if "failed" in msg and info.has_path("status/details"):
229            # print error details to standard notebook output,
230            # this looks pretty sensible vs trying to format in a widget
231            print("[Ascent Error]")
232            print(info["status/details"])
233        if ipywidgets_support:
234            view = AscentResultsViewer(info)
235            return view.show()
236        else:
237            raise Exception("ERROR: AscentViewer.show() failed: ipywidgets missing")
238