Skip to content

Commit cc06072

Browse files
authored
Refactor plot3d (#1520)
This PR substantially refactors Plot3D and DensityPlot (which share a good bit of code), in preparation for adding a new vectorized implementation of those two functions. No function is changed or added. * The the flow from builtin to eval is cleaner - no more callbacks from eval to builtin. Options processing is done prior to calling eval_*. Plot3D and DensityPlot now have their own eval functions which share implementation code in eval rather than in builtin. * A PlotOptions class has been introduced. This simplifies passing plotting options from builtin to eval. It may also be reusable for other plotting builtins. * A new GraphicsGenerator class has been added. This consolidates and regularizes some scattered code for generating Graphics and Graphics3D expressions, which are rather complex in structure. This will be further enhanced to support GraphicsComplex in the PR that introduces vectorized plotting functions. Possibly could be retrofitted to other plotting functions. * A test for DensityPlot was added, which was missed in the previous PR because I didn't realize how much code it shared with Plot3D. This is a fairly big change, but I worked carefully, frequently testing using the detailed tests for Plot3D and DensityPlot. The tests caught numerous errors as I worked, increasing confidence that the warning light was hooked up and no functional changes have been introduced. Some quick timing tests show no change in performance as far as I can see. More can be done, particularly in the areas of error checking and typing, but that shouldn't change the PR in any major way so hoping to get some eyes on it in parallel.
1 parent f63f1cd commit cc06072

File tree

5 files changed

+475
-223
lines changed

5 files changed

+475
-223
lines changed

mathics/builtin/drawing/plot.py

Lines changed: 149 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@
2727
from mathics.core.list import ListExpression
2828
from mathics.core.symbols import Symbol, SymbolList
2929
from mathics.core.systemsymbols import (
30+
SymbolAll,
3031
SymbolBlack,
3132
SymbolEdgeForm,
33+
SymbolFull,
3234
SymbolGraphics,
33-
SymbolGraphics3D,
3435
SymbolLine,
3536
SymbolLog10,
36-
SymbolPolygon,
37+
SymbolNone,
3738
SymbolRGBColor,
3839
SymbolStyle,
3940
)
@@ -50,7 +51,7 @@
5051
get_plot_range,
5152
get_plot_range_option,
5253
)
53-
from mathics.eval.drawing.plot3d import construct_density_plot, eval_plot3d
54+
from mathics.eval.drawing.plot3d import eval_DensityPlot, eval_Plot3D
5455
from mathics.eval.nevaluator import eval_N
5556

5657
# This tells documentation how to sort this module
@@ -407,7 +408,117 @@ def process_function_and_options(
407408
return functions, x_name, py_start, py_stop, x_range, y_range, expr_limits, expr
408409

409410

411+
# TODO: add more options
412+
# TODO: generalize, use for other plots
413+
class PlotOptions:
414+
"""
415+
Extract Options common to many types of plotting.
416+
This aims to reduce duplication of code,
417+
and to make it easier to pass options to eval_* routines.
418+
"""
419+
420+
# TODO: more precise types
421+
ranges: list
422+
mesh: str
423+
plotpoints: list
424+
maxdepth: int
425+
426+
def error(self, what, *args, **kwargs):
427+
if not isinstance(what, str):
428+
what = what.get_name()
429+
self.evaluation.message(what, *args, **kwargs)
430+
raise ValueError()
431+
432+
def __init__(self, expr, range_exprs, options, evaluation):
433+
self.evaluation = evaluation
434+
435+
# plot ranges
436+
self.ranges = []
437+
for range_expr in range_exprs:
438+
if not range_expr.has_form("List", 3):
439+
self.error(expr, "invrange", range_expr)
440+
if not isinstance(range_expr.elements[0], Symbol):
441+
self.error(expr, "invrange", range_expr)
442+
range = [range_expr.elements[0]]
443+
for limit_expr in range_expr.elements[1:3]:
444+
limit = limit_expr.round_to_float(evaluation)
445+
if limit is None:
446+
self.error(expr, "plln", limit_expr, range_expr)
447+
range.append(limit)
448+
if range[2] <= range[1]:
449+
self.error(expr, "invrange", range_expr)
450+
self.ranges.append(range)
451+
452+
# Mesh option
453+
mesh = expr.get_option(options, "Mesh", evaluation)
454+
if mesh not in (SymbolNone, SymbolFull, SymbolAll):
455+
evaluation.message("Mesh", "ilevels", mesh)
456+
mesh = SymbolFull
457+
self.mesh = mesh
458+
459+
# PlotPoints option
460+
plotpoints_option = expr.get_option(options, "PlotPoints", evaluation)
461+
plotpoints = plotpoints_option.to_python()
462+
463+
def check_plotpoints(steps):
464+
if isinstance(steps, int) and steps > 0:
465+
return True
466+
return False
467+
468+
if plotpoints == "System`None":
469+
plotpoints = (7, 7)
470+
elif check_plotpoints(plotpoints):
471+
plotpoints = (plotpoints, plotpoints)
472+
if not (
473+
isinstance(plotpoints, (list, tuple))
474+
and len(plotpoints) == 2
475+
and check_plotpoints(plotpoints[0])
476+
and check_plotpoints(plotpoints[1])
477+
):
478+
evaluation.message(expr.get_name(), "invpltpts", plotpoints)
479+
plotpoints = (7, 7)
480+
self.plotpoints = plotpoints
481+
482+
# MaxRecursion Option
483+
maxrec_option = expr.get_option(options, "MaxRecursion", evaluation)
484+
max_depth = maxrec_option.to_python()
485+
if isinstance(max_depth, int):
486+
if max_depth < 0:
487+
max_depth = 0
488+
evaluation.message(expr.get_name(), "invmaxrec", max_depth, 15)
489+
elif max_depth > 15:
490+
max_depth = 15
491+
evaluation.message(expr.get_name(), "invmaxrec", max_depth, 15)
492+
else:
493+
pass # valid
494+
elif max_depth == float("inf"):
495+
max_depth = 15
496+
evaluation.message(expr.get_name(), "invmaxrec", max_depth, 15)
497+
else:
498+
max_depth = 0
499+
evaluation.message(expr.get_name(), "invmaxrec", max_depth, 15)
500+
self.max_depth = max_depth
501+
502+
# ColorFunction and ColorFunctionScaling options
503+
# This was pulled from construct_density_plot (now eval_DensityPlot).
504+
# TODO: What does pop=True do? is it right?
505+
# TODO: can we move some of the subsequent processing in eval_DensityPlot to here?
506+
# TODO: what is the type of these? that may change if we do the above...
507+
self.color_function = expr.get_option(
508+
options, "ColorFunction", evaluation, pop=True
509+
)
510+
self.color_function_scaling = expr.get_option(
511+
options, "ColorFunctionScaling", evaluation, pop=True
512+
)
513+
514+
410515
class _Plot3D(Builtin):
516+
"""Common base class for Plot3D and DensityPlot"""
517+
518+
# Check for correct number of args
519+
eval_error = Builtin.generic_argument_error
520+
expected_args = 3
521+
411522
messages = {
412523
"invmaxrec": (
413524
"MaxRecursion must be a non-negative integer; the recursion value "
@@ -422,25 +533,35 @@ class _Plot3D(Builtin):
422533
"Value of PlotPoints -> `1` is not a positive integer "
423534
"or appropriate list of positive integers."
424535
),
536+
"invrange": (
537+
"Plot range `1` must be of the form {variable, min, max}, "
538+
"where max > min."
539+
),
425540
}
426541

427542
def eval(
428543
self,
429544
functions,
430-
x,
431-
xstart,
432-
xstop,
433-
y,
434-
ystart,
435-
ystop,
545+
xrange,
546+
yrange,
436547
evaluation: Evaluation,
437548
options: dict,
438549
):
439-
"""%(name)s[functions_, {x_Symbol, xstart_, xstop_},
440-
{y_Symbol, ystart_, ystop_}, OptionsPattern[%(name)s]]"""
441-
return eval_plot3d(
442-
self, functions, x, xstart, xstop, y, ystart, ystop, evaluation, options
443-
)
550+
"""%(name)s[functions_, xrange_, yrange_, OptionsPattern[%(name)s]]"""
551+
552+
# TODO: test error for too many, too few, no args
553+
554+
# parse options, bailing out if anything is wrong
555+
try:
556+
plot_options = PlotOptions(self, [xrange, yrange], options, evaluation)
557+
except ValueError:
558+
return None
559+
560+
# ask the subclass to get one or more functions as appropriate
561+
plot_options.functions = self.get_functions_param(functions)
562+
563+
# delegate to subclass, which will call the appropriate eval_* function
564+
return self.do_eval(plot_options, evaluation, options)
444565

445566

446567
class BarChart(_Chart):
@@ -563,7 +684,6 @@ class ColorDataFunction(Builtin):
563684
"""
564685

565686
summary_text = "color scheme object"
566-
pass
567687

568688

569689
class DensityPlot(_Plot3D):
@@ -608,22 +728,17 @@ class DensityPlot(_Plot3D):
608728
)
609729
summary_text = "density plot for a function"
610730

731+
# TODO: error if more than one function here
611732
def get_functions_param(self, functions):
733+
"""can only have one function"""
612734
return [functions]
613735

614-
def construct_graphics(
615-
self, triangles, mesh_points, v_min, v_max, options, evaluation
616-
):
617-
return construct_density_plot(
618-
self, triangles, mesh_points, v_min, v_max, options, evaluation
619-
)
620-
621-
def final_graphics(self, graphics, options):
622-
return Expression(
623-
SymbolGraphics,
624-
ListExpression(*graphics),
625-
*options_to_rules(options, Graphics.options),
626-
)
736+
# called by superclass
737+
def do_eval(self, plot_options, evaluation, options):
738+
"""called by superclass to call appropriate eval_* function"""
739+
graphics = eval_DensityPlot(plot_options, evaluation)
740+
graphics_expr = graphics.generate(options_to_rules(options, Graphics.options))
741+
return graphics_expr
627742

628743

629744
class DiscretePlot(_Plot):
@@ -1753,43 +1868,14 @@ class Plot3D(_Plot3D):
17531868
summary_text = "plots 3D surfaces of one or more functions"
17541869

17551870
def get_functions_param(self, functions):
1871+
"""May have a function or a list of functions"""
17561872
if functions.has_form("List", None):
17571873
return functions.elements
17581874
else:
17591875
return [functions]
17601876

1761-
def construct_graphics(
1762-
self, triangles, mesh_points, v_min, v_max, options, evaluation: Evaluation
1763-
):
1764-
graphics = []
1765-
for p1, p2, p3 in triangles:
1766-
graphics.append(
1767-
Expression(
1768-
SymbolPolygon,
1769-
ListExpression(
1770-
to_mathics_list(*p1),
1771-
to_mathics_list(*p2),
1772-
to_mathics_list(*p3),
1773-
),
1774-
)
1775-
)
1776-
# Add the Grid
1777-
for xi in range(len(mesh_points)):
1778-
line = []
1779-
for yi in range(len(mesh_points[xi])):
1780-
line.append(
1781-
to_mathics_list(
1782-
mesh_points[xi][yi][0],
1783-
mesh_points[xi][yi][1],
1784-
mesh_points[xi][yi][2],
1785-
)
1786-
)
1787-
graphics.append(Expression(SymbolLine, ListExpression(*line)))
1788-
return graphics
1789-
1790-
def final_graphics(self, graphics, options: dict):
1791-
return Expression(
1792-
SymbolGraphics3D,
1793-
ListExpression(*graphics),
1794-
*options_to_rules(options, Graphics3D.options),
1795-
)
1877+
def do_eval(self, plot_options, evaluation, options):
1878+
"""called by superclass to call appropriate eval_* function"""
1879+
graphics = eval_Plot3D(plot_options, evaluation)
1880+
graphics_expr = graphics.generate(options_to_rules(options, Graphics3D.options))
1881+
return graphics_expr

mathics/core/systemsymbols.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
SymbolFloor = Symbol("System`Floor")
105105
SymbolFormat = Symbol("System`Format")
106106
SymbolFractionBox = Symbol("System`FractionBox")
107+
SymbolFull = Symbol("System`Full")
107108
SymbolFullForm = Symbol("System`FullForm")
108109
SymbolFunction = Symbol("System`Function")
109110
SymbolGamma = Symbol("System`Gamma")
@@ -291,6 +292,7 @@
291292
SymbolUpValues = Symbol("System`UpValues")
292293
SymbolVariance = Symbol("System`Variance")
293294
SymbolVerbatim = Symbol("Verbatim")
295+
SymbolVertexColors = Symbol("VertexColors")
294296
SymbolWhitespace = Symbol("System`Whitespace")
295297
SymbolWhitespaceCharacter = Symbol("System`WhitespaceCharacter")
296298
SymbolWord = Symbol("System`Word")

0 commit comments

Comments
 (0)