Skip to content

Commit 49ad3c6

Browse files
jhoofwijkbasnijholt
authored andcommitted
add 'plot_isosurface' and 'plot_isoline' to adaptive learnerND
1 parent 3aba9d4 commit 49ad3c6

File tree

1 file changed

+188
-1
lines changed

1 file changed

+188
-1
lines changed

adaptive/learner/learnerND.py

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
from collections import OrderedDict, Iterable
3+
import functools
34
import heapq
45
import itertools
56
import random
@@ -470,6 +471,10 @@ def remove_unfinished(self):
470471
self._subtriangulations = dict()
471472
self._pending_to_simplex = dict()
472473

474+
##########################
475+
# Plotting related stuff #
476+
##########################
477+
473478
def plot(self, n=None, tri_alpha=0):
474479
"""Plot the function we want to learn, only works in 2D.
475480
@@ -588,14 +593,17 @@ def plot_slice(self, cut_mapping, n=None):
588593
def plot_3D(self, with_triangulation=False):
589594
"""Plot the learner's data in 3D using plotly.
590595
596+
Does *not* work with the
597+
`adaptive.notebook_integration.live_plot` functionality.
598+
591599
Parameters
592600
----------
593601
with_triangulation : bool, default: False
594602
Add the verticices to the plot.
595603
596604
Returns
597605
-------
598-
plot : plotly.offline.iplot object
606+
plot : `plotly.offline.iplot` object
599607
The 3D plot of ``learner.data``.
600608
"""
601609
plotly = ensure_plotly()
@@ -653,3 +661,182 @@ def _get_data(self):
653661

654662
def _set_data(self, data):
655663
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

Comments
 (0)