|  | import numpy | 
        
          |  | from scipy.spatial import ConvexHull | 
        
          |  | import matplotlib.pyplot as plot | 
        
          |  | import matplotlib.patches as patches | 
        
          |  |  | 
        
          |  |  | 
        
          |  |  | 
        
          |  | # --- Sampling routines ------------------------------------------------------- | 
        
          |  |  | 
        
          |  | ''' | 
        
          |  | Pick N points uniformly from the unit disc | 
        
          |  | This sampling algorithm does not use rejection sampling. | 
        
          |  | ''' | 
        
          |  | def disc_uniform_pick(N): | 
        
          |  | angle = (2 * numpy.pi) * numpy.random.random(N) | 
        
          |  | out = numpy.stack([numpy.cos(angle), numpy.sin(angle)], axis = 1) | 
        
          |  | out *= numpy.sqrt(numpy.random.random(N))[:,None] | 
        
          |  | return out | 
        
          |  |  | 
        
          |  |  | 
        
          |  |  | 
        
          |  | def enumerate_ring_consecutive_pairs(A): | 
        
          |  | for item in zip(A[:-1], A[1:]): | 
        
          |  | yield item | 
        
          |  | yield (A[-1], A[0]) | 
        
          |  |  | 
        
          |  |  | 
        
          |  |  | 
        
          |  | # --- Display ----------------------------------------------------------------- | 
        
          |  |  | 
        
          |  | def arc_patch(theta1, theta2, ax=None, resolution=50, **kwargs): | 
        
          |  | ax = plot.gca() | 
        
          |  | theta = numpy.linspace(numpy.radians(theta1), numpy.radians(theta2), resolution) | 
        
          |  | points = numpy.vstack((numpy.cos(theta), numpy.sin(theta))) | 
        
          |  | return patches.Polygon(points.T, closed=True, **kwargs) | 
        
          |  |  | 
        
          |  |  | 
        
          |  |  | 
        
          |  | def display(XY, theta, V, arc_list): | 
        
          |  | fig = plot.figure() | 
        
          |  | plot.axis('equal') | 
        
          |  | plot.axis('off') | 
        
          |  |  | 
        
          |  | # Display input segments | 
        
          |  | for XY_i, theta_i in zip(XY, theta): | 
        
          |  | if theta_i != 0: | 
        
          |  | alpha = numpy.arctan2(XY_i[1], XY_i[0]) | 
        
          |  | plot.gca().add_patch(arc_patch(numpy.degrees(alpha - .5 * theta_i), numpy.degrees(alpha + .5 * theta_i), zorder = 0, ec = '#f08080', fc = '#f0c0c0')) | 
        
          |  |  | 
        
          |  | cos_theta_i, sin_theta_i = numpy.cos(.5 * theta_i), numpy.sin(.5 * theta_i) | 
        
          |  | plot.plot([ cos_theta_i * XY_i[0] - sin_theta_i * XY_i[1],  cos_theta_i * XY_i[0] + sin_theta_i * XY_i[1]], | 
        
          |  | [ sin_theta_i * XY_i[0] + cos_theta_i * XY_i[1], -sin_theta_i * XY_i[0] + cos_theta_i * XY_i[1]], | 
        
          |  | c = '#f08080') | 
        
          |  | # Display arcs | 
        
          |  | for A, B in arc_list: | 
        
          |  | plot.gca().add_patch(patches.Arc([0, 0], 2, 2, theta1 = numpy.degrees(numpy.arctan2(A[1], A[0])), theta2 = numpy.degrees(numpy.arctan2(B[1], B[0])), ls = '--', ec = '#8080f0', lw = 2)) | 
        
          |  |  | 
        
          |  | arc_list = numpy.reshape(numpy.roll(numpy.reshape(arc_list, (2 * arc_list.shape[0], 2)), -1, axis = 0), (arc_list.shape[0], 2, 2)) | 
        
          |  |  | 
        
          |  | for A, B in arc_list: | 
        
          |  | plot.gca().add_patch(patches.Arc([0, 0], 2, 2, theta1 = numpy.degrees(numpy.arctan2(A[1], A[0])), theta2 = numpy.degrees(numpy.arctan2(B[1], B[0])), ec = 'k', lw = 2, fc = '#f08080')) | 
        
          |  |  | 
        
          |  | # Show all | 
        
          |  | plot.show() | 
        
          |  |  | 
        
          |  |  | 
        
          |  |  | 
        
          |  | # --- Circular Laguerre-Voronoi diagram --------------------------------------- | 
        
          |  |  | 
        
          |  | def circle_line_intersection(line_eqns): | 
        
          |  | half_sqrt_2 = .5 * 2 ** .5 | 
        
          |  | rot_pi_4 = half_sqrt_2 * numpy.array([[1, 1], [-1, 1]]) | 
        
          |  |  | 
        
          |  | # Unit equilateral triangle coordinates | 
        
          |  | sqrt_3 = 3 ** .5 | 
        
          |  | equilateral_tri_coords = numpy.array([ | 
        
          |  | [  0.,  1.], | 
        
          |  | [ -.5 * sqrt_3, -.5], | 
        
          |  | [  .5 * sqrt_3, -.5]]) | 
        
          |  |  | 
        
          |  | # Normalize the input | 
        
          |  | XY_norm = numpy.sum(line_eqns[:,:2] ** 2, axis = 1) ** .5 | 
        
          |  | XY, Z = line_eqns[:,:2] / XY_norm[:,None], -line_eqns[:,2] / XY_norm | 
        
          |  |  | 
        
          |  | # Break down circular sectors with angle > pi into pi / 2 sectors as | 
        
          |  | # it allow to keep the algorithm simple yet optimal | 
        
          |  | XYp, Zp = [], [] | 
        
          |  | for XY_i, Z_i in zip(XY, Z): | 
        
          |  | if Z_i > half_sqrt_2: | 
        
          |  | XYp.append(XY_i) | 
        
          |  | Zp.append(Z_i) | 
        
          |  | else: | 
        
          |  | cos_theta_i, sin_theta_i = Z_i, (1. - Z_i ** 2) ** .5 | 
        
          |  | M = numpy.array([[cos_theta_i, sin_theta_i], [-sin_theta_i, cos_theta_i]]) | 
        
          |  |  | 
        
          |  | if Z_i <= half_sqrt_2 and Z_i > 0: | 
        
          |  | M = numpy.dot(rot_pi_4, M.T) | 
        
          |  | XYp.extend([numpy.dot(XY_i, M), numpy.dot(XY_i, M.T)]) | 
        
          |  | Zp.extend([half_sqrt_2, half_sqrt_2]) | 
        
          |  | elif Z_i <= 0 and Z_i > -half_sqrt_2: | 
        
          |  | M = numpy.dot(rot_pi_4, M.T) | 
        
          |  | XYp.extend([numpy.dot(XY_i, M), numpy.dot(XY_i, M.T), XY_i]) | 
        
          |  | Zp.extend([half_sqrt_2, half_sqrt_2, half_sqrt_2]) | 
        
          |  | else: | 
        
          |  | Ma = numpy.dot(rot_pi_4.T, M) | 
        
          |  | XYp.extend([numpy.dot(XY_i, Ma), numpy.dot(XY_i, Ma.T)]) | 
        
          |  | Mb = numpy.dot(rot_pi_4, M) | 
        
          |  | XYp.extend([numpy.dot(-XY_i, Mb), numpy.dot(-XY_i, Mb.T)]) | 
        
          |  | Zp.extend([half_sqrt_2, half_sqrt_2, half_sqrt_2, half_sqrt_2]) | 
        
          |  |  | 
        
          |  | XYp, Zp = numpy.array(XYp), numpy.array(Zp) | 
        
          |  |  | 
        
          |  | # Augment the data with equilateral triangle coordinates, to ensure that | 
        
          |  | # we have a convex hull even with an empty input | 
        
          |  | XYp = numpy.concatenate([XYp, equilateral_tri_coords], axis = 0) | 
        
          |  | Zp = numpy.concatenate([Zp, numpy.ones(3)], axis = 0) | 
        
          |  |  | 
        
          |  | # Generate the Laguerre-Delaunay diagram | 
        
          |  | delaunay_hull = ConvexHull(XYp / Zp[:,None]).vertices | 
        
          |  |  | 
        
          |  | # Compute Laguerre-Voronoi vertices | 
        
          |  | voronoi_vertices = numpy.empty((len(delaunay_hull), 2)) | 
        
          |  | for i, (j, k) in enumerate(enumerate_ring_consecutive_pairs(delaunay_hull)): | 
        
          |  | voronoi_vertices[i] = numpy.linalg.solve(XYp[[j, k]], Zp[[j, k]]) | 
        
          |  |  | 
        
          |  | # Compute arcs | 
        
          |  | voronoi_vertices_norm = numpy.sqrt(numpy.sum(voronoi_vertices ** 2, axis = 1)) | 
        
          |  | V = voronoi_vertices / voronoi_vertices_norm[:,None] | 
        
          |  |  | 
        
          |  | arc_list = [] | 
        
          |  | for i, j in enumerate(delaunay_hull): | 
        
          |  | cos_theta_j, sin_theta_j = Zp[j], (1 - Zp[j] ** 2) ** .5 | 
        
          |  | A = [cos_theta_j * XYp[j, 0] + sin_theta_j * XYp[j, 1], -sin_theta_j * XYp[j, 0] + cos_theta_j * XYp[j, 1]] | 
        
          |  | B = [cos_theta_j * XYp[j, 0] - sin_theta_j * XYp[j, 1],  sin_theta_j * XYp[j, 0] + cos_theta_j * XYp[j, 1]] | 
        
          |  |  | 
        
          |  | if voronoi_vertices_norm[i-1] >= 1: | 
        
          |  | if voronoi_vertices_norm[i] >= 1: | 
        
          |  | if numpy.cross(V[i] - cos_theta_j * XYp[j], XYp[j]) <= -sin_theta_j and numpy.cross(V[i-1] - cos_theta_j * XYp[j], XYp[j]) >= sin_theta_j: | 
        
          |  | arc_list.append([(A, False), (B, False)]) | 
        
          |  | else: | 
        
          |  | arc_list.append([(A, False), (V[i], True)]) | 
        
          |  | else: | 
        
          |  | if voronoi_vertices_norm[i] >= 1: | 
        
          |  | arc_list.append([(V[i-1], True), (B, False)]) | 
        
          |  | else: | 
        
          |  | arc_list.append([(V[i-1], True), (V[i], True)]) | 
        
          |  |  | 
        
          |  | if len(arc_list) >= 2: | 
        
          |  | if arc_list[-2][1][1] and arc_list[-1][0][1]: | 
        
          |  | arc_list[-2][1] = arc_list[-1][1] | 
        
          |  | arc_list.pop() | 
        
          |  |  | 
        
          |  | if arc_list[-1][1][1] and arc_list[0][0][1]: | 
        
          |  | arc_list[0][0] = arc_list[-1][0] | 
        
          |  | arc_list.pop() | 
        
          |  |  | 
        
          |  | arc_list = numpy.array([(A, B) for (A, a), (B, b) in arc_list if a == False and b == False and numpy.fabs(numpy.dot(A, B) - 1) > 1e-7]) | 
        
          |  |  | 
        
          |  | # Job done | 
        
          |  | display(XY, 2 * numpy.arccos(Z), voronoi_vertices, arc_list) | 
        
          |  |  | 
        
          |  |  | 
        
          |  |  | 
        
          |  | # --- Main entry point -------------------------------------------------------- | 
        
          |  |  | 
        
          |  | def get_random_dataset(sample_count, radius): | 
        
          |  | XY = disc_uniform_pick(sample_count) | 
        
          |  | XY_norm = numpy.sqrt(numpy.sum(XY ** 2, axis = 1)) | 
        
          |  | XY /= XY_norm[:,None] | 
        
          |  | theta = 2 * numpy.arccos(XY_norm) * radius | 
        
          |  | return XY, theta | 
        
          |  |  | 
        
          |  |  | 
        
          |  |  | 
        
          |  | XY, theta = get_random_dataset(4, numpy.pi / 2) | 
        
          |  | circle_line_intersection(numpy.concatenate([XY, -numpy.cos(.5 * theta)[:,None]], axis = 1)) |