Skip to content

Commit d171d37

Browse files
authored
Contour plot (#1540)
* ContourPlot is implemented, supporting the Contours option and supporting two kinds of contour plot (see image). * In the case of Automatic PlotRanges, ContourPlot and others now pass through to Graphics a PlotRange reflecting the specified {x,xmin,xmax} etc. ranges, which may be different from the ranges of x, etc. found in the emitted graphics data, so that the correct range can be plotted. This allows e.g. contour plots that may have only contours with x,y values for a subset of the specified range to be correctly plotted. * The above tickled a bug in boxing so this was worked around. * ContourPlot requires skimage but the existing message about missing modules wasn't showing the module name, so this was fixed. <img width="60%" alt="image" src="https://github.com/user-attachments/assets/7b2212a8-8bce-40b2-b454-45069618daf2" />
1 parent 1f5dbb2 commit d171d37

File tree

7 files changed

+240
-28
lines changed

7 files changed

+240
-28
lines changed

SYMBOLS_MANIFEST.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ System`Context
265265
System`Contexts
266266
System`Continue
267267
System`ContinuedFraction
268+
System`ContourPlot
268269
System`Convert`B64Dump`B64Decode
269270
System`Convert`B64Dump`B64Encode
270271
System`ConvertersDump`$ExtensionMappings

mathics/builtin/box/graphics3d.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,17 @@ def _prepare_elements(self, elements, options, max_width=None):
250250
self.background_color = elements.background_color
251251

252252
def calc_dimensions(final_pass=True):
253+
# TODO: the code below is broken in any other case but Automatic
254+
# because it calls elements.translate which is not implemented.
255+
# Plots may pass specific plot ranges, triggering this deficiency
256+
# and causing tests to fail The following line avoids this,
257+
# and it should not change the behavior of any case which did
258+
# previously fail with an exception.
259+
#
260+
# This code should be DRYed (together with the very similar code
261+
# for the 2d case), and the missing .translate method added.
262+
plot_range = ["System`Automatic"] * 3
263+
253264
if "System`Automatic" in plot_range:
254265
xmin, xmax, ymin, ymax, zmin, zmax = elements.extent()
255266
else:
@@ -291,7 +302,7 @@ def calc_dimensions(final_pass=True):
291302
elif zmin == zmax:
292303
zmin -= 1
293304
zmax += 1
294-
elif isinstance(plot_range[1], list) and len(plot_range[1]) == 2:
305+
elif isinstance(plot_range[1], list) and len(plot_range[2]) == 2:
295306
zmin, zmax = list(map(float, plot_range[2]))
296307
zmin = elements.translate((0, 0, zmin))[2]
297308
zmax = elements.translate((0, 0, zmax))[2]

mathics/builtin/drawing/plot.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
SymbolLine,
3838
SymbolLog10,
3939
SymbolNone,
40+
SymbolPlotRange,
4041
SymbolRGBColor,
4142
SymbolSequence,
4243
SymbolStyle,
@@ -67,13 +68,15 @@
6768
from mathics.eval.drawing.plot3d_vectorized import (
6869
eval_ComplexPlot,
6970
eval_ComplexPlot3D,
71+
eval_ContourPlot,
7072
eval_DensityPlot,
7173
eval_Plot3D,
7274
)
7375
else:
7476
from mathics.eval.drawing.plot3d import (
7577
eval_ComplexPlot,
7678
eval_ComplexPlot3D,
79+
eval_ContourPlot,
7780
eval_DensityPlot,
7881
eval_Plot3D,
7982
)
@@ -467,7 +470,7 @@ def error(self, what, *args, **kwargs):
467470
def __init__(self, expr, range_exprs, options, evaluation):
468471
self.evaluation = evaluation
469472

470-
# plot ranges
473+
# plot ranges of the form {x,xmin,xmax} etc.
471474
self.ranges = []
472475
for range_expr in range_exprs:
473476
if not range_expr.has_form("List", 3):
@@ -488,6 +491,19 @@ def __init__(self, expr, range_exprs, options, evaluation):
488491
self.error(expr, "invrange", range_expr)
489492
self.ranges.append(range)
490493

494+
# Contours option
495+
contours = expr.get_option(options, "Contours", evaluation)
496+
if contours is not None:
497+
c = contours.to_python()
498+
if not (
499+
c == "System`Automatic"
500+
or isinstance(c, int)
501+
or isinstance(c, tuple)
502+
and all(isinstance(cc, (int, float)) for cc in c)
503+
):
504+
self.error(expr, "invcontour", contours)
505+
self.contours = c
506+
491507
# Mesh option
492508
mesh = expr.get_option(options, "Mesh", evaluation)
493509
if mesh not in (SymbolNone, SymbolFull, SymbolAll):
@@ -579,6 +595,9 @@ class _Plot3D(Builtin):
579595
"Plot range `1` must be of the form {variable, min, max}, "
580596
"where max > min."
581597
),
598+
"invcontour": (
599+
"Contours option must be Automatic, an integer, or a list of numbers."
600+
),
582601
}
583602

584603
# Plot3D, ComplexPlot3D
@@ -633,6 +652,39 @@ def eval(
633652
graphics = self.eval_function(plot_options, evaluation)
634653
if not graphics:
635654
return
655+
656+
# Expand PlotRange option using the {x,xmin,xmax} etc. range specifications
657+
# Pythonize it, so Symbol becomes str, numeric becomes int or float
658+
plot_range = self.get_option(options, str(SymbolPlotRange), evaluation)
659+
plot_range = plot_range.to_python()
660+
dim = 3 if self.graphics_class is Graphics3D else 2
661+
if isinstance(plot_range, str):
662+
# PlotRange -> Automatic becomes PlotRange -> {Automatic, ...}
663+
plot_range = [str(SymbolAutomatic)] * dim
664+
if isinstance(plot_range, (int, float)):
665+
# PlotRange -> s becomes PlotRange -> {Automatic,...,{-s,s}}
666+
pr = plot_range
667+
plot_range = [str(SymbolAutomatic)] * dim
668+
plot_range[-1] = [-pr, pr]
669+
elif isinstance(plot_range, (list, tuple)) and isinstance(
670+
plot_range[0], (int, float)
671+
):
672+
# PlotRange -> {s0,s1} becomes PlotRange -> {Automatic,...,{s0,s1}}
673+
pr = plot_range
674+
plot_range = [str(SymbolAutomatic)] * dim
675+
plot_range[-1] = pr
676+
677+
# now we have a list of length dim
678+
# handle Automatic ~ {xmin,xmax} etc.
679+
for i, (pr, r) in enumerate(zip(plot_range, plot_options.ranges)):
680+
# TODO: this treats Automatic and Full as the same, which isn't quite right
681+
if isinstance(pr, str) and not isinstance(r[1], complex):
682+
plot_range[i] = r[1:] # extract {xmin,xmax} from {x,xmin,xmax}
683+
684+
# unpythonize and update PlotRange option
685+
options[str(SymbolPlotRange)] = to_mathics_list(*plot_range)
686+
687+
# generate the Graphics[3D] result
636688
graphics_expr = graphics.generate(
637689
options_to_rules(options, self.graphics_class.options)
638690
)
@@ -809,6 +861,32 @@ class ComplexPlot(_Plot3D):
809861
graphics_class = Graphics
810862

811863

864+
class ContourPlot(_Plot3D):
865+
"""
866+
<url>:WMA link: https://reference.wolfram.com/language/ref/ContourPlot.html</url>
867+
<dl>
868+
<dt>'Contour'[$f$, {$x$, $x_{min}$, $x_{max}$}, {$y$, $y_{min}$, $y_{max}$}]
869+
<dd>creates a two-dimensional contour plot ofh $f$ over the region
870+
$x$ ranging from $x_{min}$ to $x_{max}$ and $y$ ranging from $y_{min}$ to $y_{max}$.
871+
872+
See <url>:Drawing Option and Option Values:
873+
/doc/reference-of-built-in-symbols/graphics-and-drawing/drawing-options-and-option-values
874+
</url> for a list of Plot options.
875+
</dl>
876+
877+
"""
878+
879+
requires = ["skimage"]
880+
summary_text = "creates a contour plot"
881+
expected_args = 3
882+
options = _Plot3D.options2d | {"Contours": "Automatic"}
883+
# TODO: other options?
884+
885+
many_functions = True
886+
eval_function = staticmethod(eval_ContourPlot)
887+
graphics_class = Graphics
888+
889+
812890
class DensityPlot(_Plot3D):
813891
"""
814892
<url>:WMA link: https://reference.wolfram.com/language/ref/DensityPlot.html</url>

mathics/core/builtin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,12 +852,14 @@ class UnavailableFunction:
852852

853853
def __init__(self, builtin):
854854
self.name = builtin.get_name()
855+
self.requires = builtin.requires
855856

856857
def __call__(self, **kwargs):
857858
kwargs["evaluation"].message(
858859
"General",
859860
"pyimport", # see messages.py for error message definition
860861
strip_context(self.name),
862+
", ".join(self.requires),
861863
)
862864

863865

mathics/core/systemsymbols.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@
228228
SymbolPiecewise = Symbol("System`Piecewise")
229229
SymbolPlot = Symbol("System`Plot")
230230
SymbolPlotLabel = Symbol("System`PlotLabel")
231+
SymbolPlotRange = Symbol("System`PlotRange")
231232
SymbolPlotRangeClipping = Symbol("System`PlotRangeClipping")
232233
SymbolPlotRegion = Symbol("System`PlotRegion")
233234
SymbolPlus = Symbol("System`Plus")

mathics/eval/drawing/plot3d.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,3 +506,10 @@ def eval_ComplexPlot(
506506
evaluation: Evaluation,
507507
):
508508
return None
509+
510+
511+
def eval_ContourPlot(
512+
plot_options,
513+
evaluation: Evaluation,
514+
):
515+
return None

0 commit comments

Comments
 (0)