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()