kernel.py 10 KB
Newer Older
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
1
'''Biocham kernel wrapper'''
2
from subprocess import check_output
3
from bisect import bisect_left
4
from signal import SIGINT
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
5
import re
6
from os import getenv
dcoudrin's avatar
dcoudrin committed
7
from decimal import Decimal
8

9
from ipykernel.ipkernel import IPythonKernel
10
from pexpect import replwrap, exceptions
11
from IPython.core.display import display
12
13
import ipywidgets as widgets
from traitlets import Float
14

SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
15
from .images import extract_image_filenames, display_data_for_image
16
from .commands import commands
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
17

18

19
class BiochamKernel(IPythonKernel):
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
20
    '''kernel class wrapping the Biocham REPL'''
21
22
23
24
25
26
27
    implementation = 'Biocham'
    implementation_version = '0.1.0'
    language = 'biocham'
    language_info = {
        'name': 'biocham',
        'mimetype': 'text/plain',
        'file_extension': '.bc',
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
28
        'pygments_lexer': 'prolog',
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
29
        'codemirror_mode': 'biocham',
30
    }
SOLIMAN Sylvain's avatar
pathes    
SOLIMAN Sylvain committed
31
    banner = check_output(['biocham', '--version']).decode('utf-8')
32
    language_version = banner.split()[1]
33
    help_links = [{
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
34
        'text': 'Biocham Manual',
35
36
        'url': 'http://lifeware.inria.fr/biocham4/doc/'
    }]
37
    shell = None
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
38
    command_sep = re.compile(r'\.\s+')
39
    timeout = Float(getenv('BIOCHAM_TIMEOUT', 120))
40

41
42
43
44
45
46
47
48
49
50
    # KernelBase actually increases the execution count but IPythonKernel
    # overrides this using self.shell.execution_count (which we don't have)
    @property
    def execution_count(self):
        return self._execution_count

    @execution_count.setter
    def execution_count(self, value):
        self._execution_count = value

51
    def __init__(self, **kwargs):
dcoudrin's avatar
dcoudrin committed
52
        super(BiochamKernel, self).__init__(**kwargs)
53
        self._execution_count = 0
54
55
56
        self._start_biocham()

    def _start_biocham(self):
57
        """Start the Biocham process"""
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
58
        self.biocham = replwrap.REPLWrapper(
SOLIMAN Sylvain's avatar
pathes    
SOLIMAN Sylvain committed
59
            'biocham --jupyter',
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
60
            'biocham: ',
61
62
            None,
            continuation_prompt='|:'
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
63
        )
64
65
        # log = open('foo.log', 'w')
        # self.biocham.child.logfile = log
66
67
68

    def do_execute(self, code, silent, store_history=True,
                   user_expressions=None, allow_stdin=False):
69
        payload, image_filenames = [], []
70
        status = 'ok'
71

72
        # inform IPython users that biocham does not support shell kernel
73
        if code.startswith('!'):
74
75
            output = 'No system kernel available.'

SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
76
77
        elif code.startswith('%') and \
                not code.startswith('% ') and '\n' not in code:
78
            output, payload = self._do_magic(code[1:])
79

80
81
82
83
        else:
            code = '\n'.join([line.strip() for line in code.splitlines() if
                              line.strip()])
            try:
84
85
86
87
88
89
90
91
                if code:
                    output = self.biocham.run_command(code,
                                                      timeout=self.timeout)
                else:
                    output = ''
                image_filenames, output = extract_image_filenames(output)
                if output.startswith('ERROR:'):
                    status = 'error'
92
            except exceptions.TIMEOUT:
93
94
                self.biocham.child.kill(SIGINT)
                self.biocham.run_command('a')
95
                output = 'non-terminated command'
96
97
                image_filenames = []
                status = 'error'
98

99
100
101
        if image_filenames:
            for filename in image_filenames:
                try:
102
                    display_data_for_image(filename)
103
104
105
106
                except ValueError as excpt:
                    message = {'name': 'stdout', 'text': str(excpt)}
                    self.send_response(self.iopub_socket, 'stream', message)
                    status = 'error'
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
107

108
109
        if not silent and output:
            if status == 'error':
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
110
111
112
113
114
115
116
117
                self.send_response(self.iopub_socket,
                                   'error',
                                   {
                                       'execution_count': self.execution_count,
                                       'ename': '',
                                       'evalue': output,
                                       'traceback': []
                                   })
118
119
120
121
122
123
124
            self.send_response(self.iopub_socket,
                               'execute_result',
                               {
                                   'execution_count': self.execution_count,
                                   'data': {'text/plain': output},
                                   'metadata': {},
                               })
125

126
        return {
127
            'status': status,
128
            'ename': '',
129
130
            # The base class increments the execution count
            'execution_count': self.execution_count,
131
            'payload': payload,
132
            'user_expressions': user_expressions,
133
        }
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
134

135
    def do_complete(self, code, cursor_pos):
136
137
        current = code[:cursor_pos]
        prefix = self.command_sep.split(current)[-1]
138
139
        return {
            'status': 'ok',
140
            'cursor_start': cursor_pos - len(prefix),
141
            'cursor_end': cursor_pos,
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
142
            'matches': _find(prefix),
143
144
        }

145
    def _do_magic(self, magic):
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
146
147
        '''handle biocham kernel %magic
        returns a pair output_string, payload'''
148
149
150
151
        if magic.startswith('load '):
            bcfile = magic[5:].split()[0]
            if not bcfile.endswith('.bc') or bcfile.startswith('.'):
                return 'Cannot read {}'.format(bcfile), []
152
153
154
155
            try:
                with open(bcfile, 'r') as bcinput:
                    lines = bcinput.read().split('.\n')
                # create a cell after current cell for each dotted command
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
156
                return 'loading {}'.format(bcfile), [{
157
158
159
160
161
                    'source': 'set_next_input',
                    'text': line.strip() + '.',
                    'replace': False,
                } for line in reversed(lines) if line.strip()]
            except FileNotFoundError:
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
162
                return "File doesn't exists", []
163

SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
164
165
166
167
        if magic.startswith('timeout '):
            self.timeout = int(magic[8:].split()[0])
            return 'Timeout set to ' + str(self.timeout), []

SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
168
        if magic.startswith('slider '):
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
169
170
171
172
            parameters = magic[7:].split()

            list_parameters = self.biocham.run_command('list_parameters.')
            current_val = []
173
174
            maximum = []
            for par in parameters:
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
175
176
                cur = re.search(r'] parameter\({}=([^)]*)\)'.format(par),
                                list_parameters)
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
177
                if cur is None or float(cur.group(1)) == 0:
178
179
                    current_val.append(float(0))
                    maximum.append(float(10))
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
180
                else:
181
                    current_val.append(float(cur.group(1)))
182
                    maximum.append(float(cur.group(1)) * 10)
183

184
185
186
            def scale(value):
                """Returns the number of decimals for a given value
                """
dcoudrin's avatar
dcoudrin committed
187
188
                # this cast is needed as otherwise precision gets wild
                value = str(value)
dcoudrin's avatar
dcoudrin committed
189
                return - Decimal(value).as_tuple().exponent
190
191
192
193

            def readout_format(value):
                f = scale(value)
                return '.{}f'.format(f)
dcoudrin's avatar
dcoudrin committed
194
195

            def step(value):
196
197
198
199
                """Returns step for a given value, cannot be inferior to 0.001
                otherwise widget will round step to 0.1
                """
                f = scale(value)
dcoudrin's avatar
dcoudrin committed
200
                if f == 0:
201
202
203
                    # if parameter value has no decimal
                    return len(value) - 1
                return float(10**-scale(value))
dcoudrin's avatar
dcoudrin committed
204

SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
205
206
207
            display_ui = [widgets.FloatSlider(
                description=par,
                value=cur,
dcoudrin's avatar
dcoudrin committed
208
209
                min=0,
                max=max_val,
dcoudrin's avatar
dcoudrin committed
210
211
                step=step(cur),
                continuous_update=False,
212
                readout_format=readout_format(cur)) for (par, cur, max_val) in
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
213
                          zip(parameters, current_val, maximum)]
214

215
216
217
218
219
220
221
222
223
            for ui_item in display_ui:
                display(ui_item)
            # show a first result as placeholder
            image_filenames, _ = extract_image_filenames(
                self.biocham.run_command('numerical_simulation. plot.'))
            handle = display_data_for_image(image_filenames[0])
            if handle is not None:
                handle, data_source = handle

224
            def on_change(change):
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
225
226
227
228
229
230
                '''observer for the changes of the slider in the notebook'''
                self.biocham.run_command(
                    'parameter({param}={value}).'.format(
                        param=change['owner'].description,
                        value=change['new']
                    ))
231
                # run a simulation with default options
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
232
233
                self.biocham.run_command('numerical_simulation.',
                                         timeout=self.timeout)
234
                # plot and get corresponding HTML output
235

236
237
                image_filenames, _ = extract_image_filenames(
                    self.biocham.run_command('plot.'))
238
                try:
239
240
241
242
                    # If slider is at max value, new max value
                    if change.new == change.owner.max:
                        change.owner.max *= 10

SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
243
                    display_data_for_image(
244
245
246
                        image_filenames[0],
                        handle, data_source
                    )
247

248
249
                except IndexError:
                    print('No plot to render.')
250

SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
251
252
            for ui_item in display_ui:
                ui_item.observe(on_change, 'value')
253

SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
254
255
256
            return '', []

        return '''The available kernel are:
257
    %lsmagic\tLists available kernel
258
    %load [file.bc]\tImport a biocham file as a notebook cell
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
259
260
    %slider <parameter> [parameter parameter]\tCreates a slider to change \
given parameter(s) in default simulation
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
261
''', []
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
262

263
264
265
266
267
268
269
270
    def do_apply(self, content, bufs, msg_id, reply_metadata):
        '''deprecated'''
        pass

    def do_clear(self):
        '''deprecated'''
        pass

SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
271

SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
272
def _find(prefix):
273
    '''find matching commands in the global command list'''
SOLIMAN Sylvain's avatar
SOLIMAN Sylvain committed
274
275
276
277
278
279
    matches = []
    index = bisect_left(commands, prefix)
    while commands[index].startswith(prefix):
        matches.append(commands[index])
        index += 1
    return matches