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

handling TimeConstrained[] in SymPy evaluation #1160

Merged
merged 7 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
27 changes: 8 additions & 19 deletions mathics/builtin/datentime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1055,7 +1055,13 @@ def eval(self, n, evaluation):
)
return

time.sleep(sleeptime)
steps = int(1000 * sleeptime)
while steps > 0:
rocky marked this conversation as resolved.
Show resolved Hide resolved
time.sleep(0.001)
if evaluation.timeout:
return SymbolNull
steps = steps - 1

return SymbolNull


Expand Down Expand Up @@ -1103,7 +1109,7 @@ def evaluate(self, evaluation):
return Expression(SymbolDateObject.evaluate(evaluation))


if sys.platform != "win32" and not hasattr(sys, "pyston_version_info"):
if sys.platform != "emscripten":

class TimeConstrained(Builtin):
"""
Expand All @@ -1124,23 +1130,6 @@ class TimeConstrained(Builtin):
the state of the Mathics3 kernel.
"""

# FIXME: these tests sometimes cause SEGVs which probably means
# that TimeConstraint has bugs.

# Consider testing via unit tests.
# >> TimeConstrained[Integrate[Sin[x]^1000000,x],1]
# = $Aborted
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried

TimeConstrained[Integrate[Sin[x]^1000000,x],1]

I am getting a RecursionError that is not handled. Worse is that calls with_traceback() with the wrong number of parameters. A PR has been created to address this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, with Pyston I got a core dump for that test. But it is reasonable: Sympy does not control by itself how much memory/work needs to converge.


# >> TimeConstrained[Integrate[Sin[x]^1000000,x], 1, Integrate[Cos[x],x]]
# = Sin[x]

# >> s=TimeConstrained[Integrate[Sin[x] ^ 3, x], a]
# : Number of seconds a is not a positive machine-sized number or Infinity.
# = TimeConstrained[Integrate[Sin[x] ^ 3, x], a]

# >> a=1; s
# = Cos[x] (-5 + Cos[2 x]) / 6

attributes = A_HOLD_ALL | A_PROTECTED
messages = {
"timc": (
Expand Down
12 changes: 3 additions & 9 deletions mathics/core/builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
from mathics.core.convert.expression import to_expression
from mathics.core.convert.op import ascii_operator_to_symbol
from mathics.core.convert.python import from_bool
from mathics.core.convert.sympy import from_sympy, to_numeric_sympy_args
from mathics.core.convert.sympy import from_sympy
from mathics.core.definitions import Definition, Definitions
from mathics.core.evaluation import Evaluation
from mathics.core.exceptions import MessageException
Expand Down Expand Up @@ -91,6 +91,7 @@
from mathics.eval.numbers.numbers import cancel
from mathics.eval.numerify import numerify
from mathics.eval.scoping import dynamic_scoping
from mathics.eval.sympy import eval_sympy

try:
import ujson
Expand Down Expand Up @@ -582,14 +583,7 @@ def eval(self, z, evaluation: Evaluation):
# converted to python and the result is converted from sympy
#
# "%(name)s[z__]"
sympy_args = to_numeric_sympy_args(z, evaluation)
if self.sympy_name is None:
return
sympy_fn = getattr(sympy, self.sympy_name)
try:
return from_sympy(tracing.run_sympy(sympy_fn, *sympy_args))
except Exception:
return
return eval_sympy(self, z, evaluation)

def get_constant(self, precision, evaluation, have_mpmath=False):
try:
Expand Down
3 changes: 2 additions & 1 deletion mathics/core/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def run_with_timeout_and_stack(request, timeout, evaluation):
if thread.is_alive():
evaluation.timeout = True
while thread.is_alive():
time.sleep(0.001)
pass
evaluation.timeout = False
evaluation.stopped = False
Expand All @@ -140,7 +141,7 @@ def run_with_timeout_and_stack(request, timeout, evaluation):
if success:
return result
else:
raise result[0].with_traceback(result[1], result[2])
raise result[1].with_traceback(result[1], result[2])
rocky marked this conversation as resolved.
Show resolved Hide resolved


class _Out(KeyComparable):
Expand Down
76 changes: 76 additions & 0 deletions mathics/eval/sympy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""
Evaluation of sympy functions
"""

import sys
from queue import Queue
from threading import Thread

import sympy

import mathics.eval.tracing as tracing
from mathics.core.convert.sympy import from_sympy, to_numeric_sympy_args


def eval_sympy_unconstrained(self, z, evaluation):
rocky marked this conversation as resolved.
Show resolved Hide resolved
"""
Evaluate an expression converting it to Sympy
and back to Mathics.
"""
sympy_args = to_numeric_sympy_args(z, evaluation)
if self.sympy_name is None:
return
sympy_fn = getattr(sympy, self.sympy_name)
try:
return from_sympy(tracing.run_sympy(sympy_fn, *sympy_args))
except Exception:
return


def eval_sympy_with_timeout(self, z, evaluation):
"""
Evaluate an expression converting it to Sympy,
and back to Mathics.
This version put the evaluation in a thread,
and check each some time if the evaluation
reached a timeout.
"""

if evaluation.timeout is None:
return eval_sympy_unconstrained(self, z, evaluation)

def _thread_target(queue) -> None:
try:
result = eval_sympy_unconstrained(self, z, evaluation)
queue.put((True, result))
except BaseException:
exc_info = sys.exc_info()
queue.put((False, exc_info))

queue = Queue(maxsize=1) # stores the result or exception

def evaluate():
rocky marked this conversation as resolved.
Show resolved Hide resolved
return

thread = Thread(target=_thread_target, args=(queue,))
thread.start()
while thread.is_alive():
thread.join(0.001)
if evaluation.timeout:
# I can kill the thread.
# just leave it...
return None

# pick the result and return
success, result = queue.get()
if success:
return result
else:
raise result[0].with_traceback(result[1], result[2])


eval_sympy = (
eval_sympy_unconstrained
if sys.platform in ("emscripten",)
else eval_sympy_with_timeout
)
63 changes: 58 additions & 5 deletions test/builtin/test_datentime.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,28 @@


@pytest.mark.skipif(
sys.platform in ("win32", "emscripten") or hasattr(sys, "pyston_version_info"),
reason="TimeConstrained needs to be rewritten",
sys.platform in ("emscripten",),
reason="TimeConstrained is based in Threads, which are not supported in Piodide",
)
def test_timeremaining():
str_expr = "TimeConstrained[1+2; TimeRemaining[], 0.9]"
result = evaluate(str_expr)
assert result is None or 0 < result.to_python() < 9


@pytest.mark.skip(reason="TimeConstrained needs to be rewritten")
@pytest.mark.skipif(
sys.platform in ("emscripten",),
reason="TimeConstrained is based in Threads, which are not supported in Piodide",
)
def test_timeconstrained1():
#
str_expr1 = "a=1.; TimeConstrained[Do[Pause[.1];a=a+1,{1000}],1]"
str_expr1 = "a=1.; TimeConstrained[Do[Pause[.01];a=a+1,{1000}],.1]"
result = evaluate(str_expr1)
str_expected = "$Aborted"
expected = evaluate(str_expected)
assert result == expected
time.sleep(1)
assert evaluate("a").to_python() == 10
assert evaluate("a").to_python() <= 10


def test_datelist():
Expand Down Expand Up @@ -122,3 +125,53 @@ def test_private_doctests_datetime(str_expr, msgs, str_expected, fail_msg):
failure_message=fail_msg,
expected_messages=msgs,
)


@pytest.mark.skipif(
sys.platform in ("emscripten",),
reason="TimeConstrained is based in Threads, which are not supported in Piodide",
)
@pytest.mark.parametrize(
("str_expr", "msgs", "str_expected", "fail_msg"),
[
##
(
"TimeConstrained[Integrate[Sin[x]^100,x],.5]",
None,
"$Aborted",
"TimeConstrained with two arguments",
),
(
"TimeConstrained[Integrate[Sin[x]^100,x],.5, Integrate[Cos[x],x]]",
None,
"Sin[x]",
"TimeConstrained with three arguments",
),
(
"a=.;s=TimeConstrained[Integrate[Sin[x] ^ 3, x], a]",
(
"Number of seconds a is not a positive machine-sized number or Infinity.",
),
"TimeConstrained[Integrate[Sin[x] ^ 3, x], a]",
"TimeConstrained unevaluated because the second argument is not numeric",
),
(
"a=1; s",
None,
"Cos[x] (-3 + Cos[x] ^ 2) / 3",
"s is now evaluated because `a` is a number.",
),
("a=.;s=.;", None, "Null", None),
],
)
def test_private_doctests_TimeConstrained(str_expr, msgs, str_expected, fail_msg):
"""TimeConstrained tests"""
check_evaluation(
str_expr,
str_expected,
to_string_expr=True,
to_string_expected=True,
hold_expected=True,
failure_message=fail_msg,
expected_messages=msgs,
)