interview_kernel.py 11.8 KB
Newer Older
1
2
3
from sys import executable
from os.path import join
#from pathlib import Path
4
from metakernel import MetaKernel
5
6
from IPython.display import HTML, Javascript
from metakernel import IPythonKernel
7

8
9
10
# 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
11
import getpass
12
13
import matplotlib
matplotlib.use('nbagg')
14
import matplotlib.pyplot as plt
15
16
17
18
19
20
from bokeh.io import output_notebook, show, export_svgs
from bokeh.plotting import figure
from bokeh.resources import CDN
from bokeh.embed import file_html, components#, notebook_div
from bokeh.models import ColumnDataSource
#from tempfile import NamedTemporaryFile
21

22
from pde_state_machine import *
23
24
25

# This "main class" is two things: a REPL loop, by subclassing the cmd2 Cmd class
# and a state machine as given by the pytransitions package
26
class Interview(MetaKernel):
27
    implementation = 'Interview'
28
    implementation_version = '1.0'
Theresa Pollinger's avatar
Theresa Pollinger committed
29
    language = 'text'
30
31
    language_version = '0.1'
    language_info = {
Theresa Pollinger's avatar
Theresa Pollinger committed
32
        'name': 'text',
33
34
        'mimetype': 'text/plain',
        'file_extension': '.txt',
Theresa Pollinger's avatar
Theresa Pollinger committed
35
        'help_links': MetaKernel.help_links,
36
    }
37
    banner = "Interview kernel\n\n" \
38
39
40
             "Hello, " + getpass.getuser() + "! I am " + "TheInterview" + \
             ", your partial differential equations and simulations expert. " \
             "Let's set up a simulation together.\n" \
41
             "Please enter anything to start the interview."
Theresa Pollinger's avatar
Theresa Pollinger committed
42

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

51
    def __init__(self, **kwargs):
52
53

        self.state_machine = PDE_States(self.poutput, self.update_prompt, self.please_prompt)
54

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

58
        self.do_execute("%matplotlib nbagg")
59
        #plt.ion()
60
61
62
63

        # To make custom magics happen, cf. https://github.com/Calysto/metakernel
        # from IPython import get_ipython
        # from metakernel import register_ipython_magics
64
        # register_ipython_magics()
65
66
67

        self.update_prompt()
        self.poutstring = ""# to collect string output to send
68
        self.outstream_name = 'stdout'
69
70
71
72
        self.richcontent = None  # to collect rich contents (images etc)

        # bokeh notebook setup
        output_notebook()
73

74
    def poutput(self, text, outstream_name='stdout'):
75
        """Accumulate the output here"""
76
        self.poutstring += str(text) + "\n"
77
        self.outstream_name = outstream_name
78
79

    ############# input processing if not explain or undo
80
81
82
    # def do_execute(self, code, silent=False, store_history=True, user_expressions=None,
    #                allow_stdin=False):
    def do_execute_direct(self, code, silent=False):
83
        """This is where the user input enters our code"""
84

85
86
        arg = LatexNodes2Text().latex_to_text(code)

87
88
        if not self.keyword_handling(arg):
            if not self.prompt_input_handling(arg):
89
                self.state_machine.handle_state_dependent_input(arg)
90
91

        if not silent:
92
            stream_content = {'name': self.outstream_name, 'text': self.poutstring}
93
94
            self.send_response(self.iopub_socket, 'stream', stream_content)

95
96
97
98
99
100
        if self.richcontent is not None:
            # We send the display_data message with the contents.
            self.send_response(self.iopub_socket, 'display_data', self.richcontent)

            self.richcontent = None

101
        self.poutstring = ""
102
        self.outstream_name = 'stdout'
103

Theresa Pollinger's avatar
Theresa Pollinger committed
104
        return  # stream_content['text']
105

106
    def please_prompt(self, query, if_yes, if_no=None, pass_other=False):
107
108
109
110
        self.poutput(str(query) + " [y/n]? ")
        self.state_machine.prompted = True
        self.state_machine.if_yes = if_yes
        self.state_machine.if_no = if_no
111
        self.state_machine.pass_other = pass_other
112
113
114
115

    def prompt_input_handling(self, arg):
        """ 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."""
116
        if self.state_machine.prompted:
117
118
119
120
            if arg == "":
                ret = True
            else:
                try:
121
                    ret = strtobool(str(arg).strip().lower())
122
                except ValueError:
123
124
                    if self.state_machine.pass_other:
                        return False
125
                    # or use as input to callback an input processing fcn..?
126
                    self.poutput("Please answer with y/n")
127
                    return True
128
            self.state_machine.prompted = False
129
            if ret:
130
131
                if self.state_machine.if_yes is not None:
                    self.state_machine.if_yes()
132
133
            elif self.state_machine.if_no is not None:
                self.state_machine.if_no()
134
135
136
            return True
        return False

137
138
139
140
141
142
143
144
145
    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
146
147
148
149
150
151
        if arg.startswith("html"):
            self.display_html()
            return True
        if arg.startswith("plt"):
            self.display_plt()
            return True
152
153
154
        if arg.startswith("bokeh"):
            self.display_bokeh()
            return True
155
156
        return False

157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
    # 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"
174
        self.state_machine.trigger('last_state')
175
176
177
178
179
180
181

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

    def update_prompt(self):
182
        self.prompt = "(" + self.state_machine.state + ")" #TODO
183

184
185
186
187
188
    def do_shutdown(self, restart):
        self.state_machine.mmtinterface.exit_mmt()

        return super(Interview, self).do_shutdown(restart)

189
    # tab completion for empty lines
190
    def do_complete(self, code, cursor_pos):
191
        """Override of cmd2 method which completes command names both for command completion and help."""
192
193
194
195
196
197
198
199
200
201
202
203
204
        # 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]
205
        else:
206
207
208
            # Call super class method.
            super(Interview, self).do_complete(code, cursor_pos)
            return
209

210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
    def display_html(self):
        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)))

229
230
        tgview_url = join(self.state_machine.mmtinterface.serverInstance, "graphs/tgview.html?type=thgraph&graphdata=", self.state_machine.mmtinterface.URIprefix, self.state_machine.mmtinterface.namespace + "?u")

231
        othercode = """
232
233
234
235
            <iframe 
                src="https://mmt.mathhub.info/graphs/tgview.html" 
                style="width: 100%; height: 510px; border: none"
            >
236
237
            </iframe>
        """
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
        # a futile attempt to set the size
        metadata = {
                        #'text/html': {
                            'width': 1600,
                            'height': 1400
                        #}
                    }
        self.Display(HTML(othercode, metadata=metadata))

    # cf. nbviewer.jupyter.org/github/bokeh/bokeh-notebooks/blob/master/tutorial/01 - Basic Plotting.ipynb
    def display_bokeh(self):
        from exaoutput import ExaOutput, ExaRunner

        # create a new plot with default tools, using figure
        p = figure(plot_width=1000, plot_height=400)

        runner = ExaRunner(ExaOutput())
        data = runner.load_data("u")
        #source = ColumnDataSource(data=data)
        source = ColumnDataSource(data=dict(x=[], u=[]))
        source.data = source.from_df(data )#[['x', 'u']])
        source.add(data.index, 'index')

        # add a circle renderer with a size, color, and alpha
        p.circle(x='index', y='u', size=2, line_color="navy", fill_color="orange", fill_alpha=0.5, source=source)
        #show(p)

        output_notebook()
        # cf. http://bokeh.pydata.org/en/0.10.0/docs/user_guide/embed.html
        self.Display(HTML(file_html(p, CDN, "my plot")))  # show the results

        # using JS requires jupyter widgets extension
        #script, div = components(p)
        #div = notebook_div(p)
        #self.Display(Javascript(script + div))  # show the results

274
275
276

    def display_plt(self):
        # plt.ion()
277
278
279

        # plot = plt.plot([3, 8, 2, 5, 1])
        #self.Display(plot)
280
281
        # plt.show() #TODO find out why there is no comm and interactive shell - and if it should be there

282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
        # cf. http://ipython-books.github.io/16-creating-a-simple-kernel-for-jupyter/
        fig = plt.figure()
        ax = fig.add_subplot(111)
        ax.plot([3, 8, 2, 5, 1])

        # We create a PNG out of this plot.
        png = _to_png(fig)

        # and send it along as rich content
        self.richcontent = dict()

        # We prepare the response with our rich data (the plot).
        self.richcontent['source'] = 'kernel'

        # This dictionary may contain different MIME representations of the output.
        self.richcontent['data'] = {
                                        'image/png': png  #TODO error: Notebook JSON is invalid: [{'image/png': ...
                                    },
        # We can specify the image size in the metadata field.
        self.richcontent['metadata'] = {
                                            'image/png': {
                                                'width': 600,
                                                'height': 400
                                            }
                                        }
        self.poutput("image!")
308
309

if __name__ == '__main__':
310
311
312
    # from ipykernel.kernelapp import IPKernelApp
    # IPKernelApp.launch_instance(kernel_class=Interview)
    Interview.run_as_main()