Skip to content

Commit afed316

Browse files
authored
Vectorized plot3d (#1522)
* Introduce a vectorized Plot3D in plot3d_vectorized.py. Since this generates Graphics3D containing GraphicsComplex with NumericArray, which no code will currently understand, for now it is enabled only if you set environment variable MATHICS_USE_VECTORIZED_PLOT=yes. (TBD how we will eventually deploy.) * Introduce a "compiler" in plot_compile.py that uses expr.to_sympy() followed by sympy.lambdify to generate a Python function that calls numpy to evaluate. This is a huge win because most Python functions can do vectorized operations on entire arrays. For now it's specific to plotting; TBD whether it might find other uses. See test_plot_compiler to get a sense of what function is currently supported. * Slightly refactors GraphicsGenerator for simplicity. Here are some timings comparing classic and vectorized Plot3D. The visual comparison was done using a prototype front-end that understands GraphicsComplex and NumericArray. Plot3D[Sin[x] Cos[y], {x,0,10}, {y,0,10}, PlotPoints->{p,p}, MaxRecursion->r] p r eval_Plot3D classic 50 2 (default) 10083 ms visible triangles on plot surface classic 100 0 3232 ms less visible artifacts than above vectorized 200 n/a 5 ms no visible artifacts Plotting is now fast enough that useful interaction with Manipulate is possible. I have a prototype of that and will submit a PR. Other possible future work is to build out support for GraphicsComplex and NumericArray in consumers of Graphics3D. It might be helpful to have a "GraphicsParser" class that takes a Graphics3D expression and turns it into Python data structures that are easy for consumers to traverse.
1 parent cc06072 commit afed316

File tree

8 files changed

+710
-46
lines changed

8 files changed

+710
-46
lines changed

mathics/builtin/drawing/plot.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
import numbers
10+
import os
1011
from abc import ABC
1112
from functools import lru_cache
1213
from math import cos, pi, sin
@@ -51,7 +52,18 @@
5152
get_plot_range,
5253
get_plot_range_option,
5354
)
54-
from mathics.eval.drawing.plot3d import eval_DensityPlot, eval_Plot3D
55+
56+
# The vectorized plot function generates GraphicsComplex using NumericArray,
57+
# which no consumer will currently understand. So lets make it opt-in for now.
58+
# If it remains opt-in we'll probably want some combination of env variables,
59+
# Set option such as $UseVectorizedPlot, and maybe a non-standard Plot3D option.
60+
# For now an env variable is simplest.
61+
# TODO: work out exactly how to deploy.
62+
if os.getenv("MATHICS3_USE_VECTORIZED_PLOT", False):
63+
from mathics.eval.drawing.plot3d_vectorized import eval_DensityPlot, eval_Plot3D
64+
else:
65+
from mathics.eval.drawing.plot3d import eval_DensityPlot, eval_Plot3D
66+
5567
from mathics.eval.nevaluator import eval_N
5668

5769
# This tells documentation how to sort this module

mathics/core/systemsymbols.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
SymbolGoldenRatio = Symbol("System`GoldenRatio")
113113
SymbolGraphics = Symbol("System`Graphics")
114114
SymbolGraphics3D = Symbol("System`Graphics3D")
115+
SymbolGraphicsComplex = Symbol("System`GraphicsComplex")
115116
SymbolGreater = Symbol("System`Greater")
116117
SymbolGreaterEqual = Symbol("System`GreaterEqual")
117118
SymbolGrid = Symbol("System`Grid")

mathics/eval/drawing/plot3d.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
SymbolSlot,
2323
)
2424
from mathics.eval.drawing.plot import compile_quiet_function
25+
from mathics.timing import Timer
2526

2627
from .util import GraphicsGenerator
2728

@@ -388,6 +389,7 @@ def triangle(x1, y1, x2, y2, x3, y3, depth=0):
388389
return triangles, mesh_points, v_min, v_max
389390

390391

392+
@Timer("eval_Plot3D")
391393
def eval_Plot3D(
392394
plot_options,
393395
evaluation: Evaluation,
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""
2+
Vectorized evaluation routines for Plot3D and DensityPlot, which share a good bit of code.
3+
4+
TODO: fill out eval_DensityPlot
5+
"""
6+
7+
import math
8+
9+
import numpy as np
10+
11+
from mathics.core.evaluation import Evaluation
12+
from mathics.core.symbols import strip_context
13+
from mathics.timing import Timer
14+
15+
from .plot_compile import plot_compile
16+
from .util import GraphicsGenerator
17+
18+
19+
@Timer("eval_Plot3D")
20+
def eval_Plot3D(
21+
plot_options,
22+
evaluation: Evaluation,
23+
):
24+
graphics = GraphicsGenerator(dim=3)
25+
26+
for function in plot_options.functions:
27+
# pull out plot options
28+
_, xmin, xmax = plot_options.ranges[0]
29+
_, ymin, ymax = plot_options.ranges[1]
30+
nx, ny = plot_options.plotpoints
31+
names = [strip_context(str(range[0])) for range in plot_options.ranges]
32+
33+
with Timer("compile"):
34+
function = plot_compile(evaluation, function, names)
35+
36+
# compute (nx, ny) grids of xs and ys for corresponding vertexes
37+
xs = np.linspace(xmin, xmax, nx)
38+
ys = np.linspace(ymin, ymax, ny)
39+
xs, ys = np.meshgrid(xs, ys)
40+
41+
# compute zs from xs and ys using compiled function
42+
with Timer("compute zs"):
43+
zs = function(**{str(names[0]): xs, str(names[1]): ys})
44+
45+
# sometimes expr gets compiled into something that returns a complex
46+
# even though the imaginary part is 0
47+
# TODO: check that imag is all 0?
48+
# TODO: needed this for Hypergeometric - look into that
49+
# assert np.all(np.isreal(zs)), "array contains complex values"
50+
zs = np.real(zs)
51+
52+
with Timer("stack"):
53+
# (nx*ny, 3) array of points, to be indexed by quads
54+
xyzs = np.stack([xs, ys, zs]).transpose(1, 2, 0).reshape(-1, 3)
55+
56+
# (nx,ny) array of numbers from 0 to n-1 that are
57+
# indexes into xyzs array for corresponding vertex
58+
inxs = np.arange(math.prod(xs.shape)).reshape(xs.shape)
59+
60+
# shift inxs array four different ways and stack to form
61+
# (4, nx-1, ny-1) array of quads represented as indexes into xyzs array
62+
quads = np.stack(
63+
[inxs[:-1, :-1], inxs[:-1, 1:], inxs[1:, 1:], inxs[1:, :-1]]
64+
)
65+
66+
# transpose and flatten to ((nx-1)*(ny-1), 4) array, suitable for use in GraphicsComplex
67+
quads = quads.T.reshape(-1, 4)
68+
69+
# ugh - indexes in Polygon are 1-based
70+
quads += 1
71+
72+
# add a GraphicsComplex for this function
73+
graphics.add_complex(xyzs, lines=None, polys=quads)
74+
75+
return graphics
76+
77+
78+
#
79+
#
80+
#
81+
82+
83+
def eval_DensityPlot(
84+
plot_options,
85+
evaluation: Evaluation,
86+
):
87+
# TODO
88+
# see plot3d.eval_DensityPlot for possible info on handling colors
89+
pass
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""
2+
"Compile" an Expression by converting it to SymPy, then use sympy.lambdify
3+
to turn it into a Python function that calls NumPy functions to evaluate it.
4+
5+
While possibly this provides an efficient function for point-wise evaluation,
6+
the main goal is to use NumPy to perform vectorized operations on arrays,
7+
which is a huge win for plotting.
8+
9+
This will need testing and building out to make it robustly applicable
10+
to a wide array of expressions. So for now this is specific to plotting
11+
and is used only when enabled, so it lives with the plotting functions.
12+
Maybe eventually consider to move elsewhere if it seems to be more
13+
widely useful.
14+
"""
15+
16+
import inspect
17+
18+
import scipy
19+
import sympy
20+
21+
from mathics.core.convert.sympy import SympyExpression, mathics_to_sympy
22+
from mathics.core.symbols import strip_context
23+
from mathics.core.util import print_expression_tree, print_sympy_tree
24+
25+
26+
# TODO: not in use yet
27+
# Add functions not found in scipy or numpy here.
28+
# Hopefully they are just thin adapater layers.
29+
# TODO: let's see how much is really needed here, and possibly consider moving
30+
# them to the Builtin somehow
31+
class AdditionalMappings:
32+
def hyppfq(p, q, x):
33+
if len(p) == 1 and len(q) == 1:
34+
return scipy.special.hyp1f1(p[0], q[0], x)
35+
else:
36+
raise Exception(f"can't handle hyppfq({p}, {q}, x)")
37+
38+
39+
# mappings = [dict(AdditionalMappings.__dict__), "numpy"]
40+
41+
42+
class CompileError(Exception):
43+
pass
44+
45+
46+
def plot_compile(evaluation, expr, names, debug=0):
47+
"""Compile the specified expression as a function of the given names"""
48+
49+
if debug >= 2:
50+
print("=== compiling expr")
51+
print_expression_tree(expr)
52+
53+
# Ask the expr Expression to generate a sympy expression and handle errors
54+
try:
55+
sympy_expr = expr.to_sympy()
56+
except Exception as oops:
57+
raise CompileError(f"{expr}.to_sympy() failed: {oops}")
58+
if isinstance(sympy_expr, SympyExpression):
59+
if debug:
60+
# duplicates lookup logic in mathics.core.convert.sympy
61+
lookup_name = expr.get_lookup_name()
62+
builtin = mathics_to_sympy.get(lookup_name)
63+
if builtin:
64+
sympy_name = getattr(builtin, "sympy_name", None) if builtin else None
65+
print(f"compile: Invalid sympy_expr {sympy_expr}")
66+
print(f"compile: {builtin}.sympy_name is {repr(sympy_name)}")
67+
else:
68+
print(f"compile: {lookup_name} not registered with mathics_to_sympy")
69+
raise CompileError(f"{expr.head}.to_sympy returns invalid sympy expr.")
70+
71+
# Strip symbols in sympy expression of context.
72+
subs = {
73+
sym: sympy.Symbol(strip_context(str(sym))) for sym in sympy_expr.free_symbols
74+
}
75+
sympy_expr = sympy_expr.subs(subs)
76+
77+
if debug >= 2:
78+
print("=== equivalent sympy", type(sympy_expr))
79+
print_sympy_tree(sympy_expr)
80+
81+
# Ask sympy to generate a function that will evaluate the expr.
82+
# Use numpy and scipy to do the evaluation so that operations are vectorized.
83+
# Augment the default numpy mappings with some additional ones not handled by default.
84+
try:
85+
symbols = sympy.symbols(names)
86+
# compiled_function = sympy.lambdify(symbols, sympy_expr, mappings)
87+
compiled_function = sympy.lambdify(
88+
symbols, sympy_expr, modules=["numpy", "scipy"]
89+
)
90+
except Exception as oops:
91+
raise CompileError(f"error compiling sympy expr {sympy_expr}: {oops}")
92+
93+
if debug >= 2:
94+
print("=== compiled python")
95+
print(inspect.getsource(compiled_function))
96+
97+
return compiled_function

mathics/eval/drawing/util.py

Lines changed: 34 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
Common utilities for plotting
33
"""
44

5-
import itertools
65

6+
from mathics.core.atoms import NumericArray
77
from mathics.core.convert.expression import to_mathics_list
88
from mathics.core.expression import Expression
99
from mathics.core.list import ListExpression
1010
from mathics.core.systemsymbols import (
1111
SymbolGraphics,
1212
SymbolGraphics3D,
13+
SymbolGraphicsComplex,
1314
SymbolLine,
1415
SymbolPolygon,
1516
SymbolRule,
@@ -25,69 +26,57 @@ class GraphicsGenerator:
2526
Support for generating Graphics and Graphics3D expressions
2627
"""
2728

28-
# TODO: more precise types
29-
# TODO: consider pre-zipping so only one for polys and one for lines
30-
# TODO: consider whether we need to store these or can we just generate as we go?
31-
poly_xyzs: list
32-
poly_xyzs_colors: list
33-
line_xyzs: list
34-
line_xyzs_colors: list
29+
# TODO: more precise typing?
30+
graphics: list
3531

3632
# 2 or 3
3733
dim: int
3834

3935
def __init__(self, dim: int):
4036
self.dim = dim
41-
self.poly_xyzs = []
42-
self.poly_xyzs_colors = []
43-
self.line_xyzs = []
44-
self.line_xyzs_colors = []
37+
self.graphics = []
38+
39+
# add polygons and lines, optionally with vertex colors
40+
def add_thing(self, thing_symbol, things, colors):
41+
arg = tuple(to_mathics_list(*thing) for thing in things)
42+
arg = ListExpression(*arg) if len(arg) > 1 else arg[0]
43+
if colors:
44+
color_arg = tuple(to_mathics_list(*color) for color in colors)
45+
color_arg = (
46+
ListExpression(*color_arg) if len(color_arg) > 1 else color_arg[0]
47+
)
48+
color_rule = Expression(SymbolRule, SymbolVertexColors, color_arg)
49+
self.graphics.append(Expression(thing_symbol, arg, color_rule))
50+
else:
51+
self.graphics.append(Expression(thing_symbol, arg))
4552

46-
# TODO: is this correct if some polys have colors and some don't?
4753
def add_polyxyzs(self, poly_xyzs, colors=None):
4854
"""Add polygons specified by explicit xy[z] coordinates"""
49-
self.poly_xyzs.append(poly_xyzs)
50-
if colors:
51-
self.poly_xyzs_colors.append(colors)
55+
self.add_thing(SymbolPolygon, poly_xyzs, colors)
5256

5357
def add_linexyzs(self, line_xyzs, colors=None):
5458
"""Add lines specified by explicit xy[z] coordinates"""
55-
self.line_xyzs.append(line_xyzs)
56-
if colors:
57-
self.line_xyzs_colors.append(colors)
59+
self.add_thing(SymbolLine, line_xyzs, colors)
60+
61+
# TODO: color
62+
def add_complex(self, xyzs, lines=None, polys=None):
63+
complex = [NumericArray(xyzs)]
64+
if polys is not None:
65+
polys_expr = Expression(SymbolPolygon, NumericArray(polys))
66+
complex.append(polys_expr)
67+
if lines is not None:
68+
polys_expr = Expression(SymbolLines, NumericArray(lines))
69+
complex.append(lines_expr)
70+
gc_expr = Expression(SymbolGraphicsComplex, *complex)
71+
self.graphics.append(gc_expr)
5872

5973
def generate(self, options):
6074
"""
6175
Generates Graphics[3D] expression from supplied lines, polygons (etc.)
6276
"""
63-
64-
# holds the elements of the final Graphics[3D] expr
65-
graphics = []
66-
67-
# add polygons and lines, optionally with vertex colors
68-
def add_thing(thing_symbol, thingss, colorss):
69-
for things, colors in itertools.zip_longest(thingss, colorss):
70-
arg = tuple(to_mathics_list(*thing) for thing in things)
71-
arg = ListExpression(*arg) if len(arg) > 1 else arg[0]
72-
if colors:
73-
color_arg = tuple(to_mathics_list(*color) for color in colors)
74-
color_arg = (
75-
ListExpression(*color_arg)
76-
if len(color_arg) > 1
77-
else color_arg[0]
78-
)
79-
color_rule = Expression(SymbolRule, SymbolVertexColors, color_arg)
80-
graphics.append(Expression(thing_symbol, arg, color_rule))
81-
else:
82-
graphics.append(Expression(thing_symbol, arg))
83-
84-
add_thing(SymbolPolygon, self.poly_xyzs, self.poly_xyzs_colors)
85-
add_thing(SymbolLine, self.line_xyzs, self.line_xyzs_colors)
86-
87-
# generate Graphics[3D] expression
8877
graphics_expr = Expression(
8978
SymbolGraphics3D if self.dim == 3 else SymbolGraphics,
90-
ListExpression(*graphics),
79+
ListExpression(*self.graphics),
9180
*options,
9281
)
9382

0 commit comments

Comments
 (0)