Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0578be4
basic implementation of Manipulate[] (needs corresponding changes in …
l1ebl Apr 26, 2016
d051e6f
removed IPython dependency, fixes wrong display of symbol name of man…
l1ebl Apr 27, 2016
2acaed0
ipywidgets dependency is now optional; Manipulate will only work if i…
l1ebl Apr 28, 2016
dd88e1e
better error message when no ipywidgets is available
l1ebl Apr 28, 2016
cd610f6
should fix Manipulate[] docs for tests
l1ebl Apr 28, 2016
fc38b59
should fix Manipulate[] docs for tests (again)
l1ebl Apr 29, 2016
d0ae012
adds ListAnimate[]; adds integer range widgets; fixes a couple of pro…
l1ebl Apr 29, 2016
a59d0c2
relaxes wrong step size check
l1ebl Apr 29, 2016
461e55a
removed debug code (try catch in Manipulate class)
l1ebl Apr 30, 2016
029c1cc
fixed wrong ident from last check in; added ">>" to Manipulate examples
l1ebl Apr 30, 2016
86c4ff5
fixes a python 2 compatility problem in the Maniulations constructor
l1ebl Apr 30, 2016
2528386
now detects if it runs inside Juptyter, and if not, issues a sensible…
l1ebl Apr 30, 2016
60a8320
fixup Manipulate tests (no notebook)
sn6uv Apr 30, 2016
7e1e29f
remove incomplete ListAnimate
sn6uv Apr 30, 2016
825d917
docs for other manipulate forms
sn6uv Apr 30, 2016
06526c4
removed PatternDispatcher
l1ebl May 6, 2016
67ede5e
variable name cleanup
l1ebl May 6, 2016
d6e773b
always display errors during widget creation
l1ebl May 14, 2016
a74a4a6
better error handling
l1ebl May 15, 2016
97e1e29
fixes a bug that had use of labels fill the wrong symbol
l1ebl May 15, 2016
5420906
cleanup evaluation.error -> evaluation.message
sn6uv May 15, 2016
05a5ea2
adapted to match latest changes in IMathics and Mathics
l1ebl Aug 17, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions mathics/builtin/__init__.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from mathics.builtin import (
algebra, arithmetic, assignment, attributes, calculus, combinatorial,
comparison, control, datentime, diffeqns, evaluation, exptrig, functional,
graphics, graphics3d, image, inout, integer, linalg, lists, logic, numbertheory,
graphics, graphics3d, image, inout, integer, linalg, lists, logic, manipulate, numbertheory,
numeric, options, patterns, plot, physchemdata, randomnumbers, recurrence,
specialfunctions, scoping, strings, structure, system, tensors)

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

Expand Down
312 changes: 312 additions & 0 deletions mathics/builtin/manipulate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import unicode_literals
from __future__ import absolute_import

from mathics.core.expression import String, strip_context
from mathics import settings
from mathics.core.evaluation import Evaluation, Output

from mathics.builtin.base import Builtin
from mathics.core.expression import Expression, Symbol, Integer, from_python

try:
from ipykernel.kernelbase import Kernel
_jupyter = True
except ImportError:
_jupyter = False

try:
from ipywidgets import IntSlider, FloatSlider, ToggleButtons, Box, DOMWidget
from IPython.core.formatters import IPythonDisplayFormatter
_ipywidgets = True
except ImportError:
# fallback to non-Manipulate-enabled build if we don't have ipywidgets installed.
_ipywidgets = False


"""
A basic implementation of Manipulate[]. There is currently no support for Dynamic[] elements.
This implementation is basically a port from ipywidget.widgets.interaction for Mathics.
"""

def _interactive(interact_f, kwargs_widgets):
# this is a modified version of interactive() in ipywidget.widgets.interaction

container = Box(_dom_classes=['widget-interact'])
container.children = [w for w in kwargs_widgets if isinstance(w, DOMWidget)]

def call_f(name=None, old=None, new=None):
kwargs = dict((widget._kwarg, widget.value) for widget in kwargs_widgets)
try:
interact_f(**kwargs)
except Exception as e:
container.log.warn("Exception in interact callback: %s", e, exc_info=True)

for widget in kwargs_widgets:
widget.on_trait_change(call_f, 'value')

container.on_displayed(lambda _: call_f(None, None, None))

return container


class IllegalWidgetArguments(Exception):
def __init__(self, var):
super(IllegalWidgetArguments, self).__init__()
self.var = var


class JupyterWidgetError(Exception):
def __init__(self, err):
super(JupyterWidgetError, self).__init__()
self.err = err


class ManipulateParameter(Builtin): # parses one Manipulate[] parameter spec, e.g. {x, 1, 2}, see _WidgetInstantiator
context = 'System`Private`'

rules = {
# detect x and {x, default} and {x, default, label}.
'System`Private`ManipulateParameter[{s_Symbol, r__}]':
'System`Private`ManipulateParameter[{Symbol -> s, Label -> s}, {r}]',
'System`Private`ManipulateParameter[{{s_Symbol, d_}, r__}]':
'System`Private`ManipulateParameter[{Symbol -> s, Default -> d, Label -> s}, {r}]',
'System`Private`ManipulateParameter[{{s_Symbol, d_, l_}, r__}]':
'System`Private`ManipulateParameter[{Symbol -> s, Default -> d, Label -> l}, {r}]',

# detect different kinds of widgets. on the use of the duplicate key "Default ->", see _WidgetInstantiator.add()
'System`Private`ManipulateParameter[var_, {min_?RealNumberQ, max_?RealNumberQ}]':
'Join[{Type -> "Continuous", Minimum -> min, Maximum -> max, Default -> min}, var]',
'System`Private`ManipulateParameter[var_, {min_?RealNumberQ, max_?RealNumberQ, step_?RealNumberQ}]':
'Join[{Type -> "Discrete", Minimum -> min, Maximum -> max, Step -> step, Default -> min}, var]',
'System`Private`ManipulateParameter[var_, {opt_List}] /; Length[opt] > 0':
'Join[{Type -> "Options", Options -> opt, Default -> Part[opt, 1]}, var]'
}


def _manipulate_label(x): # gets the label that is displayed for a symbol or name
if isinstance(x, String):
return x.get_string_value()
elif isinstance(x, Symbol):
return strip_context(x.get_name())
else:
return str(x)


def _create_widget(widget, **kwargs):
try:
return widget(**kwargs)
except Exception as e:
raise JupyterWidgetError(str(e))


class _WidgetInstantiator():
# we do not want to have widget instances (like FloatSlider) get into the evaluation pipeline (e.g. via Expression
# or Atom), since there might be all kinds of problems with serialization of these widget classes. therefore, the
# elegant recursive solution for parsing parameters (like in Table[]) is not feasible here; instead, we must create
# and use the widgets in one "transaction" here, without holding them in expressions or atoms.

def __init__(self):
self._widgets = [] # the ipywidget widgets to control the manipulated variables
self._parsers = {} # lambdas to decode the widget values into Mathics expressions

def add(self, expression, evaluation):
expr = Expression('System`Private`ManipulateParameter', expression).evaluate(evaluation)
if expr.get_head_name() != 'System`List': # if everything was parsed ok, we get a List
return False
# convert the rules given us by ManipulateParameter[] into a dict. note: duplicate keys
# will be overwritten, the latest one wins.
kwargs = {'evaluation': evaluation}
for rule in expr.leaves:
if rule.get_head_name() != 'System`Rule' or len(rule.leaves) != 2:
return False
kwargs[strip_context(rule.leaves[0].to_python()).lower()] = rule.leaves[1]
widget = kwargs['type'].get_string_value()
del kwargs['type']
getattr(self, '_add_%s_widget' % widget.lower())(**kwargs) # create the widget
return True

def get_widgets(self):
return self._widgets

def build_callback(self, callback):
parsers = self._parsers

def new_callback(**kwargs):
callback(**dict((name, parsers[name](value)) for (name, value) in kwargs.items()))

return new_callback

def _add_continuous_widget(self, symbol, label, default, minimum, maximum, evaluation):
minimum_value = minimum.to_python()
maximum_value = maximum.to_python()
if minimum_value > maximum_value:
raise IllegalWidgetArguments(symbol)
else:
defval = min(max(default.to_python(), minimum_value), maximum_value)
widget = _create_widget(FloatSlider, value=defval, min=minimum_value, max=maximum_value)
self._add_widget(widget, symbol.get_name(), lambda x: from_python(x), label)

def _add_discrete_widget(self, symbol, label, default, minimum, maximum, step, evaluation):
minimum_value = minimum.to_python()
maximum_value = maximum.to_python()
step_value = step.to_python()
if minimum_value > maximum_value or step_value <= 0 or step_value > (maximum_value - minimum_value):
raise IllegalWidgetArguments(symbol)
else:
default_value = min(max(default.to_python(), minimum_value), maximum_value)
if all(isinstance(x, Integer) for x in [minimum, maximum, default, step]):
widget = _create_widget(IntSlider, value=default_value, min=minimum_value, max=maximum_value,
step=step_value)
else:
widget = _create_widget(FloatSlider, value=default_value, min=minimum_value, max=maximum_value,
step=step_value)
self._add_widget(widget, symbol.get_name(), lambda x: from_python(x), label)

def _add_options_widget(self, symbol, options, default, label, evaluation):
formatted_options = []
for i, option in enumerate(options.leaves):
data = evaluation.format_all_outputs(option)
formatted_options.append((data['text/plain'], i))

default_index = 0
for i, option in enumerate(options.leaves):
if option.same(default):
default_index = i

widget = _create_widget(ToggleButtons, options=formatted_options, value=default_index)
self._add_widget(widget, symbol.get_name(), lambda j: options.leaves[j], label)

def _add_widget(self, widget, name, parse, label):
if not widget.description:
widget.description = _manipulate_label(label)
widget._kwarg = name # see _interactive() above
self._parsers[name] = parse
self._widgets.append(widget)


class ManipulateOutput(Output):
def max_stored_size(self, settings):
return self.output.max_stored_size(settings)

def out(self, out):
return self.output.out(out)

def clear_output(wait=False):
raise NotImplementedError

def display_data(self, result):
raise NotImplementedError


class Manipulate(Builtin):
"""
<dl>
<dt>'Manipulate[$expr1$, {$u$, $u_min$, $u_max$}]'
<dd>interactively compute and display an expression with different values of $u$.
<dt>'Manipulate[$expr1$, {$u$, $u_min$, $u_max$, $du$}]'
<dd>allows $u$ to vary between $u_min$ and $u_max$ in steps of $du$.
<dt>'Manipulate[$expr1$, {{$u$, $u_init$}, $u_min$, $u_max$, ...}]'
<dd>starts with initial value of $u_init$.
<dt>'Manipulate[$expr1$, {{$u$, $u_init$, $u_lbl$}, ...}]'
<dd>labels the $u$ controll by $u_lbl$.
<dt>'Manipulate[$expr1$, {$u$, {$u_1$, $u_2$, ...}}]'
<dd>sets $u$ to take discrete values $u_1$, $u_2$, ... .
<dt>'Manipulate[$expr1$, {$u$, ...}, {$v$, ...}, ...]'
<dd>control each of $u$, $v$, ... .
</dl>

>> Manipulate[N[Sin[y]], {y, 1, 20, 2}]
: Manipulate[] only works inside a Jupyter notebook.
= Manipulate[N[Sin[y]], {y, 1, 20, 2}]

>> Manipulate[i ^ 3, {i, {2, x ^ 4, a}}]
: Manipulate[] only works inside a Jupyter notebook.
= Manipulate[i ^ 3, {i, {2, x ^ 4, a}}]

>> Manipulate[x ^ y, {x, 1, 20}, {y, 1, 3}]
: Manipulate[] only works inside a Jupyter notebook.
= Manipulate[x ^ y, {x, 1, 20}, {y, 1, 3}]

>> Manipulate[N[1 / x], {{x, 1}, 0, 2}]
: Manipulate[] only works inside a Jupyter notebook.
= Manipulate[N[1 / x], {{x, 1}, 0, 2}]

>> Manipulate[N[1 / x], {{x, 1}, 0, 2, 0.1}]
: Manipulate[] only works inside a Jupyter notebook.
= Manipulate[N[1 / x], {{x, 1}, 0, 2, 0.1}]
"""

# TODO: correct in the jupyter interface but can't be checked in tests
"""
#> Manipulate[x, {x}]
= Manipulate[x, {x}]

#> Manipulate[x, {x, 1, 0}]
: 'Illegal variable range or step parameters for `x`.
= Manipulate[x, {x, 1, 0}]
"""

attributes = ('HoldAll',) # we'll call ReleaseHold at the time of evaluation below

messages = {
'jupyter': 'Manipulate[] only works inside a Jupyter notebook.',
'imathics': 'Your IMathics kernel does not seem to support all necessary operations. ' +
'Please check that you have the latest version installed.',
'widgetmake': 'Jupyter widget construction failed with "``".',
'widgetargs': 'Illegal variable range or step parameters for ``.',
'widgetdisp': 'Jupyter failed to display the widget.',
}

requires = (
'ipywidgets',
)

def apply(self, expr, args, evaluation):
'Manipulate[expr_, args__]'
if (not _jupyter) or (not Kernel.initialized()) or (Kernel.instance() is None):
return evaluation.message('Manipulate', 'jupyter')

instantiator = _WidgetInstantiator() # knows about the arguments and their widgets

for arg in args.get_sequence():
try:
if not instantiator.add(arg, evaluation): # not a valid argument pattern?
return
except IllegalWidgetArguments as e:
return evaluation.message('Manipulate', 'widgetargs', strip_context(str(e.var)))
except JupyterWidgetError as e:
return evaluation.message('Manipulate', 'widgetmake', e.err)

clear_output_callback = evaluation.output.clear
display_data_callback = evaluation.output.display # for pushing updates

try:
clear_output_callback(wait=True)
except NotImplementedError:
return evaluation.message('Manipulate', 'imathics')

def callback(**kwargs):
clear_output_callback(wait=True)

line_no = evaluation.definitions.get_line_no()

vars = [Expression('Set', Symbol(name), value) for name, value in kwargs.items()]
evaluatable = Expression('ReleaseHold', Expression('Module', Expression('List', *vars), expr))

result = evaluation.evaluate(evaluatable, timeout=settings.TIMEOUT)
if result:
display_data_callback(data=result.result, metadata={})

evaluation.definitions.set_line_no(line_no) # do not increment line_no for manipulate computations

widgets = instantiator.get_widgets()
if len(widgets) > 0:
box = _interactive(instantiator.build_callback(callback), widgets) # create the widget
formatter = IPythonDisplayFormatter()
if not formatter(box): # make the widget appear on the Jupyter notebook
return evaluation.message('Manipulate', 'widgetdisp')

return Symbol('Null') # the interactive output is pushed via kernel.display_data_callback (see above)