Skip to content

Commit fbc25a4

Browse files
authored
Merge pull request #483 from poke1024/manipulate4mathics
Manipulate[]
2 parents 28de9d3 + 05a5ea2 commit fbc25a4

File tree

2 files changed

+314
-2
lines changed

2 files changed

+314
-2
lines changed

mathics/builtin/__init__.py

100644100755
+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from mathics.builtin import (
88
algebra, arithmetic, assignment, attributes, calculus, combinatorial,
99
comparison, control, datentime, diffeqns, evaluation, exptrig, functional,
10-
graphics, graphics3d, image, inout, integer, linalg, lists, logic, numbertheory,
10+
graphics, graphics3d, image, inout, integer, linalg, lists, logic, manipulate, numbertheory,
1111
numeric, options, patterns, plot, physchemdata, randomnumbers, recurrence,
1212
specialfunctions, scoping, strings, structure, system, tensors)
1313

@@ -19,7 +19,7 @@
1919
modules = [
2020
algebra, arithmetic, assignment, attributes, calculus, combinatorial,
2121
comparison, control, datentime, diffeqns, evaluation, exptrig, functional,
22-
graphics, graphics3d, image, inout, integer, linalg, lists, logic, numbertheory,
22+
graphics, graphics3d, image, inout, integer, linalg, lists, logic, manipulate, numbertheory,
2323
numeric, options, patterns, plot, physchemdata, randomnumbers, recurrence,
2424
specialfunctions, scoping, strings, structure, system, tensors]
2525

mathics/builtin/manipulate.py

+312
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
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

Comments
 (0)