Skip to content

Commit 23a54d0

Browse files
authored
More updates to vectorized Plot3D (#1529)
* Change the default PlotPoints when running vectorized Plot3D to {200,200} * Color the plotted surfaces using a built-in palette with different colors for each surface. * Add a mesh of lines showing the contours of the surfaces. Here are a couple screen shots showing what these changes enable in my [demo front-end](https://github.com/bdlucas1/mathics-demo). Find instructions there if you want to try it out interactively. (Rocky this is a different repo from the one you previously looked at.) <img width="40%" alt="demo-180" src="https://github.com/user-attachments/assets/da905354-0e68-4689-b748-ec1cb82081f9" />. <img width="40%" alt="demo-020" src="https://github.com/user-attachments/assets/d9f81380-b281-40fe-8bbc-e1ec070c423f" />
1 parent 6a3892f commit 23a54d0

File tree

3 files changed

+120
-49
lines changed

3 files changed

+120
-49
lines changed

mathics/builtin/drawing/plot.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@
6060
# Set option such as $UseVectorizedPlot, and maybe a non-standard Plot3D option.
6161
# For now an env variable is simplest.
6262
# TODO: work out exactly how to deploy.
63-
if os.getenv("MATHICS3_USE_VECTORIZED_PLOT", False):
63+
use_vectorized_plot = os.getenv("MATHICS3_USE_VECTORIZED_PLOT", False)
64+
if use_vectorized_plot:
6465
from mathics.eval.drawing.plot3d_vectorized import eval_DensityPlot, eval_Plot3D
6566
else:
6667
from mathics.eval.drawing.plot3d import eval_DensityPlot, eval_Plot3D
@@ -489,8 +490,9 @@ def check_plotpoints(steps):
489490
return True
490491
return False
491492

493+
default_plotpoints = (200, 200) if use_vectorized_plot else (7, 7)
492494
if plotpoints == "System`None":
493-
plotpoints = (7, 7)
495+
plotpoints = default_plotpoints
494496
elif check_plotpoints(plotpoints):
495497
plotpoints = (plotpoints, plotpoints)
496498
if not (
@@ -500,7 +502,7 @@ def check_plotpoints(steps):
500502
and check_plotpoints(plotpoints[1])
501503
):
502504
evaluation.message(expr.get_name(), "invpltpts", plotpoints)
503-
plotpoints = (7, 7)
505+
plotpoints = default_plotpoints
504506
self.plotpoints = plotpoints
505507

506508
# MaxRecursion Option

mathics/eval/drawing/plot3d_vectorized.py

Lines changed: 96 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from mathics.core.evaluation import Evaluation
1212
from mathics.core.symbols import strip_context
13+
from mathics.core.systemsymbols import SymbolNone, SymbolRGBColor
1314
from mathics.timing import Timer
1415

1516
from .plot_compile import plot_compile
@@ -26,55 +27,107 @@ def eval_Plot3D(
2627
# pull out plot options
2728
_, xmin, xmax = plot_options.ranges[0]
2829
_, ymin, ymax = plot_options.ranges[1]
29-
nx, ny = plot_options.plotpoints
3030
names = [strip_context(str(range[0])) for range in plot_options.ranges]
3131

32-
# compute (nx, ny) grids of xs and ys for corresponding vertexes
33-
xs = np.linspace(xmin, xmax, nx)
34-
ys = np.linspace(ymin, ymax, ny)
35-
xs, ys = np.meshgrid(xs, ys)
32+
# Mesh option
33+
nmesh = 20
34+
if plot_options.mesh is SymbolNone:
35+
nmesh = 0
36+
37+
# color-blind friendly palette from https://davidmathlogic.com/colorblind
38+
palette = [
39+
(255, 176, 0), # orange
40+
(100, 143, 255), # blue
41+
(220, 38, 127), # red
42+
(50, 150, 140), # green
43+
(120, 94, 240), # purple
44+
(254, 97, 0), # dark orange
45+
(0, 114, 178), # dark blue
46+
]
47+
48+
# compile the functions
49+
with Timer("compile"):
50+
compiled_functions = [
51+
plot_compile(evaluation, function, names)
52+
for function in plot_options.functions
53+
]
54+
55+
def compute_over_grid(nx, ny):
56+
"""
57+
For each function, computes an (nx*ny, 3) array of coordinates (xyzs),
58+
and an (nx, ny) array of indices (inxs) into xyzs representing
59+
the index in xyzs of the corresponding position in the grid.
60+
Returns an iterator over (xyzs,inxs) pairs, one for each function.
61+
62+
This is used for computing the full grid of quads representing the
63+
surface defined by each function, and also for computing a sparse
64+
grid used to display a mesh of lines on the surface.
65+
"""
66+
67+
# compute (nx, ny) grids of xs and ys for corresponding vertexes
68+
xs = np.linspace(xmin, xmax, nx)
69+
ys = np.linspace(ymin, ymax, ny)
70+
xs, ys = np.meshgrid(xs, ys)
71+
72+
# (nx,ny) array of numbers from 0 to n-1 that are
73+
# indexes into xyzs array for corresponding vertex
74+
# +1 because these will be used as WL indexes, which are 1-based
75+
inxs = np.arange(math.prod(xs.shape)).reshape(xs.shape) + 1
76+
77+
for function in compiled_functions:
78+
# compute zs from xs and ys using compiled function
79+
with Timer("compute zs"):
80+
zs = function(**{str(names[0]): xs, str(names[1]): ys})
81+
82+
# sometimes expr gets compiled into something that returns a complex
83+
# even though the imaginary part is 0
84+
# TODO: check that imag is all 0?
85+
# TODO: needed this for Hypergeometric - look into that
86+
# assert np.all(np.isreal(zs)), "array contains complex values"
87+
zs = np.real(zs)
88+
89+
# if it's a constant, make it a full array
90+
if isinstance(zs, (float, int, complex)):
91+
zs = np.full(xs.shape, zs)
3692

37-
for function in plot_options.functions:
38-
with Timer("compile"):
39-
function = plot_compile(evaluation, function, names)
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-
# if it's a constant, make it a full array
53-
if isinstance(zs, (float, int, complex)):
54-
zs = np.full(xs.shape, zs)
55-
56-
with Timer("stack"):
5793
# (nx*ny, 3) array of points, to be indexed by quads
5894
xyzs = np.stack([xs, ys, zs]).transpose(1, 2, 0).reshape(-1, 3)
5995

60-
# (nx,ny) array of numbers from 0 to n-1 that are
61-
# indexes into xyzs array for corresponding vertex
62-
inxs = np.arange(math.prod(xs.shape)).reshape(xs.shape)
63-
64-
# shift inxs array four different ways and stack to form
65-
# (4, nx-1, ny-1) array of quads represented as indexes into xyzs array
66-
quads = np.stack(
67-
[inxs[:-1, :-1], inxs[:-1, 1:], inxs[1:, 1:], inxs[1:, :-1]]
68-
)
69-
70-
# transpose and flatten to ((nx-1)*(ny-1), 4) array, suitable for use in GraphicsComplex
71-
quads = quads.T.reshape(-1, 4)
72-
73-
# ugh - indexes in Polygon are 1-based
74-
quads += 1
75-
76-
# add a GraphicsComplex for this function
77-
graphics.add_complex(xyzs, lines=None, polys=quads)
96+
yield xyzs, inxs
97+
98+
# generate the quads and emit a GraphicsComplex containing them
99+
for i, (xyzs, inxs) in enumerate(compute_over_grid(*plot_options.plotpoints)):
100+
# shift inxs array four different ways and stack to form
101+
# (4, nx-1, ny-1) array of quads represented as indexes into xyzs array
102+
quads = np.stack([inxs[:-1, :-1], inxs[:-1, 1:], inxs[1:, 1:], inxs[1:, :-1]])
103+
104+
# transpose and flatten to ((nx-1)*(ny-1), 4) array, suitable for use in GraphicsComplex
105+
quads = quads.T.reshape(-1, 4)
106+
107+
# choose a color
108+
rgb = palette[i % len(palette)]
109+
rgb = [c / 255.0 for c in rgb]
110+
# graphics.add_color(SymbolRGBColor, rgb)
111+
graphics.add_directives([SymbolRGBColor, *rgb])
112+
113+
# add a GraphicsComplex displaying a surface for this function
114+
graphics.add_complex(xyzs, lines=None, polys=quads)
115+
116+
# if requested by the Mesh attribute create a mesh of lines covering the surfaces
117+
if nmesh:
118+
# meshes are black for now
119+
graphics.add_directives([SymbolRGBColor, 0, 0, 0])
120+
121+
with Timer("Mesh"):
122+
nx, ny = plot_options.plotpoints
123+
# Do nmesh lines in each direction, each line formed
124+
# from one row or one column of the inxs array.
125+
# Each mesh line has high res (nx or ny) so it follows
126+
# the contours of the surface.
127+
for xyzs, inxs in compute_over_grid(nx, nmesh):
128+
graphics.add_complex(xyzs, lines=inxs, polys=None)
129+
for xyzs, inxs in compute_over_grid(nmesh, ny):
130+
graphics.add_complex(xyzs, lines=inxs.T, polys=None)
78131

79132
return graphics
80133

mathics/eval/drawing/util.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55

66
from mathics.core.atoms import NumericArray
7-
from mathics.core.convert.expression import to_mathics_list
7+
from mathics.core.convert.expression import to_expression, to_mathics_list
88
from mathics.core.expression import Expression
99
from mathics.core.list import ListExpression
10+
from mathics.core.symbols import Symbol
1011
from mathics.core.systemsymbols import (
1112
SymbolGraphics,
1213
SymbolGraphics3D,
@@ -58,14 +59,29 @@ def add_linexyzs(self, line_xyzs, colors=None):
5859
"""Add lines specified by explicit xy[z] coordinates"""
5960
self.add_thing(SymbolLine, line_xyzs, colors)
6061

61-
# TODO: color
62+
def add_color(self, symbol, components):
63+
expr = to_expression(symbol, *components)
64+
self.graphics.append(expr)
65+
66+
def add_directives(self, *ds):
67+
def cvt(d):
68+
if isinstance(d, list) and len(d) > 0 and isinstance(d[0], Symbol):
69+
expr = to_expression(d[0], *(cvt(dd) for dd in d[1:]))
70+
return expr
71+
else:
72+
return d
73+
74+
for d in ds:
75+
expr = cvt(d)
76+
self.graphics.append(expr)
77+
6278
def add_complex(self, xyzs, lines=None, polys=None):
6379
complex = [NumericArray(xyzs)]
6480
if polys is not None:
6581
polys_expr = Expression(SymbolPolygon, NumericArray(polys))
6682
complex.append(polys_expr)
6783
if lines is not None:
68-
polys_expr = Expression(SymbolLines, NumericArray(lines))
84+
lines_expr = Expression(SymbolLine, NumericArray(lines))
6985
complex.append(lines_expr)
7086
gc_expr = Expression(SymbolGraphicsComplex, *complex)
7187
self.graphics.append(gc_expr)

0 commit comments

Comments
 (0)