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
    # from ipykernel.kernelapp import IPKernelApp
    # IPKernelApp.launch_instance(kernel_class=Interview)
323
    Interview.run_as_main()