|
| 1 | +#!/usr/bin/env python |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | + |
| 4 | +from __future__ import unicode_literals |
| 5 | +from __future__ import absolute_import |
| 6 | + |
| 7 | +from mathics.core.expression import String, strip_context |
| 8 | +from mathics import settings |
| 9 | +from mathics.core.evaluation import Evaluation, Output |
| 10 | + |
| 11 | +from mathics.builtin.base import Builtin |
| 12 | +from mathics.core.expression import Expression, Symbol, Integer, from_python |
| 13 | + |
| 14 | +try: |
| 15 | + from ipykernel.kernelbase import Kernel |
| 16 | + _jupyter = True |
| 17 | +except ImportError: |
| 18 | + _jupyter = False |
| 19 | + |
| 20 | +try: |
| 21 | + from ipywidgets import IntSlider, FloatSlider, ToggleButtons, Box, DOMWidget |
| 22 | + from IPython.core.formatters import IPythonDisplayFormatter |
| 23 | + _ipywidgets = True |
| 24 | +except ImportError: |
| 25 | + # fallback to non-Manipulate-enabled build if we don't have ipywidgets installed. |
| 26 | + _ipywidgets = False |
| 27 | + |
| 28 | + |
| 29 | +""" |
| 30 | +A basic implementation of Manipulate[]. There is currently no support for Dynamic[] elements. |
| 31 | +This implementation is basically a port from ipywidget.widgets.interaction for Mathics. |
| 32 | +""" |
| 33 | + |
| 34 | +def _interactive(interact_f, kwargs_widgets): |
| 35 | + # this is a modified version of interactive() in ipywidget.widgets.interaction |
| 36 | + |
| 37 | + container = Box(_dom_classes=['widget-interact']) |
| 38 | + container.children = [w for w in kwargs_widgets if isinstance(w, DOMWidget)] |
| 39 | + |
| 40 | + def call_f(name=None, old=None, new=None): |
| 41 | + kwargs = dict((widget._kwarg, widget.value) for widget in kwargs_widgets) |
| 42 | + try: |
| 43 | + interact_f(**kwargs) |
| 44 | + except Exception as e: |
| 45 | + container.log.warn("Exception in interact callback: %s", e, exc_info=True) |
| 46 | + |
| 47 | + for widget in kwargs_widgets: |
| 48 | + widget.on_trait_change(call_f, 'value') |
| 49 | + |
| 50 | + container.on_displayed(lambda _: call_f(None, None, None)) |
| 51 | + |
| 52 | + return container |
| 53 | + |
| 54 | + |
| 55 | +class IllegalWidgetArguments(Exception): |
| 56 | + def __init__(self, var): |
| 57 | + super(IllegalWidgetArguments, self).__init__() |
| 58 | + self.var = var |
| 59 | + |
| 60 | + |
| 61 | +class JupyterWidgetError(Exception): |
| 62 | + def __init__(self, err): |
| 63 | + super(JupyterWidgetError, self).__init__() |
| 64 | + self.err = err |
| 65 | + |
| 66 | + |
| 67 | +class ManipulateParameter(Builtin): # parses one Manipulate[] parameter spec, e.g. {x, 1, 2}, see _WidgetInstantiator |
| 68 | + context = 'System`Private`' |
| 69 | + |
| 70 | + rules = { |
| 71 | + # detect x and {x, default} and {x, default, label}. |
| 72 | + 'System`Private`ManipulateParameter[{s_Symbol, r__}]': |
| 73 | + 'System`Private`ManipulateParameter[{Symbol -> s, Label -> s}, {r}]', |
| 74 | + 'System`Private`ManipulateParameter[{{s_Symbol, d_}, r__}]': |
| 75 | + 'System`Private`ManipulateParameter[{Symbol -> s, Default -> d, Label -> s}, {r}]', |
| 76 | + 'System`Private`ManipulateParameter[{{s_Symbol, d_, l_}, r__}]': |
| 77 | + 'System`Private`ManipulateParameter[{Symbol -> s, Default -> d, Label -> l}, {r}]', |
| 78 | + |
| 79 | + # detect different kinds of widgets. on the use of the duplicate key "Default ->", see _WidgetInstantiator.add() |
| 80 | + 'System`Private`ManipulateParameter[var_, {min_?RealNumberQ, max_?RealNumberQ}]': |
| 81 | + 'Join[{Type -> "Continuous", Minimum -> min, Maximum -> max, Default -> min}, var]', |
| 82 | + 'System`Private`ManipulateParameter[var_, {min_?RealNumberQ, max_?RealNumberQ, step_?RealNumberQ}]': |
| 83 | + 'Join[{Type -> "Discrete", Minimum -> min, Maximum -> max, Step -> step, Default -> min}, var]', |
| 84 | + 'System`Private`ManipulateParameter[var_, {opt_List}] /; Length[opt] > 0': |
| 85 | + 'Join[{Type -> "Options", Options -> opt, Default -> Part[opt, 1]}, var]' |
| 86 | + } |
| 87 | + |
| 88 | + |
| 89 | +def _manipulate_label(x): # gets the label that is displayed for a symbol or name |
| 90 | + if isinstance(x, String): |
| 91 | + return x.get_string_value() |
| 92 | + elif isinstance(x, Symbol): |
| 93 | + return strip_context(x.get_name()) |
| 94 | + else: |
| 95 | + return str(x) |
| 96 | + |
| 97 | + |
| 98 | +def _create_widget(widget, **kwargs): |
| 99 | + try: |
| 100 | + return widget(**kwargs) |
| 101 | + except Exception as e: |
| 102 | + raise JupyterWidgetError(str(e)) |
| 103 | + |
| 104 | + |
| 105 | +class _WidgetInstantiator(): |
| 106 | + # we do not want to have widget instances (like FloatSlider) get into the evaluation pipeline (e.g. via Expression |
| 107 | + # or Atom), since there might be all kinds of problems with serialization of these widget classes. therefore, the |
| 108 | + # elegant recursive solution for parsing parameters (like in Table[]) is not feasible here; instead, we must create |
| 109 | + # and use the widgets in one "transaction" here, without holding them in expressions or atoms. |
| 110 | + |
| 111 | + def __init__(self): |
| 112 | + self._widgets = [] # the ipywidget widgets to control the manipulated variables |
| 113 | + self._parsers = {} # lambdas to decode the widget values into Mathics expressions |
| 114 | + |
| 115 | + def add(self, expression, evaluation): |
| 116 | + expr = Expression('System`Private`ManipulateParameter', expression).evaluate(evaluation) |
| 117 | + if expr.get_head_name() != 'System`List': # if everything was parsed ok, we get a List |
| 118 | + return False |
| 119 | + # convert the rules given us by ManipulateParameter[] into a dict. note: duplicate keys |
| 120 | + # will be overwritten, the latest one wins. |
| 121 | + kwargs = {'evaluation': evaluation} |
| 122 | + for rule in expr.leaves: |
| 123 | + if rule.get_head_name() != 'System`Rule' or len(rule.leaves) != 2: |
| 124 | + return False |
| 125 | + kwargs[strip_context(rule.leaves[0].to_python()).lower()] = rule.leaves[1] |
| 126 | + widget = kwargs['type'].get_string_value() |
| 127 | + del kwargs['type'] |
| 128 | + getattr(self, '_add_%s_widget' % widget.lower())(**kwargs) # create the widget |
| 129 | + return True |
| 130 | + |
| 131 | + def get_widgets(self): |
| 132 | + return self._widgets |
| 133 | + |
| 134 | + def build_callback(self, callback): |
| 135 | + parsers = self._parsers |
| 136 | + |
| 137 | + def new_callback(**kwargs): |
| 138 | + callback(**dict((name, parsers[name](value)) for (name, value) in kwargs.items())) |
| 139 | + |
| 140 | + return new_callback |
| 141 | + |
| 142 | + def _add_continuous_widget(self, symbol, label, default, minimum, maximum, evaluation): |
| 143 | + minimum_value = minimum.to_python() |
| 144 | + maximum_value = maximum.to_python() |
| 145 | + if minimum_value > maximum_value: |
| 146 | + raise IllegalWidgetArguments(symbol) |
| 147 | + else: |
| 148 | + defval = min(max(default.to_python(), minimum_value), maximum_value) |
| 149 | + widget = _create_widget(FloatSlider, value=defval, min=minimum_value, max=maximum_value) |
| 150 | + self._add_widget(widget, symbol.get_name(), lambda x: from_python(x), label) |
| 151 | + |
| 152 | + def _add_discrete_widget(self, symbol, label, default, minimum, maximum, step, evaluation): |
| 153 | + minimum_value = minimum.to_python() |
| 154 | + maximum_value = maximum.to_python() |
| 155 | + step_value = step.to_python() |
| 156 | + if minimum_value > maximum_value or step_value <= 0 or step_value > (maximum_value - minimum_value): |
| 157 | + raise IllegalWidgetArguments(symbol) |
| 158 | + else: |
| 159 | + default_value = min(max(default.to_python(), minimum_value), maximum_value) |
| 160 | + if all(isinstance(x, Integer) for x in [minimum, maximum, default, step]): |
| 161 | + widget = _create_widget(IntSlider, value=default_value, min=minimum_value, max=maximum_value, |
| 162 | + step=step_value) |
| 163 | + else: |
| 164 | + widget = _create_widget(FloatSlider, value=default_value, min=minimum_value, max=maximum_value, |
| 165 | + step=step_value) |
| 166 | + self._add_widget(widget, symbol.get_name(), lambda x: from_python(x), label) |
| 167 | + |
| 168 | + def _add_options_widget(self, symbol, options, default, label, evaluation): |
| 169 | + formatted_options = [] |
| 170 | + for i, option in enumerate(options.leaves): |
| 171 | + data = evaluation.format_all_outputs(option) |
| 172 | + formatted_options.append((data['text/plain'], i)) |
| 173 | + |
| 174 | + default_index = 0 |
| 175 | + for i, option in enumerate(options.leaves): |
| 176 | + if option.same(default): |
| 177 | + default_index = i |
| 178 | + |
| 179 | + widget = _create_widget(ToggleButtons, options=formatted_options, value=default_index) |
| 180 | + self._add_widget(widget, symbol.get_name(), lambda j: options.leaves[j], label) |
| 181 | + |
| 182 | + def _add_widget(self, widget, name, parse, label): |
| 183 | + if not widget.description: |
| 184 | + widget.description = _manipulate_label(label) |
| 185 | + widget._kwarg = name # see _interactive() above |
| 186 | + self._parsers[name] = parse |
| 187 | + self._widgets.append(widget) |
| 188 | + |
| 189 | + |
| 190 | +class ManipulateOutput(Output): |
| 191 | + def max_stored_size(self, settings): |
| 192 | + return self.output.max_stored_size(settings) |
| 193 | + |
| 194 | + def out(self, out): |
| 195 | + return self.output.out(out) |
| 196 | + |
| 197 | + def clear_output(wait=False): |
| 198 | + raise NotImplementedError |
| 199 | + |
| 200 | + def display_data(self, result): |
| 201 | + raise NotImplementedError |
| 202 | + |
| 203 | + |
| 204 | +class Manipulate(Builtin): |
| 205 | + """ |
| 206 | + <dl> |
| 207 | + <dt>'Manipulate[$expr1$, {$u$, $u_min$, $u_max$}]' |
| 208 | + <dd>interactively compute and display an expression with different values of $u$. |
| 209 | + <dt>'Manipulate[$expr1$, {$u$, $u_min$, $u_max$, $du$}]' |
| 210 | + <dd>allows $u$ to vary between $u_min$ and $u_max$ in steps of $du$. |
| 211 | + <dt>'Manipulate[$expr1$, {{$u$, $u_init$}, $u_min$, $u_max$, ...}]' |
| 212 | + <dd>starts with initial value of $u_init$. |
| 213 | + <dt>'Manipulate[$expr1$, {{$u$, $u_init$, $u_lbl$}, ...}]' |
| 214 | + <dd>labels the $u$ controll by $u_lbl$. |
| 215 | + <dt>'Manipulate[$expr1$, {$u$, {$u_1$, $u_2$, ...}}]' |
| 216 | + <dd>sets $u$ to take discrete values $u_1$, $u_2$, ... . |
| 217 | + <dt>'Manipulate[$expr1$, {$u$, ...}, {$v$, ...}, ...]' |
| 218 | + <dd>control each of $u$, $v$, ... . |
| 219 | + </dl> |
| 220 | +
|
| 221 | + >> Manipulate[N[Sin[y]], {y, 1, 20, 2}] |
| 222 | + : Manipulate[] only works inside a Jupyter notebook. |
| 223 | + = Manipulate[N[Sin[y]], {y, 1, 20, 2}] |
| 224 | +
|
| 225 | + >> Manipulate[i ^ 3, {i, {2, x ^ 4, a}}] |
| 226 | + : Manipulate[] only works inside a Jupyter notebook. |
| 227 | + = Manipulate[i ^ 3, {i, {2, x ^ 4, a}}] |
| 228 | +
|
| 229 | + >> Manipulate[x ^ y, {x, 1, 20}, {y, 1, 3}] |
| 230 | + : Manipulate[] only works inside a Jupyter notebook. |
| 231 | + = Manipulate[x ^ y, {x, 1, 20}, {y, 1, 3}] |
| 232 | +
|
| 233 | + >> Manipulate[N[1 / x], {{x, 1}, 0, 2}] |
| 234 | + : Manipulate[] only works inside a Jupyter notebook. |
| 235 | + = Manipulate[N[1 / x], {{x, 1}, 0, 2}] |
| 236 | +
|
| 237 | + >> Manipulate[N[1 / x], {{x, 1}, 0, 2, 0.1}] |
| 238 | + : Manipulate[] only works inside a Jupyter notebook. |
| 239 | + = Manipulate[N[1 / x], {{x, 1}, 0, 2, 0.1}] |
| 240 | + """ |
| 241 | + |
| 242 | + # TODO: correct in the jupyter interface but can't be checked in tests |
| 243 | + """ |
| 244 | + #> Manipulate[x, {x}] |
| 245 | + = Manipulate[x, {x}] |
| 246 | +
|
| 247 | + #> Manipulate[x, {x, 1, 0}] |
| 248 | + : 'Illegal variable range or step parameters for `x`. |
| 249 | + = Manipulate[x, {x, 1, 0}] |
| 250 | + """ |
| 251 | + |
| 252 | + attributes = ('HoldAll',) # we'll call ReleaseHold at the time of evaluation below |
| 253 | + |
| 254 | + messages = { |
| 255 | + 'jupyter': 'Manipulate[] only works inside a Jupyter notebook.', |
| 256 | + 'imathics': 'Your IMathics kernel does not seem to support all necessary operations. ' + |
| 257 | + 'Please check that you have the latest version installed.', |
| 258 | + 'widgetmake': 'Jupyter widget construction failed with "``".', |
| 259 | + 'widgetargs': 'Illegal variable range or step parameters for ``.', |
| 260 | + 'widgetdisp': 'Jupyter failed to display the widget.', |
| 261 | + } |
| 262 | + |
| 263 | + requires = ( |
| 264 | + 'ipywidgets', |
| 265 | + ) |
| 266 | + |
| 267 | + def apply(self, expr, args, evaluation): |
| 268 | + 'Manipulate[expr_, args__]' |
| 269 | + if (not _jupyter) or (not Kernel.initialized()) or (Kernel.instance() is None): |
| 270 | + return evaluation.message('Manipulate', 'jupyter') |
| 271 | + |
| 272 | + instantiator = _WidgetInstantiator() # knows about the arguments and their widgets |
| 273 | + |
| 274 | + for arg in args.get_sequence(): |
| 275 | + try: |
| 276 | + if not instantiator.add(arg, evaluation): # not a valid argument pattern? |
| 277 | + return |
| 278 | + except IllegalWidgetArguments as e: |
| 279 | + return evaluation.message('Manipulate', 'widgetargs', strip_context(str(e.var))) |
| 280 | + except JupyterWidgetError as e: |
| 281 | + return evaluation.message('Manipulate', 'widgetmake', e.err) |
| 282 | + |
| 283 | + clear_output_callback = evaluation.output.clear |
| 284 | + display_data_callback = evaluation.output.display # for pushing updates |
| 285 | + |
| 286 | + try: |
| 287 | + clear_output_callback(wait=True) |
| 288 | + except NotImplementedError: |
| 289 | + return evaluation.message('Manipulate', 'imathics') |
| 290 | + |
| 291 | + def callback(**kwargs): |
| 292 | + clear_output_callback(wait=True) |
| 293 | + |
| 294 | + line_no = evaluation.definitions.get_line_no() |
| 295 | + |
| 296 | + vars = [Expression('Set', Symbol(name), value) for name, value in kwargs.items()] |
| 297 | + evaluatable = Expression('ReleaseHold', Expression('Module', Expression('List', *vars), expr)) |
| 298 | + |
| 299 | + result = evaluation.evaluate(evaluatable, timeout=settings.TIMEOUT) |
| 300 | + if result: |
| 301 | + display_data_callback(data=result.result, metadata={}) |
| 302 | + |
| 303 | + evaluation.definitions.set_line_no(line_no) # do not increment line_no for manipulate computations |
| 304 | + |
| 305 | + widgets = instantiator.get_widgets() |
| 306 | + if len(widgets) > 0: |
| 307 | + box = _interactive(instantiator.build_callback(callback), widgets) # create the widget |
| 308 | + formatter = IPythonDisplayFormatter() |
| 309 | + if not formatter(box): # make the widget appear on the Jupyter notebook |
| 310 | + return evaluation.message('Manipulate', 'widgetdisp') |
| 311 | + |
| 312 | + return Symbol('Null') # the interactive output is pushed via kernel.display_data_callback (see above) |
0 commit comments