|
1 | 1 | # -*- coding: utf-8 -*-
|
2 | 2 | from collections import OrderedDict, Iterable
|
| 3 | +import functools |
3 | 4 | import heapq
|
4 | 5 | import itertools
|
5 | 6 | import random
|
@@ -470,6 +471,10 @@ def remove_unfinished(self):
|
470 | 471 | self._subtriangulations = dict()
|
471 | 472 | self._pending_to_simplex = dict()
|
472 | 473 |
|
| 474 | + ########################## |
| 475 | + # Plotting related stuff # |
| 476 | + ########################## |
| 477 | + |
473 | 478 | def plot(self, n=None, tri_alpha=0):
|
474 | 479 | """Plot the function we want to learn, only works in 2D.
|
475 | 480 |
|
@@ -588,14 +593,17 @@ def plot_slice(self, cut_mapping, n=None):
|
588 | 593 | def plot_3D(self, with_triangulation=False):
|
589 | 594 | """Plot the learner's data in 3D using plotly.
|
590 | 595 |
|
| 596 | + Does *not* work with the |
| 597 | + `adaptive.notebook_integration.live_plot` functionality. |
| 598 | +
|
591 | 599 | Parameters
|
592 | 600 | ----------
|
593 | 601 | with_triangulation : bool, default: False
|
594 | 602 | Add the verticices to the plot.
|
595 | 603 |
|
596 | 604 | Returns
|
597 | 605 | -------
|
598 |
| - plot : plotly.offline.iplot object |
| 606 | + plot : `plotly.offline.iplot` object |
599 | 607 | The 3D plot of ``learner.data``.
|
600 | 608 | """
|
601 | 609 | plotly = ensure_plotly()
|
@@ -653,3 +661,182 @@ def _get_data(self):
|
653 | 661 |
|
654 | 662 | def _set_data(self, data):
|
655 | 663 | self.tell_many(*zip(*data.items()))
|
| 664 | + |
| 665 | + def _get_iso(self, level=0.0, which='surface'): |
| 666 | + if which == 'surface': |
| 667 | + if self.ndim != 3 or self.vdim != 1: |
| 668 | + raise Exception('Isosurface plotting is only supported' |
| 669 | + ' for a 3D input and 1D output') |
| 670 | + get_surface = True |
| 671 | + get_line = False |
| 672 | + elif which == 'line': |
| 673 | + if self.ndim != 2 or self.vdim != 1: |
| 674 | + raise Exception('Isoline plotting is only supported' |
| 675 | + ' for a 2D input and 1D output') |
| 676 | + get_surface = False |
| 677 | + get_line = True |
| 678 | + |
| 679 | + vertices = [] # index -> (x,y,z) |
| 680 | + faces_or_lines = [] # tuple of indices of the corner points |
| 681 | + |
| 682 | + @functools.lru_cache() |
| 683 | + def _get_vertex_index(a, b): |
| 684 | + vertex_a = self.tri.vertices[a] |
| 685 | + vertex_b = self.tri.vertices[b] |
| 686 | + value_a = self.data[vertex_a] |
| 687 | + value_b = self.data[vertex_b] |
| 688 | + |
| 689 | + da = abs(value_a - level) |
| 690 | + db = abs(value_b - level) |
| 691 | + dab = da + db |
| 692 | + |
| 693 | + new_pt = (db / dab * np.array(vertex_a) |
| 694 | + + da / dab * np.array(vertex_b)) |
| 695 | + |
| 696 | + new_index = len(vertices) |
| 697 | + vertices.append(new_pt) |
| 698 | + return new_index |
| 699 | + |
| 700 | + for simplex in self.tri.simplices: |
| 701 | + plane_or_line = [] |
| 702 | + for a, b in itertools.combinations(simplex, 2): |
| 703 | + va = self.data[self.tri.vertices[a]] |
| 704 | + vb = self.data[self.tri.vertices[b]] |
| 705 | + if min(va, vb) < level <= max(va, vb): |
| 706 | + vi = _get_vertex_index(a, b) |
| 707 | + should_add = True |
| 708 | + for pi in plane_or_line: |
| 709 | + if np.allclose(vertices[vi], vertices[pi]): |
| 710 | + should_add = False |
| 711 | + if should_add: |
| 712 | + plane_or_line.append(vi) |
| 713 | + |
| 714 | + if get_surface and len(plane_or_line) == 3: |
| 715 | + faces_or_lines.append(plane_or_line) |
| 716 | + elif get_surface and len(plane_or_line) == 4: |
| 717 | + faces_or_lines.append(plane_or_line[:3]) |
| 718 | + faces_or_lines.append(plane_or_line[1:]) |
| 719 | + elif get_line and len(plane_or_line) == 2: |
| 720 | + faces_or_lines.append(plane_or_line) |
| 721 | + |
| 722 | + if len(faces_or_lines) == 0: |
| 723 | + r_min = min(self.data[v] for v in self.tri.vertices) |
| 724 | + r_max = max(self.data[v] for v in self.tri.vertices) |
| 725 | + |
| 726 | + raise ValueError( |
| 727 | + f"Could not draw isosurface for level={level}, as" |
| 728 | + " this value is not inside the function range. Please choose" |
| 729 | + f" a level strictly inside interval ({r_min}, {r_max})" |
| 730 | + ) |
| 731 | + |
| 732 | + return vertices, faces_or_lines |
| 733 | + |
| 734 | + def plot_isoline(self, level=0.0, n=None, tri_alpha=0): |
| 735 | + """Plot the isoline at a specific level, only works in 2D. |
| 736 | +
|
| 737 | + Parameters |
| 738 | + ---------- |
| 739 | + level : float, default: 0 |
| 740 | + The value of the function at which you would like to see |
| 741 | + the isoline. |
| 742 | + n : int |
| 743 | + The number of boxes in the interpolation grid along each axis. |
| 744 | + This is passed to `plot`. |
| 745 | + tri_alpha : float |
| 746 | + The opacity of the overlaying triangulation. This is passed |
| 747 | + to `plot`. |
| 748 | +
|
| 749 | + Returns |
| 750 | + ------- |
| 751 | + `holoviews.core.Overlay` |
| 752 | + The plot of the isoline(s). This overlays a `plot` with a |
| 753 | + `holoviews.element.Path`. |
| 754 | + """ |
| 755 | + hv = ensure_holoviews() |
| 756 | + if n == -1: |
| 757 | + plot = hv.Path([]) |
| 758 | + else: |
| 759 | + plot = self.plot(n=n, tri_alpha=tri_alpha) |
| 760 | + |
| 761 | + if isinstance(level, Iterable): |
| 762 | + for l in level: |
| 763 | + plot = plot * self.plot_isoline(level=l, n=-1) |
| 764 | + return plot |
| 765 | + |
| 766 | + vertices, lines = self.self._get_iso(level, which='line') |
| 767 | + paths = [[vertices[i], vertices[j]] for i, j in lines] |
| 768 | + contour = hv.Path(paths) |
| 769 | + |
| 770 | + contour_opts = dict(color='black') |
| 771 | + contour = contour.opts(style=contour_opts) |
| 772 | + return plot * contour |
| 773 | + |
| 774 | + def plot_isosurface(self, level=0.0, hull_opacity=0.2): |
| 775 | + """Plots a linearly interpolated isosurface. |
| 776 | +
|
| 777 | + This is the 3D analog of an isoline. Does *not* work with the |
| 778 | + `adaptive.notebook_integration.live_plot` functionality. |
| 779 | +
|
| 780 | + Parameters |
| 781 | + ---------- |
| 782 | + level : float, default: 0.0 |
| 783 | + the function value which you are interested in. |
| 784 | + hull_opacity : float, default: 0.0 |
| 785 | + the opacity of the hull of the domain. |
| 786 | +
|
| 787 | + Returns |
| 788 | + ------- |
| 789 | + plot : `plotly.offline.iplot` object |
| 790 | + The plot object of the isosurface. |
| 791 | + """ |
| 792 | + plotly = ensure_plotly() |
| 793 | + |
| 794 | + vertices, faces = self._get_iso(level, which='surface') |
| 795 | + x, y, z = zip(*vertices) |
| 796 | + |
| 797 | + fig = plotly.figure_factory.create_trisurf( |
| 798 | + x=x, y=y, z=z, plot_edges=False, |
| 799 | + simplices=faces, title="Isosurface") |
| 800 | + isosurface = fig.data[0] |
| 801 | + isosurface.update(lighting=dict(ambient=1, diffuse=1, |
| 802 | + roughness=1, specular=0, fresnel=0)) |
| 803 | + |
| 804 | + if hull_opacity < 1e-3: |
| 805 | + # Do not compute the hull_mesh. |
| 806 | + return plotly.offline.iplot(fig) |
| 807 | + |
| 808 | + hull_mesh = self._get_hull_mesh(opacity=hull_opacity) |
| 809 | + return plotly.offline.iplot([isosurface, hull_mesh]) |
| 810 | + |
| 811 | + def _get_hull_mesh(self, opacity=0.2): |
| 812 | + plotly = ensure_plotly() |
| 813 | + hull = scipy.spatial.ConvexHull(self._bounds_points) |
| 814 | + |
| 815 | + # Find the colors of each plane, giving triangles which are coplanar |
| 816 | + # the same color, such that a square face has the same color. |
| 817 | + color_dict = {} |
| 818 | + |
| 819 | + def _get_plane_color(simplex): |
| 820 | + simplex = tuple(simplex) |
| 821 | + # If the volume of the two triangles combined is zero then they |
| 822 | + # belong to the same plane. |
| 823 | + for simplex_key, color in color_dict.items(): |
| 824 | + points = [hull.points[i] for i in set(simplex_key + simplex)] |
| 825 | + points = np.array(points) |
| 826 | + if np.linalg.matrix_rank(points[1:] - points[0]) < 3: |
| 827 | + return color |
| 828 | + if scipy.spatial.ConvexHull(points).volume < 1e-5: |
| 829 | + return color |
| 830 | + color_dict[simplex] = tuple(random.randint(0, 255) |
| 831 | + for _ in range(3)) |
| 832 | + return color_dict[simplex] |
| 833 | + |
| 834 | + colors = [_get_plane_color(simplex) for simplex in hull.simplices] |
| 835 | + |
| 836 | + x, y, z = zip(*self._bounds_points) |
| 837 | + i, j, k = hull.simplices.T |
| 838 | + lighting = dict(ambient=1, diffuse=1, roughness=1, |
| 839 | + specular=0, fresnel=0) |
| 840 | + return plotly.graph_objs.Mesh3d(x=x, y=y, z=z, i=i, j=j, k=k, |
| 841 | + facecolor=colors, opacity=opacity, |
| 842 | + lighting=lighting) |
0 commit comments