From 0578be49f8714e3e4fcbd3685abdb1b9a8238189 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Tue, 26 Apr 2016 19:57:07 +0200 Subject: [PATCH 01/22] basic implementation of Manipulate[] (needs corresponding changes in imathics kernel.py) --- mathics/builtin/__init__.py | 4 +- mathics/builtin/manipulate.py | 222 ++++++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+), 2 deletions(-) mode change 100644 => 100755 mathics/builtin/__init__.py create mode 100755 mathics/builtin/manipulate.py diff --git a/mathics/builtin/__init__.py b/mathics/builtin/__init__.py old mode 100644 new mode 100755 index ae61814af6..78e6868af4 --- a/mathics/builtin/__init__.py +++ b/mathics/builtin/__init__.py @@ -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) @@ -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] diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py new file mode 100755 index 0000000000..a4571106b5 --- /dev/null +++ b/mathics/builtin/manipulate.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals +from __future__ import absolute_import + +from mathics.core.expression import Real, String +from mathics import settings +from mathics.core.evaluation import Evaluation + +from mathics.builtin.base import Builtin +from mathics.core.expression import Expression, Symbol + +from IPython.core.formatters import DisplayFormatter +from mathics.core.evaluation import EvaluationResult + +from ipywidgets import (FloatSlider, ToggleButtons, Box, DOMWidget) + +""" +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 + + +def _strip_namespace(name): + return name.split('`')[1] # e.g. "Global`x" will turn into "x" + + +def _manipulate_label(x): + if isinstance(x, String): + return x.get_string_value() + elif isinstance(x, Symbol): + return _strip_namespace(x.get_name()) + else: + return str(x) + + +class PatternDispatcher: + '''A pattern matcher similar to the one for BuiltIns, but usable in any context (e.g. locally)''' + + def __init__(self): + from mathics.core.parser import parse_builtin_rule + from mathics.builtin.patterns import Pattern + + self._patterns = [] + for name in dir(self): + if name.startswith('apply_'): + function = getattr(self, name) + self._patterns.append((Pattern.create( + parse_builtin_rule(function.__doc__)), function)) + + def dispatch(self, expr, evaluation): + from mathics.builtin.patterns import StopGenerator + + class StopGenerator_MatchQ(StopGenerator): + pass + + try: + for pattern, function in self._patterns: + def yield_func(vars, rest): + function(**dict((_strip_namespace(key), value) for (key, value) in vars.items())) + raise StopGenerator_MatchQ(Symbol("True")) + + pattern.match(yield_func, expr, {}, evaluation) + except StopGenerator_MatchQ: + return True + + return False + + +class Manipulations(PatternDispatcher): + def __init__(self, evaluation): + super(Manipulations, self).__init__() + self._evaluation = evaluation + self._widgets = [] # the ipywidget widgets to control the manipulated variables + self._parsers = {} # lambdas to decode the widget values into Mathics expressions + + def get_widgets(self): + return self._widgets + + 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) + + 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_min_max_var(self, i, imin, imax, idefault, ilabel): + widget = FloatSlider(value=idefault.value, min=imin.value, max=imax.value) + self._add_widget(widget, i.get_name(), lambda x: Real(x), ilabel) + + def _add_min_max_step_var(self, i, imin, imax, idefault, di, ilabel): + widget = FloatSlider(value=idefault.value, min=imin.value, max=imax.value, step=di.value) + self._add_widget(widget, i.get_name(), lambda x: Real(x), ilabel) + + def _add_discrete_options_var(self, symbol, options, idefault, ilabel): + formatted_options = [] + for i, option in enumerate(options.leaves): + data = self._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(idefault): + default_index = i + + widget = ToggleButtons(options=formatted_options, value=default_index) + self._add_widget(widget, symbol.get_name(), lambda i: options.leaves[i], ilabel) + + def apply_min_max_var1(self, i, imin, imax): + '{i_Symbol, imin_?RealNumberQ, imax_?RealNumberQ}' + self._add_min_max_var(i, imin, imax, imin, i) + + def apply_min_max_var2(self, i, imin, imax, idefault): + '{{i_Symbol, idefault_?RealNumberQ}, imin_?RealNumberQ, imax_?RealNumberQ}' + self._add_min_max_var(i, imin, imax, idefault, i) + + def apply_min_max_var3(self, i, imin, imax, idefault, ilabel): + '{{i_Symbol, idefault_?RealNumberQ, ilabel_}, imin_?RealNumberQ, imax_?RealNumberQ}' + self._add_min_max_var(i, imin, imax, idefault, ilabel) + + def apply_min_max_step_var1(self, i, imin, imax, di): + '{i_Symbol, imin_?RealNumberQ, imax_?RealNumberQ, di_?RealNumberQ}' + self._add_min_max_step_var(i, imin, imax, imin, di, i) + + def apply_min_max_step_var2(self, i, imin, imax, di, idefault): + '{{i_Symbol, idefault_?RealNumberQ}, imin_?RealNumberQ, imax_?RealNumberQ, di_?RealNumberQ}' + self._add_min_max_step_var(i, imin, imax, idefault, di, i) + + def apply_min_max_step_var3(self, i, imin, imax, di, idefault, ilabel): + '{{i_Symbol, idefault_?RealNumberQ, ilabel_}, imin_?RealNumberQ, imax_?RealNumberQ, di_?RealNumberQ}' + self._add_min_max_step_var(i, imin, imax, idefault, di, ilabel) + + def apply_discrete_options_var1(self, i, options): + '{i_Symbol, options_List}' + if len(options.leaves) > 0: + self._add_discrete_options_var(i, options, options.leaves[0], i) + + def apply_discrete_options_var2(self, i, options, idefault): + '{{i_Symbol, idefault_}, options_List}' + self._add_discrete_options_var(i, options, idefault, i) + + def apply_discrete_options_var3(self, i, options, idefault, ilabel): + '{{i_Symbol, idefault_, ilabel_}, options_List}' + self._add_discrete_options_var(i, options, idefault, ilabel) + +class Manipulate(Builtin): + """ +
+
'Manipulate[$expr1$, $arg1$, ...]' +
allows you to interactively compute and display an expression with different argument values. +
+ + >> Manipulate[N[Sin[y]], {y, 1, 20, 2}] + + >> Manipulate[i^3, {i, {2, x^4, a}}] + + >> Manipulate[x^y, {x, 1, 20}, {y, 1, 3}] + + """ + + attributes = ('HoldAll',) # we'll call ReleaseHold at the time of evaluation below + + def apply(self, expr, args, evaluation): + 'Manipulate[expr_, args__]' + + manip = Manipulations(evaluation) # knows about the arguments and their widgets + + for arg in args.get_sequence(): + if not manip.dispatch(arg, evaluation): # not a valid argument pattern? + return Expression(self.get_name(), expr, *args.get_sequence()) # identity + + clear_output_callback = evaluation.clear_output_callback + display_data_callback = evaluation.display_data_callback # for pushing updates + + def callback(**kwargs): + clear_output_callback(wait=True) + + line_no = evaluation.definitions.get_line_no() + + new_evaluation = Evaluation(evaluation.definitions, result_callback=display_data_callback, + out_callback=evaluation.out_callback) + + vars = [Expression('Set', Symbol(name), value) for name, value in kwargs.items()] + evaluatable = Expression('ReleaseHold', Expression('Module', Expression('List', *vars), expr)) + new_evaluation.evaluate([evaluatable], timeout=settings.TIMEOUT) + + evaluation.definitions.set_line_no(line_no) # do not increment line_no for manipulate computations + + widgets = manip.get_widgets() + if len(widgets) > 0: + box = _interactive(manip.build_callback(callback), widgets) + formatter = DisplayFormatter() + display_data_callback(EvaluationResult(evaluation.definitions.get_line_no(), formatter.format(box))) + + return Symbol('Null') # the interactive output is pushed via kernel.display_data_callback (see above) From d051e6ff731baf3d8c91b7c89bd08ebacd18c599 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 27 Apr 2016 23:36:28 +0200 Subject: [PATCH 02/22] removed IPython dependency, fixes wrong display of symbol name of manipulated variable --- mathics/builtin/manipulate.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index a4571106b5..e3d2cd99e7 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -11,9 +11,6 @@ from mathics.builtin.base import Builtin from mathics.core.expression import Expression, Symbol -from IPython.core.formatters import DisplayFormatter -from mathics.core.evaluation import EvaluationResult - from ipywidgets import (FloatSlider, ToggleButtons, Box, DOMWidget) """ @@ -43,7 +40,7 @@ def call_f(name=None, old=None, new=None): def _strip_namespace(name): - return name.split('`')[1] # e.g. "Global`x" will turn into "x" + return name.split('`')[-1] # e.g. "Global`x" will turn into "x" def _manipulate_label(x): @@ -215,8 +212,13 @@ def callback(**kwargs): widgets = manip.get_widgets() if len(widgets) > 0: - box = _interactive(manip.build_callback(callback), widgets) - formatter = DisplayFormatter() - display_data_callback(EvaluationResult(evaluation.definitions.get_line_no(), formatter.format(box))) + box = _interactive(manip.build_callback(callback), widgets) # create the widget + + # the following code is a boiled down version from IPython.core.formatters.IPythonDisplayFormatter. + # note that '_ipython_display_' is a magic constant defined in print_method of IPythonDisplayFormatter. + + method = getattr(box, '_ipython_display_') + if method is not None: + method() # make the widget appear on the Jupyter notebook return Symbol('Null') # the interactive output is pushed via kernel.display_data_callback (see above) From 2acaed0ec095c80b9e753971947f78f73781e044 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 28 Apr 2016 22:19:04 +0200 Subject: [PATCH 03/22] ipywidgets dependency is now optional; Manipulate will only work if ipywidgets is installed --- mathics/builtin/manipulate.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index e3d2cd99e7..4be12f1675 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -11,7 +11,12 @@ from mathics.builtin.base import Builtin from mathics.core.expression import Expression, Symbol -from ipywidgets import (FloatSlider, ToggleButtons, Box, DOMWidget) +try: + from ipywidgets import (FloatSlider, ToggleButtons, Box, DOMWidget) + _enabled = True +except ImportError: + # fallback to non-Manipulate-enabled build if we don't have ipywidgets installed. + _enabled = False """ A basic implementation of Manipulate[]. There is currently no support for Dynamic[] elements. @@ -187,6 +192,9 @@ class Manipulate(Builtin): def apply(self, expr, args, evaluation): 'Manipulate[expr_, args__]' + if not _enabled: + return Symbol('$Failed') + manip = Manipulations(evaluation) # knows about the arguments and their widgets for arg in args.get_sequence(): From dd88e1e8744dd09085002386a7dc4bc502af6af8 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 28 Apr 2016 22:24:51 +0200 Subject: [PATCH 04/22] better error message when no ipywidgets is available --- mathics/builtin/manipulate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index 4be12f1675..65596c1b28 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -189,11 +189,15 @@ class Manipulate(Builtin): attributes = ('HoldAll',) # we'll call ReleaseHold at the time of evaluation below + messages = { + 'noipywidget': 'Manipulate[] needs the ipywidgets module to work.', + } + def apply(self, expr, args, evaluation): 'Manipulate[expr_, args__]' if not _enabled: - return Symbol('$Failed') + return evaluation.message('Manipulate', 'noipywidget') manip = Manipulations(evaluation) # knows about the arguments and their widgets From cd610f66c8a688dd88dd0cdeee3753bd2589aafc Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 28 Apr 2016 23:50:45 +0200 Subject: [PATCH 05/22] should fix Manipulate[] docs for tests --- mathics/builtin/manipulate.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index 65596c1b28..ef4c0a2b7c 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -179,11 +179,14 @@ class Manipulate(Builtin):
allows you to interactively compute and display an expression with different argument values. - >> Manipulate[N[Sin[y]], {y, 1, 20, 2}] + >> Manipulate[N[Sin[y]], {y, 1, 20, 2}] //FullForm + = Null - >> Manipulate[i^3, {i, {2, x^4, a}}] + >> Manipulate[i^3, {i, {2, x^4, a}}] //FullForm + = Null - >> Manipulate[x^y, {x, 1, 20}, {y, 1, 3}] + >> Manipulate[x^y, {x, 1, 20}, {y, 1, 3}] //FullForm + = Null """ From fc38b5945fd8d3344f2b076e9635fc6f8471fe77 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 06:14:58 +0200 Subject: [PATCH 06/22] should fix Manipulate[] docs for tests (again) --- mathics/builtin/manipulate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index ef4c0a2b7c..c76ffe3b73 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -12,7 +12,7 @@ from mathics.core.expression import Expression, Symbol try: - from ipywidgets import (FloatSlider, ToggleButtons, Box, DOMWidget) + from ipywidgetsX import (FloatSlider, ToggleButtons, Box, DOMWidget) _enabled = True except ImportError: # fallback to non-Manipulate-enabled build if we don't have ipywidgets installed. @@ -200,7 +200,8 @@ def apply(self, expr, args, evaluation): 'Manipulate[expr_, args__]' if not _enabled: - return evaluation.message('Manipulate', 'noipywidget') + evaluation.message('Manipulate', 'noipywidget') + return Symbol('Null') manip = Manipulations(evaluation) # knows about the arguments and their widgets From d0ae012f2a8ea703d1ba13a55de73f8eca8356ca Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 16:28:46 +0200 Subject: [PATCH 07/22] adds ListAnimate[]; adds integer range widgets; fixes a couple of problems with not evaluating variable argument range expressions properly --- mathics/builtin/manipulate.py | 108 ++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 36 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index c76ffe3b73..be247c6faf 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -9,10 +9,10 @@ from mathics.core.evaluation import Evaluation from mathics.builtin.base import Builtin -from mathics.core.expression import Expression, Symbol +from mathics.core.expression import Expression, Symbol, Integer, from_python try: - from ipywidgetsX import (FloatSlider, ToggleButtons, Box, DOMWidget) + from ipywidgets import (IntSlider, FloatSlider, ToggleButtons, Box, DOMWidget) _enabled = True except ImportError: # fallback to non-Manipulate-enabled build if we don't have ipywidgets installed. @@ -90,6 +90,12 @@ def yield_func(vars, rest): return False +class IllegalWidgetArguments(Exception): + def __init__(self, var): + super(IllegalWidgetArguments, self).__init__() + self.var = var + + class Manipulations(PatternDispatcher): def __init__(self, evaluation): super(Manipulations, self).__init__() @@ -114,12 +120,30 @@ def new_callback(**kwargs): return new_callback def _add_min_max_var(self, i, imin, imax, idefault, ilabel): - widget = FloatSlider(value=idefault.value, min=imin.value, max=imax.value) - self._add_widget(widget, i.get_name(), lambda x: Real(x), ilabel) + imin_value = imin.to_python() + imax_value = imax.to_python() + if imin_value > imax_value: + raise IllegalWidgetArguments(i) + else: + idefault_value = min(max(idefault.to_python(), imin_value), imax_value) + widget = FloatSlider(value=idefault_value, min=imin_value, max=imax_value) + self._add_widget(widget, i.get_name(), lambda x: from_python(x), ilabel) def _add_min_max_step_var(self, i, imin, imax, idefault, di, ilabel): - widget = FloatSlider(value=idefault.value, min=imin.value, max=imax.value, step=di.value) - self._add_widget(widget, i.get_name(), lambda x: Real(x), ilabel) + imin_value = imin.to_python() + imax_value = imax.to_python() + di_value = di.to_python() + if imin_value > imax_value or di_value <= 0 or di_value >= (imax_value - imin_value): + raise IllegalWidgetArguments(i) + else: + idefault_value = min(max(idefault.to_python(), imin_value), imax_value) + if all(isinstance(x, Integer) for x in [imin, imax, idefault, di]): + widget = IntSlider(value=idefault_value, min=imin_value, max=imax_value, + step=di_value) + else: + widget = FloatSlider(value=idefault_value, min=imin_value, max=imax_value, + step=di_value) + self._add_widget(widget, i.get_name(), lambda x: from_python(x), ilabel) def _add_discrete_options_var(self, symbol, options, idefault, ilabel): formatted_options = [] @@ -172,6 +196,13 @@ def apply_discrete_options_var3(self, i, options, idefault, ilabel): '{{i_Symbol, idefault_, ilabel_}, options_List}' self._add_discrete_options_var(i, options, idefault, ilabel) + +class ListAnimate(Builtin): + rules = { + 'ListAnimate[l_List]': 'Manipulate[Part[l, i], {i, 1, Length[l], 1}]' + } + + class Manipulate(Builtin): """
@@ -179,14 +210,11 @@ class Manipulate(Builtin):
allows you to interactively compute and display an expression with different argument values.
- >> Manipulate[N[Sin[y]], {y, 1, 20, 2}] //FullForm - = Null + Manipulate[N[Sin[y]], {y, 1, 20, 2}] - >> Manipulate[i^3, {i, {2, x^4, a}}] //FullForm - = Null + Manipulate[i^3, {i, {2, x^4, a}}] - >> Manipulate[x^y, {x, 1, 20}, {y, 1, 3}] //FullForm - = Null + Manipulate[x^y, {x, 1, 20}, {y, 1, 3}] """ @@ -194,6 +222,7 @@ class Manipulate(Builtin): messages = { 'noipywidget': 'Manipulate[] needs the ipywidgets module to work.', + 'widgetargs': 'Illegal variable range or step parameters for ``.' } def apply(self, expr, args, evaluation): @@ -203,38 +232,45 @@ def apply(self, expr, args, evaluation): evaluation.message('Manipulate', 'noipywidget') return Symbol('Null') - manip = Manipulations(evaluation) # knows about the arguments and their widgets + try: + manip = Manipulations(evaluation) # knows about the arguments and their widgets - for arg in args.get_sequence(): - if not manip.dispatch(arg, evaluation): # not a valid argument pattern? - return Expression(self.get_name(), expr, *args.get_sequence()) # identity + for arg in args.get_sequence(): + try: + if not manip.dispatch(arg.evaluate(evaluation), evaluation): # not a valid argument pattern? + return Expression(self.get_name(), expr, *args.get_sequence()) # identity + except IllegalWidgetArguments as e: + evaluation.message('Manipulate', 'widgetargs', _strip_namespace(str(e.var))) - clear_output_callback = evaluation.clear_output_callback - display_data_callback = evaluation.display_data_callback # for pushing updates + clear_output_callback = evaluation.clear_output_callback + display_data_callback = evaluation.display_data_callback # for pushing updates - def callback(**kwargs): - clear_output_callback(wait=True) + def callback(**kwargs): + clear_output_callback(wait=True) - line_no = evaluation.definitions.get_line_no() + line_no = evaluation.definitions.get_line_no() - new_evaluation = Evaluation(evaluation.definitions, result_callback=display_data_callback, - out_callback=evaluation.out_callback) + new_evaluation = Evaluation(evaluation.definitions, result_callback=display_data_callback, + out_callback=evaluation.out_callback) - vars = [Expression('Set', Symbol(name), value) for name, value in kwargs.items()] - evaluatable = Expression('ReleaseHold', Expression('Module', Expression('List', *vars), expr)) - new_evaluation.evaluate([evaluatable], timeout=settings.TIMEOUT) + vars = [Expression('Set', Symbol(name), value) for name, value in kwargs.items()] + evaluatable = Expression('ReleaseHold', Expression('Module', Expression('List', *vars), expr)) + new_evaluation.evaluate([evaluatable], timeout=settings.TIMEOUT) - evaluation.definitions.set_line_no(line_no) # do not increment line_no for manipulate computations + evaluation.definitions.set_line_no(line_no) # do not increment line_no for manipulate computations - widgets = manip.get_widgets() - if len(widgets) > 0: - box = _interactive(manip.build_callback(callback), widgets) # create the widget + widgets = manip.get_widgets() + if len(widgets) > 0: + box = _interactive(manip.build_callback(callback), widgets) # create the widget - # the following code is a boiled down version from IPython.core.formatters.IPythonDisplayFormatter. - # note that '_ipython_display_' is a magic constant defined in print_method of IPythonDisplayFormatter. + # the following code is a boiled down version from IPython.core.formatters.IPythonDisplayFormatter. + # note that '_ipython_display_' is a magic constant defined in print_method of IPythonDisplayFormatter. - method = getattr(box, '_ipython_display_') - if method is not None: - method() # make the widget appear on the Jupyter notebook + method = getattr(box, '_ipython_display_') + if method is not None: + method() # make the widget appear on the Jupyter notebook - return Symbol('Null') # the interactive output is pushed via kernel.display_data_callback (see above) + return Symbol('Null') # the interactive output is pushed via kernel.display_data_callback (see above) + except: + import sys + return String(repr(sys.exc_info())) \ No newline at end of file From a59d0c2e96eda54b665cac2502b60e3272135917 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 23:43:56 +0200 Subject: [PATCH 08/22] relaxes wrong step size check --- mathics/builtin/manipulate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index be247c6faf..5066b336a2 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -133,7 +133,7 @@ def _add_min_max_step_var(self, i, imin, imax, idefault, di, ilabel): imin_value = imin.to_python() imax_value = imax.to_python() di_value = di.to_python() - if imin_value > imax_value or di_value <= 0 or di_value >= (imax_value - imin_value): + if imin_value > imax_value or di_value <= 0 or di_value > (imax_value - imin_value): raise IllegalWidgetArguments(i) else: idefault_value = min(max(idefault.to_python(), imin_value), imax_value) From 461e55a5eb83e4b87c3627d162af29b5ffb6acb0 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 30 Apr 2016 07:51:33 +0200 Subject: [PATCH 09/22] removed debug code (try catch in Manipulate class) --- mathics/builtin/manipulate.py | 40 ++++++++++++++++------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index 5066b336a2..b9055250f5 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -232,7 +232,6 @@ def apply(self, expr, args, evaluation): evaluation.message('Manipulate', 'noipywidget') return Symbol('Null') - try: manip = Manipulations(evaluation) # knows about the arguments and their widgets for arg in args.get_sequence(): @@ -245,32 +244,29 @@ def apply(self, expr, args, evaluation): clear_output_callback = evaluation.clear_output_callback display_data_callback = evaluation.display_data_callback # for pushing updates - def callback(**kwargs): - clear_output_callback(wait=True) + def callback(**kwargs): + clear_output_callback(wait=True) - line_no = evaluation.definitions.get_line_no() + line_no = evaluation.definitions.get_line_no() - new_evaluation = Evaluation(evaluation.definitions, result_callback=display_data_callback, - out_callback=evaluation.out_callback) + new_evaluation = Evaluation(evaluation.definitions, result_callback=display_data_callback, + out_callback=evaluation.out_callback) - vars = [Expression('Set', Symbol(name), value) for name, value in kwargs.items()] - evaluatable = Expression('ReleaseHold', Expression('Module', Expression('List', *vars), expr)) - new_evaluation.evaluate([evaluatable], timeout=settings.TIMEOUT) + vars = [Expression('Set', Symbol(name), value) for name, value in kwargs.items()] + evaluatable = Expression('ReleaseHold', Expression('Module', Expression('List', *vars), expr)) + new_evaluation.evaluate([evaluatable], timeout=settings.TIMEOUT) - evaluation.definitions.set_line_no(line_no) # do not increment line_no for manipulate computations + evaluation.definitions.set_line_no(line_no) # do not increment line_no for manipulate computations - widgets = manip.get_widgets() - if len(widgets) > 0: - box = _interactive(manip.build_callback(callback), widgets) # create the widget + widgets = manip.get_widgets() + if len(widgets) > 0: + box = _interactive(manip.build_callback(callback), widgets) # create the widget - # the following code is a boiled down version from IPython.core.formatters.IPythonDisplayFormatter. - # note that '_ipython_display_' is a magic constant defined in print_method of IPythonDisplayFormatter. + # the following code is a boiled down version from IPython.core.formatters.IPythonDisplayFormatter. + # note that '_ipython_display_' is a magic constant defined in print_method of IPythonDisplayFormatter. - method = getattr(box, '_ipython_display_') - if method is not None: - method() # make the widget appear on the Jupyter notebook + method = getattr(box, '_ipython_display_') + if method is not None: + method() # make the widget appear on the Jupyter notebook - return Symbol('Null') # the interactive output is pushed via kernel.display_data_callback (see above) - except: - import sys - return String(repr(sys.exc_info())) \ No newline at end of file + return Symbol('Null') # the interactive output is pushed via kernel.display_data_callback (see above) From 029c1cc1d02eb06bcd4745214ccdc9113f99eea3 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 30 Apr 2016 07:53:51 +0200 Subject: [PATCH 10/22] fixed wrong ident from last check in; added ">>" to Manipulate examples --- mathics/builtin/manipulate.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index b9055250f5..7cb67d93ce 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -210,11 +210,14 @@ class Manipulate(Builtin):
allows you to interactively compute and display an expression with different argument values. - Manipulate[N[Sin[y]], {y, 1, 20, 2}] + >> Manipulate[N[Sin[y]], {y, 1, 20, 2}] //FullForm + Null - Manipulate[i^3, {i, {2, x^4, a}}] + Manipulate[i^3, {i, {2, x^4, a}}] //FullForm + Null - Manipulate[x^y, {x, 1, 20}, {y, 1, 3}] + Manipulate[x^y, {x, 1, 20}, {y, 1, 3}] //FullForm + Null """ @@ -232,17 +235,17 @@ def apply(self, expr, args, evaluation): evaluation.message('Manipulate', 'noipywidget') return Symbol('Null') - manip = Manipulations(evaluation) # knows about the arguments and their widgets + manip = Manipulations(evaluation) # knows about the arguments and their widgets - for arg in args.get_sequence(): - try: - if not manip.dispatch(arg.evaluate(evaluation), evaluation): # not a valid argument pattern? - return Expression(self.get_name(), expr, *args.get_sequence()) # identity - except IllegalWidgetArguments as e: - evaluation.message('Manipulate', 'widgetargs', _strip_namespace(str(e.var))) + for arg in args.get_sequence(): + try: + if not manip.dispatch(arg.evaluate(evaluation), evaluation): # not a valid argument pattern? + return Expression(self.get_name(), expr, *args.get_sequence()) # identity + except IllegalWidgetArguments as e: + evaluation.message('Manipulate', 'widgetargs', _strip_namespace(str(e.var))) - clear_output_callback = evaluation.clear_output_callback - display_data_callback = evaluation.display_data_callback # for pushing updates + clear_output_callback = evaluation.clear_output_callback + display_data_callback = evaluation.display_data_callback # for pushing updates def callback(**kwargs): clear_output_callback(wait=True) From 86c4ff59f8d60b7e04d0bb105a89abbfc179f501 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 30 Apr 2016 08:01:05 +0200 Subject: [PATCH 11/22] fixes a python 2 compatility problem in the Maniulations constructor --- mathics/builtin/manipulate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index 7cb67d93ce..f6606e7c8e 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -11,6 +11,8 @@ from mathics.builtin.base import Builtin from mathics.core.expression import Expression, Symbol, Integer, from_python +import six + try: from ipywidgets import (IntSlider, FloatSlider, ToggleButtons, Box, DOMWidget) _enabled = True @@ -98,7 +100,10 @@ def __init__(self, var): class Manipulations(PatternDispatcher): def __init__(self, evaluation): - super(Manipulations, self).__init__() + if six.PY2: + PatternDispatcher.__init__(self) + else: + super(Manipulations, self).__init__() self._evaluation = evaluation self._widgets = [] # the ipywidget widgets to control the manipulated variables self._parsers = {} # lambdas to decode the widget values into Mathics expressions From 25283860853783f1711049d876f92a06ffac2d52 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 30 Apr 2016 08:12:16 +0200 Subject: [PATCH 12/22] now detects if it runs inside Juptyter, and if not, issues a sensible error message --- mathics/builtin/manipulate.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index f6606e7c8e..e3e9d4b359 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from __future__ import absolute_import -from mathics.core.expression import Real, String +from mathics.core.expression import String from mathics import settings from mathics.core.evaluation import Evaluation @@ -13,12 +13,19 @@ import six +try: + from ipykernel.kernelbase import Kernel + _jupyter = True +except ImportError: + _jupyter = False + try: from ipywidgets import (IntSlider, FloatSlider, ToggleButtons, Box, DOMWidget) - _enabled = True + _ipywidgets = True except ImportError: # fallback to non-Manipulate-enabled build if we don't have ipywidgets installed. - _enabled = False + _ipywidgets = False + """ A basic implementation of Manipulate[]. There is currently no support for Dynamic[] elements. @@ -229,6 +236,7 @@ class Manipulate(Builtin): attributes = ('HoldAll',) # we'll call ReleaseHold at the time of evaluation below messages = { + 'jupyter': 'Manipulate[] only works inside a Jupyter notebook.', 'noipywidget': 'Manipulate[] needs the ipywidgets module to work.', 'widgetargs': 'Illegal variable range or step parameters for ``.' } @@ -236,8 +244,12 @@ class Manipulate(Builtin): def apply(self, expr, args, evaluation): 'Manipulate[expr_, args__]' - if not _enabled: - evaluation.message('Manipulate', 'noipywidget') + if (not _jupyter) or (not Kernel.initialized()) or (Kernel.instance() is None): + evaluation.error('Manipulate', 'jupyter') + return Symbol('Null') + + if not _ipywidgets: + evaluation.error('Manipulate', 'noipywidget') return Symbol('Null') manip = Manipulations(evaluation) # knows about the arguments and their widgets From 60a8320efd35d0ac39be311e9cefec8345f88631 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Sat, 30 Apr 2016 20:18:22 +1000 Subject: [PATCH 13/22] fixup Manipulate tests (no notebook) --- mathics/builtin/manipulate.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index e3e9d4b359..578ff0174e 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -222,15 +222,17 @@ class Manipulate(Builtin):
allows you to interactively compute and display an expression with different argument values. - >> Manipulate[N[Sin[y]], {y, 1, 20, 2}] //FullForm - Null + >> 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}}] //FullForm - Null - - Manipulate[x^y, {x, 1, 20}, {y, 1, 3}] //FullForm - Null + >> 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}] """ attributes = ('HoldAll',) # we'll call ReleaseHold at the time of evaluation below @@ -245,12 +247,10 @@ def apply(self, expr, args, evaluation): 'Manipulate[expr_, args__]' if (not _jupyter) or (not Kernel.initialized()) or (Kernel.instance() is None): - evaluation.error('Manipulate', 'jupyter') - return Symbol('Null') + return evaluation.message('Manipulate', 'jupyter') if not _ipywidgets: - evaluation.error('Manipulate', 'noipywidget') - return Symbol('Null') + return evaluation.message('Manipulate', 'noipywidget') manip = Manipulations(evaluation) # knows about the arguments and their widgets From 7e1e29f2edf88ce38ef689be68ae156b545a5736 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Sat, 30 Apr 2016 20:37:28 +1000 Subject: [PATCH 14/22] remove incomplete ListAnimate --- mathics/builtin/manipulate.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index 578ff0174e..e96c79530f 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -209,12 +209,6 @@ def apply_discrete_options_var3(self, i, options, idefault, ilabel): self._add_discrete_options_var(i, options, idefault, ilabel) -class ListAnimate(Builtin): - rules = { - 'ListAnimate[l_List]': 'Manipulate[Part[l, i], {i, 1, Length[l], 1}]' - } - - class Manipulate(Builtin): """
From 825d91706927809c2bffdef29291da07b4439f53 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Sat, 30 Apr 2016 21:04:00 +1000 Subject: [PATCH 15/22] docs for other manipulate forms --- mathics/builtin/manipulate.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index e96c79530f..ae2bee0d59 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -212,8 +212,18 @@ def apply_discrete_options_var3(self, i, options, idefault, ilabel): class Manipulate(Builtin): """
-
'Manipulate[$expr1$, $arg1$, ...]' -
allows you to interactively compute and display an expression with different argument values. +
'Manipulate[$expr1$, {$u$, $u_min$, $u_max$}]' +
interactively compute and display an expression with different values of $u$. +
'Manipulate[$expr1$, {$u$, $u_min$, $u_max$, $du$}]' +
allows $u$ to vary between $u_min$ and $u_max$ in steps of $du$. +
'Manipulate[$expr1$, {{$u$, $u_init$}, $u_min$, $u_max$, ...}]' +
starts with initial value of $u_init$. +
'Manipulate[$expr1$, {{$u$, $u_init$, $u_lbl$}, ...}]' +
labels the $u$ controll by $u_lbl$. +
'Manipulate[$expr1$, {$u$, {$u_1$, $u_2$, ...}}]' +
sets $u$ to take discrete values $u_1$, $u_2$, ... . +
'Manipulate[$expr1$, {$u$, ...}, {$v$, ...}, ...]' +
control each of $u$, $v$, ... .
>> Manipulate[N[Sin[y]], {y, 1, 20, 2}] @@ -227,6 +237,24 @@ class Manipulate(Builtin): >> 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 @@ -251,7 +279,7 @@ def apply(self, expr, args, evaluation): for arg in args.get_sequence(): try: if not manip.dispatch(arg.evaluate(evaluation), evaluation): # not a valid argument pattern? - return Expression(self.get_name(), expr, *args.get_sequence()) # identity + return except IllegalWidgetArguments as e: evaluation.message('Manipulate', 'widgetargs', _strip_namespace(str(e.var))) From 06526c458206a7754a572f7ef3734d9e62d518b8 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 6 May 2016 07:30:10 +0200 Subject: [PATCH 16/22] removed PatternDispatcher --- mathics/builtin/manipulate.py | 205 ++++++++++++++-------------------- 1 file changed, 84 insertions(+), 121 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index ae2bee0d59..d45e08da7c 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -4,15 +4,13 @@ from __future__ import unicode_literals from __future__ import absolute_import -from mathics.core.expression import String +from mathics.core.expression import String, strip_context from mathics import settings from mathics.core.evaluation import Evaluation from mathics.builtin.base import Builtin from mathics.core.expression import Expression, Symbol, Integer, from_python -import six - try: from ipykernel.kernelbase import Kernel _jupyter = True @@ -53,160 +51,126 @@ def call_f(name=None, old=None, new=None): return container -def _strip_namespace(name): - return name.split('`')[-1] # e.g. "Global`x" will turn into "x" +class IllegalWidgetArguments(Exception): + def __init__(self, var): + super(IllegalWidgetArguments, self).__init__() + self.var = var + +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, Label -> s, Default -> d}, {r}]', + 'System`Private`ManipulateParameter[{{s_Symbol, d_, l_}, r__}]': + 'System`Private`ManipulateParameter[{Symbol -> i, Label -> l, Default -> d}, {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, ds_?RealNumberQ}]': + 'Join[{Type -> "Discrete", Minimum -> min, Maximum -> max, Step -> ds, 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): + +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_namespace(x.get_name()) + return strip_context(x.get_name()) else: return str(x) -class PatternDispatcher: - '''A pattern matcher similar to the one for BuiltIns, but usable in any context (e.g. locally)''' +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): - from mathics.core.parser import parse_builtin_rule - from mathics.builtin.patterns import Pattern - - self._patterns = [] - for name in dir(self): - if name.startswith('apply_'): - function = getattr(self, name) - self._patterns.append((Pattern.create( - parse_builtin_rule(function.__doc__)), function)) - - def dispatch(self, expr, evaluation): - from mathics.builtin.patterns import StopGenerator - - class StopGenerator_MatchQ(StopGenerator): - pass - - try: - for pattern, function in self._patterns: - def yield_func(vars, rest): - function(**dict((_strip_namespace(key), value) for (key, value) in vars.items())) - raise StopGenerator_MatchQ(Symbol("True")) - - pattern.match(yield_func, expr, {}, evaluation) - except StopGenerator_MatchQ: - return True - - return False - - -class IllegalWidgetArguments(Exception): - def __init__(self, var): - super(IllegalWidgetArguments, self).__init__() - self.var = var - - -class Manipulations(PatternDispatcher): - def __init__(self, evaluation): - if six.PY2: - PatternDispatcher.__init__(self) - else: - super(Manipulations, self).__init__() - self._evaluation = evaluation 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 _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) - 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_min_max_var(self, i, imin, imax, idefault, ilabel): - imin_value = imin.to_python() - imax_value = imax.to_python() - if imin_value > imax_value: - raise IllegalWidgetArguments(i) + def _add_continuous_widget(self, symbol, label, default, minimum, maximum, evaluation): + minval = minimum.to_python() + maxval = maximum.to_python() + if minval > maxval: + raise IllegalWidgetArguments(symbol) else: - idefault_value = min(max(idefault.to_python(), imin_value), imax_value) - widget = FloatSlider(value=idefault_value, min=imin_value, max=imax_value) - self._add_widget(widget, i.get_name(), lambda x: from_python(x), ilabel) - - def _add_min_max_step_var(self, i, imin, imax, idefault, di, ilabel): - imin_value = imin.to_python() - imax_value = imax.to_python() - di_value = di.to_python() + defval = min(max(default.to_python(), minval), maxval) + widget = FloatSlider(value=defval, min=minval, max=maxval) + 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): + imin_value = minimum.to_python() + imax_value = maximum.to_python() + di_value = step.to_python() if imin_value > imax_value or di_value <= 0 or di_value > (imax_value - imin_value): - raise IllegalWidgetArguments(i) + raise IllegalWidgetArguments(symbol) else: - idefault_value = min(max(idefault.to_python(), imin_value), imax_value) - if all(isinstance(x, Integer) for x in [imin, imax, idefault, di]): + idefault_value = min(max(default.to_python(), imin_value), imax_value) + if all(isinstance(x, Integer) for x in [minimum, maximum, default, step]): widget = IntSlider(value=idefault_value, min=imin_value, max=imax_value, step=di_value) else: widget = FloatSlider(value=idefault_value, min=imin_value, max=imax_value, step=di_value) - self._add_widget(widget, i.get_name(), lambda x: from_python(x), ilabel) + self._add_widget(widget, symbol.get_name(), lambda x: from_python(x), label) - def _add_discrete_options_var(self, symbol, options, idefault, ilabel): + def _add_options_widget(self, symbol, options, default, label, evaluation): formatted_options = [] for i, option in enumerate(options.leaves): - data = self._evaluation.format_all_outputs(option) + 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(idefault): + if option.same(default): default_index = i widget = ToggleButtons(options=formatted_options, value=default_index) - self._add_widget(widget, symbol.get_name(), lambda i: options.leaves[i], ilabel) - - def apply_min_max_var1(self, i, imin, imax): - '{i_Symbol, imin_?RealNumberQ, imax_?RealNumberQ}' - self._add_min_max_var(i, imin, imax, imin, i) - - def apply_min_max_var2(self, i, imin, imax, idefault): - '{{i_Symbol, idefault_?RealNumberQ}, imin_?RealNumberQ, imax_?RealNumberQ}' - self._add_min_max_var(i, imin, imax, idefault, i) - - def apply_min_max_var3(self, i, imin, imax, idefault, ilabel): - '{{i_Symbol, idefault_?RealNumberQ, ilabel_}, imin_?RealNumberQ, imax_?RealNumberQ}' - self._add_min_max_var(i, imin, imax, idefault, ilabel) + self._add_widget(widget, symbol.get_name(), lambda j: options.leaves[j], label) - def apply_min_max_step_var1(self, i, imin, imax, di): - '{i_Symbol, imin_?RealNumberQ, imax_?RealNumberQ, di_?RealNumberQ}' - self._add_min_max_step_var(i, imin, imax, imin, di, i) - - def apply_min_max_step_var2(self, i, imin, imax, di, idefault): - '{{i_Symbol, idefault_?RealNumberQ}, imin_?RealNumberQ, imax_?RealNumberQ, di_?RealNumberQ}' - self._add_min_max_step_var(i, imin, imax, idefault, di, i) - - def apply_min_max_step_var3(self, i, imin, imax, di, idefault, ilabel): - '{{i_Symbol, idefault_?RealNumberQ, ilabel_}, imin_?RealNumberQ, imax_?RealNumberQ, di_?RealNumberQ}' - self._add_min_max_step_var(i, imin, imax, idefault, di, ilabel) - - def apply_discrete_options_var1(self, i, options): - '{i_Symbol, options_List}' - if len(options.leaves) > 0: - self._add_discrete_options_var(i, options, options.leaves[0], i) - - def apply_discrete_options_var2(self, i, options, idefault): - '{{i_Symbol, idefault_}, options_List}' - self._add_discrete_options_var(i, options, idefault, i) - - def apply_discrete_options_var3(self, i, options, idefault, ilabel): - '{{i_Symbol, idefault_, ilabel_}, options_List}' - self._add_discrete_options_var(i, options, idefault, ilabel) + 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 Manipulate(Builtin): @@ -267,21 +231,20 @@ class Manipulate(Builtin): 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') if not _ipywidgets: return evaluation.message('Manipulate', 'noipywidget') - manip = Manipulations(evaluation) # knows about the arguments and their widgets + instantiator = _WidgetInstantiator() # knows about the arguments and their widgets for arg in args.get_sequence(): try: - if not manip.dispatch(arg.evaluate(evaluation), evaluation): # not a valid argument pattern? + if not instantiator.add(arg, evaluation): # not a valid argument pattern? return except IllegalWidgetArguments as e: - evaluation.message('Manipulate', 'widgetargs', _strip_namespace(str(e.var))) + evaluation.message('Manipulate', 'widgetargs', strip_context(str(e.var))) clear_output_callback = evaluation.clear_output_callback display_data_callback = evaluation.display_data_callback # for pushing updates @@ -300,9 +263,9 @@ def callback(**kwargs): evaluation.definitions.set_line_no(line_no) # do not increment line_no for manipulate computations - widgets = manip.get_widgets() + widgets = instantiator.get_widgets() if len(widgets) > 0: - box = _interactive(manip.build_callback(callback), widgets) # create the widget + box = _interactive(instantiator.build_callback(callback), widgets) # create the widget # the following code is a boiled down version from IPython.core.formatters.IPythonDisplayFormatter. # note that '_ipython_display_' is a magic constant defined in print_method of IPythonDisplayFormatter. From 67ede5e9b65459c10500396aadd7d5332dee51d4 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 6 May 2016 07:46:06 +0200 Subject: [PATCH 17/22] variable name cleanup --- mathics/builtin/manipulate.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index d45e08da7c..fc95f9f2ef 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -72,8 +72,8 @@ class ManipulateParameter(Builtin): # parses one Manipulate[] parameter spec, e. # 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, ds_?RealNumberQ}]': - 'Join[{Type -> "Discrete", Minimum -> min, Maximum -> max, Step -> ds, 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]' } @@ -126,29 +126,29 @@ def new_callback(**kwargs): return new_callback def _add_continuous_widget(self, symbol, label, default, minimum, maximum, evaluation): - minval = minimum.to_python() - maxval = maximum.to_python() - if minval > maxval: + 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(), minval), maxval) - widget = FloatSlider(value=defval, min=minval, max=maxval) + defval = min(max(default.to_python(), minimum_value), maximum_value) + 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): - imin_value = minimum.to_python() - imax_value = maximum.to_python() - di_value = step.to_python() - if imin_value > imax_value or di_value <= 0 or di_value > (imax_value - imin_value): + 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: - idefault_value = min(max(default.to_python(), imin_value), imax_value) + default_value = min(max(default.to_python(), minimum_value), maximum_value) if all(isinstance(x, Integer) for x in [minimum, maximum, default, step]): - widget = IntSlider(value=idefault_value, min=imin_value, max=imax_value, - step=di_value) + widget = IntSlider(value=default_value, min=minimum_value, max=maximum_value, + step=step_value) else: - widget = FloatSlider(value=idefault_value, min=imin_value, max=imax_value, - step=di_value) + 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): From d6e773b793bb7cec83df15ca7a727f29e6a27fbe Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 14 May 2016 21:32:00 +0200 Subject: [PATCH 18/22] always display errors during widget creation --- mathics/builtin/manipulate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index fc95f9f2ef..285e3fc59d 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -226,6 +226,7 @@ class Manipulate(Builtin): messages = { 'jupyter': 'Manipulate[] only works inside a Jupyter notebook.', 'noipywidget': 'Manipulate[] needs the ipywidgets module to work.', + 'generr': 'Widget creation failed: ``', 'widgetargs': 'Illegal variable range or step parameters for ``.' } @@ -245,6 +246,9 @@ def apply(self, expr, args, evaluation): return except IllegalWidgetArguments as e: evaluation.message('Manipulate', 'widgetargs', strip_context(str(e.var))) + except Exception as e: + evaluation.message('Manipulate', 'generr', str(e)) # usually some Jupyter error + return Symbol('$Aborted') clear_output_callback = evaluation.clear_output_callback display_data_callback = evaluation.display_data_callback # for pushing updates From a74a4a6c1f2b574ab039fd0d3797eaa65697b03d Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sun, 15 May 2016 09:47:25 +0200 Subject: [PATCH 19/22] better error handling --- mathics/builtin/manipulate.py | 50 ++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index 285e3fc59d..e5b682943c 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -19,6 +19,7 @@ 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. @@ -57,6 +58,12 @@ def __init__(self, var): 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`' @@ -88,6 +95,13 @@ def _manipulate_label(x): # gets the label that is displayed for a symbol or na 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 @@ -132,7 +146,7 @@ def _add_continuous_widget(self, symbol, label, default, minimum, maximum, evalu raise IllegalWidgetArguments(symbol) else: defval = min(max(default.to_python(), minimum_value), maximum_value) - widget = FloatSlider(value=defval, min=minimum_value, max=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): @@ -144,10 +158,10 @@ def _add_discrete_widget(self, symbol, label, default, minimum, maximum, step, e 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 = IntSlider(value=default_value, min=minimum_value, max=maximum_value, + widget = _create_widget(IntSlider, value=default_value, min=minimum_value, max=maximum_value, step=step_value) else: - widget = FloatSlider(value=default_value, min=minimum_value, max=maximum_value, + 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) @@ -162,7 +176,7 @@ def _add_options_widget(self, symbol, options, default, label, evaluation): if option.same(default): default_index = i - widget = ToggleButtons(options=formatted_options, value=default_index) + 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): @@ -226,8 +240,11 @@ class Manipulate(Builtin): messages = { 'jupyter': 'Manipulate[] only works inside a Jupyter notebook.', 'noipywidget': 'Manipulate[] needs the ipywidgets module to work.', - 'generr': 'Widget creation failed: ``', - 'widgetargs': 'Illegal variable range or step parameters for ``.' + '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.', } def apply(self, expr, args, evaluation): @@ -245,14 +262,18 @@ def apply(self, expr, args, evaluation): if not instantiator.add(arg, evaluation): # not a valid argument pattern? return except IllegalWidgetArguments as e: - evaluation.message('Manipulate', 'widgetargs', strip_context(str(e.var))) - except Exception as e: - evaluation.message('Manipulate', 'generr', str(e)) # usually some Jupyter error + evaluation.error('Manipulate', 'widgetargs', strip_context(str(e.var))) + except JupyterWidgetError as e: + evaluation.error('Manipulate', 'widgetmake', e.err) return Symbol('$Aborted') clear_output_callback = evaluation.clear_output_callback display_data_callback = evaluation.display_data_callback # for pushing updates + if clear_output_callback is None or display_data_callback is None: + evaluation.error('Manipulate', 'imathics') + return Symbol('$Aborted') + def callback(**kwargs): clear_output_callback(wait=True) @@ -270,12 +291,9 @@ def callback(**kwargs): widgets = instantiator.get_widgets() if len(widgets) > 0: box = _interactive(instantiator.build_callback(callback), widgets) # create the widget - - # the following code is a boiled down version from IPython.core.formatters.IPythonDisplayFormatter. - # note that '_ipython_display_' is a magic constant defined in print_method of IPythonDisplayFormatter. - - method = getattr(box, '_ipython_display_') - if method is not None: - method() # make the widget appear on the Jupyter notebook + formatter = IPythonDisplayFormatter() + if not formatter(box): # make the widget appear on the Jupyter notebook + evaluation.error('Manipulate', 'widgetdisp') + return Symbol('$Aborted') return Symbol('Null') # the interactive output is pushed via kernel.display_data_callback (see above) From 97e1e29a789795d006caf8beda49c938b7c5fdf9 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sun, 15 May 2016 10:06:55 +0200 Subject: [PATCH 20/22] fixes a bug that had use of labels fill the wrong symbol --- mathics/builtin/manipulate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index e5b682943c..c584b56804 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -72,9 +72,9 @@ class ManipulateParameter(Builtin): # parses one Manipulate[] parameter spec, e. '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, Label -> s, Default -> d}, {r}]', + 'System`Private`ManipulateParameter[{Symbol -> s, Default -> d, Label -> s}, {r}]', 'System`Private`ManipulateParameter[{{s_Symbol, d_, l_}, r__}]': - 'System`Private`ManipulateParameter[{Symbol -> i, Label -> l, Default -> d}, {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}]': From 5420906f6c69f2497ad04908136a33f9d3de2000 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Sun, 15 May 2016 19:02:16 +1000 Subject: [PATCH 21/22] cleanup evaluation.error -> evaluation.message --- mathics/builtin/manipulate.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index c584b56804..5657f63d5e 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -262,17 +262,15 @@ def apply(self, expr, args, evaluation): if not instantiator.add(arg, evaluation): # not a valid argument pattern? return except IllegalWidgetArguments as e: - evaluation.error('Manipulate', 'widgetargs', strip_context(str(e.var))) + return evaluation.message('Manipulate', 'widgetargs', strip_context(str(e.var))) except JupyterWidgetError as e: - evaluation.error('Manipulate', 'widgetmake', e.err) - return Symbol('$Aborted') + return evaluation.message('Manipulate', 'widgetmake', e.err) clear_output_callback = evaluation.clear_output_callback display_data_callback = evaluation.display_data_callback # for pushing updates if clear_output_callback is None or display_data_callback is None: - evaluation.error('Manipulate', 'imathics') - return Symbol('$Aborted') + return evaluation.message('Manipulate', 'imathics') def callback(**kwargs): clear_output_callback(wait=True) @@ -293,7 +291,6 @@ def callback(**kwargs): box = _interactive(instantiator.build_callback(callback), widgets) # create the widget formatter = IPythonDisplayFormatter() if not formatter(box): # make the widget appear on the Jupyter notebook - evaluation.error('Manipulate', 'widgetdisp') - return Symbol('$Aborted') + return evaluation.message('Manipulate', 'widgetdisp') return Symbol('Null') # the interactive output is pushed via kernel.display_data_callback (see above) From 05a5ea2a64f06e945bd6bfa7a180d919b643447c Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 17 Aug 2016 13:08:27 +0200 Subject: [PATCH 22/22] adapted to match latest changes in IMathics and Mathics --- mathics/builtin/manipulate.py | 42 ++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index 5657f63d5e..58c95ff203 100755 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -6,7 +6,7 @@ from mathics.core.expression import String, strip_context from mathics import settings -from mathics.core.evaluation import Evaluation +from mathics.core.evaluation import Evaluation, Output from mathics.builtin.base import Builtin from mathics.core.expression import Expression, Symbol, Integer, from_python @@ -18,7 +18,7 @@ _jupyter = False try: - from ipywidgets import (IntSlider, FloatSlider, ToggleButtons, Box, DOMWidget) + from ipywidgets import IntSlider, FloatSlider, ToggleButtons, Box, DOMWidget from IPython.core.formatters import IPythonDisplayFormatter _ipywidgets = True except ImportError: @@ -187,6 +187,20 @@ def _add_widget(self, widget, name, parse, label): 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): """
@@ -239,7 +253,6 @@ class Manipulate(Builtin): messages = { 'jupyter': 'Manipulate[] only works inside a Jupyter notebook.', - 'noipywidget': 'Manipulate[] needs the ipywidgets module to work.', '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 "``".', @@ -247,14 +260,15 @@ class Manipulate(Builtin): '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') - if not _ipywidgets: - return evaluation.message('Manipulate', 'noipywidget') - instantiator = _WidgetInstantiator() # knows about the arguments and their widgets for arg in args.get_sequence(): @@ -266,10 +280,12 @@ def apply(self, expr, args, evaluation): except JupyterWidgetError as e: return evaluation.message('Manipulate', 'widgetmake', e.err) - clear_output_callback = evaluation.clear_output_callback - display_data_callback = evaluation.display_data_callback # for pushing updates + clear_output_callback = evaluation.output.clear + display_data_callback = evaluation.output.display # for pushing updates - if clear_output_callback is None or display_data_callback is None: + try: + clear_output_callback(wait=True) + except NotImplementedError: return evaluation.message('Manipulate', 'imathics') def callback(**kwargs): @@ -277,12 +293,12 @@ def callback(**kwargs): line_no = evaluation.definitions.get_line_no() - new_evaluation = Evaluation(evaluation.definitions, result_callback=display_data_callback, - out_callback=evaluation.out_callback) - vars = [Expression('Set', Symbol(name), value) for name, value in kwargs.items()] evaluatable = Expression('ReleaseHold', Expression('Module', Expression('List', *vars), expr)) - new_evaluation.evaluate([evaluatable], timeout=settings.TIMEOUT) + + 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