From 455e428d0751c5866094a2eb66af949e775f577c Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Fri, 7 Nov 2025 11:21:49 -0500 Subject: [PATCH 01/18] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 87 ----------------------- .github/ISSUE_TEMPLATE/feature_request.md | 21 ------ 2 files changed, 108 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index bcaad605f..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - - -## Description - - - -## How to Reproduce - - - -## Output Given - - - - -## Expected behavior - - - -## Your Environment - - - -## Workarounds - - - -## Priority - - - -## Additional context - - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index c51ecfbc0..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- -*Note: If the feature is about adding or filling out an existing deficiency in the Mathics3 language, please file this as an [issue](https://github.com/Mathics3/mathics-core/issues/new?assignees=&labels=&projects=&template=bug_report.md&title=).* - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. From 16f50291b712a312b6dde70ec13847d2c62fafe8 Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Mon, 24 Nov 2025 12:01:39 -0500 Subject: [PATCH 02/18] Revert "Update issue templates" This reverts commit 455e428d0751c5866094a2eb66af949e775f577c. --- .github/ISSUE_TEMPLATE/bug_report.md | 87 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 21 ++++++ 2 files changed, 108 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..bcaad605f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,87 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + +## Description + + + +## How to Reproduce + + + +## Output Given + + + + +## Expected behavior + + + +## Your Environment + + + +## Workarounds + + + +## Priority + + + +## Additional context + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..c51ecfbc0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- +*Note: If the feature is about adding or filling out an existing deficiency in the Mathics3 language, please file this as an [issue](https://github.com/Mathics3/mathics-core/issues/new?assignees=&labels=&projects=&template=bug_report.md&title=).* + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 16be55c042d1cd836548a48be3e91b82402a1493 Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Tue, 25 Nov 2025 14:55:03 -0500 Subject: [PATCH 03/18] Update Combinatorica-repo --- mathics/Packages/Combinatorica-repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/Packages/Combinatorica-repo b/mathics/Packages/Combinatorica-repo index eedbb07f5..be90b6180 160000 --- a/mathics/Packages/Combinatorica-repo +++ b/mathics/Packages/Combinatorica-repo @@ -1 +1 @@ -Subproject commit eedbb07f5e4ec09d3f9f973283d46c8e72520c88 +Subproject commit be90b6180ae460b8698686952d35387dda18c627 From c0e3a47e18c29e8cb781601d885a8ab80fb2a35e Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Fri, 5 Dec 2025 01:05:15 -0500 Subject: [PATCH 04/18] Initial take on ContourPlot --- SYMBOLS_MANIFEST.txt | 1 + mathics/builtin/drawing/plot.py | 28 ++++ mathics/eval/drawing/plot3d_vectorized.py | 153 ++++++++++++++++++---- 3 files changed, 157 insertions(+), 25 deletions(-) diff --git a/SYMBOLS_MANIFEST.txt b/SYMBOLS_MANIFEST.txt index cc534ee1c..9a2f9c552 100644 --- a/SYMBOLS_MANIFEST.txt +++ b/SYMBOLS_MANIFEST.txt @@ -265,6 +265,7 @@ System`Context System`Contexts System`Continue System`ContinuedFraction +System`ContourPlot System`Convert`B64Dump`B64Decode System`Convert`B64Dump`B64Encode System`ConvertersDump`$ExtensionMappings diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index 1cebf9c4c..ee2416fe4 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -67,6 +67,7 @@ from mathics.eval.drawing.plot3d_vectorized import ( eval_ComplexPlot, eval_ComplexPlot3D, + eval_ContourPlot, eval_DensityPlot, eval_Plot3D, ) @@ -74,6 +75,7 @@ from mathics.eval.drawing.plot3d import ( eval_ComplexPlot, eval_ComplexPlot3D, + eval_ContourPlot, eval_DensityPlot, eval_Plot3D, ) @@ -809,6 +811,32 @@ class ComplexPlot(_Plot3D): graphics_class = Graphics +class ContourPlot(_Plot3D): + """ + :WMA link: https://reference.wolfram.com/language/ref/ContourPlot.html +
+
'Contour'[$f$, {$x$, $x_{min}$, $x_{max}$}, {$y$, $y_{min}$, $y_{max}$}] +
creates a two-dimensional contour plot ofh $f$ over the region + $x$ ranging from $x_{min}$ to $x_{max}$ and $y$ ranging from $y_{min}$ to $y_{max}$. + + See :Drawing Option and Option Values: + /doc/reference-of-built-in-symbols/graphics-and-drawing/drawing-options-and-option-values + for a list of Plot options. +
+ + """ + + summary_text = "creates a contour plot" + expected_args = 3 + options = _Plot3D.options2d + # TODO: updates? # + + many_functions = True + eval_function = staticmethod(eval_ContourPlot) + graphics_class = Graphics + + + class DensityPlot(_Plot3D): """ :WMA link: https://reference.wolfram.com/language/ref/DensityPlot.html diff --git a/mathics/eval/drawing/plot3d_vectorized.py b/mathics/eval/drawing/plot3d_vectorized.py index d08c50f3d..d535fb69a 100644 --- a/mathics/eval/drawing/plot3d_vectorized.py +++ b/mathics/eval/drawing/plot3d_vectorized.py @@ -8,9 +8,10 @@ import numpy as np from mathics.builtin.colors.color_internals import convert_color +from mathics.core.expression import Expression from mathics.core.evaluation import Evaluation from mathics.core.symbols import strip_context -from mathics.core.systemsymbols import SymbolNone, SymbolRGBColor +from mathics.core.systemsymbols import SymbolNone, SymbolRGBColor, SymbolEqual, SymbolSubtract from mathics.timing import Timer from .plot_compile import plot_compile @@ -124,28 +125,65 @@ def compute_over_grid(nx, ny): return graphics +# color-blind friendly palette from https://davidmathlogic.com/colorblind +# palette3 is for 3d plots, i.e. surfaces +palette3 = [ + (255, 176, 0), # orange + (100, 143, 255), # blue + (220, 38, 127), # red + (50, 150, 140), # green + (120, 94, 240), # purple + (254, 97, 0), # dark orange + (0, 114, 178), # dark blue +] + + +# palette 2 is for 2d plots, i.e. lines +# same colors as palette3 but in a little different order +palette2 = [ + (100, 143, 255), # blue + (255, 176, 0), # orange + (50, 150, 140), # green + (220, 38, 127), # red + (120, 94, 240), # purple + (254, 97, 0), # dark orange + (0, 114, 178), # dark blue +] + + +def palette_color_directive(palette, i): + """ returns a directive in a form suitable for graphics.add_directives """ + """ for setting the color of an entire entity such as a line or surface """ + rgb = palette[i % len(palette)] + rgb = [c / 255.0 for c in rgb] + return [SymbolRGBColor, *rgb] + + +@Timer("density_colors") +def density_colors(zs): + """default color palette for DensityPlot and ContourPlot (f(x) form)""" + z_min, z_max = min(zs), max(zs) + zs = zs[:, np.newaxis] # allow broadcasting + #c_min, c_max = [0.3, 0.00, 0.3], [1.0, 0.95, 0.8] + c_min, c_max = [0.5, 0, 0.1], [1.0, 0.9, 0.5] + c_min, c_max = ( + np.full((len(zs), 3), c_min), + np.full((len(zs), 3), c_max), + ) + colors = ((zs - z_min) * c_max + (z_max - zs) * c_min) / (z_max - z_min) + return colors + + @Timer("eval_Plot3D") def eval_Plot3D( plot_options, evaluation: Evaluation, ): def emit(graphics, i, xyzs, quads): - # color-blind friendly palette from https://davidmathlogic.com/colorblind - palette = [ - (255, 176, 0), # orange - (100, 143, 255), # blue - (220, 38, 127), # red - (50, 150, 140), # green - (120, 94, 240), # purple - (254, 97, 0), # dark orange - (0, 114, 178), # dark blue - ] # choose a color - rgb = palette[i % len(palette)] - rgb = [c / 255.0 for c in rgb] - # graphics.add_color(SymbolRGBColor, rgb) - graphics.add_directives([SymbolRGBColor, *rgb]) + color_directive = palette_color_directive(palette3, i) + graphics.add_directives(color_directive) # add a GraphicsComplex displaying a surface for this function graphics.add_complex(xyzs, lines=None, polys=quads) @@ -159,18 +197,10 @@ def eval_DensityPlot( evaluation: Evaluation, ): def emit(graphics, i, xyzs, quads): + # Fixed palette for now # TODO: accept color options - with Timer("compute colors"): - zs = xyzs[:, 2] - z_min, z_max = min(zs), max(zs) - zs = zs[:, np.newaxis] # allow broadcasting - c_min, c_max = [0.5, 0, 0.1], [1.0, 0.9, 0.5] - c_min, c_max = ( - np.full((len(zs), 3), c_min), - np.full((len(zs), 3), c_max), - ) - colors = ((zs - z_min) * c_max + (z_max - zs) * c_min) / (z_max - z_min) + colors = density_colors(xyzs[:, 2]) # flatten the points and add the quads graphics.add_complex(xyzs[:, 0:2], lines=None, polys=quads, colors=colors) @@ -178,6 +208,77 @@ def emit(graphics, i, xyzs, quads): return make_plot(plot_options, evaluation, dim=2, is_complex=False, emit=emit) +@Timer("eval_ContourPlot") +def eval_ContourPlot( + plot_options, + evaluation: Evaluation, +): + import skimage.measure + + # whether to show a background similar to density plot, except quantized + background = len(plot_options.functions) == 1 + contour_levels = None + + # convert fs of the form a==b to a-b, inplicit contour level 0 + plot_options.functions = list(plot_options.functions) # so we can modify it + for i, f in enumerate(plot_options.functions): + if f.head == SymbolEqual: + f = Expression(SymbolSubtract, *f.elements[0:2]) + plot_options.functions[i] = f + contour_levels = [0] + background = False + + def emit(graphics, i, xyzs, quads): + + # set line color + if background: + # showing a background, so just black lines + color_directive = [SymbolRGBColor, 0, 0, 0] + else: + # no background - each curve gets its own color + color_directive = palette_color_directive(palette2, i) + graphics.add_directives(color_directive) + + # get data + nx, ny = plot_options.plotpoints + _, xmin, xmax = plot_options.ranges[0] + _, ymin, ymax = plot_options.ranges[1] + zs = xyzs[:,2] # this is a linear list matching with quads + + # n contours if not specified + n = 8 + levels = contour_levels + if not levels: + zmin, zmax = np.min(zs), np.max(zs) + dz_per_level = (zmax - zmin) / n + levels = np.arange(n) * dz_per_level + zmin + + # one contour line per contour level + for level in levels: + + # find contours and add lines + with Timer("contours"): + zgrid = zs.reshape((nx, ny)) # find_contours needs it as an array + contours = skimage.measure.find_contours(zgrid, level) + + # add lines + for segment in contours: + segment[:,0] = segment[:,0] * ((xmax-xmin)/nx) + xmin + segment[:,1] = segment[:,1] * ((ymax-ymin)/ny) + ymin + graphics.add_linexyzs(segment) + + # background + if background: + zs = np.floor(zs / dz_per_level) # quantize the zs to match the contours + colors = density_colors(zs) # and get colors using the quantized zs + graphics.add_complex(xyzs[:, 0:2], lines=None, polys=quads, colors=colors) + + + #plot_options.plotpoints = [n * 10 for n in plot_options.plotpoints] + return make_plot(plot_options, evaluation, dim=2, is_complex=False, emit=emit) + + + @Timer("complex colors") def complex_colors(zs, s=None): # hue depends on phase @@ -229,3 +330,5 @@ def emit(graphics, i, xyzs, quads): ) return make_plot(plot_options, evaluation, dim=2, is_complex=True, emit=emit) + + From 55525f2ff45bf19292c4a086a1826808a35653c1 Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Fri, 5 Dec 2025 11:05:16 -0500 Subject: [PATCH 05/18] Handle Automatic PlotRanges --- mathics/builtin/drawing/plot.py | 23 ++++++++++++++++++++++- mathics/core/systemsymbols.py | 1 + mathics/eval/drawing/plot3d_vectorized.py | 4 ++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index ee2416fe4..831841e8a 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -40,6 +40,7 @@ SymbolRGBColor, SymbolSequence, SymbolStyle, + SymbolPlotRange, ) from mathics.eval.drawing.charts import draw_bar_chart, eval_chart from mathics.eval.drawing.colors import COLOR_PALETTES, get_color_palette @@ -469,7 +470,7 @@ def error(self, what, *args, **kwargs): def __init__(self, expr, range_exprs, options, evaluation): self.evaluation = evaluation - # plot ranges + # plot ranges of the form {x,xmin,xmax} etc. self.ranges = [] for range_expr in range_exprs: if not range_expr.has_form("List", 3): @@ -635,6 +636,25 @@ def eval( graphics = self.eval_function(plot_options, evaluation) if not graphics: return + + # update non-numeric PlotRanges with the specified {x,xmin,xmax} range options + plot_range = self.get_option(options, str(SymbolPlotRange), evaluation) + plot_range = plot_range.to_python() + # from here we've pythonized it, so Symbol becomes str, numeric becomes int or float + if isinstance(plot_range, str): + plot_range = [plot_range] * len(plot_options.ranges) + elif isinstance(plot_range, (int,float)): + # TODO: single numeric PlotRange is Automaic for all but last dimension - is correct? + all_but_last = [str(SymbolAutomatic)] * (len(plot_options.ranges)-1) + plot_range = all_but_last + [plot_range] + for i, (pr, r) in enumerate(zip(plot_range, plot_options.ranges)): + # TODO: this treats Automatic and Full as the same, which isn't quite right + if isinstance(pr, str): + plot_range[i] = r[1:] # extract {xmin,xmax} from {x,xmin,xmax} + # unpythonize and update + options[str(SymbolPlotRange)] = to_mathics_list(*plot_range) + + # generate the Graphics[3D] result graphics_expr = graphics.generate( options_to_rules(options, self.graphics_class.options) ) @@ -826,6 +846,7 @@ class ContourPlot(_Plot3D): """ + requires = ["skimage"] summary_text = "creates a contour plot" expected_args = 3 options = _Plot3D.options2d diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index ae554c96d..69ace5548 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -228,6 +228,7 @@ SymbolPiecewise = Symbol("System`Piecewise") SymbolPlot = Symbol("System`Plot") SymbolPlotLabel = Symbol("System`PlotLabel") +SymbolPlotRange = Symbol("System`PlotRange") SymbolPlotRangeClipping = Symbol("System`PlotRangeClipping") SymbolPlotRegion = Symbol("System`PlotRegion") SymbolPlus = Symbol("System`Plus") diff --git a/mathics/eval/drawing/plot3d_vectorized.py b/mathics/eval/drawing/plot3d_vectorized.py index d535fb69a..f7c846023 100644 --- a/mathics/eval/drawing/plot3d_vectorized.py +++ b/mathics/eval/drawing/plot3d_vectorized.py @@ -217,7 +217,7 @@ def eval_ContourPlot( # whether to show a background similar to density plot, except quantized background = len(plot_options.functions) == 1 - contour_levels = None + contour_levels = None # TODO: implement Contours option # convert fs of the form a==b to a-b, inplicit contour level 0 plot_options.functions = list(plot_options.functions) # so we can modify it @@ -246,7 +246,7 @@ def emit(graphics, i, xyzs, quads): zs = xyzs[:,2] # this is a linear list matching with quads # n contours if not specified - n = 8 + n = 8 # TODO: need to pick "nice" number so levels have few digits levels = contour_levels if not levels: zmin, zmax = np.min(zs), np.max(zs) From 8e325a250b9372e58bfb860b89a4f4a7ccd6f35a Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Fri, 5 Dec 2025 12:37:56 -0500 Subject: [PATCH 06/18] Fill out PlotRange when passing from Plot* to Graphics* --- mathics/builtin/drawing/plot.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index 831841e8a..fd2794e5f 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -637,21 +637,33 @@ def eval( if not graphics: return - # update non-numeric PlotRanges with the specified {x,xmin,xmax} range options + # Expand PlotRange option using the {x,xmin,xmax} etc. range specifications + # Pythonize it, so Symbol becomes str, numeric becomes int or float plot_range = self.get_option(options, str(SymbolPlotRange), evaluation) plot_range = plot_range.to_python() - # from here we've pythonized it, so Symbol becomes str, numeric becomes int or float + + # expand to list with one more dimension than the {x,xmin,xmax} etc. range specifications if isinstance(plot_range, str): - plot_range = [plot_range] * len(plot_options.ranges) + # PlotRange -> Automatic ~ PlotRange -> {Automatic,...} + plot_range = [plot_range] * (len(plot_options.ranges) + 1) elif isinstance(plot_range, (int,float)): - # TODO: single numeric PlotRange is Automaic for all but last dimension - is correct? - all_but_last = [str(SymbolAutomatic)] * (len(plot_options.ranges)-1) + # PlotRange -> s ~ PlotRange -> {Automatic, ..., {-s,s}} + # TODO: {-s,s} may not be optimal for all plot types - some prefer {0,s} + all_but_last = [str(SymbolAutomatic)] * len(plot_options.ranges) + plot_range = all_but_last + [[-plot_range, plot_range]] + elif isinstance(plot_range, (list,tuple)) and isinstance(plot_range[0], (int, float)): + # PlotRange -> {s0,s1} ~ PlotRange -> {Automatic, ..., {s0,s1}} + all_but_last = [str(SymbolAutomatic)] * len(plot_options.ranges) plot_range = all_but_last + [plot_range] + + # now we have a list, one for each range spec plus one + # handle Automatic ~ {xmin,xmax} etc. for i, (pr, r) in enumerate(zip(plot_range, plot_options.ranges)): # TODO: this treats Automatic and Full as the same, which isn't quite right if isinstance(pr, str): plot_range[i] = r[1:] # extract {xmin,xmax} from {x,xmin,xmax} - # unpythonize and update + + # unpythonize and update PlotRange options[str(SymbolPlotRange)] = to_mathics_list(*plot_range) # generate the Graphics[3D] result From 4ceb4b1835b101238145013d529fd367820d3288 Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Fri, 5 Dec 2025 15:01:39 -0500 Subject: [PATCH 07/18] Update plot3d.py --- mathics/eval/drawing/plot3d.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mathics/eval/drawing/plot3d.py b/mathics/eval/drawing/plot3d.py index d297247c1..9edc31acb 100644 --- a/mathics/eval/drawing/plot3d.py +++ b/mathics/eval/drawing/plot3d.py @@ -506,3 +506,10 @@ def eval_ComplexPlot( evaluation: Evaluation, ): return None + + +def eval_ContourPlot( + plot_options, + evaluation: Evaluation, +): + return None From 2a8d99e1acb03990089afc012f32854342828871 Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Fri, 5 Dec 2025 15:06:27 -0500 Subject: [PATCH 08/18] Update Combinatorica-repo --- mathics/Packages/Combinatorica-repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/Packages/Combinatorica-repo b/mathics/Packages/Combinatorica-repo index be90b6180..eedbb07f5 160000 --- a/mathics/Packages/Combinatorica-repo +++ b/mathics/Packages/Combinatorica-repo @@ -1 +1 @@ -Subproject commit be90b6180ae460b8698686952d35387dda18c627 +Subproject commit eedbb07f5e4ec09d3f9f973283d46c8e72520c88 From 16cea5bb33c507dfec4d4f1dcfa9fbe91a814779 Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Fri, 5 Dec 2025 17:57:46 -0500 Subject: [PATCH 09/18] Fix test failure --- mathics/builtin/box/graphics3d.py | 19 +++++++++++++++++-- mathics/builtin/drawing/plot.py | 23 +++++++++++------------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/mathics/builtin/box/graphics3d.py b/mathics/builtin/box/graphics3d.py index dc112128a..2be5d8d87 100644 --- a/mathics/builtin/box/graphics3d.py +++ b/mathics/builtin/box/graphics3d.py @@ -250,6 +250,19 @@ def _prepare_elements(self, elements, options, max_width=None): self.background_color = elements.background_color def calc_dimensions(final_pass=True): + + # TODO: the code below is broken in any other case but Automatic + # because it calls elements.translate which is not implemented. + # A change in Plot3D caused it to pass PlotRanges that tickled + # the bug, causing test failures. The following line restores + # the old behavior for this code and the tests pass. + # It should not change the behavior of any case which did + # previously fail with an exception. + # + # The right thing to do is significantly DRY this code (together + # with the very similar code for the 2d case) and fix the bug. + plot_range = ["System`Automatic"] * 3 + if "System`Automatic" in plot_range: xmin, xmax, ymin, ymax, zmin, zmax = elements.extent() else: @@ -291,14 +304,16 @@ def calc_dimensions(final_pass=True): elif zmin == zmax: zmin -= 1 zmax += 1 - elif isinstance(plot_range[1], list) and len(plot_range[1]) == 2: + elif isinstance(plot_range[1], list) and len(plot_range[2]) == 2: zmin, zmax = list(map(float, plot_range[2])) zmin = elements.translate((0, 0, zmin))[2] zmax = elements.translate((0, 0, zmax))[2] else: + raise raise BoxExpressionError except (ValueError, TypeError): - raise BoxExpressionError + raise + #raise BoxExpressionError boxscale = [1.0, 1.0, 1.0] if boxratios[0] != "System`Automatic": diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index fd2794e5f..9d0d3ffbe 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -642,19 +642,18 @@ def eval( plot_range = self.get_option(options, str(SymbolPlotRange), evaluation) plot_range = plot_range.to_python() - # expand to list with one more dimension than the {x,xmin,xmax} etc. range specifications + # expand to list of length dim + dim = 3 if self.graphics_class is Graphics3D else 2 if isinstance(plot_range, str): - # PlotRange -> Automatic ~ PlotRange -> {Automatic,...} - plot_range = [plot_range] * (len(plot_options.ranges) + 1) - elif isinstance(plot_range, (int,float)): - # PlotRange -> s ~ PlotRange -> {Automatic, ..., {-s,s}} - # TODO: {-s,s} may not be optimal for all plot types - some prefer {0,s} - all_but_last = [str(SymbolAutomatic)] * len(plot_options.ranges) - plot_range = all_but_last + [[-plot_range, plot_range]] - elif isinstance(plot_range, (list,tuple)) and isinstance(plot_range[0], (int, float)): - # PlotRange -> {s0,s1} ~ PlotRange -> {Automatic, ..., {s0,s1}} - all_but_last = [str(SymbolAutomatic)] * len(plot_options.ranges) - plot_range = all_but_last + [plot_range] + # PlotRange -> Automatic becomes PlotRange -> {Automatic,...} + plot_range = [plot_range] * dim + elif isinstance(plot_range, (int,float,list,tuple)): + # PlotRange -> s becomes PlotRange -> {Automatic,...,{-s,s}} + # PlotRange -> {s0,s1} becomes PlotRange -> {Automatic,...,{s0,s1}} + s = plot_range + plot_range = [str(SymbolAutomatic)] * len(plot_options.ranges) + if dim > len(plot_range): + plot_range += s if isinstance(s, (list,tuple)) else [-s,s] # now we have a list, one for each range spec plus one # handle Automatic ~ {xmin,xmax} etc. From fb2b165e64576e5c345fe481d32e638071ecd656 Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Fri, 5 Dec 2025 22:08:50 -0500 Subject: [PATCH 10/18] Don't copy ComplexPlot* ranges to PlotRange --- mathics/builtin/drawing/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index 9d0d3ffbe..65beb5f2f 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -659,7 +659,7 @@ def eval( # handle Automatic ~ {xmin,xmax} etc. for i, (pr, r) in enumerate(zip(plot_range, plot_options.ranges)): # TODO: this treats Automatic and Full as the same, which isn't quite right - if isinstance(pr, str): + if isinstance(pr, str) and not isinstance(r[1], complex): plot_range[i] = r[1:] # extract {xmin,xmax} from {x,xmin,xmax} # unpythonize and update PlotRange From 1d3d9e1940aaea2ef3b3ded30b7924eea03b65d3 Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Sat, 6 Dec 2025 00:50:31 -0500 Subject: [PATCH 11/18] Fix PlotRange calculation --- mathics/builtin/drawing/plot.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index 65beb5f2f..9f415f147 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -641,28 +641,29 @@ def eval( # Pythonize it, so Symbol becomes str, numeric becomes int or float plot_range = self.get_option(options, str(SymbolPlotRange), evaluation) plot_range = plot_range.to_python() - - # expand to list of length dim dim = 3 if self.graphics_class is Graphics3D else 2 if isinstance(plot_range, str): - # PlotRange -> Automatic becomes PlotRange -> {Automatic,...} - plot_range = [plot_range] * dim - elif isinstance(plot_range, (int,float,list,tuple)): + # PlotRange -> Automatic becomes PlotRange -> {Automatic, ...} + plot_range = [str(SymbolAutomatic)] * dim + if isinstance(plot_range, (int,float)): # PlotRange -> s becomes PlotRange -> {Automatic,...,{-s,s}} + pr = plot_range + plot_range = [str(SymbolAutomatic)] * dim + plot_range[-1] = [-pr,pr] + elif isinstance(plot_range, (list,tuple)) and isinstance(plot_range[0], (int,float)): # PlotRange -> {s0,s1} becomes PlotRange -> {Automatic,...,{s0,s1}} - s = plot_range - plot_range = [str(SymbolAutomatic)] * len(plot_options.ranges) - if dim > len(plot_range): - plot_range += s if isinstance(s, (list,tuple)) else [-s,s] + pr = plot_range + plot_range = [str(SymbolAutomatic)] * dim + plot_range[-1] = pr - # now we have a list, one for each range spec plus one + # now we have a list of length dim # handle Automatic ~ {xmin,xmax} etc. for i, (pr, r) in enumerate(zip(plot_range, plot_options.ranges)): # TODO: this treats Automatic and Full as the same, which isn't quite right if isinstance(pr, str) and not isinstance(r[1], complex): plot_range[i] = r[1:] # extract {xmin,xmax} from {x,xmin,xmax} - # unpythonize and update PlotRange + # unpythonize and update PlotRange option options[str(SymbolPlotRange)] = to_mathics_list(*plot_range) # generate the Graphics[3D] result From 4905105b6ff12a992deccea22cfac6ac185fe88e Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Sat, 6 Dec 2025 08:57:33 -0500 Subject: [PATCH 12/18] Fix error message for missing required module --- mathics/core/builtin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index f91e4ea76..f1784391e 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -852,12 +852,14 @@ class UnavailableFunction: def __init__(self, builtin): self.name = builtin.get_name() + self.requires = builtin.requires def __call__(self, **kwargs): kwargs["evaluation"].message( "General", "pyimport", # see messages.py for error message definition strip_context(self.name), + ", ".join(self.requires), ) From 97242e90d4f504ade38210ae4bf8c76056daea6e Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Sat, 6 Dec 2025 09:06:25 -0500 Subject: [PATCH 13/18] Formatting --- mathics/builtin/box/graphics3d.py | 4 +-- mathics/builtin/drawing/plot.py | 11 +++--- mathics/eval/drawing/plot3d_vectorized.py | 43 +++++++++++------------ 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/mathics/builtin/box/graphics3d.py b/mathics/builtin/box/graphics3d.py index 2be5d8d87..18d27b4c6 100644 --- a/mathics/builtin/box/graphics3d.py +++ b/mathics/builtin/box/graphics3d.py @@ -250,7 +250,6 @@ def _prepare_elements(self, elements, options, max_width=None): self.background_color = elements.background_color def calc_dimensions(final_pass=True): - # TODO: the code below is broken in any other case but Automatic # because it calls elements.translate which is not implemented. # A change in Plot3D caused it to pass PlotRanges that tickled @@ -312,8 +311,7 @@ def calc_dimensions(final_pass=True): raise raise BoxExpressionError except (ValueError, TypeError): - raise - #raise BoxExpressionError + raise BoxExpressionError boxscale = [1.0, 1.0, 1.0] if boxratios[0] != "System`Automatic": diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index 9f415f147..91e796b9b 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -37,10 +37,10 @@ SymbolLine, SymbolLog10, SymbolNone, + SymbolPlotRange, SymbolRGBColor, SymbolSequence, SymbolStyle, - SymbolPlotRange, ) from mathics.eval.drawing.charts import draw_bar_chart, eval_chart from mathics.eval.drawing.colors import COLOR_PALETTES, get_color_palette @@ -645,12 +645,14 @@ def eval( if isinstance(plot_range, str): # PlotRange -> Automatic becomes PlotRange -> {Automatic, ...} plot_range = [str(SymbolAutomatic)] * dim - if isinstance(plot_range, (int,float)): + if isinstance(plot_range, (int, float)): # PlotRange -> s becomes PlotRange -> {Automatic,...,{-s,s}} pr = plot_range plot_range = [str(SymbolAutomatic)] * dim - plot_range[-1] = [-pr,pr] - elif isinstance(plot_range, (list,tuple)) and isinstance(plot_range[0], (int,float)): + plot_range[-1] = [-pr, pr] + elif isinstance(plot_range, (list, tuple)) and isinstance( + plot_range[0], (int, float) + ): # PlotRange -> {s0,s1} becomes PlotRange -> {Automatic,...,{s0,s1}} pr = plot_range plot_range = [str(SymbolAutomatic)] * dim @@ -869,7 +871,6 @@ class ContourPlot(_Plot3D): graphics_class = Graphics - class DensityPlot(_Plot3D): """ :WMA link: https://reference.wolfram.com/language/ref/DensityPlot.html diff --git a/mathics/eval/drawing/plot3d_vectorized.py b/mathics/eval/drawing/plot3d_vectorized.py index f7c846023..79c2a7e37 100644 --- a/mathics/eval/drawing/plot3d_vectorized.py +++ b/mathics/eval/drawing/plot3d_vectorized.py @@ -8,10 +8,15 @@ import numpy as np from mathics.builtin.colors.color_internals import convert_color -from mathics.core.expression import Expression from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression from mathics.core.symbols import strip_context -from mathics.core.systemsymbols import SymbolNone, SymbolRGBColor, SymbolEqual, SymbolSubtract +from mathics.core.systemsymbols import ( + SymbolEqual, + SymbolNone, + SymbolRGBColor, + SymbolSubtract, +) from mathics.timing import Timer from .plot_compile import plot_compile @@ -152,7 +157,7 @@ def compute_over_grid(nx, ny): def palette_color_directive(palette, i): - """ returns a directive in a form suitable for graphics.add_directives """ + """returns a directive in a form suitable for graphics.add_directives""" """ for setting the color of an entire entity such as a line or surface """ rgb = palette[i % len(palette)] rgb = [c / 255.0 for c in rgb] @@ -164,7 +169,7 @@ def density_colors(zs): """default color palette for DensityPlot and ContourPlot (f(x) form)""" z_min, z_max = min(zs), max(zs) zs = zs[:, np.newaxis] # allow broadcasting - #c_min, c_max = [0.3, 0.00, 0.3], [1.0, 0.95, 0.8] + # c_min, c_max = [0.3, 0.00, 0.3], [1.0, 0.95, 0.8] c_min, c_max = [0.5, 0, 0.1], [1.0, 0.9, 0.5] c_min, c_max = ( np.full((len(zs), 3), c_min), @@ -180,7 +185,6 @@ def eval_Plot3D( evaluation: Evaluation, ): def emit(graphics, i, xyzs, quads): - # choose a color color_directive = palette_color_directive(palette3, i) graphics.add_directives(color_directive) @@ -197,7 +201,6 @@ def eval_DensityPlot( evaluation: Evaluation, ): def emit(graphics, i, xyzs, quads): - # Fixed palette for now # TODO: accept color options colors = density_colors(xyzs[:, 2]) @@ -217,10 +220,10 @@ def eval_ContourPlot( # whether to show a background similar to density plot, except quantized background = len(plot_options.functions) == 1 - contour_levels = None # TODO: implement Contours option + contour_levels = None # TODO: implement Contours option # convert fs of the form a==b to a-b, inplicit contour level 0 - plot_options.functions = list(plot_options.functions) # so we can modify it + plot_options.functions = list(plot_options.functions) # so we can modify it for i, f in enumerate(plot_options.functions): if f.head == SymbolEqual: f = Expression(SymbolSubtract, *f.elements[0:2]) @@ -229,7 +232,6 @@ def eval_ContourPlot( background = False def emit(graphics, i, xyzs, quads): - # set line color if background: # showing a background, so just black lines @@ -243,10 +245,10 @@ def emit(graphics, i, xyzs, quads): nx, ny = plot_options.plotpoints _, xmin, xmax = plot_options.ranges[0] _, ymin, ymax = plot_options.ranges[1] - zs = xyzs[:,2] # this is a linear list matching with quads + zs = xyzs[:, 2] # this is a linear list matching with quads # n contours if not specified - n = 8 # TODO: need to pick "nice" number so levels have few digits + n = 8 # TODO: need to pick "nice" number so levels have few digits levels = contour_levels if not levels: zmin, zmax = np.min(zs), np.max(zs) @@ -255,28 +257,25 @@ def emit(graphics, i, xyzs, quads): # one contour line per contour level for level in levels: - # find contours and add lines with Timer("contours"): - zgrid = zs.reshape((nx, ny)) # find_contours needs it as an array + zgrid = zs.reshape((nx, ny)) # find_contours needs it as an array contours = skimage.measure.find_contours(zgrid, level) # add lines for segment in contours: - segment[:,0] = segment[:,0] * ((xmax-xmin)/nx) + xmin - segment[:,1] = segment[:,1] * ((ymax-ymin)/ny) + ymin + segment[:, 0] = segment[:, 0] * ((xmax - xmin) / nx) + xmin + segment[:, 1] = segment[:, 1] * ((ymax - ymin) / ny) + ymin graphics.add_linexyzs(segment) # background if background: - zs = np.floor(zs / dz_per_level) # quantize the zs to match the contours - colors = density_colors(zs) # and get colors using the quantized zs + zs = np.floor(zs / dz_per_level) # quantize the zs to match the contours + colors = density_colors(zs) # and get colors using the quantized zs graphics.add_complex(xyzs[:, 0:2], lines=None, polys=quads, colors=colors) - - - #plot_options.plotpoints = [n * 10 for n in plot_options.plotpoints] - return make_plot(plot_options, evaluation, dim=2, is_complex=False, emit=emit) + # plot_options.plotpoints = [n * 10 for n in plot_options.plotpoints] + return make_plot(plot_options, evaluation, dim=2, is_complex=False, emit=emit) @Timer("complex colors") @@ -330,5 +329,3 @@ def emit(graphics, i, xyzs, quads): ) return make_plot(plot_options, evaluation, dim=2, is_complex=True, emit=emit) - - From 7843824a964529c5dae9614ac358485955f1183a Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Sat, 6 Dec 2025 11:03:42 -0500 Subject: [PATCH 14/18] Add Contours option --- mathics/builtin/drawing/plot.py | 15 +++++++++-- mathics/eval/drawing/plot3d_vectorized.py | 32 +++++++++++++++-------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index 91e796b9b..e231dd16a 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -491,6 +491,14 @@ def __init__(self, expr, range_exprs, options, evaluation): self.error(expr, "invrange", range_expr) self.ranges.append(range) + # Contours option + contours = expr.get_option(options, "Contours", evaluation) + if contours is not None: + c = contours.to_python() + if not (c=="System`Automatic" or isinstance(c, int) or isinstance(c, tuple) and all(isinstance(cc, (int,float)) for cc in c) ): + self.error(expr, "invcontour", contours) + self.contours = c + # Mesh option mesh = expr.get_option(options, "Mesh", evaluation) if mesh not in (SymbolNone, SymbolFull, SymbolAll): @@ -582,6 +590,9 @@ class _Plot3D(Builtin): "Plot range `1` must be of the form {variable, min, max}, " "where max > min." ), + "invcontour": ( + "Contours option must be Automatic, an integer, or a list of numbers." + ) } # Plot3D, ComplexPlot3D @@ -863,8 +874,8 @@ class ContourPlot(_Plot3D): requires = ["skimage"] summary_text = "creates a contour plot" expected_args = 3 - options = _Plot3D.options2d - # TODO: updates? # + options = _Plot3D.options2d | {"Contours": "Automatic"} + # TODO: other options? many_functions = True eval_function = staticmethod(eval_ContourPlot) diff --git a/mathics/eval/drawing/plot3d_vectorized.py b/mathics/eval/drawing/plot3d_vectorized.py index 79c2a7e37..a2736ade4 100644 --- a/mathics/eval/drawing/plot3d_vectorized.py +++ b/mathics/eval/drawing/plot3d_vectorized.py @@ -220,7 +220,7 @@ def eval_ContourPlot( # whether to show a background similar to density plot, except quantized background = len(plot_options.functions) == 1 - contour_levels = None # TODO: implement Contours option + contour_levels = plot_options.contours # convert fs of the form a==b to a-b, inplicit contour level 0 plot_options.functions = list(plot_options.functions) # so we can modify it @@ -247,13 +247,18 @@ def emit(graphics, i, xyzs, quads): _, ymin, ymax = plot_options.ranges[1] zs = xyzs[:, 2] # this is a linear list matching with quads - # n contours if not specified - n = 8 # TODO: need to pick "nice" number so levels have few digits + # process contour_levels levels = contour_levels - if not levels: - zmin, zmax = np.min(zs), np.max(zs) - dz_per_level = (zmax - zmin) / n - levels = np.arange(n) * dz_per_level + zmin + zmin, zmax = np.min(zs), np.max(zs) + if isinstance(levels, str): + # TODO: need to pick "nice" number so levels have few digits + # an odd number ensures there is a contour at 0 if range is balanced + levels = 9 + if isinstance(levels, int): + # computed contour levels have equal distance between them, + # and half that between first/last contours and zmin/zmax + dz = (zmax - zmin) / levels + levels = zmin + np.arange(levels) * dz + dz/2 # one contour line per contour level for level in levels: @@ -268,11 +273,16 @@ def emit(graphics, i, xyzs, quads): segment[:, 1] = segment[:, 1] * ((ymax - ymin) / ny) + ymin graphics.add_linexyzs(segment) - # background + # background is solid colors between contour lines if background: - zs = np.floor(zs / dz_per_level) # quantize the zs to match the contours - colors = density_colors(zs) # and get colors using the quantized zs - graphics.add_complex(xyzs[:, 0:2], lines=None, polys=quads, colors=colors) + with Timer("contour background"): + # add extra levels below zmin and above zmax to define end ranges + levels = [zmin-(levels[0]-zmin)] + list(levels) + [zmax+(zmax-levels[-1])] + for lo, hi in zip(levels[:-1], levels[1:]): + # use masks and fancy indexing to assign (lo+hi)/2 to all zs between lo and hi + zs[(lo < zs) & (zs <= hi)] = (lo + hi) / 2 + colors = density_colors(zs) # same colors as density plot + graphics.add_complex(xyzs[:, 0:2], lines=None, polys=quads, colors=colors) # plot_options.plotpoints = [n * 10 for n in plot_options.plotpoints] return make_plot(plot_options, evaluation, dim=2, is_complex=False, emit=emit) From e25eca9d19c541849ff1421702444776eef3a736 Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Sat, 6 Dec 2025 11:13:17 -0500 Subject: [PATCH 15/18] Update comment --- mathics/builtin/box/graphics3d.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mathics/builtin/box/graphics3d.py b/mathics/builtin/box/graphics3d.py index 18d27b4c6..1cc98b083 100644 --- a/mathics/builtin/box/graphics3d.py +++ b/mathics/builtin/box/graphics3d.py @@ -252,14 +252,13 @@ def _prepare_elements(self, elements, options, max_width=None): def calc_dimensions(final_pass=True): # TODO: the code below is broken in any other case but Automatic # because it calls elements.translate which is not implemented. - # A change in Plot3D caused it to pass PlotRanges that tickled - # the bug, causing test failures. The following line restores - # the old behavior for this code and the tests pass. - # It should not change the behavior of any case which did + # Plots may pass specific plot ranges, triggering this deficiency + # and causing tests to fail The following line avoids this, + # and it should not change the behavior of any case which did # previously fail with an exception. # - # The right thing to do is significantly DRY this code (together - # with the very similar code for the 2d case) and fix the bug. + # This code should be DRYed (together with the very similar code + # for the 2d case), and the missing .translate method added. plot_range = ["System`Automatic"] * 3 if "System`Automatic" in plot_range: From 0773f42247a428af8a95200a1d6bda4e2e141947 Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Sat, 6 Dec 2025 11:15:03 -0500 Subject: [PATCH 16/18] Formatting --- mathics/builtin/drawing/plot.py | 9 +++++++-- mathics/eval/drawing/plot3d_vectorized.py | 14 ++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index e231dd16a..cf59c329d 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -495,7 +495,12 @@ def __init__(self, expr, range_exprs, options, evaluation): contours = expr.get_option(options, "Contours", evaluation) if contours is not None: c = contours.to_python() - if not (c=="System`Automatic" or isinstance(c, int) or isinstance(c, tuple) and all(isinstance(cc, (int,float)) for cc in c) ): + if not ( + c == "System`Automatic" + or isinstance(c, int) + or isinstance(c, tuple) + and all(isinstance(cc, (int, float)) for cc in c) + ): self.error(expr, "invcontour", contours) self.contours = c @@ -592,7 +597,7 @@ class _Plot3D(Builtin): ), "invcontour": ( "Contours option must be Automatic, an integer, or a list of numbers." - ) + ), } # Plot3D, ComplexPlot3D diff --git a/mathics/eval/drawing/plot3d_vectorized.py b/mathics/eval/drawing/plot3d_vectorized.py index a2736ade4..d5c8272e4 100644 --- a/mathics/eval/drawing/plot3d_vectorized.py +++ b/mathics/eval/drawing/plot3d_vectorized.py @@ -258,7 +258,7 @@ def emit(graphics, i, xyzs, quads): # computed contour levels have equal distance between them, # and half that between first/last contours and zmin/zmax dz = (zmax - zmin) / levels - levels = zmin + np.arange(levels) * dz + dz/2 + levels = zmin + np.arange(levels) * dz + dz / 2 # one contour line per contour level for level in levels: @@ -277,12 +277,18 @@ def emit(graphics, i, xyzs, quads): if background: with Timer("contour background"): # add extra levels below zmin and above zmax to define end ranges - levels = [zmin-(levels[0]-zmin)] + list(levels) + [zmax+(zmax-levels[-1])] + levels = ( + [zmin - (levels[0] - zmin)] + + list(levels) + + [zmax + (zmax - levels[-1])] + ) for lo, hi in zip(levels[:-1], levels[1:]): # use masks and fancy indexing to assign (lo+hi)/2 to all zs between lo and hi zs[(lo < zs) & (zs <= hi)] = (lo + hi) / 2 - colors = density_colors(zs) # same colors as density plot - graphics.add_complex(xyzs[:, 0:2], lines=None, polys=quads, colors=colors) + colors = density_colors(zs) # same colors as density plot + graphics.add_complex( + xyzs[:, 0:2], lines=None, polys=quads, colors=colors + ) # plot_options.plotpoints = [n * 10 for n in plot_options.plotpoints] return make_plot(plot_options, evaluation, dim=2, is_complex=False, emit=emit) From 833d790af62bcad1bee54bccc8f28f19d247495a Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Sat, 6 Dec 2025 11:28:00 -0500 Subject: [PATCH 17/18] Remove debugging code --- mathics/builtin/box/graphics3d.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mathics/builtin/box/graphics3d.py b/mathics/builtin/box/graphics3d.py index 1cc98b083..e6e3d8eea 100644 --- a/mathics/builtin/box/graphics3d.py +++ b/mathics/builtin/box/graphics3d.py @@ -307,7 +307,6 @@ def calc_dimensions(final_pass=True): zmin = elements.translate((0, 0, zmin))[2] zmax = elements.translate((0, 0, zmax))[2] else: - raise raise BoxExpressionError except (ValueError, TypeError): raise BoxExpressionError From 28e1fc57d813a67a1be09ec444c8f95dc0205c03 Mon Sep 17 00:00:00 2001 From: Bruce Lucas Date: Sat, 6 Dec 2025 23:26:17 -0500 Subject: [PATCH 18/18] Little simpler. slightly faster --- mathics/eval/drawing/plot3d_vectorized.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/mathics/eval/drawing/plot3d_vectorized.py b/mathics/eval/drawing/plot3d_vectorized.py index d5c8272e4..1d7e94674 100644 --- a/mathics/eval/drawing/plot3d_vectorized.py +++ b/mathics/eval/drawing/plot3d_vectorized.py @@ -276,15 +276,11 @@ def emit(graphics, i, xyzs, quads): # background is solid colors between contour lines if background: with Timer("contour background"): - # add extra levels below zmin and above zmax to define end ranges - levels = ( - [zmin - (levels[0] - zmin)] - + list(levels) - + [zmax + (zmax - levels[-1])] - ) + # use masks and fancy indexing to assign (lo+hi)/2 to all zs between lo and hi + zs[zs <= levels[0]] = zmin for lo, hi in zip(levels[:-1], levels[1:]): - # use masks and fancy indexing to assign (lo+hi)/2 to all zs between lo and hi zs[(lo < zs) & (zs <= hi)] = (lo + hi) / 2 + zs[levels[-1] < zs] = zmax colors = density_colors(zs) # same colors as density plot graphics.add_complex( xyzs[:, 0:2], lines=None, polys=quads, colors=colors