-
-
Notifications
You must be signed in to change notification settings - Fork 66
Implement ContourPlot3D #1592
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: sphericalplot3d
Are you sure you want to change the base?
Implement ContourPlot3D #1592
Changes from 9 commits
a297cd0
f08e0e3
53cf528
0d8f64e
3003b16
71d6e79
02e719e
c18fc54
3d29ba8
0dbbdf3
51cac24
ce35374
2d3fc2b
1291cf1
aeb40c6
3d80b8b
27e6c40
191617b
bfd9708
c538823
3313aaf
f4f9857
f11940b
6b14674
137be41
501b9c6
c322d6a
6539f04
3f80707
7824f99
882a8f2
2b61d48
0df365f
66315b5
7ed0d87
a34ebdf
23d2d93
c7c11af
3a11b75
4bcc264
511bf37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -113,6 +113,8 @@ def eval( | |
| if isinstance(self, ParametricPlot3D) and len(plot_options.ranges) == 1: | ||
| # ParametricPlot3D with one independent variable generating a curve | ||
| default_plot_points = (1000,) | ||
| elif isinstance(self, ContourPlot3D): | ||
| default_plot_points = (50, 50, 50) | ||
| elif plot.use_vectorized_plot: | ||
| default_plot_points = (200, 200) | ||
| else: | ||
|
|
@@ -237,6 +239,40 @@ class ContourPlot(_Plot3D): | |
| graphics_class = Graphics | ||
|
|
||
|
|
||
| class ContourPlot3D(_Plot3D): | ||
| """ | ||
| <url>:WMA link: https://reference.wolfram.com/language/ref/ContourPlot3D.html</url> | ||
| <url>:Isosurface: https://en.wikipedia.org/wiki/Isosurface</usr> | ||
| <dl> | ||
| <dt>'ContourPlot3D'[$f(x,y,z)$, {$x$, $x_{min}$, $x_{max}$}, {$y$, $y_{min}$, $y_{max}$, {$y$, $y_{min}$, $y_{max}$}] | ||
| <dd>creates a three-dimensional contour plot of $f(x,y,z)$ over the specified region on $x$, $y$, and $z$. | ||
|
|
||
| >> ContourPlot3D[x ^ 2 + y ^ 2 - z ^ 2, {x, -1, 1}, {y, -1, 1}, {z, -1, 1}] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Examples don't belong inside If it is not fixed in a day or so, I'll put in a PR to fix this.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
| = ContourPlot3D[x ^ 2 + y ^ 2 - z ^ 2, {x, -1, 1}, {y, -1, 1}, {z, -1, 1}] | ||
|
|
||
| Multiple isosurfaces (3d contours) of a second degree equation form conical suraces, hyperboloids in this case. | ||
|
|
||
| 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> | ||
|
|
||
| """ | ||
|
|
||
| requires = ["skimage"] | ||
| summary_text = "creates a 3d contour plot" | ||
| expected_args = 4 | ||
| options = _Plot3D.options3d | { | ||
| "Contours": "Automatic", | ||
| "BoxRatios": "{1,1,1}", | ||
| "Mesh": "None", | ||
| } | ||
| # TODO: other options? | ||
|
|
||
| many_functions = False | ||
| graphics_class = Graphics3D | ||
|
|
||
|
|
||
| class DensityPlot(_Plot3D): | ||
| """ | ||
| <url>:heat map:https://en.wikipedia.org/wiki/Heat_map</url>(<url>:WMA link: https://reference.wolfram.com/language/ref/DensityPlot.html</url>) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| from mathics.core.systemsymbols import ( | ||
| SymbolAbsoluteThickness, | ||
| SymbolEqual, | ||
| SymbolFull, | ||
| SymbolNone, | ||
| SymbolRGBColor, | ||
| SymbolSubtract, | ||
|
|
@@ -222,6 +223,33 @@ def emit(graphics, i, xyzs, _, quads): | |
| return make_surfaces(plot_options, evaluation, dim=2, is_complex=False, emit=emit) | ||
|
|
||
|
|
||
| def equations_to_contours(plot_options): | ||
| """ | ||
| For contour plots, convert functions of the form f==g to f-g, | ||
| and adjust contours ot [0] and disable background | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ot? is to? Or something else?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
| """ | ||
| for i, f in enumerate(plot_options.functions): | ||
| if hasattr(f, "head") and f.head == SymbolEqual: | ||
| f = Expression(SymbolSubtract, *f.elements[0:2]) | ||
| plot_options.functions[i] = f | ||
| plot_options.contours = [0] | ||
| plot_options.background = False | ||
|
|
||
|
|
||
| def choose_contour_levels(plot_options, vmin, vmax, default): | ||
| levels = plot_options.contours | ||
| 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 = default | ||
| if isinstance(levels, int): | ||
| # computed contour levels have equal distance between them, | ||
| # and half that between first/last contours and vmin/vmax | ||
| dv = (vmax - vmin) / levels | ||
| levels = vmin + np.arange(levels) * dv + dv / 2 | ||
| return levels | ||
|
|
||
|
|
||
| @Timer("eval_ContourPlot") | ||
| def eval_ContourPlot( | ||
| plot_options, | ||
|
|
@@ -230,21 +258,14 @@ def eval_ContourPlot( | |
| import skimage.measure | ||
|
|
||
| # whether to show a background similar to density plot, except quantized | ||
| background = len(plot_options.functions) == 1 | ||
| contour_levels = plot_options.contours | ||
| plot_options.background = len(plot_options.functions) == 1 | ||
|
|
||
| # 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 hasattr(f, "head") and f.head == SymbolEqual: | ||
| f = Expression(SymbolSubtract, *f.elements[0:2]) | ||
| plot_options.functions[i] = f | ||
| contour_levels = [0] | ||
| background = False | ||
| # adjust functions, contours, and background for functions of the form f==g | ||
| equations_to_contours(plot_options) | ||
|
|
||
| def emit(graphics, i, xyzs, _, quads): | ||
| # set line color | ||
| if background: | ||
| if plot_options.background: | ||
| # showing a background, so just black lines | ||
| color_directive = [SymbolRGBColor, 0, 0, 0] | ||
| else: | ||
|
|
@@ -259,17 +280,8 @@ def emit(graphics, i, xyzs, _, quads): | |
| zs = xyzs[:, 2] # this is a linear list matching with quads | ||
|
|
||
| # process contour_levels | ||
| levels = contour_levels | ||
| 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 | ||
| levels = choose_contour_levels(plot_options, zmin, zmax, default=9) | ||
|
|
||
| # one contour line per contour level | ||
| for level in levels: | ||
|
|
@@ -285,7 +297,7 @@ def emit(graphics, i, xyzs, _, quads): | |
| graphics.add_linexyzs(segment) | ||
|
|
||
| # background is solid colors between contour lines | ||
| if background: | ||
| if plot_options.background: | ||
| with Timer("contour background"): | ||
| # use masks and fancy indexing to assign (lo+hi)/2 to all zs between lo and hi | ||
| zs[zs <= levels[0]] = zmin | ||
|
|
@@ -301,6 +313,81 @@ def emit(graphics, i, xyzs, _, quads): | |
| return make_surfaces(plot_options, evaluation, dim=2, is_complex=False, emit=emit) | ||
|
|
||
|
|
||
| @Timer("eval_ContourPlot3D") | ||
| def eval_ContourPlot3D( | ||
| plot_options, | ||
| evaluation: Evaluation, | ||
| ): | ||
| import skimage.measure | ||
|
|
||
| # adjust functions, contours, and background for functions of the form f==g | ||
| equations_to_contours(plot_options) | ||
|
|
||
| # pull out options | ||
| _, xmin, xmax = plot_options.ranges[0] | ||
| _, ymin, ymax = plot_options.ranges[1] | ||
| _, zmin, zmax = plot_options.ranges[2] | ||
| nx, ny, nz = plot_options.plot_points | ||
| names = [strip_context(str(r[0])) for r in plot_options.ranges] | ||
|
|
||
| # compile the functions | ||
| with Timer("compile"): | ||
| compiled_functions = compile_exprs(evaluation, plot_options.functions, names) | ||
|
|
||
| # construct (nx,ny,nz) grid | ||
| xs = np.linspace(xmin, xmax, nx) | ||
| ys = np.linspace(ymin, ymax, ny) | ||
| zs = np.linspace(zmin, zmax, nz) | ||
| xs, ys, zs = np.meshgrid(xs, ys, zs) | ||
|
|
||
| # just one function | ||
| function = compiled_functions[0] | ||
|
|
||
| # compute function values fs over the grid | ||
| with Timer("compute fs"): | ||
| fs = function(**{n: v for n, v in zip(names, [xs, ys, zs])}) | ||
|
|
||
| # process contour_levels | ||
| fmin, fmax = np.min(fs), np.max(fs) | ||
| levels = choose_contour_levels(plot_options, fmin, fmax, default=7) | ||
|
|
||
| # find contour for each level and emit it | ||
| graphics = GraphicsGenerator(dim=3) | ||
| for i, level in enumerate(levels): | ||
| color_directive = palette_color_directive(palette3, i) | ||
| graphics.add_directives(color_directive) | ||
|
|
||
| # find contour for this level | ||
| with Timer("3d contours"): | ||
| verts, faces, normals, values = skimage.measure.marching_cubes(fs, level) | ||
| verts[:, (0, 1)] = verts[:, (1, 0)] # skimage bug? | ||
|
|
||
| # marching_cubes gives back coordinates relative to grid unit, so rescale to x, y, z | ||
| offset = [xmin, ymin, zmin] | ||
| scale = [ | ||
| (xmax - xmin) / (nx - 1), | ||
| (ymax - ymin) / (ny - 1), | ||
| (zmax - zmin) / (nz - 1), | ||
| ] | ||
| verts = np.array(offset) + verts * np.array(scale) | ||
|
|
||
| # WL is 1-based | ||
| faces += 1 | ||
|
|
||
| # emit faces as GraphicsComplex | ||
| graphics.add_complex(verts, lines=None, polys=faces, colors=None) | ||
|
|
||
| # emit mesh as GraphicsComplex | ||
| # TODO: this should share vertices with previous GraphicsComplex | ||
| if plot_options.mesh is SymbolFull: | ||
| # TODO: each segment emitted twice - is there reasonable way to fix? | ||
| lines = np.array([faces[:, [0, 1]], faces[:, [1, 2]], faces[:, [2, 0]]]) | ||
| graphics.add_directives([SymbolRGBColor, 0, 0, 0]) | ||
| graphics.add_complex(verts, lines=lines, polys=None, colors=None) | ||
|
|
||
| return graphics | ||
|
|
||
|
|
||
| @Timer("complex colors") | ||
| def complex_colors(zs, s=None): | ||
| # hue depends on phase | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.