interview_kernel.py 13.9 KB
Newer Older
1
2
from os.path import join
#from pathlib import Path
3

4
from metakernel import MetaKernel
5
6
from IPython.display import HTML, Javascript
from metakernel import IPythonKernel
7
import ipywidgets as widgets
8

9
10
11
# http://mattoc.com/python-yes-no-prompt-cli.html
# https://github.com/phfaist/pylatexenc for directly converting Latex commands to unicode
from pylatexenc.latex2text import LatexNodes2Text
12
import getpass
13
from bokeh.io import output_notebook
14

15
16
17
18
from . import pde_state_machine
#import pde_state_machine
from . import string_handling
#import string_handling
19
from distutils.util import strtobool
20

21

22
23
"""This is a Jupyter kernel derived from MetaKernel. To use it, install it with the install.py script and run 
"jupyter notebook --debug --NotebookApp.token='' " from terminal. """
24
25


26
class Interview(MetaKernel):
27

28
    implementation = 'Interview'
Theresa Pollinger's avatar
Theresa Pollinger committed
29
    implementation_version = '1.0'
Theresa Pollinger's avatar
Theresa Pollinger committed
30
    language = 'text'
Theresa Pollinger's avatar
Theresa Pollinger committed
31
    language_version = '1.0'
32
    language_info = {
Theresa Pollinger's avatar
Theresa Pollinger committed
33
        'name': 'text',
34
35
        'mimetype': 'text/plain',
        'file_extension': '.txt',
Theresa Pollinger's avatar
Theresa Pollinger committed
36
        'help_links': MetaKernel.help_links,
37
    }
Theresa Pollinger's avatar
Theresa Pollinger committed
38

39
    banner = \
40
41
"""**Hello, """ + getpass.getuser() + """! I am MoSIS 1.0, your partial differential equations and simulations tool.**
Let's set up a model and simulation.
42
43

To see a recap of what we know so far, enter `recap <optional keyword>`. 
44
To interactively visualize the current theory graph, enter `tgwiev` or `tgview mpd`. 
45
46
47
48
Otherwise, you can always answer with \LaTeX-type input.


"""
Theresa Pollinger's avatar
Theresa Pollinger committed
49
    #To get explanations, enter `explain <optional keyword>`.
50
    #You can inspect the currently loaded MMT theories under http://localhost:43397  #TODO
51

52
    def __init__(self, install_run=False, **kwargs):
53

54
        # call superclass constructor
55
56
        super(Interview, self).__init__(**kwargs)

57
58
59
        # To make custom magics happen, cf. https://github.com/Calysto/metakernel
        # from IPython import get_ipython
        # from metakernel import register_ipython_magics
60
        # register_ipython_magics()
61
62

        self.poutstring = ""# to collect string output to send
63
        self.outstream_name = 'stdout'
64

65
        self.state_machine, self.my_markdown_greeting = self.set_initial_message(install_run)
66
        self.toggle_button_counter = 0
67

68
        self.update_prompt()
69
70
        # bokeh notebook setup
        output_notebook()
71

72
73
74
    def set_initial_message(self, install_run=False):
        # set it up -- without server communication capabilities if we are just installing
        self.state_machine = pde_state_machine.PDE_States(self.poutput, self.update_prompt, self.please_prompt,
75
                                                     self.display_html, install_run, self.toggle_show_button)
76
77
78
79
80
81
82
        # already send some input to state machine, to capture initial output and have it displayed via kernel.js
        # /  not displayed in the real thing
        self.state_machine.handle_state_dependent_input("anything")   # TODO compatibility with not-notebook?
        my_markdown_greeting = Interview.banner + self.poutstring
        self.poutstring = ""
        return self.state_machine, my_markdown_greeting

83
    def poutput(self, text, outstream_name='stdout'):
84
        """Accumulate the output here"""
85
        self.poutstring += str(text) + "\n"
86
        self.outstream_name = outstream_name
87
88

    ############# input processing if not explain or undo
89
90
    # def do_execute(self, code, silent=False, store_history=True, user_expressions=None,
    #                allow_stdin=False):
91
    def do_execute_direct(self, code, silent=False, allow_stdin=True):
92
        """This is where the user input enters our code"""
93

94
        arg = string_handling.replace_times_to_cdot(LatexNodes2Text().latex_to_text(code)).strip()
95

96
97
        if not self.keyword_handling(arg):
            if not self.prompt_input_handling(arg):
98
                self.state_machine.handle_state_dependent_input(arg)
99
100

        if not silent:
101
102
103
104
105
106
107
108
109
110
111
112
            if self.outstream_name == "stderr": #TODO make errors markdown but red
                # string output
                stream_content = {'name': self.outstream_name, 'text': self.poutstring}
                self.send_response(self.iopub_socket, 'stream', stream_content)
            else:
                # for other mime types, cf. http://ipython.org/ipython-doc/stable/notebook/nbformat.html
                data_content = {"data": {
                                            "text/markdown": self.poutstring,
                                        },
                                "metadata": {}
                                }
                self.send_response(self.iopub_socket, 'display_data', data_content)
113

114
        self.poutstring = ""
115
        self.outstream_name = 'stdout'
116

Theresa Pollinger's avatar
Theresa Pollinger committed
117
        return  # stream_content['text']
118

119
    def please_prompt(self, query, if_yes, if_no=None, pass_other=False):
120
        self.poutput(str(query)) # + " [y/n]? ")
121
122
123
        self.state_machine.prompted = True
        self.state_machine.if_yes = if_yes
        self.state_machine.if_no = if_no
124
        self.state_machine.pass_other = pass_other
125
        self.display_widget()
126

127
    def prompt_input_handling(self, arg):  # TODO make this widget-ed
128
129
        """ If we asked for a yes-no answer, execute what was specified in please_prompt.
        return true if the input was handled here, and false if not."""
130
        if self.state_machine.prompted:
131
132
133
134
            if arg == "":
                ret = True
            else:
                try:
135
                    ret = strtobool(str(arg).strip().lower())
136
                except ValueError:
137
138
                    if self.state_machine.pass_other:
                        return False
139
                    # or use as input to callback an input processing fcn..?
140
                    self.poutput("Please answer with y/n")
141
                    return True
142
            self.state_machine.prompted = False
143
            if ret:
144
145
                if self.state_machine.if_yes is not None:
                    self.state_machine.if_yes()
146
147
            elif self.state_machine.if_no is not None:
                self.state_machine.if_no()
148
149
150
            return True
        return False

151
152
153
154
155
156
157
158
159
    def keyword_handling(self, arg):
        """ If keywords for special meta-functions are given,
        executes the corresponding functions and returns true if it did."""
        if arg.startswith("explain"):
            self.state_machine.explain(arg)
            return True
        if arg.startswith("recap"):
            self.state_machine.recap(arg)
            return True
160
161
        if arg.startswith("tgview"):
            self.display_tgview(arg)
162
            return True
163
164
        if arg.startswith("undo"):
            self.do_undo(arg)
165
            return True
166
167
168
        if arg.startswith("widget"):
            self.display_widget()
            return True
169
170
171
        if arg.startswith("omdoc"):
            self.poutput(self.state_machine.mmtinterface.get_omdoc_theories())
            return True
172
173
        return False

174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
    # called when user types 'explain [expression]'
    def do_explain(self, expression):
        "Explain an expression or the theoretical background to what we are currently looking for"
        if expression:
            explanation = "hello, " + expression  # TODO query flexiformal content through mmt
        else:
            explanation = 'hello'
        self.poutput(explanation)

    def help_explain(self):
        self.poutput('\n'.join(['explain [expression]',
                                'explain the expression given or the theory currently used',
                                ]))

    # called when user types 'undo'
    def do_undo(self, expression):
        "Go back to the last question"
191
        self.state_machine.trigger('last_state')
192
193
194
195
196
197
198

    def help_undo(self):
        self.poutput('\n'.join(['undo',
                                'Go back to the last question',
                                ]))

    def update_prompt(self):
199
        self.prompt = "(" + self.state_machine.state + ")" #TODO
200

201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
    def toggle_show_button(self, button_text, hidden_text):
        # have a running id to uniquely identify the texts and buttons
        self.toggle_button_counter += 1
        counter_str = str(self.toggle_button_counter)
        # use html line breaks and have html display verbatim
        hidden_text = hidden_text.replace("\n", "<br>")

        self.Display(HTML('''
                    <div id="stacktrace''' + counter_str + '''"  style="display:none;"> ''' + hidden_text + '''</div>
                    <input id="button''' + counter_str + '''" type="button" name="button''' + counter_str + '''" value="''' + button_text + '''" onclick="toggle()" />
                    <script>
                        function toggle() {
                            var elem = document.getElementById("button''' + counter_str + '''")
                            if(elem.value == "''' + button_text + '''"){
                                elem.value = "Hide";
                                document.getElementById("stacktrace''' + counter_str + '''").style.display = "block";
                            }
                            else {
                                elem.value = "''' + button_text + '''";
                                document.getElementById("stacktrace''' + counter_str + '''").style.display = "none";
                            }
                        }
                    </script>
            '''))

226
    # tab completion for empty lines
227
    def do_complete(self, code, cursor_pos):
228
        """Override of cmd2 method which completes command names both for command completion and help."""
229
230
231
232
233
234
235
236
237
238
239
240
241
        # define the "default" input for the different states we can be in
        state_dependent_default_input = {
            'greeting': 'hi',
            'dimensions': '1',
            'domain': ['Ω = [ 0 ; 1 ]'],
            'unknowns': ['u : Ω → ℝ'],
            'parameters': ['f :  ℝ → ℝ = [x: ℝ] x '],  # ['f : Ω → ℝ = [x:Ω] x ⋅ x'],
            'pdes': ['∆u = f(x)'],
            'bcs': ['u = 0'],  # ,'u (1) = x_1**2'],
            'sim': ['FD'],
        }
        if not code or state_dependent_default_input[self.state_machine.state].startswith(code):
            return state_dependent_default_input[self.state_machine.state]
242
        else:
243
244
245
            # Call super class method.
            super(Interview, self).do_complete(code, cursor_pos)
            return
246

247
248
249
    def display_html(self, code=None):

        # highlight some of the code entered and show line numbers (just to play around)
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
        #self.Display(HTML("""
        #<style type="text/css">
        #      .styled-background { background-color: #ff7; }
        #</style>
        #<script>
        #if (typeof markedText !== 'undefined') {
        #        markedText.clear();
        #}
        #IPython.notebook.select_prev()
        #var cell = IPython.notebook.get_selected_cell();
        #markedText = cell.code_mirror.markText({line: %s, col: %s},
        #                                       {line: %s, col: %s},
        #                                       {className: "styled-background"});
        #cell.show_line_numbers(1)
        #IPython.notebook.select_next()
        #</script>
        #                    """ % (1, 0, 3, 0)))
267

268
        output_notebook()
269
270
271
272
273
        if code:
            self.Display(HTML(code))

    def display_tgview(self, args=''):
        """displays the theory graph viewer as html, cf. https://github.com/UniFormal/TGView/wiki/"""
274

275
        args = args.replace("tgview", '', 1).strip()
276

277
        server_url = str(self.state_machine.mmtinterface.mmt_frontend_base_url)
278
279

        if args == '':
280
281
            url_args_dict = dict(type="pgraph",
                                 graphdata=self.state_machine.mmtinterface.namespace)
282
            # if applicable, highlight the ephemeral parts https://github.com/UniFormal/TGView/issues/25
283
            thynames = string_handling.get_recursively(self.state_machine.simdata, "theoryname")
284
285
            # if thynames:
            #    url_args_dict["highlight"] = ",".join(thynames)
Theresa Pollinger's avatar
Theresa Pollinger committed
286
287
            # for now, highlight the "persistent ephemeral" theories, cf https://github.com/UniFormal/MMT/issues/326
            url_args_dict["highlight"] = "actual*,ephemeral*,u,q,α,SHE"
288
        else:
289
            model_name = self.state_machine.generate_mpd_theories()
Theresa Pollinger's avatar
Theresa Pollinger committed
290
            if model_name is None:
291
292
                model_name = "Model"
            url_args_dict = dict(type="mpd",
Theresa Pollinger's avatar
Theresa Pollinger committed
293
294
                                 graphdata=self.state_machine.mmtinterface.namespace + "?" + model_name,
                                 highlight="MPD_pde*")
295

296
        # have the side bars go away
297
        url_args_dict["viewOnlyMode"] = "true"
298

299
        tgview_url = string_handling.build_url(server_url, "graphs/tgview.html", args_dict=url_args_dict)
300
301

        code = """
302
            <iframe 
303
                src="{}" 
304
305
                style="width: 100%; height: 510px; border: none"
            >
306
            </iframe>
307
308
309
        """.format(tgview_url)

        self.display_html(code)
Theresa Pollinger's avatar
Theresa Pollinger committed
310
        print(tgview_url)
311

312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
    def display_widget(self):
        # needs jupyter nbextension enable --py widgetsnbextension
        from IPython.display import display
        from IPython.core.formatters import IPythonDisplayFormatter
        w = widgets.ToggleButton(
            value=False,
            description='Click me',
            disabled=False,
            button_style='', # 'success', 'info', 'warning', 'danger' or ''
            tooltip='Description',
            icon='check'
        )
        f = IPythonDisplayFormatter()
        # these should all do it, but all return the same string
        #f(w) # = "ToggleButton(value=False, description='Click me', icon='check', tooltip='Description')"
        #self._ipy_formatter(w)  # = "
        #display(w) # = "
        # self.Display(w)  # = "
        widgets.ToggleButton(
            value=False,
            description='Click me',
            disabled=False,
            button_style='',  # 'success', 'info', 'warning', 'danger' or ''
            tooltip='Description',
            icon='check'
        )

339
340

if __name__ == '__main__':
341
342
    # from ipykernel.kernelapp import IPKernelApp
    # IPKernelApp.launch_instance(kernel_class=Interview)
343
    Interview.run_as_main()