Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ ChangeLog.orig
ChangeLog.rej
Documents/
Homepage/
Test/
#Test/
_Copies_/
_Database_/
build/
Expand Down
27 changes: 21 additions & 6 deletions mathics/builtin/drawing/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from mathics.core.evaluation import Evaluation
from mathics.core.expression import Expression
from mathics.core.list import ListExpression
from mathics.core.symbols import Symbol
from mathics.core.symbols import Symbol, SymbolList
from mathics.core.systemsymbols import (
SymbolAll,
SymbolAutomatic,
Expand Down Expand Up @@ -413,11 +413,21 @@ class PlotOptions:
plot_points: list
maxdepth: int

def __init__(self, builtin, range_exprs, options, dim, evaluation):
def __init__(self, builtin, functions, range_exprs, options, dim, evaluation):
def error(*args, **kwargs):
evaluation.message(builtin.get_name(), *args, **kwargs)
raise ValueError()

# convert functions to list of lists of exprs
def to_list(expr):
if isinstance(expr, Expression) and expr.head is SymbolList:
return [to_list(e) for e in expr.elements]
else:
return expr

functions = to_list(functions)
self.functions = functions if isinstance(functions, list) else [functions]

# plot ranges of the form {x,xmin,xmax} etc. (returns Symbol)
self.ranges = []
for range_expr in range_exprs:
Expand Down Expand Up @@ -471,11 +481,16 @@ def error(*args, **kwargs):
self.exclusions = exclusions

# Mesh option (returns Symbol)
mesh = builtin.get_option(options, "Mesh", evaluation)
if mesh not in (SymbolNone, SymbolFull, SymbolAll):
mesh = builtin.get_option(options, "Mesh", evaluation).to_python(
preserve_symbols=True
)
if isinstance(mesh, (list, tuple)) and all(isinstance(m, int) for m in mesh):
self.mesh = mesh
elif mesh not in (SymbolNone, SymbolFull, SymbolAll):
evaluation.message("Mesh", "ilevels", mesh)
mesh = SymbolFull
self.mesh = mesh
self.mesh = SymbolFull
else:
self.mesh = mesh

# PlotPoints option (returns Symbol)
plot_points_option = builtin.get_option(options, "PlotPoints", evaluation)
Expand Down
13 changes: 10 additions & 3 deletions mathics/builtin/drawing/plot_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ def eval(self, functions, ranges, evaluation: Evaluation, options: dict):
# parse options, bailing out if anything is wrong
try:
ranges = ranges.elements if ranges.head is SymbolSequence else [ranges]
plot_options = plot.PlotOptions(self, ranges, options, 2, evaluation)
plot_options = plot.PlotOptions(
self, functions, ranges, options, 2, evaluation
)
except ValueError:
return None

Expand All @@ -84,10 +86,15 @@ def eval(self, functions, ranges, evaluation: Evaluation, options: dict):
apply_function = self.apply_function
if not plot.use_vectorized_plot:
apply_function = lru_cache(apply_function)
plot_options.apply_function = apply_function

# additional options specific to this class
# TODO: PlotOptions has already regularized .functions to be a list
# (of lists) of functions, used by the _Plot3d builtins.
# But _Plot builtins still need to be reworked to use it,
# so we still use the old mechanism here.
plot_options.functions = self.get_functions_param(functions)
plot_options.apply_function = apply_function

# additional options specific to this class
plot_options.use_log_scale = self.use_log_scale
plot_options.expect_list = self.expect_list
if plot_options.plot_points is None:
Expand Down
88 changes: 74 additions & 14 deletions mathics/builtin/drawing/plot_plot3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,22 +99,27 @@ def eval(
try:
dim = 3 if self.graphics_class is Graphics3D else 2
ranges = ranges.elements if ranges.head is SymbolSequence else [ranges]
plot_options = plot.PlotOptions(self, ranges, options, dim, evaluation)
plot_options = plot.PlotOptions(
self, functions, ranges, options, dim, evaluation
)
except ValueError:
return None

# TODO: consult many_functions variable set by subclass and error
# if many_functions is False but multiple are supplied
if functions.has_form("List", None):
plot_options.functions = functions.elements
else:
plot_options.functions = [functions]

# supply default value
if plot_options.plot_points is None:
default_plot_points = (200, 200) if plot.use_vectorized_plot else (7, 7)
if isinstance(self, ParametricPlot3D) and len(plot_options.ranges) == 1:
# ParametricPlot3D with one independent variable generating a curve
default_plot_points = (1000,)
elif plot.use_vectorized_plot:
default_plot_points = (200, 200)
else:
default_plot_points = (7, 7)
plot_options.plot_points = default_plot_points

# supply apply_function which knows how to take the plot parameters
# and produce xs, ys, and zs
plot_options.apply_function = self.apply_function

# subclass must set eval_function and graphics_class
eval_function = plot.get_plot_eval_function(self.__class__)
with np.errstate(all="ignore"): # suppress numpy warnings
Expand All @@ -125,11 +130,14 @@ def eval(
# now we have a list of length dim
# handle Automatic ~ {xmin,xmax} etc.
# TODO: dowstream consumers might be happier if we used data range where applicable
for i, (pr, r) in enumerate(zip(plot_options.plot_range, plot_options.ranges)):
# TODO: this treats Automatic and Full as the same, which isn't quite right
if isinstance(pr, (str, Symbol)) and not isinstance(r[1], complex):
# extract {xmin,xmax} from {x,xmin,xmax}
plot_options.plot_range[i] = r[1:]
if not isinstance(self, ParametricPlot3D):
for i, (pr, r) in enumerate(
zip(plot_options.plot_range, plot_options.ranges)
):
# TODO: this treats Automatic and Full as the same, which isn't quite right
if isinstance(pr, (str, Symbol)) and not isinstance(r[1], complex):
# extract {xmin,xmax} from {x,xmin,xmax}
plot_options.plot_range[i] = r[1:]

# unpythonize and update PlotRange option
options[str(SymbolPlotRange)] = to_mathics_list(*plot_options.plot_range)
Expand All @@ -140,6 +148,10 @@ def eval(
)
return graphics_expr

def apply_function(self, function, names, us, vs):
parms = {str(names[0]): us, str(names[1]): vs}
return us, vs, function(**parms)


class ComplexPlot3D(_Plot3D):
"""
Expand All @@ -163,6 +175,10 @@ class ComplexPlot3D(_Plot3D):
many_functions = True
graphics_class = Graphics3D

def apply_function(self, function, names, us, vs):
parms = {str(names[0]): us + vs * 1j}
return us, vs, function(**parms)


class ComplexPlot(_Plot3D):
"""
Expand All @@ -186,6 +202,10 @@ class ComplexPlot(_Plot3D):
many_functions = False
graphics_class = Graphics

def apply_function(self, function, names, us, vs):
parms = {str(names[0]): us + vs * 1j}
return us, vs, function(**parms)


class ContourPlot(_Plot3D):
"""
Expand Down Expand Up @@ -244,6 +264,46 @@ class DensityPlot(_Plot3D):
graphics_class = Graphics


class ParametricPlot3D(_Plot3D):
"""
<url>:Parametric equation: https://en.wikipedia.org/wiki/Parametric_equation</url>
<url>:WMA link: https://reference.wolfram.com/language/ref/ParametricPlot3D.html</url>
<dl>
<dt>'ParametricPlot3D'[${x(u,v), y(u,v), z(u,v)}$, {$u$, $u_{min}$, $u_{max}$}, {$v$, $v_{min}$, $v_{max}$}]
<dd>creates a three-dimensional surface using the functions $x$, $y$, $z$ over the specified ranges for parameters $u$ and $v$.

<dt>'ParametricPlot3D'[${x(u), y(u), z(u)}$, {$u$, $u_{min}$, $u_{max}$}]
<dd>creates a three-dimensional space curve using the functions $x$, $y$, $z$ over the specified range for parameter $u$.

See <url>:Drawing Option and Option Values:
/doc/reference-of-built-in-symbols/graphics-and-drawing/drawing-options-and-option-values
</url> for a list of Plot options.
</dl>

>> ParametricPlot3D[{Sin[t] + 2 Sin[2 t], Cos[t] - 2 Cos[2 t], -Sin[3 t]}, {t, 0, 2 Pi}]
= ...

A function of a single parameter $t$ generates a trefoil knot.

>> ParametricPlot3D[{(2 + Cos[v]) Cos[u], (2 + Cos[v]) Sin[u], Sin[v]}, {u, 0, 2 Pi}, {v, 0, 2 Pi}]
= ...

A function of two parameters $u$ and $v$ generates a torus.

"""

summary_text = "plot a parametric surface or curve in three dimensions"
expected_args = 3
options = _Plot3D.options3d

many_functions = True
graphics_class = Graphics3D

def apply_function(self, functions, names, *parms):
parms = {str(n): p for n, p in zip(names, parms)}
return [f(**parms) for f in functions]


class Plot3D(_Plot3D):
"""
<url>:WMA link: https://reference.wolfram.com/language/ref/Plot3D.html</url>
Expand Down
1 change: 1 addition & 0 deletions mathics/core/systemsymbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
SymbolAborted = Symbol("System`$Aborted")
SymbolAbs = Symbol("System`Abs")
SymbolAbsoluteTime = Symbol("AbsoluteTime")
SymbolAbsoluteThickness = Symbol("System`AbsoluteThickness")
SymbolAccuracy = Symbol("System`Accuracy")
SymbolAlignmentPoint = Symbol("System`AlignmentPoint")
SymbolAll = Symbol("System`All")
Expand Down
5 changes: 4 additions & 1 deletion mathics/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ def print_expression_tree(
if file is None:
file = sys.stdout

if isinstance(expr, Symbol):
if isinstance(expr, (tuple, list)):
for e in expr:
print_expression_tree(e, indent, marker, file, approximate)
elif isinstance(expr, Symbol):
print(f"{indent}{marker(expr)}{expr}", file=file)
elif not hasattr(expr, "elements"):
if isinstance(expr, MachineReal) and approximate:
Expand Down
7 changes: 7 additions & 0 deletions mathics/eval/drawing/plot3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,3 +513,10 @@ def eval_ContourPlot(
evaluation: Evaluation,
):
return None


def eval_ParametricPlot3D(
plot_options,
evaluation: Evaluation,
):
return None
Loading
Loading