Skip to content

Commit 1e98d7d

Browse files
authored
Merge pull request #1 from Mathics3/adjust_tests_and_context
Add existing vectorized Plot(2D) routines. Adjust tests and context
2 parents a3af897 + af4a02d commit 1e98d7d

File tree

8 files changed

+162
-44
lines changed

8 files changed

+162
-44
lines changed

pymathics/vectorizedplot/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,34 @@
2727
# The try block is needed because at installation time, dependencies are not
2828
# available. After the installation is successfull, we want to make this availabe.
2929
try:
30+
from pymathics.vectorizedplot.plot_plot import (
31+
LogPlot,
32+
ParametricPlot,
33+
Plot,
34+
PolarPlot,
35+
)
3036
from pymathics.vectorizedplot.plot_plot3d import (
37+
ComplexPlot,
38+
ComplexPlot3D,
3139
ContourPlot,
3240
ContourPlot3D,
41+
DensityPlot,
3342
ParametricPlot3D,
43+
Plot3D,
3444
SphericalPlot3D,
3545
)
3646

3747
_BUILTINS_ = (
48+
"ComplexPlot",
49+
"ComplexPlot3D",
3850
"ContourPlot",
3951
"ContourPlot3D",
52+
"DensityPlot",
53+
"LogPlot",
54+
"ParametricPlot",
55+
"PolarPlot",
56+
"Plot",
57+
"Plot3D",
4058
"ParametricPlot3D",
4159
"SphericalPlot3D",
4260
)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
Vectorized evaluation routines for Plot and related subclasses of _Plot
3+
"""
4+
5+
import numpy as np
6+
from mathics.builtin.graphics import Graphics
7+
from mathics.builtin.options import filter_from_iterable, options_to_rules
8+
from mathics.core.convert.lambdify import lambdify_compile
9+
from mathics.core.element import BaseElement
10+
from mathics.core.evaluation import Evaluation
11+
from mathics.core.expression import Expression
12+
from mathics.core.symbols import SymbolList, strip_context
13+
from mathics.timing import Timer
14+
15+
from .colors import palette2, palette_color_directive
16+
from .util import GraphicsGenerator
17+
18+
19+
@Timer("eval_Plot_vectorized")
20+
def eval_Plot_vectorized(plot_options, options, evaluation: Evaluation):
21+
# Note on naming: we use t to refer to the independent variable initially.
22+
# For Plot etc. it will be x, but for ParametricPlot it is better called t,
23+
# and for PolarPlot theta. After we call the apply_function supplied by
24+
# the plotting class we will then have actual plot coordinate xs and ys.
25+
tname, tmin, tmax = plot_options.ranges[0]
26+
nt = plot_options.plot_points
27+
28+
# ParametricPlot passes a List of two functions, but lambdify_compile doesn't handle that
29+
# TODO: we should be receiving this as a Python list not an expression List?
30+
# TODO: can lambidfy_compile handle list with an appropriate to_sympy?
31+
def compile_maybe_list(evaluation, function, names):
32+
if isinstance(function, Expression) and function.head == SymbolList:
33+
fs = [lambdify_compile(evaluation, f, names) for f in function.elements]
34+
35+
def compiled(vs):
36+
return [f(vs) for f in fs]
37+
38+
else:
39+
compiled = lambdify_compile(evaluation, function, names)
40+
return compiled
41+
42+
# compile the functions
43+
with Timer("compile"):
44+
names = [strip_context(str(tname))]
45+
compiled_functions = [
46+
compile_maybe_list(evaluation, function, names)
47+
for function in plot_options.functions
48+
]
49+
50+
# compute requested regularly spaced points over the requested range
51+
ts = np.linspace(tmin, tmax, nt)
52+
53+
# 1-based indexes into point array to form a line
54+
line = np.arange(nt) + 1
55+
56+
# compute the curves and accumulate in a GraphicsGenerator
57+
graphics = GraphicsGenerator(dim=2)
58+
for i, function in enumerate(compiled_functions):
59+
# compute xs and ys from ts using the compiled function
60+
# and the apply_function supplied by the plot class
61+
with Timer("compute xs and ys"):
62+
xs, ys = plot_options.apply_function(function, ts)
63+
64+
# If the result is not numerical, we assume that the plot have failed.
65+
if isinstance(ys, BaseElement):
66+
return None
67+
68+
# sometimes expr gets compiled into something that returns a complex
69+
# even though the imaginary part is 0
70+
# TODO: check that imag is all 0?
71+
# assert np.all(np.isreal(zs)), "array contains complex values"
72+
xs = np.real(xs)
73+
ys = np.real(ys)
74+
75+
# take log if requested; downstream axes will adjust accordingly
76+
if plot_options.use_log_scale:
77+
ys = np.log10(ys)
78+
79+
# if it's a constant, make it a full array
80+
if isinstance(xs, (float, int, complex)):
81+
xs = np.full(ts.shape, xs)
82+
if isinstance(ys, (float, int, complex)):
83+
ys = np.full(ts.shape, ys)
84+
85+
# (nx, 2) array of points, to be indexed by lines
86+
xys = np.stack([xs, ys]).T
87+
88+
# give it a color from the 2d graph default color palette
89+
color = palette_color_directive(palette2, i)
90+
graphics.add_directives(color)
91+
92+
# emit this line
93+
graphics.add_complex(xys, lines=line, polys=None)
94+
95+
# copy options to output and generate the Graphics expr
96+
options = options_to_rules(options, filter_from_iterable(Graphics.options))
97+
graphics_expr = graphics.generate(options)
98+
return graphics_expr

pymathics/vectorizedplot/plot.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,6 @@ def to_list(expr):
556556
plot_range = [symbol_type("System`Automatic")] * dim
557557
plot_range[-1] = pr
558558
self.plot_range = plot_range
559-
560559
# ColorFunction and ColorFunctionScaling options
561560
# This was pulled from construct_density_plot (now eval_DensityPlot).
562561
# TODO: What does pop=True do? is it right?

pymathics/vectorizedplot/plot_plot.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"""
66

77
from abc import ABC
8-
from functools import lru_cache
98
from typing import Callable
109

1110
import numpy as np
@@ -17,8 +16,7 @@
1716
from mathics.core.symbols import SymbolTrue
1817
from mathics.core.systemsymbols import SymbolLogPlot, SymbolPlotRange, SymbolSequence
1918

20-
from pymathics.vectorizedplot.eval.drawing.plot import eval_Plot
21-
from pymathics.vectorizedplot.eval.drawing.plot_vectorized import eval_Plot_vectorized
19+
from pymathics.vectorizedplot.eval.plot_vectorized import eval_Plot_vectorized
2220

2321
from . import plot
2422

@@ -48,7 +46,7 @@ class _Plot(Builtin, ABC):
4846
"appropriate list of constraints."
4947
),
5048
}
51-
49+
context = "System`"
5250
options = Graphics.options.copy()
5351
options.update(
5452
{
@@ -84,8 +82,6 @@ def eval(self, functions, ranges, evaluation: Evaluation, options: dict):
8482
# because ndarray is unhashable, and in any case probably isn't useful
8583
# TODO: does caching results in the classic case have demonstrable performance benefit?
8684
apply_function = self.apply_function
87-
if not plot.use_vectorized_plot:
88-
apply_function = lru_cache(apply_function)
8985
plot_options.apply_function = apply_function
9086

9187
# TODO: PlotOptions has already regularized .functions to be a list
@@ -98,7 +94,7 @@ def eval(self, functions, ranges, evaluation: Evaluation, options: dict):
9894
plot_options.use_log_scale = self.use_log_scale
9995
plot_options.expect_list = self.expect_list
10096
if plot_options.plot_points is None:
101-
default_plot_points = 1000 if plot.use_vectorized_plot else 57
97+
default_plot_points = 1000
10298
plot_options.plot_points = default_plot_points
10399

104100
# pass through the expanded plot_range options
@@ -110,7 +106,7 @@ def eval(self, functions, ranges, evaluation: Evaluation, options: dict):
110106
options[str(SymbolLogPlot)] = SymbolTrue
111107

112108
# this will be either the vectorized or the classic eval function
113-
eval_function = eval_Plot_vectorized if plot.use_vectorized_plot else eval_Plot
109+
eval_function = eval_Plot_vectorized
114110
with np.errstate(all="ignore"): # suppress numpy warnings
115111
graphics = eval_function(plot_options, options, evaluation)
116112
return graphics
@@ -188,6 +184,7 @@ class Plot(_Plot):
188184
= -Graphics-
189185
"""
190186

187+
context = "System`"
191188
summary_text = "plot curves of one or more functions"
192189

193190
def apply_function(self, f: Callable, x_value):

pymathics/vectorizedplot/plot_plot3d.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class _Plot3D(Builtin):
3030
"""Common base class for Plot3D, DensityPlot, ComplexPlot, ComplexPlot3D"""
3131

3232
attributes = A_HOLD_ALL | A_PROTECTED
33+
context = "System`"
3334

3435
# Check for correct number of args
3536
eval_error = Builtin.generic_argument_error
@@ -84,10 +85,6 @@ class _Plot3D(Builtin):
8485
# 'MaxRecursion': '2', # FIXME causes bugs in svg output see #303
8586
}
8687

87-
def contribute(self, definitions, is_pymodule=False):
88-
print("contribute with ", type(self))
89-
super().contribute(definitions, is_pymodule)
90-
9188
def eval(
9289
self,
9390
functions,

test/helper.py

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,32 @@
22
import time
33
from typing import Optional
44

5+
from mathics.core.element import BaseElement
56
from mathics.core.load_builtin import import_and_load_builtins
67
from mathics.core.symbols import Symbol
78
from mathics.session import MathicsSession
89

910
import_and_load_builtins()
1011

11-
# Set up a Mathics session with definitions.
12+
# Set up two Mathics session with definitions, one for the vectorized routines and
13+
# other for the standard.
1214
# For consistency set the character encoding ASCII which is
1315
# the lowest common denominator available on all systems.
14-
session = MathicsSession(character_encoding="ASCII")
1516

17+
SESSIONS = {
18+
# test.helper session is going to be set up with the library.
19+
True: MathicsSession(character_encoding="ASCII"),
20+
# Default non-vectorized
21+
False: MathicsSession(character_encoding="ASCII"),
22+
}
1623

17-
def reset_session(add_builtin=True, catch_interrupt=False):
18-
global session
19-
session.reset()
2024

21-
22-
def evaluate_value(str_expr: str):
23-
expr = session.evaluate(str_expr)
25+
def expr_to_value(expr: BaseElement):
2426
if isinstance(expr, Symbol):
2527
return expr.name
2628
return expr.value
2729

2830

29-
def evaluate(str_expr: str):
30-
return session.evaluate(str_expr)
31-
32-
3331
def check_evaluation(
3432
str_expr: str,
3533
str_expected: str,
@@ -39,6 +37,7 @@ def check_evaluation(
3937
to_string_expected: bool = True,
4038
to_python_expected: bool = False,
4139
expected_messages: Optional[tuple] = None,
40+
use_vectorized: bool = True,
4241
):
4342
"""
4443
Helper function to test Mathics expression against
@@ -66,34 +65,41 @@ def check_evaluation(
6665
expected_messages ``Optional[tuple[str]]``: If a tuple of strings are passed into this parameter, messages and prints raised during
6766
the evaluation of ``str_expr`` are compared with the elements of the list. If ``None``, this comparison
6867
is ommited.
68+
69+
use_vectorized: bool
70+
If True, use the session with `pymathics.vectorizedplot` loaded.
6971
"""
72+
current_session = SESSIONS[use_vectorized]
73+
7074
if str_expr is None:
71-
reset_session()
72-
evaluate('LoadModule["pymathics.vectorizedplot"]')
75+
current_session.reset()
76+
current_session.evaluate('LoadModule["pymathics.vectorizedplot"]')
7377
return
7478

7579
if to_string_expr:
7680
str_expr = f"ToString[{str_expr}]"
77-
result = evaluate_value(str_expr)
81+
result = expr_to_value(current_session.evaluate(str_expr))
7882
else:
79-
result = evaluate(str_expr)
83+
result = current_session.evaluate(str_expr)
8084

81-
outs = [out.text for out in session.evaluation.out]
85+
outs = [out.text for out in current_session.evaluation.out]
8286

8387
if to_string_expected:
8488
if hold_expected:
8589
expected = str_expected
8690
else:
8791
str_expected = f"ToString[{str_expected}]"
88-
expected = evaluate_value(str_expected)
92+
expected = expr_to_value(current_session.evaluate(str_expected))
8993
else:
9094
if hold_expected:
9195
if to_python_expected:
9296
expected = str_expected
9397
else:
94-
expected = evaluate(f"HoldForm[{str_expected}]").elements[0]
98+
expected = current_session.evaluate(
99+
f"HoldForm[{str_expected}]"
100+
).elements[0]
95101
else:
96-
expected = evaluate(str_expected)
102+
expected = current_session.evaluate(str_expected)
97103
if to_python_expected:
98104
expected = expected.to_python(string_quotes=False)
99105

test/test_plot.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Unit tests from mathics.builtin.drawing.plot
44
"""
55

6-
from test.helper import check_evaluation, session
6+
from test.helper import SESSIONS, check_evaluation
77

88
import pytest
99
from mathics.core.expression import Expression
@@ -37,6 +37,7 @@ def test__listplot():
3737
hold_expected=True,
3838
failure_message=fail_msg,
3939
expected_messages=msgs,
40+
use_vectorized=False,
4041
)
4142

4243

@@ -248,6 +249,7 @@ def test_plot(str_expr, msgs, str_expected, fail_msg):
248249
hold_expected=True,
249250
failure_message=fail_msg,
250251
expected_messages=msgs,
252+
use_vectorized=False,
251253
)
252254

253255

@@ -302,6 +304,8 @@ def mark(parent_expr, marker):
302304

303305

304306
def eval_and_check_structure(str_expr, str_expected):
307+
session = SESSIONS[False]
308+
session.reset()
305309
expr = session.parse(str_expr)
306310
result = expr.evaluate(session.evaluation)
307311
expected = session.parse(str_expected)

0 commit comments

Comments
 (0)