Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manipulate[] #483

Merged
merged 22 commits into from Aug 24, 2016
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 …
poke1024 Apr 26, 2016
d051e6f
removed IPython dependency, fixes wrong display of symbol name of man…
poke1024 Apr 27, 2016
2acaed0
ipywidgets dependency is now optional; Manipulate will only work if i…
poke1024 Apr 28, 2016
dd88e1e
better error message when no ipywidgets is available
poke1024 Apr 28, 2016
cd610f6
should fix Manipulate[] docs for tests
poke1024 Apr 28, 2016
fc38b59
should fix Manipulate[] docs for tests (again)
poke1024 Apr 29, 2016
d0ae012
adds ListAnimate[]; adds integer range widgets; fixes a couple of pro…
poke1024 Apr 29, 2016
a59d0c2
relaxes wrong step size check
poke1024 Apr 29, 2016
461e55a
removed debug code (try catch in Manipulate class)
poke1024 Apr 30, 2016
029c1cc
fixed wrong ident from last check in; added ">>" to Manipulate examples
poke1024 Apr 30, 2016
86c4ff5
fixes a python 2 compatility problem in the Maniulations constructor
poke1024 Apr 30, 2016
2528386
now detects if it runs inside Juptyter, and if not, issues a sensible…
poke1024 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
poke1024 May 6, 2016
67ede5e
variable name cleanup
poke1024 May 6, 2016
d6e773b
always display errors during widget creation
poke1024 May 14, 2016
a74a4a6
better error handling
poke1024 May 15, 2016
97e1e29
fixes a bug that had use of labels fill the wrong symbol
poke1024 May 15, 2016
5420906
cleanup evaluation.error -> evaluation.message
sn6uv May 15, 2016
05a5ea2
adapted to match latest changes in IMathics and Mathics
poke1024 Aug 17, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions mathics/builtin/__init__.py 100644 → 100755
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
@@ -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)