interview_kernel.py 12.5 KB
Newer Older
1
2
3
from sys import executable
from os.path import join
#from pathlib import Path
4

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

10
11
12
# 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
13
import getpass
14
from bokeh.io import output_notebook
15

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

22

23
24
"""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. """
25
26


27
class Interview(MetaKernel):
28

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

40
41
42
43
44
45
46
    kernel_json = {
        "argv": [
            executable, "-m", "interview_kernel", "-f", "{connection_file}"],
        "display_name": "TheInterview",
        "language": "text",
        "name": "interview_kernel"
    }
47

48
49
50
51
52
53
    banner = \
"""**Hello, """ + getpass.getuser() + """! I am TheInterview, your partial differential equations and simulations expert.**
Let's set up a model and simulation together.

To get explanations, enter `explain <optional keyword>`. 
To see a recap of what we know so far, enter `recap <optional keyword>`. 
54
To interactively visualize the current theory graph, enter `tgwiev` or `tgview mpd`. 
55
56
57
58
Otherwise, you can always answer with \LaTeX-type input.


"""
59
    #You can inspect the currently loaded MMT theories under http://localhost:43397  #TODO
60

61
    def __init__(self, **kwargs):
62

63
        self.state_machine = pde_state_machine.PDE_States(self.poutput, self.update_prompt, self.please_prompt, self.display_html)
64

65
        # call superclass constructor
66
67
        super(Interview, self).__init__(**kwargs)

68
69
70
        # To make custom magics happen, cf. https://github.com/Calysto/metakernel
        # from IPython import get_ipython
        # from metakernel import register_ipython_magics
71
        # register_ipython_magics()
72
73
74

        self.update_prompt()
        self.poutstring = ""# to collect string output to send
75
        self.outstream_name = 'stdout'
76
77
78
79
80

        # already send some input to state machine, to capture initial output and have it displayed via kernel.js
        self.state_machine.handle_state_dependent_input("anything")   # TODO compatibility with not-notebook?
        self.my_markdown_greeting = Interview.banner + self.poutstring
        self.poutstring = ""
81
82
83

        # bokeh notebook setup
        output_notebook()
84

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

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

96
97
        arg = LatexNodes2Text().latex_to_text(code)

98
99
        if not self.keyword_handling(arg):
            if not self.prompt_input_handling(arg):
100
                self.state_machine.handle_state_dependent_input(arg)
101
102

        if not silent:
103
104
105
106
            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)
107
108
109
110
111
112
            #    data_content = {
            #                        "ename": "InterviewError",
            #                        "evalue": self.poutstring,
            #                        "traceback": [self.poutstring],
            #                    }
            #    self.send_response(self.iopub_socket, 'error', data_content)
113
114
115
116
117
118
119
120
            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)
121

122
        self.poutstring = ""
123
        self.outstream_name = 'stdout'
124

Theresa Pollinger's avatar
Theresa Pollinger committed
125
        return  # stream_content['text']
126

127
    def please_prompt(self, query, if_yes, if_no=None, pass_other=False):
128
        self.poutput(str(query)) # + " [y/n]? ")
129
130
131
        self.state_machine.prompted = True
        self.state_machine.if_yes = if_yes
        self.state_machine.if_no = if_no
132
        self.state_machine.pass_other = pass_other
133
        self.display_widget()
134

135
    def prompt_input_handling(self, arg):  # TODO make this widget-ed
136
137
        """ 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."""
138
        if self.state_machine.prompted:
139
140
141
142
            if arg == "":
                ret = True
            else:
                try:
143
                    ret = strtobool(str(arg).strip().lower())
144
                except ValueError:
145
146
                    if self.state_machine.pass_other:
                        return False
147
                    # or use as input to callback an input processing fcn..?
148
                    self.poutput("Please answer with y/n")
149
                    return True
150
            self.state_machine.prompted = False
151
            if ret:
152
153
                if self.state_machine.if_yes is not None:
                    self.state_machine.if_yes()
154
155
            elif self.state_machine.if_no is not None:
                self.state_machine.if_no()
156
157
158
            return True
        return False

159
160
161
162
163
164
165
166
167
    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
168
169
        if arg.startswith("tgview"):
            self.display_tgview(arg)
170
            return True
171
172
        if arg.startswith("undo"):
            self.do_undo(arg)
173
            return True
174
175
176
        if arg.startswith("widget"):
            self.display_widget()
            return True
177
178
        return False

179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
    # 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"
196
        self.state_machine.trigger('last_state')
197
198
199
200
201
202
203

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

    def update_prompt(self):
204
        self.prompt = "(" + self.state_machine.state + ")" #TODO
205
206

    # tab completion for empty lines
207
    def do_complete(self, code, cursor_pos):
208
        """Override of cmd2 method which completes command names both for command completion and help."""
209
210
211
212
213
214
215
216
217
218
219
220
221
        # 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]
222
        else:
223
224
225
            # Call super class method.
            super(Interview, self).do_complete(code, cursor_pos)
            return
226

227
228
229
    def display_html(self, code=None):

        # highlight some of the code entered and show line numbers (just to play around)
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
        #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)))
247

248
        output_notebook()
249
250
251
252
253
        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/"""
254

255
        args = args.replace("tgview", '', 1).strip()
256

257
        server_url = str(self.state_machine.mmtinterface.mmt_base_url)
258
259

        if args == '':
260
261
            url_args_dict = dict(type="pgraph",
                                 graphdata=self.state_machine.mmtinterface.namespace)
262
            # if applicable, highlight the ephemeral parts https://github.com/UniFormal/TGView/issues/25
263
            thynames = string_handling.get_recursively(self.state_machine.simdata, "theoryname")
264
265
            # if thynames:
            #    url_args_dict["highlight"] = ",".join(thynames)
Theresa Pollinger's avatar
Theresa Pollinger committed
266
267
            # for now, highlight the "persistent ephemeral" theories, cf https://github.com/UniFormal/MMT/issues/326
            url_args_dict["highlight"] = "actual*,ephemeral*,u,q,α,SHE"
268
        else:
269
            model_name = self.state_machine.generate_mpd_theories()
Theresa Pollinger's avatar
Theresa Pollinger committed
270
            if model_name is None:
271
272
                model_name = "Model"
            url_args_dict = dict(type="mpd",
Theresa Pollinger's avatar
Theresa Pollinger committed
273
274
                                 graphdata=self.state_machine.mmtinterface.namespace + "?" + model_name,
                                 highlight="MPD_pde*")
275

276
        # have the side bars go away
277
        url_args_dict["viewOnlyMode"] = "true"
278

279
280
        tgview_url = string_handling.build_url(server_url, "graphs/tgview.html", args_dict=url_args_dict)
        # print(tgview_url)
281
282

        code = """
283
            <iframe 
284
                src="{}" 
285
286
                style="width: 100%; height: 510px; border: none"
            >
287
            </iframe>
288
289
290
        """.format(tgview_url)

        self.display_html(code)
291

292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
    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'
        )

319
320

if __name__ == '__main__':
321
322
323
    # from ipykernel.kernelapp import IPKernelApp
    # IPKernelApp.launch_instance(kernel_class=Interview)
    Interview.run_as_main()