def hadrian_min(vectorized_f, xbnds, ybnds, xtol, ytol, swarm=8, mx_iters=5, inc=False): """ hadrian_min is a stochastic, hill climbing minimization algorithm. It uses a stratified sampling technique (Latin Hypercube) to get good coverage of potential new points. It also uses vectorized function evaluations to drive concurrent function evaluations. It is named after the Roman Emperor Hadrian, the most famous Latin hill mountain climber of ancient times. """ assert xbnds[1] > xbnds[0] assert ybnds[1] > ybnds[0] assert xtol > 0 assert ytol > 0 # simplexes are simplex indexes # vertexes are vertex indexes # points are spatial coordinates bnds = np.vstack((xbnds, ybnds)) points = latin_sample(np.vstack((xbnds, ybnds)), swarm) z = vectorized_f(points) # exclude corners from possibilities, but add them to the triangulation # this bounds the domain, but ensures they don't get picked points = np.append(points, list(product(xbnds, ybnds)), axis=0) z = np.append(z, 4*[z.max()]) tri = Delaunay(points, incremental=inc) del points for step in range(1, mx_iters+1): i, vertexes = get_minimum_neighbors(tri, z) disp = tri.points[i] - tri.points[np.unique(vertexes)] disp /= np.array([xtol, ytol]) err = err_mean(disp) if err < 1.: return tri.points[i], z[i], tri.points, z, 1 tri_points = tri.points[vertexes] bnds = np.cumsum(area(tri_points)) bnds /= bnds[-1] indx = np.searchsorted(bnds, np.random.rand(swarm)) new_pts = sample_triangle(tri_points[indx]) new_z = vectorized_f(new_pts) if inc: tri.add_points(new_pts) else: # make a new triangulation if I can't append points points = np.append(tri.points, new_pts, axis=0) tri = Delaunay(points) z = np.append(z, new_z) return None, None, tri.points, z, step
def run(self): self.uniform_refine_boundary(n=5) node = self.mesh.entity('node') tri = Delaunay(node, incremental=True) fnode = self.advance() tri.add_points(fnode) return tri, fnode
class DelaunayChooser: def __init__(self, points): self.points = points self.tri = Delaunay(self.points, incremental=True) self.data = Data(5) def add_points(self, pnt): self.points = np.append(self.points, pnt, axis=0) self.tri.add_points(pnt) self.update_data() def next_point(self, eps): if (random.random() < eps): loc = np.where(self.data.areas == np.amax(self.data.areas)) r = [0] if (len(loc[0]) > 1): r = np.random.randint(0, len(loc[0]) - 1, 1) return (self.points[loc[0][r]] + self.points[loc[1][r]] + self.points[loc[2][r]]) / 3 else: loc = np.where(self.data.lengths == np.amax(self.data.lengths)) r = [0] if (len(loc[0]) > 1): r = np.random.randint(0, len(loc[0]) - 1, 1) return (self.points[loc[0][r]] + self.points[loc[1][r]]) / 2 def update_data(self): if (len(self.points) >= self.data.size): self.data.grow(2 * len(self.points)) else: self.data.grow(len(self.points)) for s in self.tri.simplices: a = self.length(s[0], s[1]) b = self.length(s[1], s[2]) c = self.length(s[0], s[2]) self.data.lengths[s[0]][s[1]] = a self.data.lengths[s[1]][s[2]] = b self.data.lengths[s[0]][s[2]] = c self.data.areas[s[0]][s[1]][ s[2]] = (a + b + c) * (-a + b + c) * (a - b + c) * (a + b - c) #print(self.data.areas[s[0]][s[1]][s[2]]) def length(self, p1, p2): return np.sqrt(np.sum((self.points[p1] - self.points[p2])**2)) def plot(self, ax): ax.triplot(self.points[:, 0], self.points[:, 1], self.tri.simplices.copy())
def in_hull(p, hull, extendy=False): """ Test if points in `p` are in `hull` `p` should be a `NxK` coordinates of `N` points in `K` dimensions `hull` is either a scipy.spatial.Delaunay object or the `MxK` array of the coordinates of `M` points in `K`dimensions for which Delaunay triangulation will be computed """ from scipy.spatial import Delaunay if not isinstance(hull, Delaunay): hull = Delaunay(hull, incremental=True) if extendy: pts = hull.points minx = np.min(pts[:, 0]) maxx = np.max(pts[:, 0]) new_pts = [[minx, 0], [maxx, 0]] hull.add_points(new_pts) return hull.find_simplex(p) >= 0
class plasma_mesh(object): def __init__(self, Lx=0., Ly=0., N_init=0, config=0, q_0=0.): self.Lx = Lx self.Ly = Ly self.N = N_init self.Grid = False #boundary conditions if config == BC_config.closed: self.q_bc = 0 elif config == BC_config.constant_flux: self.q_bc = q_0 else: raise Exception('Not done yet') def create_grid(self, config=0): if config == Bfield_config.uniform: #create uniform grid of points, then triangulate nrows = int(np.sqrt(self.N) * float(self.Ly) / float(self.Lx)) ncols = int(np.sqrt(self.N) * float(self.Lx) / float(self.Ly)) ax = float(self.Lx) / float(ncols) ay = float(self.Ly) / float(nrows) for row, col in itool.product(range(nrows + 1), range(ncols + 1)): if row == 0 and col == 0: self.nodes = np.array([[0, 0]]) else: self.nodes = np.append(self.nodes, [[ax * col, ay * row]], axis=0) self.Grid = Delaunay( self.nodes, incremental=True) #allow incrementing to add points self.N = np.shape( self.nodes)[0] #fix N to match actual number of vertices else: raise Exception('Define a vaild magnetic configuration') def create_cell_values(self, config=0, T_0=0., n_0=0): self.ncells = np.shape(self.Grid.simplices)[0] if config == profile_config.random: #random starting T and n profile self.T = list(T_0 * np.random.random(self.ncells)) self.n = list(T_0 * np.random.random(self.ncells)) elif config == T_config.maxwellian: #2D maxwellian starting profile w/ randomization raise Exception('I\'m not done with this yet') else: raise Exception('Define a valid temperature initialization') # def conduction_radiation(self,grad_Tx,grad_Ty,radiation_func): def cell_solver(self): q_checker = np.zeros((3, self.ncells)) for cell in range(self.ncells + 1): q_nn = [] for neighbor in range(3): if self.Grid.neighbors[cell][ neighbor] == -1: #if one of the "neighbors" is the boundary q_nn.append(self.q_bc) else: boundary_pts = [] for pts in range(3): if pts != neighbor: boundary_pts.append( self.nodes[self.Grid.simplices[cell][pts]]) def update_grid_points(self): if not self.Grid: raise Exception( 'Grid must be initialized first, use method "create_grid"') self.Grid.add_points([[0.45, 0.05], [0.45, 0.15]]) self.nodes = np.append(self.nodes, [[0.45, 0.05], [0.45, 0.15]], axis=0)
def __init__(self, cat, viz=False, periodic=False, buff=5.): """Initialize tesselation. Parameters ---------- cat : Catalog Catalog of objects used to compute the Voronoi tesselation. viz : bool Compute visualization. periodic : bool Use periodic boundary conditions. buff : float Width of incremental buffer shells for periodic computation. """ coords = cat.coord[cat.nnls == np.arange(len(cat.nnls))] if periodic: print("Triangulating...") Del = Delaunay(coords, incremental=True, qhull_options='QJ') sim = Del.simplices simlen = len(sim) cids = np.arange(len(coords)) print("Finding periodic neighbors...") n = 0 coords2, cids = getBuff(coords, cids, cat.cmin, cat.cmax, buff, n) coords3 = coords.tolist() coords3.extend(coords2) Del.add_points(coords2) sim = Del.simplices while np.amin(sim[simlen:]) < len(coords): n = n + 1 simlen = len(sim) coords2, cids = getBuff(coords, cids, cat.cmin, cat.cmax, buff, n) coords3.extend(coords2) Del.add_points(coords2) sim = Del.simplices for i in range(len(sim)): sim[i] = cids[sim[i]] print("Tesselating...") Vor = Voronoi(coords3) ver = Vor.vertices reg = np.array(Vor.regions)[Vor.point_region] del Vor print("Computing volumes...") vol = np.zeros(len(reg)) cut = np.arange(len(coords)) hul = [] for r in reg[cut]: try: ch = ConvexHull(ver[r]) except: ch = ConvexHull(ver[r], qhull_options='QJ') hul.append(ch) #hul = [ConvexHull(ver[r]) for r in reg[cut]] vol[cut] = np.array([h.volume for h in hul]) self.volumes = vol else: print("Tesselating...") Vor = Voronoi(coords) ver = Vor.vertices reg = np.array(Vor.regions)[Vor.point_region] del Vor ve2 = ver.T vth = np.arctan2(np.sqrt(ve2[0]**2. + ve2[1]**2.), ve2[2]) vph = np.arctan2(ve2[1], ve2[0]) vrh = np.array([np.sqrt((v**2.).sum()) for v in ver]) crh = np.array([np.sqrt((c**2.).sum()) for c in coords]) rmx = np.amax(crh) rmn = np.amin(crh) print("Computing volumes...") vol = np.zeros(len(reg)) cu1 = np.array([-1 not in r for r in reg]) cu2 = np.array([ np.product(np.logical_and(vrh[r] > rmn, vrh[r] < rmx), dtype=bool) for r in reg[cu1] ]).astype(bool) msk = cat.mask nsd = hp.npix2nside(len(msk)) pid = hp.ang2pix(nsd, vth, vph) imk = msk[pid] cu3 = np.array([ np.product(imk[r], dtype=bool) for r in reg[cu1][cu2] ]).astype(bool) cut = np.arange(len(vol)) cut = cut[cu1][cu2][cu3] hul = [] for r in reg[cut]: try: ch = ConvexHull(ver[r]) except: ch = ConvexHull(ver[r], qhull_options='QJ') hul.append(ch) #hul = [ConvexHull(ver[r]) for r in reg[cut]] vol[cut] = np.array([h.volume for h in hul]) self.volumes = vol if viz: self.vertIDs = reg vecut = np.zeros(len(vol), dtype=bool) vecut[cut] = True self.vecut = vecut self.verts = ver print("Triangulating...") Del = Delaunay(coords, qhull_options='QJ') sim = Del.simplices nei = [] lut = [[] for _ in range(len(vol))] print("Consolidating neighbors...") for i in range(len(sim)): for j in sim[i]: lut[j].append(i) for i in range(len(vol)): cut = np.array(lut[i]) nei.append(np.unique(sim[cut])) self.neighbors = np.array(nei)
def create_Delaunay(images_train, reference): centers = np.array(list(map(lambda x: x['center'], images_train)))[:, :, 0] pca = PCA(.95) pca.fit(centers) train_c = pca.transform(centers) #print(reference) ref = np.array([train_c[i] for i in reference]) #ref = [train_c[i] for i in reference] indices = list(range(len(ref))) tri = Delaunay(ref, incremental=True) def orderConvexHull(points, ch): mp = {} for c in ch: if c[0] not in mp: mp[c[0]] = [] if c[1] not in mp: mp[c[1]] = [] mp[c[0]].append(c[1]) mp[c[1]].append(c[0]) #print() ls = [ch[0][0], ch[0][1]] while ls[0] != ls[-1]: two = mp[ls[-1]] if two[0] == ls[-2]: ls.append(two[1]) else: ls.append(two[0]) # Find top point, opengl convention. I.e., max y maxy = -1e10 maxyi = -1 for i, p in enumerate(ls): if points[p][1] > maxy: maxy = points[p][1] maxyi = i if points[ls[(maxyi + 1) % len(ls)]][0] > points[ls[maxyi]][0]: return ls else: return ls[::-1] convexList = orderConvexHull(tri.points, tri.convex_hull) convexList = convexList[:-1] pp = tri.points #print(tri.points) plt.plot(train_c[:, 0], train_c[:, 1], 'o') plt.triplot(pp[:, 0], pp[:, 1], tri.simplices.copy()) plt.plot(pp[:, 0], pp[:, 1], 'rx') plt.savefig('output/xx.png') newpoints = [] def func(v1): #print("v1", v1) v1 = np.array([v1[0], v1[1], 0]) v2 = np.array([0, 0, -1]) v3 = np.cross(v1, v2) #print("v3", v3) v3 = v3 / np.linalg.norm(v3) return v3 for i in range(len(convexList)): p0 = convexList[i] #if indices[p0] == 255: continue pa = convexList[(i + 1) % len(convexList)] pb = convexList[(len(convexList) + i - 1) % len(convexList)] va = func(tri.points[pa] - tri.points[p0]) vb = -func(tri.points[pb] - tri.points[p0]) vs = (va + vb) * 0.5 vs = vs / np.linalg.norm(vs) newpoints.append(tri.points[p0] + vs[:2] * 0.8) indices += [indices[p0]] print(indices) tri.add_points(newpoints) pp = tri.points plt.triplot(pp[:, 0], pp[:, 1], tri.simplices.copy()) plt.plot(pp[:, 0], pp[:, 1], 'o') plt.savefig('output/xxx.png') size = 512 # Create empty black canvas imBary = Image.new('RGB', (size, size)) drawBary = ImageDraw.Draw(imBary) imInd = Image.new('RGB', (size, size)) drawInd = ImageDraw.Draw(imInd) imIndVis = Image.new('RGB', (size, size)) drawIndVis = ImageDraw.Draw(imIndVis) maxcoordinate = np.max(np.abs(tri.points)) scaler = size * 0.5 * 0.98 / maxcoordinate shifter = size / 2 vlist = [] for i in range(size): for j in range(size): vx = i vy = j vx = (vx - shifter) / scaler vy = (shifter - vy) / scaler vlist.append((vx, vy)) ind = tri.find_simplex(vlist) simscaler = int(math.floor(255 / (len(tri.simplices) - 1))) for i in range(size): for j in range(size): val = ind[i * size + j] if val > -1: #print(tri.simplices[val]) ids = [indices[tri.simplices[val][x]] for x in range(3)] #print(ids) drawInd.point([(i, j)], fill=(ids[0], ids[1], ids[2])) drawIndVis.point([(i, j)], fill=(ids[0] * simscaler, ids[1] * simscaler, ids[2] * simscaler)) bary = tri.transform[val, :2, :2] * np.matrix(vlist[ i * size + j] - tri.transform[val, 2, :]).transpose() drawBary.point([(i, j)], fill=(bary[0] * 255, bary[1] * 255, (1 - bary[0] - bary[1]) * 255)) imBary.save('output/bary.png') imInd.save('output/indices.png') imIndVis.save('output/indicesVis.png') exit()
class Delaunay_d(object): def __init__(self, d, iteration): self.datafile = 'go_data_trail' self.file1 = "trail1.gif" self.file2 = "trail2.gif" self.img0 = Image.open(self.file1).convert('L') self.img1 = Image.open(self.file2).convert('L') self.width, self.height = self.img0.size self.globalbestPoint = [] # 0.3922227 , 0.19259966 , 0.23557716 , 0.43344293 self.d, self.iteration = d, iteration self.Mn, self.gn, self.vn, self.bestRho = np.inf, np.inf, np.inf, np.inf self.bestPoint = [] self.vertex_values = defaultdict(list) self.volumes = defaultdict(list) self.vertices = [[0, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 1], [0, 1, 1, 0], [1, 1, 0, 0], [0, 1, 0, 1], [1, 0, 1, 0], [1, 0, 0, 1], [0, 1, 1, 1], [1, 1, 1, 0], [1, 0, 1, 1], [1, 1, 0, 1], [1, 1, 1, 1], [0.3, 0.5, 0.6, 0.9]] self.T = Delaunay(self.vertices, incremental=True, qhull_options='Qt Fv') self.T.coplanar self.simplices = np.array(self.vertices)[self.T.simplices] for vertex in self.vertices: # print self.func(vertex), vertex if self.Mn > self.func(vertex): self.globalbestPoint = vertex self.Mn = min(self.Mn, self.func(vertex)) self.error = [0 for _ in xrange(self.iteration)] def ScaleRotateTranslate(self, image, angle, center=None, new_center=None, scale=None): if center is None: return image.rotate(angle) # angle = angle / 180.0 * math.pi nx, ny = x, y = center sx = sy = 1.0 if new_center: nx, ny = new_center if scale: sx, sy = scale cosine = np.cos(np.deg2rad(angle)) sine = np.sin(np.deg2rad(angle)) a = cosine / sx b = sine / sx c = x-nx*a-ny*b d = -sine / sy e = cosine / sy f = y - nx*d - ny*e return image.transform(image.size, Image.AFFINE, (a,b,c,d,e,f), resample=Image.BICUBIC) def MSE(self, imageA, imageB): return MI.mutual_information(imageA, imageB) def func(self, p): # print tuple(p), self.vertex_values idx = tuple(p) if not self.vertex_values[idx]: x = self.width / 2.0 + (40 * p[0] - 20.0) y = self.height / 2.0 + (40 * p[1] - 20.0) # r = 0.24273422 * 40.0 - 20.0 # s = 0.42331118 * 0.4 + 0.8 r = 0.5 * 40.0 - 20.0 s = 0.5 * 0.4 + 0.8 x0, y0, x1, y1 = math.floor(x), math.floor(y), math.ceil(x), math.ceil(y) # clockwise if x0 == x1 and y0 != y1: v0 = self.MSE(self.img0, self.ScaleRotateTranslate(self.img1, r, (self.width / 2.0, self.height/2.0), (x0, y0), (s, s))) v1 = self.MSE(self.img0, self.ScaleRotateTranslate(self.img1, r, (self.width / 2.0, self.height/2.0), (x0, y1), (s, s))) rs = (y1-y)/(y1-y0)*v0 + (y-y0)/(y1-y0)*v1 elif x0 != x1 and y0 == y1: v0 = self.MSE(self.img0, self.ScaleRotateTranslate(self.img1, r, (self.width / 2.0, self.height/2.0), (x0, y0), (s, s))) v1 = self.MSE(self.img0, self.ScaleRotateTranslate(self.img1, r, (self.width / 2.0, self.height/2.0), (x1, y0), (s, s))) rs = (x1-x)/(x1-x0)*v0 + (x-x0)/(x1-x0)*v1 elif x0 == x1 and y0 == y1: rs = self.MSE(self.img0, self.ScaleRotateTranslate(self.img1, r, (self.width / 2.0, self.height/2.0), (x, y), (s, s))) else: v0 = self.MSE(self.img0, self.ScaleRotateTranslate(self.img1, r, (self.width / 2.0, self.height/2.0), (x0, y0), (s, s))) v1 = self.MSE(self.img0, self.ScaleRotateTranslate(self.img1, r, (self.width / 2.0, self.height/2.0), (x1, y0), (s, s))) v2 = self.MSE(self.img0, self.ScaleRotateTranslate(self.img1, r, (self.width / 2.0, self.height/2.0), (x0, y1), (s, s))) v3 = self.MSE(self.img0, self.ScaleRotateTranslate(self.img1, r, (self.width / 2.0, self.height/2.0), (x1, y1), (s, s))) l1 = (x1-x)/(x1-x0)*v0 + (x-x0)/(x1-x0)*v1 l2 = (x1-x)/(x1-x0)*v2 + (x-x0)/(x1-x0)*v3 rs = (y1-y)/(y1-y0)*l1 + (y-y0)/(y1-y0)*l2 # trans_B = self.ScaleRotateTranslate(self.img1, r, (self.width / 2.0, self.height/2.0), (x, y), (s, s)) # rs = self.MSE(self.img0, trans_B) self.vertex_values[idx] = 1.0 / rs else: return self.vertex_values[idx] return 1.0 / rs def volume(self, simplex): idx = tuple(map(tuple, simplex)) temp = self.volumes[idx] if temp: return temp matrix = np.copy(simplex) matrix = np.insert(matrix, 0, 1, axis=1) rs = 1.0/2/3/4 * abs(np.linalg.det(matrix)) self.volumes[idx] = rs return rs def calRho(self): # print "In this iteration has: ", len(np.array(self.vertices)[self.T.simplices]) for simplex in np.array(self.vertices)[self.T.simplices]: # if simplex visited before, then skip # idx = tuple(map(tuple, simplex)) # if self.volumes[idx]: # continue v = self.volume(simplex) if v == 0.0 or np.isinf(v): continue if v < self.vn: self.vn = v # q = 34.346004 q = 27.152900397563425 # print 'hahahahahahahahahaahahahaha' self.gn = np.sqrt(q * self.vn * np.log(1.0 / self.vn)) ff = np.sum( self.func(s) for s in simplex ) / (self.d+1) rho = v / (ff - self.Mn + self.gn)**2 if rho > self.bestRho: self.bestPoint = np.sum(simplex, axis=0) / (self.d + 1) # print 'Best Point this time is: ', self.bestPoint self.bestRho = rho return ff def iter(self): for n in range(self.iteration): self.bestRho = 0.0 self.calRho() l = len(self.simplices) print "###################", n, "###################", "simplices: ", l if self.Mn > self.func(self.bestPoint): self.globalbestPoint = self.bestPoint # print "error: ", self.func(self.bestPoint), "Best Point: ", self.bestPoint, " Value: ", self.func(self.bestPoint) self.Mn = min(self.Mn, self.func(self.bestPoint)) self.error[n] += self.Mn self.vertices.append(self.bestPoint) self.T.add_points([self.bestPoint]) self.simplices = np.array(self.vertices)[self.T.simplices] # p = self.globalbestPoint # x = self.width / 2.0 + (100 * p[0] - 50.0) # y = self.height / 2.0 + (100 * p[1] - 50.0) # r = p[2] * 40.0 - 20.0 # s = p[3] * 0.4 + 0.8 # print "error: ", self.error[n],"Best Global: ", self.globalbestPoint,"\nBest Point: ", (x,y,r,s), " Value: ", self.func(self.bestPoint) print "error: ", self.error[n],"Best Global: ", self.globalbestPoint,"\nBest Point: ", self.bestPoint, " Value: ", self.func(self.bestPoint) np.savetxt(self.datafile, self.vertices) def plot(self): self.vertices = np.array(self.vertices) plt.triplot(self.vertices[:,0], self.vertices[:,1]) # plt.plot(self.vertices[:,0], self.vertices[:,1], '.') plt.plot(self.globalbestPoint[0], self.globalbestPoint[1], 'o') plt.show() def show(self): img0 = Image.open(self.file1) img1 = Image.open(self.file2) p = self.globalbestPoint # p = [ 0.42237259 , 0.22260757 , 0.24273422, 0.42331118] x = self.width / 2.0 + (40 * p[0] - 20.0) y = self.height / 2.0 + (40 * p[1] - 20.0) # r = 0.24273422 * 40.0 - 20.0 # s = 0.42331118 * 0.4 + 0.8 r = 0.5 * 40.0 - 20.0 s = 0.5 * 0.4 + 0.8 print (x,y,r,s) img1 = self.ScaleRotateTranslate(img1, r, (self.width / 2.0, self.height/2.0), (x, y), (s, s)) # img1 = self.ScaleRotateTranslate(self.img1, r, None, (x, self.height-y), (s, s)) img0.show() img1.show() img1.save('rs-trail1.png') rs = (Image.blend(img1, img0, 0.5)) rs.save('rs-trail2.png') rs.show() def test2(self): file = open('go_data2') vertices = [] for line in file.readlines(): temp = [] for i in line.split(' '): temp.append(float(i)) vertices.append(temp) m = np.inf for i, arr in enumerate(vertices): arr = np.array(arr) val = self.func(arr) if m > val: m = val print i, arr, val def test3(self): fig = plt.figure() ax = fig.gca(projection='3d') X = np.arange(-1, 1, 0.1) Y = np.arange(-1, 1, 0.1) X, Y = np.meshgrid(X, Y) zs = np.array( [self.func([x,y]) for x,y in zip(np.ravel(X), np.ravel(Y))]) Z = zs.reshape(X.shape) surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False) ax.zaxis.set_major_locator(LinearLocator(10)) ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f')) fig.colorbar(surf, shrink=0.5, aspect=5) plt.show()
class VoronoiStrata(Strata): @beartype def __init__( self, seeds: np.ndarray = None, seeds_number: PositiveInteger = None, dimension: PositiveInteger = None, decomposition_iterations: PositiveInteger = 1, random_state: RandomStateType = None ): """ Define a geometric decomposition of the n-dimensional unit hypercube into disjoint and space-filling Voronoi strata. :param seeds: An array of dimension :math:`N * n` specifying the seeds of all strata. The seeds of the strata are the coordinates of the point inside each stratum that defines the stratum. The user must provide `seeds` or `seeds_number` and `dimension` :param seeds_number: The number of seeds to randomly generate. Seeds are generated by random sampling on the unit hypercube. The user must provide `seeds` or `seeds_number` and `dimension` :param dimension: The dimension of the unit hypercube in which to generate random seeds. Used only if `seeds_number` is provided. The user must provide `seeds` or `seeds_number` and `dimension` :param decomposition_iterations: Number of iterations to perform to create a Centroidal Voronoi decomposition. If :code:`decomposition_iterations = 0`, the Voronoi decomposition is based on the provided or generated seeds. If :code:`decomposition_iterations >= 1`, the seed points are moved to the centroids of the Voronoi cells in each iteration and a new Voronoi decomposition is performed. This process is repeated `decomposition_iterations` times to create a Centroidal Voronoi decomposition. """ super().__init__(seeds=seeds, random_state=random_state) self.logger = logging.getLogger(__name__) self.seeds_number = seeds_number self.dimension = dimension self.decomposition_iterations = decomposition_iterations self.voronoi: Voronoi = None """ Defines a Voronoi decomposition of the set of reflected points. When creating the Voronoi decomposition on the unit hypercube, the code reflects the points on the unit hypercube across all faces of the unit hypercube. This causes the Voronoi decomposition to create edges along the faces of the hypercube. This object is not the Voronoi decomposition of the unit hypercube. It is the Voronoi decomposition of all points and their reflections from which the unit hypercube is extracted. To access the vertices in the unit hypercube, see the attribute :py:attr:`vertices`.""" self.vertices: list = [] """A list of the vertices for each Voronoi stratum on the unit hypercube.""" if self.seeds is not None: if self.seeds_number is not None or self.dimension is not None: self.logger.info( "UQpy: Ignoring 'nseeds' and 'dimension' attributes because 'seeds' are provided" ) self.dimension = self.seeds.shape[1] self.stratify() def stratify(self): """ Performs the Voronoi stratification. """ self.logger.info("UQpy: Creating Voronoi stratification ...") initial_seeds = self.seeds if self.seeds is None: initial_seeds = stats.uniform.rvs(size=[self.seeds_number, self.dimension], random_state=self.random_state) if self.decomposition_iterations == 0: cent, vol = self.create_volume(initial_seeds) self.volume = np.asarray(vol) else: for i in range(self.decomposition_iterations): cent, vol = self.create_volume(initial_seeds) initial_seeds = np.asarray(cent) self.volume = np.asarray(vol) self.seeds = initial_seeds self.logger.info("UQpy: Voronoi stratification created.") def create_volume(self, initial_seeds): self.voronoi, bounded_regions = self.voronoi_unit_hypercube(initial_seeds) cent, vol = [], [] for region in bounded_regions: vertices = self.voronoi.vertices[region + [region[0]], :] centroid, volume = self.compute_voronoi_centroid_volume(vertices) self.vertices.append(vertices) cent.append(centroid[0, :]) vol.append(volume) return cent, vol @staticmethod def voronoi_unit_hypercube(seeds): from scipy.spatial import Voronoi # Mirror the seeds in both low and high directions for each dimension bounded_points = seeds dimension = seeds.shape[1] for i in range(dimension): seeds_del = np.delete(bounded_points, i, 1) if i == 0: points_temp1 = np.hstack([np.atleast_2d(-bounded_points[:, i]).T, seeds_del]) points_temp2 = np.hstack([np.atleast_2d(2 - bounded_points[:, i]).T, seeds_del]) elif i == dimension - 1: points_temp1 = np.hstack([seeds_del, np.atleast_2d(-bounded_points[:, i]).T]) points_temp2 = np.hstack([seeds_del, np.atleast_2d(2 - bounded_points[:, i]).T]) else: points_temp1 = np.hstack([seeds_del[:, :i], np.atleast_2d(-bounded_points[:, i]).T, seeds_del[:, i:], ]) points_temp2 = np.hstack([seeds_del[:, :i], np.atleast_2d(2 - bounded_points[:, i]).T, seeds_del[:, i:],]) seeds = np.append(seeds, points_temp1, axis=0) seeds = np.append(seeds, points_temp2, axis=0) vor = Voronoi(seeds, incremental=True) regions = [None] * bounded_points.shape[0] for i in range(bounded_points.shape[0]): regions[i] = vor.regions[vor.point_region[i]] bounded_regions = regions return vor, bounded_regions @staticmethod def compute_voronoi_centroid_volume(vertices): """ This function computes the centroid and volume of a Voronoi cell from its vertices. :param vertices: Coordinates of the vertices that define the Voronoi cell. :return: Centroid and Volume of the Voronoi cell """ from scipy.spatial import Delaunay, ConvexHull tess = Delaunay(vertices) dimension = np.shape(vertices)[1] w = np.zeros((tess.nsimplex, 1)) cent = np.zeros((tess.nsimplex, dimension)) for i in range(tess.nsimplex): # pylint: disable=E1136 ch = ConvexHull(tess.points[tess.simplices[i]]) w[i] = ch.volume cent[i, :] = np.mean(tess.points[tess.simplices[i]], axis=0) volume = np.sum(w) centroid = np.matmul(np.divide(w, volume).T, cent) return centroid, volume def sample_strata(self, nsamples_per_stratum, random_state): from scipy.spatial import Delaunay, ConvexHull samples_in_strata, weights = list(), list() for j in range( len(self.vertices) ): # For each bounded region (Voronoi stratification) vertices = self.vertices[j][:-1, :] seed = self.seeds[j, :].reshape(1, -1) seed_and_vertices = np.concatenate([vertices, seed]) # Create Dealunay Triangulation using seed and vertices of each stratum delaunay_obj = Delaunay(seed_and_vertices) # Compute volume of each delaunay volume = list() for i in range(len(delaunay_obj.vertices)): vert = delaunay_obj.vertices[i] ch = ConvexHull(seed_and_vertices[vert]) volume.append(ch.volume) temp_prob = np.array(volume) / sum(volume) a = list(range(len(delaunay_obj.vertices))) for k in range(int(nsamples_per_stratum[j])): simplex = random_state.choice(a, p=temp_prob) new_samples = SimplexSampling( nodes=seed_and_vertices[delaunay_obj.vertices[simplex]], nsamples=1, random_state=self.random_state, ).samples samples_in_strata.append(new_samples) self.extend_weights(nsamples_per_stratum, j, weights) return samples_in_strata, weights def compute_centroids(self): # if self.mesh is None: # self.add_boundary_points_and_construct_delaunay() self.mesh.centroids = np.zeros([self.mesh.nsimplex, self.dimension]) self.mesh.volumes = np.zeros([self.mesh.nsimplex, 1]) from scipy.spatial import qhull, ConvexHull for j in range(self.mesh.nsimplex): try: ConvexHull(self.points[self.mesh.vertices[j]]) self.mesh.centroids[j, :], self.mesh.volumes[j] = \ DelaunayStrata.compute_delaunay_centroid_volume(self.points[self.mesh.vertices[j]]) except qhull.QhullError: self.mesh.centroids[j, :], self.mesh.volumes[j] = (np.mean(self.points[self.mesh.vertices[j]]), 0,) def initialize(self, samples_number, training_points): self.add_boundary_points_and_construct_delaunay(samples_number, training_points) self.mesh.old_vertices = self.mesh.vertices.copy() def add_boundary_points_and_construct_delaunay( self, samples_number, training_points ): """ This method add the corners of :math:`[0, 1]^n` hypercube to the existing samples, which are used to construct a Delaunay Triangulation. """ self.mesh_vertices = training_points.copy() self.points_to_samplesU01 = np.arange(0, training_points.shape[0]) for i in range(np.shape(self.voronoi.vertices)[0]): if any(np.logical_and(self.voronoi.vertices[i, :] >= -1e-10, self.voronoi.vertices[i, :] <= 1e-10,)) or \ any(np.logical_and(self.voronoi.vertices[i, :] >= 1 - 1e-10, self.voronoi.vertices[i, :] <= 1 + 1e-10,)): self.mesh_vertices = np.vstack([self.mesh_vertices, self.voronoi.vertices[i, :]]) self.points_to_samplesU01 = np.hstack([np.array([-1]), self.points_to_samplesU01, ]) from scipy.spatial.qhull import Delaunay # Define the simplex mesh to be used for gradient estimation and sampling self.mesh = Delaunay( self.mesh_vertices, furthest_site=False, incremental=True, qhull_options=None,) self.points = getattr(self.mesh, "points") def calculate_strata_metrics(self, index): self.compute_centroids() s = np.zeros(self.mesh.nsimplex) for j in range(self.mesh.nsimplex): s[j] = self.mesh.volumes[j] ** 2 return s def update_strata_and_generate_samples( self, dimension, points_to_add, bins2break, samples_u01, random_state ): new_points = np.zeros([points_to_add, dimension]) for j in range(points_to_add): new_points[j, :] = self._generate_sample( bins2break[j], random_state=random_state ) self._update_strata(new_point=new_points, samples_u01=samples_u01) return new_points def calculate_gradient_strata_metrics(self, index): # Estimate the variance over each simplex by Delta Method. Moments of the simplices are computed using # Eq. (19) from the following reference: # Good, I.J. and Gaskins, R.A. (1971). The Centroid Method of Numerical Integration. Numerische # Mathematik. 16: 343--359. var = np.zeros((self.mesh.nsimplex, self.dimension)) s = np.zeros(self.mesh.nsimplex) for j in range(self.mesh.nsimplex): for k in range(self.dimension): std = np.std(self.points[self.mesh.vertices[j]][:, k]) var[j, k] = ( self.mesh.volumes[j] * math.factorial(self.dimension) / math.factorial(self.dimension + 2) ) * (self.dimension * std ** 2) s[j] = np.sum(self.dy_dx[j, :] * var[j, :] * self.dy_dx[j, :]) * ( self.mesh.volumes[j] ** 2 ) self.dy_dx_old = self.dy_dx return s def estimate_gradient( self, surrogate, step_size, samples_number, index, samples_u01, training_points, qoi, max_train_size=None, ): self.mesh.centroids = np.zeros([self.mesh.nsimplex, self.dimension]) self.mesh.volumes = np.zeros([self.mesh.nsimplex, 1]) from scipy.spatial import qhull, ConvexHull for j in range(self.mesh.nsimplex): try: ConvexHull(self.points[self.mesh.vertices[j]]) self.mesh.centroids[j, :], self.mesh.volumes[j] = DelaunayStrata.compute_delaunay_centroid_volume( self.points[self.mesh.vertices[j]]) except qhull.QhullError: self.mesh.centroids[j, :], self.mesh.volumes[j] = (np.mean(self.points[self.mesh.vertices[j]]), 0,) if max_train_size is None or len(training_points) <= max_train_size or index == training_points.shape[0]: from UQpy.utilities.Utilities import calculate_gradient # Use the entire sample set to train the surrogate model (more expensive option) self.dy_dx = calculate_gradient( surrogate, step_size, np.atleast_2d(training_points), np.atleast_2d(np.array(qoi)).T, self.mesh.centroids,) # dy_dx = self.calculate_gradient( # np.atleast_2d(training_points), qoi, self.mesh.centroids, surrogate) else: # Use only max_train_size points to train the surrogate model (more economical option) # Build a mapping from the new vertex indices to the old vertex indices. self.mesh.new_vertices, self.mesh.new_indices = [], [] self.mesh.new_to_old = np.zeros([self.mesh.vertices.shape[0], ]) * np.nan j, k = 0, 0 while (j < self.mesh.vertices.shape[0] and k < self.mesh.old_vertices.shape[0]): if np.all(self.mesh.vertices[j, :] == self.mesh.old_vertices[k, :]): self.mesh.new_to_old[j] = int(k) j += 1 k = 0 else: k += 1 if k == self.mesh.old_vertices.shape[0]: self.mesh.new_vertices.append(self.mesh.vertices[j]) self.mesh.new_indices.append(j) j += 1 k = 0 # Find the nearest neighbors to the most recently added point from sklearn.neighbors import NearestNeighbors knn = NearestNeighbors(n_neighbors=max_train_size) knn.fit(np.atleast_2d(samples_u01)) neighbors = knn.kneighbors(np.atleast_2d(samples_u01[-1]), return_distance=False) # For every simplex, check if at least dimension-1 vertices are in the neighbor set. # Only update the gradient in simplices that meet this criterion. update_list = [] for j in range(self.mesh.vertices.shape[0]): self.vertices_in_U01 = self.points_to_samplesU01[self.mesh.vertices[j]] self.vertices_in_U01[np.isnan(self.vertices_in_U01)] = 10 ** 18 v_set = set(self.vertices_in_U01) v_list = list(self.vertices_in_U01) if len(v_set) != len(v_list): continue else: if all(np.isin(self.vertices_in_U01, np.hstack([neighbors, np.atleast_2d(10 ** 18)]),)): update_list.append(j) update_array = np.asarray(update_list) # Initialize the gradient vector self.dy_dx = np.zeros((self.mesh.new_to_old.shape[0], self.dimension)) # For those simplices that will not be updated, use the previous gradient for j in range(self.dy_dx.shape[0]): if np.isnan(self.mesh.new_to_old[j]): continue else: self.dy_dx[j, :] = self.dy_dx_old[int(self.mesh.new_to_old[j]), :] # For those simplices that will be updated, compute the new gradient from UQpy.utilities.Utilities import calculate_gradient self.dy_dx[update_array, :] = calculate_gradient(surrogate, step_size, np.atleast_2d(training_points)[neighbors], np.atleast_2d(np.array(qoi)[neighbors]).T, self.mesh.centroids[update_array]) def _update_strata(self, new_point, samples_u01): i_ = samples_u01.shape[0] p_ = new_point.shape[0] # Update the matrices to have recognize the new point self.points_to_samplesU01 = np.hstack([self.points_to_samplesU01, np.arange(i_, i_ + p_)]) self.mesh.old_vertices = self.mesh.vertices # Update the Delaunay triangulation mesh to include the new point. self.mesh.add_points(new_point) self.points = getattr(self.mesh, "points") self.mesh_vertices = np.vstack([self.mesh_vertices, new_point]) # Compute the strata weights. self.voronoi, bounded_regions = VoronoiStrata.voronoi_unit_hypercube(samples_u01) self.centroids = [] self.volume = [] for region in bounded_regions: vertices = self.voronoi.vertices[region + [region[0]]] centroid, volume = VoronoiStrata.compute_voronoi_centroid_volume(vertices) self.centroids.append(centroid[0, :]) self.volume.append(volume) def _generate_sample(self, bin_, random_state): import itertools tmp_vertices = self.points[self.mesh.simplices[int(bin_), :]] col_one = np.array(list(itertools.combinations(np.arange(self.dimension + 1), self.dimension))) self.mesh.sub_simplex = np.zeros_like(tmp_vertices) # node: an array containing mid-point of edges for m in range(self.dimension + 1): self.mesh.sub_simplex[m, :] = ( np.sum(tmp_vertices[col_one[m] - 1, :], 0) / self.dimension) # Using the Simplex class to generate a new sample in the sub-simplex new = SimplexSampling(nodes=self.mesh.sub_simplex, nsamples=1, random_state=random_state).samples return new
class Delaunay(Sampler): """ Delaunay Class. Inherits from the Sampler class and augments pick and update with the mechanics of the Delanauy triangulation method Attributes ---------- triangulation : scipy.spatial.qhull.Delaunay The Delaunay triangulation model object simplex_cache : dict Cached values of simplices for Delaunay triangulation explore_priority : float The priority of exploration against exploitation See Also -------- Sampler : Base Class """ name = 'Delaunay' def __init__(self, lower, upper, explore_priority=0.0001): """ Initialise the Delaunay class. .. note :: Currently only supports rectangular type restrictions on the parameter space Parameters ---------- lower : array_like Lower or minimum bounds for the parameter space upper : array_like Upper or maximum bounds for the parameter space explore_priority : float, optional The priority of exploration against exploitation """ Sampler.__init__(self, lower, upper) self.triangulation = None # Delaunay model self.simplex_cache = {} # Pre-computed values of simplices self.explore_priority = explore_priority def update(self, uid, y_true): """ Update a job with its observed value. Parameters ---------- uid : str A hexadecimal ID that identifies the job to be updated y_true : float The observed value corresponding to the job identified by 'uid' Returns ------- int Index location in the data lists 'Delaunay.X' and 'Delaunay.y' corresponding to the job being updated """ return self._update(uid, y_true) def pick(self): """ Pick the next feature location for the next observation to be taken. This uses the recursive Delaunay subdivision algorithm. Returns ------- numpy.ndarray Location in the parameter space for the next observation to be taken str A random hexadecimal ID to identify the corresponding job """ n = len(self.X) # -- note that we are assuming the points in X are not reordered by # the scipy Delaunay implementation n_corners = 2 ** self.dims if n < n_corners + 1: # Bootstrap with a regular sampling strategy to get it started xq = grid_sample(self.lower, self.upper, n) yq_exp = [0.] else: X = self.X() # calling returns the value as an array y = self.y() virtual = self.virtual_flag() # Otherwise, recursive subdivide the edges with the Delaunay model if not self.triangulation: self.triangulation = ScipyDelaunay(X, incremental=True) # Weight by hyper-volume simplices = [tuple(s) for s in self.triangulation.vertices] cache = self.simplex_cache def get_value(s): # Computes the sample value as: # hyper-volume of simplex * variance of values in simplex ind = list(s) value = (np.var(y[ind]) + self.explore_priority) * \ np.linalg.det((X[ind] - X[ind[0]])[1:]) if not np.max(virtual[ind]): cache[s] = value return value # Mostly the simplices won't change from call to call - cache! sample_value = [cache[s] if s in cache else get_value(s) for s in simplices] # alternatively, a nicely vectorised computation might work here # profile and check what the bottleneck is # Extract the points in the highest value simplex simplex_indices = list(simplices[np.argmax(sample_value)]) simplex = X[simplex_indices] simplex_v = y[simplex_indices] # Weight the position in this simplex based on value deviation eps = 1e-3 weight = eps + np.abs(simplex_v - np.mean(simplex_v)) weight /= np.sum(weight) xq = np.sum(weight * simplex, axis=0) # dot yq_exp = np.sum(weight * simplex_v, axis=0) self.triangulation.add_points(xq[np.newaxis, :]) # incremental uid = Sampler._assign(self, xq, yq_exp) return xq, uid
class AlphaShape(object): """ Compute the alpha shape (or concave hull) of points. Parameters ---------- points : (Mx2) array The coordinates of the points. alpha : float Influences the shape of the alpha shape. Higher values lead to more edges being deleted. inc : bool If True adding and deleting points is allowed (at the cost of some efficiency). Attributes ---------- points : (Mx2) array The coordinates of the points used to compute the alpha shape. alpha : float The alpha used to compute the alpha shape. tri : SciPy Delaunay object The Delaunay triangulation of the points. edges : set The edges between the points resulting from the Delaunay triangulation. edge_points : list of arrays Each array contains the points which are connected by the edges resulting from the Delaunay triangulation. m : Shapely MultiLineString The Delaunay triangulation edges converted to a Shapely MultiLineString. triangles : array of Shapely polygons The Delaunay triangles converted to Shapely polygons. shape : Shapely polygon The resulting alpha shape of the to_shape method. Methods ------- add_points(points) Adds points to the alpha shape. Can only be used if 'inc' has been set to True. remove_last_added() Removes the last added points by the 'add_points' method and recreates the alpha shape. """ def __init__(self, points, alpha, inc=False): if len(points) < 4: raise ValueError('Not enough points to compute an alpha shape.') self.saved = None self.area = 0 self.alpha = alpha self.points = points self.tri = Delaunay(points, incremental=inc) self.edges = set() self.edge_points = [] self._add_edges(self.tri.vertices) @staticmethod def _find_outliers(data, m): """ Find outliers by Median Absolute Deviation (MAD). Parameters ---------- data : array Data to find outliers of. m : float or int Threshold value of the median deviation of a data entry to be considered an outlier. Returns ------- outliers : array Boolean array where outliers are marked as False. """ d = np.abs(data - np.median(data)) mdev = np.median(d) s = d/mdev if mdev else 0. return s < m @staticmethod def remove_array_from_list(L, arr): """ Removes an array from a list. Parameters ---------- L : list A list with arrays of which one has to be removed. arr : array The array which needs to be removed from the list. """ i = 0 size = len(L) while i != size and not np.array_equal(L[i], arr): i += 1 if i != size: L.pop(i) else: raise ValueError('array not found in list.') def _triangle_geometry(self, triangle): """ Compute the area and circumradius of a triangle. Parameters ---------- triangle : (1x3) array-like The indices of the points which form the triangle. Returns ------- area : float The area of the triangle circum_r : float The circumradius of the triangle """ pa = self.points[triangle[0]] pb = self.points[triangle[1]] pc = self.points[triangle[2]] # Lengths of sides of triangle a = math.hypot((pa[0]-pb[0]), (pa[1]-pb[1])) b = math.hypot((pb[0]-pc[0]), (pb[1]-pc[1])) c = math.hypot((pc[0]-pa[0]), (pc[1]-pa[1])) # Semiperimeter of triangle s = (a + b + c)/2.0 # Area of triangle by Heron's formula area = math.sqrt(s*(s-a)*(s-b)*(s-c)) if area != 0: circum_r = a*b*c/(4.0*area) else: circum_r = 0 return area, circum_r def _add_edge(self, i, j): """ Add an edge (line) between two points. Parameters ---------- i : int Index of a point. j : int Index of a point. """ if (i, j) in self.edges or (j, i) in self.edges: return self.edges.add((i, j)) self.edge_points.append(self.points[[i, j]]) def _add_edges(self, vertices): """ Add the edges between the given vertices if the circumradius of the triangle is bigger than 1/alpha. Parameters ---------- vertices : (Mx3) array Indices of the points forming the vertices of a triangulation. """ for ia, ib, ic in vertices: area, circum_r = self._triangle_geometry((ia, ib, ic)) if area != 0: if circum_r < 1.0/self.alpha: self._add_edge(ia, ib) self._add_edge(ib, ic) self._add_edge(ic, ia) self.area += area def _remove_edge(self, i, j): """ Remove an edge (line) between two points. Parameters ---------- i : int Index of a point. j : int Index of a point. """ if (i, j) not in self.edges and (j, i) not in self.edges: return if (i, j) in self.edges: self.edges.remove((i, j)) self.remove_array_from_list(self.edge_points, self.points[[i, j]]) if (j, i) in self.edges: self.edges.remove((j, i)) self.remove_array_from_list(self.edge_points, self.points[[j, i]]) def _remove_edges(self, vertices): """ Remove the edges between the given vertices if the circumradius of the triangle is bigger than 1/alpha. Parameters ---------- vertices : (Mx3) array Indices of the points forming the vertices of a triangulation. """ for ia, ib, ic in vertices: area, circum_r = self._triangle_geometry((ia, ib, ic)) if area != 0: if circum_r < 1.0/self.alpha: self.area -= area self._remove_edge(ia, ib) self._remove_edge(ib, ic) self._remove_edge(ic, ia) def to_shape(self, remove_interior=None): """ Convert the alpha shape to a Shapely polygon object. Parameters ---------- remove_interior : float or int If remove_interior parameter is set it will attempt to remove a possible interior of a alpha shape by detecting outlying polygons in 'triangles', since interiors are often way bigger than the actual triangles. Outliers detected using a Median Absolute Deviation (MAD). """ self.m = geometry.MultiLineString(self.edge_points) self.triangles = np.array(list(polygonize(self.m))) if remove_interior is not None: tri_area = np.array([t.area for t in self.triangles]) outliers = self._find_outliers(tri_area, m=remove_interior) if type(outliers) is bool: outliers = np.array([outliers]) self.triangles = self.triangles[outliers] self.shape = cascaded_union(self.triangles) def add_points(self, points): """ Adds points to the alpha shape. Parameters ---------- points : (Mx2) array The coordinates of the points. """ # Save current properties self.saved = {'points': self.points.copy(), 'edge_points': self.edge_points[:], 'edges': self.edges.copy(), 'area': self.area} # Determine old and new triangles old = self.tri.vertices.copy() old_rows = old.view(np.dtype((np.void, old.dtype.itemsize*old.shape[1]))) self.tri.add_points(points) new = self.tri.vertices new_rows = new.view(np.dtype((np.void, new.dtype.itemsize*new.shape[1]))) old_tri = np.setdiff1d(old_rows, new_rows).view(new.dtype).reshape(-1, new.shape[1]) new_tri = np.setdiff1d(new_rows, old_rows).view(new.dtype).reshape(-1, new.shape[1]) # Remove old triangles and add new self._remove_edges(old_tri) self.points = np.vstack((self.points, points)) self._add_edges(new_tri) def remove_last_added(self): """ Removes the last added points. """ if self.saved is None: raise RuntimeError('No points were added.') self.points = self.saved['points'] self.edge_points = self.saved['edge_points'] self.edges = self.saved['edges'] self.area = self.saved['area'] self.tri = Delaunay(self.points, incremental=True) self.saved = None
class AdaptiveMesh(object): """ Implementation of an adaptive mesh for a given mapping f:domain->image. We start from a Delaunay triangulation in the domain of f. This grid will be distorted in the image space. We refine the mesh by subdividing large or broken triangles. This process can be iterated, e.g. wehn a triangle is cut multiple times (use threshold for minimal size of triangle in domain space). Points outside of the domain (e.g. raytrace fails) should be mapped to image point (np.nan,np.nan) and are handled separately. ToDo: add unit tests """ def __init__(self,initial_domain,mapping): """ Initialize mesh, mapping and image points. initial_domain ... 2d array of shape (nPoints,2) mapping ... function image=mapping(domain) that accepts a list of domain points and returns corresponding image points """ from scipy.spatial import Delaunay assert( initial_domain.ndim==2 and initial_domain.shape[1] == 2) self.initial_domain = initial_domain; self.mapping = mapping; # triangulation of initial domain self.__tri = Delaunay(initial_domain,incremental=True); self.simplices = self.__tri.simplices; # calculate distorted grid self.initial_image = self.mapping(self.initial_domain); assert( self.initial_image.ndim==2) assert( self.initial_image.shape==(self.initial_domain.shape[0],2)) # current domain and image during refinement and for plotting self.domain = self.initial_domain; self.image = self.initial_image; # initial domain area self.initial_domain_area = np.sum(self.get_area_in_domain()); def get_mesh(self): """ return triangles and points in domain and image space domain,image: coordinate array of shape (nPoints,2) simplices: index array for vertices of each triangle, shape (nTriangles,3) Returns: (domain,image,simplices) """ return self.domain,self.image,self.simplices; def plot_triangulation(self,skip_triangle=None): """ plot current triangulation of adaptive mesh in domain and image space Parameters ---------- skip_triangle : function mask=skip_triangle(simplices), optional function that accepts a ndarray of simplices of shape (nTriangles, 3) and returns a flag for each triangle indicating that it should not be drawn Return ------ handle to new matplotlib figure """ simplices = self.simplices.copy(); if skip_triangle is not None: skip = skip_triangle(simplices); skipped_simplices=simplices[skip]; simplices=simplices[~skip]; fig,(ax1,ax2)= plt.subplots(2); ax1.set_title("Sampling + Triangulation in Domain"); if skip_triangle is not None and np.sum(skip)>0: ax1.triplot(self.domain[:,0], self.domain[:,1], skipped_simplices,'k:'); if len(simplices)>0: ax1.triplot(self.domain[:,0], self.domain[:,1], simplices,'b-'); ax1.plot(self.initial_domain[:,0],self.initial_domain[:,1],'r.') ax2.set_title("Sampling + Triangulation in Image") if len(simplices)>0: ax2.triplot(self.image[:,0], self.image[:,1], simplices,'b-'); ax2.plot(self.initial_image[:,0],self.initial_image[:,1],'r.') # if aspect is close to 1 force aspect to be 1 if 1./4 < ax1.get_data_ratio() < 4: ax1.set_aspect('equal'); if 1./4 < ax2.get_data_ratio() < 4: ax2.set_aspect('equal'); return fig; def get_area_in_domain(self,simplices=None): """ calculate signed area of given simplices in domain space simplices ... (opt) list of simplices, shape (nTriangles,3) Returns: 1d vector of size nTriangles containing the signed area of each triangle (positive: ccw orientation, negative: cw orientation of vertices) """ if simplices is None: simplices = self.simplices; x,y = self.domain[simplices].T; # See http://geomalgorithms.com/a01-_area.html#2D%20Polygons return 0.5 * ( (x[1]-x[0])*(y[2]-y[0]) - (x[2]-x[0])*(y[1]-y[0]) ); def get_area_in_image(self,simplices=None): """ calculate signed area of given simplices in image space (see get_area_in_domain()) """ if simplices is None: simplices = self.simplices; x,y = self.image[simplices].T; # See http://geomalgorithms.com/a01-_area.html#2D%20Polygons return 0.5 * ( (x[1]-x[0])*(y[2]-y[0]) - (x[2]-x[0])*(y[1]-y[0]) ); def find_broken_triangles(self,simplices=None,lthresh=None,): """ identify triangles that are cut in image space or include invalid vertices Parameters ---------- simplices : ndarray of ints, shape (nTriangles,3), optional Indices of the points forming the simplices in the triangulation. lthresh : float, optional absolute threshold for longest side of broken triangle Returns ------- bBroken: boolean vector of length nTriangles indicates, if triangle is broken """ if simplices is None: simplices = self.simplices; # x and y coordinates for each vertex in each triangle triangles = self.image[simplices] # calculate maximum of (squared) length of two sides of each triangle # (X[0]-X[1])**2 + (Y[0]-Y[1])**2; (X[1]-X[2])**2 + (Y[1]-Y[2])**2 max_lensq = np.max(np.sum(np.diff(triangles,axis=1)**2,axis=2),axis=1); # default: mark triangle as broken, if max side is 3 times larger than median value if lthresh is None: lthresh = 3*np.sqrt(np.nanmedian(max_lensq)); # valid triangles: all sides smaller than lthresh, none of its vertices invalid (np.nan) bValid = max_lensq < lthresh**2; return ~bValid; # Note: differs from (max_lensq >= lthresh**2), if some vertices are invalid! def find_skinny_triangles(self,simplices=None,rthresh=5,Athresh=1e-10): """ Identify triangles that deviate strongly from regular triangle (have very small angles) Parameters ---------- simplices : ndarray of ints, shape (nTriangles,3), optional Indices of the points forming the simplices in the triangulation. rthresh : float, optional relative threshold, specifies, how much smaller (area) a triangle can be compared to the corresponding regular triangle Athresh : float, optional area in domain threshold (relative to total domain area) Returns ------- bSkinny: boolean vector of length nTriangles indicates, if triangle is skinny """ if simplices is None: simplices = self.simplices; # x and y coordinates for each vertex in each triangle, shape (nTriangles,3,2) triangles = self.image[simplices]; # calculate (squared) length of each edge in triangle, shape (nTriangles,3) # (X[0]-X[1])**2 + (Y[0]-Y[1])**2; (X[1]-X[2])**2 + (Y[1]-Y[2])**2, (X[2]-X[0])**2 + (Y[2]-Y[0])**2 lensq = np.sum(np.diff(triangles[:,[0,1,2,0]],axis=1)**2,axis=2) # calculate (signed) area of triangles x,y = triangles.T area = 0.5 * ( (x[1]-x[0])*(y[2]-y[0]) - (x[2]-x[0])*(y[1]-y[0]) ); # skinny triangles: area of triangle is much smaller (by factor rthresh) # than the area of regular triangle sqrt(3)/4*maxlensq ~ 0.433*maxlensq bSkinny = np.abs(area) < (0.433/rthresh) * np.nanmax(lensq,axis=1); # note: nan's in area are not catched so far bSkinny&= np.abs(area)/self.initial_domain_area > Athresh # triangle is large return bSkinny def refine_skinny_triangles_complicated(self,skip_triangle=None,rthresh=5,scale_sampling=0.5,bPlot=False): """ subdivide skinny triangles in the image mesh (have very small angles) Parameters ---------- skip_triangle: function mask=skip_triangle(simplices), optional function, that accepts a list of simplices of shape (nTriangles, 3) and returns a flag for each triangle indicating if it is ignored rthresh : float, optional relative threshold, specifies, how much smaller (area) a triangle can be compared to the corresponding regular triangle scale_sampling : float, optional increasing scale_sampling will increase the number of subdivisions, a typical range is between 0.5 (default) and 1 bPlot : boolean if True, the triangulation including the skinny triangles is shown Returns -------- number of points added to the triangulation Note ---- Artefacts occur at the boundary of the split triangles, if the neighboring triangle is not split (very thin, overlapping triangles). Separation should maybe be done differently (using Delaunay triangulation, just splitting largest side by two☺) See Shewchuk, Delaunay Refinement Algorithms for Triangular Mesh Generation http://www.cs.berkeley.edu/~jrs/papers/2dj.pdf """ bSkinny = self.find_skinny_triangles(self.simplices,rthresh=rthresh); if skip_triangle is not None: bSkinny &= ~skip_triangle(self.simplices); if np.sum(bSkinny)==0: return; # nothing to do simplices = self.simplices[bSkinny]; # shape (nTriangles,3) triangles = self.image[simplices]; # shape (nTriangles,3,2) nPointsOrigMesh = self.image.shape[0]; # find largest side of triangle in image space -> named as CA len_edge = np.sqrt( np.sum(np.diff(triangles[:,[0,1,2,0]],axis=1)**2,axis=2) ); # shape (nTriangles,3) indC = np.argmax(len_edge,axis=1); area = np.abs(self.get_area_in_image(simplices)); # iterate over all triangles and subdivide them # # notation for skinny triangle (oriented ccw) # C . # /| CA: longest side of triangle # / | # / | # A /___| B # # subdivision of skinny triangle: # we add nCB points along CB, nBA points along BA # and nCB+nBA+1 points along CA and create new triangles # as indicated below for nCB=3, nBA=1. # # p0 p1 p2 p3 p4 p5 # .______.______.______B______.______A # / \ / \ / \ / \ / \ / # / \ / \ / \ / \ / \ / # C_____\/_____\/_____\/_____\/_____\/ # q0 q1 q2 q3 q4 q5 # # New triangles (all oriented ccw): # lower triangles: (q_i, q_{i+1}, p_i) for i=0,...,nCB+nBA # upper triangles: (p_i, q_{i+1}, p_{i+1}) for i=0,...,nCB+nBA # # Special case: nCB=0=nBA # one lower and one upper triangle is created new_domain_points=[]; new_simplices=[]; for k,simplex in enumerate(simplices): # vertices of simplex in domain space, ordered such that # edge CA is largest side of triangle in image space vertices = self.domain[simplex]; # shape (3,2); ind_sort = (indC[k]+np.arange(3))%3; # index array for sorting vertices as C,A,B C,A,B = vertices[ind_sort]; ca,ba,cb = len_edge[k,ind_sort]; assert ca>=ba and ca>=cb, "unexpected error in naming of skinny triangle" # estimate number of subdivisions (from ratio of heigt h_CA of triangle ABC vs CB and CA) hCA = 2*area[k]/ca; nCB = int(np.floor(cb/hCA*0.86*scale_sampling)); # Note: cb>h_CA, i.e. nCB would be always >1 without factor 0.8 nBA = int(np.floor(ba/hCA*0.86*scale_sampling)); # 0.86 ~ sqrt(3)/2, height in regular triangle nCA = nCB+nBA+1; #print k,nCB,nBA # offset for indices of points p and q in list of domain_points p=nPointsOrigMesh + len(new_domain_points); q=nPointsOrigMesh + len(new_domain_points)+nCA+1; # create new sampling points for x in np.arange(1,nCB+2)/(nCB+1.): # [1/n, 2/n, ..., 1], at least [1,] new_domain_points.append((1-x)*C + x*B); # p0, ..., p_nCB for x in np.arange(1,nBA+2)/(nBA+1.): # [1/n, 2/n, ..., 1], at least [1,] new_domain_points.append((1-x)*B + x*A); # p_{nCB+1}, ..., p_nCA for x in np.arange(0,nCA+1)/(nCA+1.): # [0, 1/n, ..., (n-1)/n], at least [0,0.5] new_domain_points.append((1-x)*C + x*A); # q_0, ..., q_nCA; # create new simplices (see figure above) new_simplices.extend( [(q+i, q+i+1, p+i ) for i in range(0,nCA)] ); # lower triangles new_simplices.extend( [(p+i, q+i+1, p+i+1) for i in range(0,nCA)] ); # upper triangles # update points in mesh (points are no longer unique!) logging.debug("refining_skinny_triangles(): adding %d points"%len(new_domain_points)); new_domain_points=np.asarray(new_domain_points); new_image_points=self.mapping(new_domain_points); self.domain= np.vstack((self.domain,new_domain_points)); self.image = np.vstack((self.image, new_image_points)); if bPlot: from matplotlib.collections import PolyCollection fig = self.plot_triangulation(); fig.suptitle("DEBUG: refine_skinny_triangles()"); ax1,ax2 = fig.axes; params = dict(facecolors='r', edgecolors='none', alpha=0.3); ax1.add_collection(PolyCollection(self.domain[simplices],**params)); ax2.add_collection(PolyCollection(self.image[simplices],**params)); ax2.plot(new_image_points[:,0],new_image_points[:,1],'k.') # sanity check that total area did not change after segmentation old = np.sum(np.abs(self.get_area_in_domain(simplices))); new = np.sum(np.abs(self.get_area_in_domain(new_simplices))); assert(abs((old-new)/old)<1e-10) # segmentation of triangle has no holes/overlaps # update list of simplices return self.__add_new_simplices(np.asarray(new_simplices),bSkinny); def refine_skinny_triangles(self,skip_triangle=None,rthresh=5,Athresh=1e-10,scale_sampling=0.5,bPlot=False): """ subdivide skinny triangles in the image mesh (have very small angles) Parameters ---------- skip_triangle: function mask=skip_triangle(simplices), optional function, that accepts a list of simplices of shape (nTriangles, 3) and returns a flag for each triangle indicating if it is ignored rthresh : float, optional relative threshold, specifies, how much smaller (area) a triangle can be compared to the corresponding regular triangle Athresh : float, optional area in domain threshold (relative to total domain area) scale_sampling : float, optional increasing scale_sampling will increase the number of subdivisions, a typical range is between 0.5 (default) and 1 bPlot : boolean if True, the triangulation including the skinny triangles is shown Returns -------- number of points added to the triangulation Note ---- ToDo: remove duplicate points !! See Shewchuk, Delaunay Refinement Algorithms for Triangular Mesh Generation http://www.cs.berkeley.edu/~jrs/papers/2dj.pdf """ # check if mesh is still a Delaunay mesh if self.__tri is None: raise RuntimeError('Mesh is no longer a Delaunay mesh. Subdivision not implemented for this case.'); bSkinny = self.find_skinny_triangles(self.simplices,rthresh=rthresh,Athresh=Athresh); if skip_triangle is not None: bSkinny &= ~skip_triangle(self.simplices); if np.sum(bSkinny)==0: return; # nothing to do simplices = self.simplices[bSkinny]; # shape (nTriangles,3) triangles = self.image[simplices]; # shape (nTriangles,3,2) nTriangles= triangles.shape[0]; # identify the shortest edge of the triangle in image space (not cut) lensq = np.sum( np.diff(triangles[:,[0,1,2,0]],axis=1)**2, axis=2); # shape (nTriangles,3) min_edge = np.argmin( lensq,axis=1); # shape (nTriangles) # find point as C (opposit to min_edge) and calculate midpoint on CA and CB indC = min_edge-1; A,B,C,new_domain_points,new_image_points = \ self.__resample_edges_of_triangle(simplices,indC,x=(0.5,)); # unique domain_points new_domain_points = np.vstack(set(map(tuple, new_domain_points.reshape(2*nTriangles,2)))) new_image_points = self.mapping(new_domain_points); if bPlot: from matplotlib.collections import PolyCollection fig = self.plot_triangulation(); fig.suptitle("DEBUG: refine_skinny_triangles()"); ax1,ax2 = fig.axes; params = dict(facecolors='r', edgecolors='none', alpha=0.3); ax1.add_collection(PolyCollection(self.domain[simplices],**params)); ax1.plot(new_domain_points[:,0],new_domain_points[:,1],'k.') ax2.add_collection(PolyCollection(self.image[simplices],**params)); ax2.plot(new_image_points[:,0],new_image_points[:,1],'k.') # update triangulation logging.debug("refining_skinny_triangles(): adding %d points"% (new_domain_points.shape[0])); self.image = np.vstack((self.image,new_image_points)); self.domain= np.vstack((self.domain,new_domain_points)); self.__tri.add_points(new_domain_points); self.simplices = self.__tri.simplices; return 2*nTriangles; def refine_large_triangles(self,is_large): """ subdivide large triangles in the image mesh Parameters ---------- is_large : function, mask=is_large(triangles) function, which accepts a list of simplices of shape (nTriangles, 3) and returns a flag for each triangle indicating if it should be subdivided Returns -------- number of points added to the triangulation Note ---- Additional points are added at the center of gravity of large triangles and the Delaunay triangulation is recalculated. Edge flips can occur. This procedure is suboptimal, as it produces skinny triangles. """ # check if mesh is still a Delaunay mesh if self.__tri is None: raise RuntimeError('Mesh is no longer a Delaunay mesh. Subdivision not implemented for this case.'); ind = is_large(self.simplices); if np.sum(ind)==0: return; # nothing to do # add center of gravity for critical triangles new_domain_points = np.sum(self.domain[self.simplices[ind]],axis=1)/3; # shape (nTriangles,2) # remove invalid points (coordinates are nan) # new_domain_points = new_domain_points[~np.any(np.isnan(new_domain_points),axis=1)] # update triangulation self.__tri.add_points(new_domain_points); logging.debug("refining_large_triangles(): adding %d points"%(new_domain_points.shape[0])) # calculate image points and update data new_image_points = self.mapping(new_domain_points); self.image = np.vstack((self.image,new_image_points)); self.domain= np.vstack((self.domain,new_domain_points)); # remove degenerated triangles (p1,p2 identical to A or B) => area is 0 simplices = self.__tri.simplices; area = self.get_area_in_domain(simplices); degenerated = np.abs(area/self.initial_domain_area)<1e-10; assert(np.all(area[~degenerated]>0)); # by construction all triangles are oriented ccw self.simplices = simplices[~degenerated]; # remove degenerate triangles return new_domain_points.shape[0]; def refine_broken_triangles(self,is_broken,nDivide=10,bPlot=False,bPlotTriangles=[0]): """ subdivide triangles which contain discontinuities in the image mesh or invalid vertices is_broken ... function mask=is_broken(triangles) that accepts a list of simplices of shape (nTriangles, 3) and returns a flag for each triangle indicating if it should be subdivided nDivide ... (opt) number of subdivisions of each side of broken triangle bPlot ... (opt) plot sampling and selected points for debugging bPlotTriangles (opt) list of triangle indices for which segmentation should be shown returns: number of new triangles Note: The resulting mesh will be no longer a Delaunay mesh (identical points might be present, circumference rule not guaranteed). Mesh functions, that need this property (like refine_large_triangles()) will not work after calling this function. """ broken = is_broken(self.simplices); # shape (nSimplices) simplices = self.simplices[broken]; # shape (nTriangles,3) triangles = self.image[simplices]; # shape (nTriangles,3,2) # check if any of the triangles has an invalid vertex (x or y coordinate is np.nan) bInvalidVertex = np.any(np.isnan(triangles),axis=2); # shape (nTriangles,3) if np.sum(bInvalidVertex)>0: raise RuntimeError("Mesh contains invalid points. Call Mesh.refine_invalid_triangles() first."); # check if subdivision is needed at all nTriangles = np.sum(broken) if nTriangles==0: return 0; # noting to do! nPointsOrigMesh = self.image.shape[0]; # add new simplices: # segmentation of each broken triangle is generated in a cyclic manner, # starting with isolated point C and the two closest new sampling points # in image space, p1 + p2), continues with p3,p4,A,B. # # C # /\ # / \ \\\ largest segments of triangle in image space # p1 * * p2 * new sampling points # ....///....\\\.............. discontinuity # p3 * * p4 # / \ new triangles: # /____________\ (C,p1,p2), isolated point + closest two new points # A B (p1,p3,p2),(p2,p3,p4) new broken triangles, only between new sampling points # (p4,p3,A), (p4,A,B): rest # # Note: one has to pay attention, that C,p1,p3,A are located on same side # of the triangle, otherwise the partition will fail! # identify the shortest edge of the triangle in image space (not cut) lensq = np.sum( np.diff(triangles[:,[0,1,2,0]],axis=1)**2, axis=2); # shape (nTriangles,3) min_edge = np.argmin( lensq,axis=1); # shape (nTriangles) # find point as C (opposit to min_edge) and resample CA and CB indC = min_edge-1; A,B,C,domain_points,image_points = self.__resample_edges_of_triangle(simplices,indC,nDivide=nDivide); # shape (nDivide,2,nTriangles,2) # determine indices of broken segments (largest elements in CA and CB) len_segments = np.sum(np.diff(image_points,axis=0)**2,axis=-1); # shape (nDivide-1,2,nTriangle) largest_segments = np.argmax(len_segments,axis=0); # shape (2,nTriangle) edge_points = np.asarray((largest_segments,largest_segments+1)); # shape (2,2,nTriangle) # set points p1 ... p4 for segmentation of triangle # see http://stackoverflow.com/questions/15660885/correctly-indexing-a-multidimensional-numpy-array-with-another-array-of-indices idx_tuple = (edge_points[...,np.newaxis],) + tuple(np.ogrid[:2,:nTriangles,:2]); new_domain_points = domain_points[idx_tuple]; new_image_points = image_points[idx_tuple]; # shape (2,2,nTriangle,2), indicating iDistance,iEdge,iTriangle,(x/y) # update points in mesh (points are no longer unique!) logging.debug("refining_broken_triangles(): adding %d points"%(4*nTriangles)); self.image = np.vstack((self.image,new_image_points.reshape(-1,2))); self.domain= np.vstack((self.domain,new_domain_points.reshape(-1,2))); if bPlot: from matplotlib.collections import PolyCollection fig = self.plot_triangulation(skip_triangle=is_broken); fig.suptitle("DEBUG: refine_broken_triangles()"); ax1,ax2 = fig.axes; #params = dict(facecolors='r', edgecolors='none', alpha=0.3); #ax1.add_collection(PolyCollection(self.domain[simplices],**params)); ax1.plot(domain_points[...,0].flat,domain_points[...,1].flat,'k.',label='test points'); ax1.plot(new_domain_points[...,0].flat,new_domain_points[...,1].flat,'g.',label='selected points'); ax1.legend(loc=0); ax2.plot(image_points[...,0].flat,image_points[...,1].flat,'k.') ax2.plot(new_image_points[...,0].flat,new_image_points[...,1].flat,'g.',label='selected points'); # indices for points p1 ... p4 in new list of points self.domain # (offset by number of points in the original mesh!) # Note: by construction, the order of p1 ... p4 corresponds exactly to the order # shown above (first tuple contains points closest to C, # first on CA, then on CB, second tuple beyond the discontinuity) (p1,p2),(p3,p4) = np.arange(4*nTriangles).reshape(2,2,nTriangles) + nPointsOrigMesh; # shape (nTriangles,) # construct the five triangles from points t1=np.vstack((C,p1,p2)); # shape (3,nTriangles) t2=np.vstack((p1,p3,p2)); t3=np.vstack((p2,p3,p4)); t4=np.vstack((p3,A,p4)); t5=np.vstack((p4,A,B)); new_simplices = np.hstack((t1,t2,t3,t4,t5)).T; # shape (5*nTriangles,3), reshape as (5,nTriangles,3) to obtain subdivision of each triangle # DEBUG subdivision of triangles if bPlot: ax1.add_collection(PolyCollection(self.domain[t1.T],edgecolor='none',facecolor='b',alpha=0.3)); ax1.add_collection(PolyCollection(self.domain[t2.T],edgecolor='none',facecolor='r',alpha=0.3)); ax1.add_collection(PolyCollection(self.domain[t3.T],edgecolor='none',facecolor='y',alpha=0.3)); ax1.add_collection(PolyCollection(self.domain[t4.T],edgecolor='none',facecolor='g',alpha=0.3)); ax1.add_collection(PolyCollection(self.domain[t5.T],edgecolor='none',facecolor='k',alpha=0.3)); for t in bPlotTriangles: # select index of triangle to look at BCA=[B[t],C[t],A[t]]; subdiv=[ C[t],p1[t],p2[t],p3[t],p4[t],A[t] ]; pt=self.domain[BCA]; ax1.plot(pt[...,0],pt[...,1],'g') pt=self.image[BCA]; ax2.plot(pt[...,0],pt[...,1],'g') pt=self.domain[subdiv]; ax1.plot(pt[...,0],pt[...,1],'r') pt=self.image[subdiv]; ax2.plot(pt[...,0],pt[...,1],'r') # sanity check that total area did not change after segmentation old = np.sum(np.abs(self.get_area_in_domain(simplices))); new = np.sum(np.abs(self.get_area_in_domain(new_simplices))); assert(abs((old-new)/old)<1e-10) # segmentation of triangle has no holes/overlaps # update list of simplices return self.__add_new_simplices(new_simplices,broken); def refine_invalid_triangles(self,nDivide=10,bPlot=False,bPlotTriangles=[0]): """ subdivide triangles which have one or two invalid vertices (x or y coordinate are np.nan) nDivide ... (opt) number of subdivisions of each side of triangle bPlot ... (opt) plot sampling and selected points for debugging bPlotTriangles (opt) list of triangle indices for which segmentation should be shown returns: number of new triangles Note: This function might also reuse refine_broken_triangles(), if we replace NaN's by a very large but finit number. However it might be less clean. Note: The resulting mesh will be no longer a Delaunay mesh (identical points might be present, circumference rule n ot guaranteed) and the total area in domain is reduced. """ vertices = self.image[self.simplices]; # shape (nSimplices,3,2) bInvalidVertex = np.any(np.isnan(vertices),axis=2); # shape (nSimplices,3) if ~np.any(bInvalidVertex): return 0; # all valid: nothing to do if np.all(bInvalidVertex): # all invalid: can do nothing logging.warning('all rays are invalid'); return 0; # we consider three cases: # 1. one vertex is invalid (generate two new triangles) # 2. two vertices are invalid (generate one new triangle) # 3. all vertices are invalid (triangle is skipped) # all triangles with only valid vertices are unchanged nInvalidVertices = np.sum(bInvalidVertex,axis=1); # shape (nSimplices) new_simplices=[]; ind_case1 = nInvalidVertices==1; new_simplices.extend(self.__subdivide_triangles_with_one_invalid_vertex(ind_case1,nDivide=nDivide)); ind_case2 = nInvalidVertices==2; new_simplices.extend(self.__subdivide_triangles_with_two_invalid_vertices(ind_case2,nDivide=nDivide)); new_simplices=np.reshape(new_simplices,(-1,3)); # update list of simplices bReplace=nInvalidVertices>0; # includes case 1, 2 and 3 return self.__add_new_simplices(new_simplices,bReplace); def __subdivide_triangles_with_one_invalid_vertex(self,bInvalid,nDivide=10): """ case 1: one point is invalid (chosen as point C) adds new points p1 and p2 to mesh and returns new simplices C x x x x invalid points x x o new triangle vertices (first valid from C) x x p1 o o p2 / \ new triangles: /___________\ (p1,A,p2),(A,B,p2) A B """ if ~np.any(bInvalid): return []; # nothing to do simplices = self.simplices[bInvalid]; # shape (nTriangles,3) triangles = self.image[simplices]; # shape (nTriangles,3,2) nTriangles= triangles.shape[0]; nPointsOrigMesh = self.image.shape[0]; # find invalid point as C (index on first axis) and resample CA and CB indC = np.where(np.any(np.isnan(triangles),axis=-1))[1]; A,B,C,domain_points,image_points = self.__resample_edges_of_triangle(simplices,indC,nDivide=nDivide); # shape (nDivide,2,nTriangles,2) assert(np.all(np.any(np.isnan(self.image[C]),axis=-1))); # all points C should be invalid # iterate over all triangles and subdivide them new_domain_points=[]; new_image_points=[]; new_simplices=[]; for k in range(nTriangles): # find index of first valid point p1 on CA and p2 on CB ind = np.any(np.isnan(image_points[:,:,k]),axis=-1); # shape (nDivide,2) p1=np.where(~ind[:,0])[0][0]; # first valid point on CA p2=np.where(~ind[:,1])[0][0]; # first valid ponit on CB new_domain_points.extend((domain_points[p1,0,k,:], domain_points[p2,1,k,:])); new_image_points.extend( ( image_points[p1,0,k,:], image_points[p2,1,k,:])); # calculate index for points p1 and p2 in self.domain = [self.domain, new_domain_points] P1 = nPointsOrigMesh+2*k; P2=P1+1; new_simplices.extend(((P1,A[k],P2), (A[k],B[k],P2))); # update points in mesh (points are no longer unique!) logging.debug("refine_invalid_triangles(case1): adding %d points"%(2*nTriangles)); self.image = np.vstack((self.image,np.reshape(new_image_points,(2*nTriangles,2)))); self.domain= np.vstack((self.domain,np.reshape(new_domain_points,(2*nTriangles,2)))); return np.reshape(new_simplices,(2*nTriangles,3)); def __subdivide_triangles_with_two_invalid_vertices(self,bInvalid,nDivide=10): """ case 2: two points are invalid (chosen as points A and B) C /\ / \ x invalid points / \ o new triangle vertices (last valid from C) / \ p1 o o p2 x x new triangle: xxxxxxxxxxxxxx (p1,p2,C) A B """ if ~np.any(bInvalid): return []; # nothing to do simplices = self.simplices[bInvalid]; # shape (nTriangles,3) triangles = self.image[simplices]; # shape (nTriangles,3,2) nTriangles= triangles.shape[0]; nPointsOrigMesh = self.image.shape[0]; # find valid point as C (index on first axis) and resample CA and CB indC = np.where(~np.any(np.isnan(triangles),axis=-1))[1]; A,B,C,domain_points,image_points = self.__resample_edges_of_triangle(simplices,indC,nDivide=nDivide); # shape (nDivide,2,nTriangles,2) assert(np.all(np.any(np.isnan(self.image[A]),axis=-1))); # all points A should be invalid assert(np.all(np.any(np.isnan(self.image[B]),axis=-1))); # all points B should be invalid # iterate over all triangles and subdivide them new_domain_points=[]; new_image_points=[]; new_simplices=[]; for k in range(nTriangles): # find index of first valid point p1 on CA and p2 on CB ind = np.any(np.isnan(image_points[:,:,k]),axis=-1); # shape (nDivide,2) p1=np.where(~ind[:,0])[0][-1]; # last valid point on CA p2=np.where(~ind[:,1])[0][-1]; # last valid ponit on CB new_domain_points.extend((domain_points[p1,0,k,:], domain_points[p2,1,k,:])); new_image_points.extend( ( image_points[p1,0,k,:], image_points[p2,1,k,:])); # calculate index for points p1 and p2 in self.domain = [self.domain, new_domain_points] P1 = nPointsOrigMesh+2*k; P2=P1+1; new_simplices.append((P1,P2,C[k])); # update points in mesh (points are no longer unique!) logging.debug("refine_invalid_triangles(case2): adding %d points"%(2*nTriangles)); self.image = np.vstack((self.image,np.reshape(new_image_points,(2*nTriangles,2)))); self.domain= np.vstack((self.domain,np.reshape(new_domain_points,(2*nTriangles,2)))); return np.reshape(new_simplices,(nTriangles,3)); def __resample_edges_of_triangle(self,simplices,indC,x=None,nDivide=10): """ generate dense sampling on edges CA and CB on given simplices Parameters ---------- simplices : ndarray of shape (nTriangles,3) vertex indices of triangles that should be resampled indC : vector of length nTriangles vertex number (mod 3) that should be used as point C x : vector of floats, optional indicates position of sampling points nDivide : integer, optional (only active if x=None) number of sampling points on CA and CB Returns ------- A,B,C : vector of ints, length (nTriangles) indices of points A,B,C domain_points : ndarray of shape (nDivide,2,nTriangle,2) sampling points along CA,CB in domain, indices are (iPoint,iSide,iTriangle,xy) image_points : ndarray of shape (nDivide,2,nTriangle,2) sampling points along CA,CB in image """ # handle optional arguments (sampling along CA and CB) if x is None: x = np.linspace(0,1,nDivide,endpoint=True) else: x = np.asarray(x); nDivide=x.size; # get indices of points ABC as shown above (C is isolated point) nTriangles = simplices.shape[0]; ind_triangle = np.arange(nTriangles) C = simplices[ind_triangle,(indC)%3]; A = simplices[ind_triangle,(indC+1)%3]; B = simplices[ind_triangle,(indC-1)%3]; # create dense sampling along C->B and C->A in domain space CA = np.outer(1-x,self.domain[C]) + np.outer(x,self.domain[A]); CB = np.outer(1-x,self.domain[C]) + np.outer(x,self.domain[B]); # map sampling on CA and CB to image space domain_points= np.hstack((CA,CB)).reshape(nDivide,2,nTriangles,2); image_points = self.mapping(domain_points.reshape(-1,2)).reshape(nDivide,2,nTriangles,2); return A,B,C,domain_points,image_points; def __add_new_simplices(self,new_simplices,bReplace): """ add list of new simplices to Mesh and Replace old simplices indicated by boolean array perform sanity checks beforehand new_simplices ... shape(nTriangles,3) bReplace ... shape(self.simplices.shape[0]) returns: number of added triangles """ # remove degenerated triangles (p1,p2 identical to A or B) => area is 0 area = self.get_area_in_domain(new_simplices); degenerated = np.abs(area/self.initial_domain_area)<1e-10; new_simplices = new_simplices[~degenerated]; # remove degenerate triangles assert(np.all(area[~degenerated]>0)); # by construction all triangles are oriented ccw # update simplices in mesh self.__tri = None; # delete initial Delaunay triangulation self.simplices=np.vstack((self.simplices[~bReplace], new_simplices)); # no longer Delaunay return new_simplices.shape[0];
class AdaptiveParametricSpaceMapper: def __init__(self, parameters, target, error_norm): self.parameters = parameters self.target = target self.vertices = np.empty((0, len(parameters)), dtype=float) self.funvals = [] self.error_norm = error_norm self.create_initial_mesh() self.iteration_times = [] def rescale_parameters(self, values): rescaled = np.ndarray(values.shape) for parameter_id in range(len(self.parameters)): pmin = self.parameters[parameter_id][1] pmax = self.parameters[parameter_id][2] rescaled[parameter_id] = pmin + (pmax - pmin) * values[parameter_id] return rescaled def rescale_parameters_multiple(self, values): rescaled = np.ndarray(values.shape) for parameter_id in range(len(self.parameters)): pmin = self.parameters[parameter_id][1] pmax = self.parameters[parameter_id][2] rescaled[:, parameter_id] = pmin + (pmax - pmin) * values[:, parameter_id] return rescaled def inverse_rescale_parameters_multiple(self, values): rescaled = np.ndarray(values.shape) for parameter_id in range(len(self.parameters)): pmin = self.parameters[parameter_id][1] pmax = self.parameters[parameter_id][2] rescaled[:, parameter_id] = (values[:, parameter_id] - pmin) / (pmax - pmin) return rescaled # initial mesh is a n-parallelipiped def create_initial_mesh(self): self.vertices = np.empty( (2**len(self.parameters) + 1, len(self.parameters)), dtype=np.float) for vertex_id in range(2**len(self.parameters)): for parameter_id in range(len(self.parameters)): if vertex_id % (2**(parameter_id + 1)) >= 2**parameter_id: self.vertices[vertex_id, parameter_id] = 1 else: self.vertices[vertex_id, parameter_id] = 0 self.vertices[2**len(self.parameters), :] = np.mean( self.vertices[:2**len(self.parameters), :], axis=0) #grids1d = [] #for parameter_id in range(len(self.parameters)): # grids1d.append(np.linspace(0, 1, 3)) #gridsnd = np.asarray(np.meshgrid(*(tuple(grids1d)))) #gridsnd = np.reshape(gridsnd, (len(self.parameters), 3**len(self.parameters))) #self.vertices = gridsnd.T self.triangulation = Delaunay(np.asarray(self.vertices), incremental=True) self.funvals = [] def error_estimator(self): nsimplex = self.triangulation.simplices.shape[0] simplex_error = np.empty((nsimplex, )) for simplex_id in range(nsimplex): simplex_points = self.triangulation.simplices[simplex_id, :] simplex_funvals = [] for simplex_point in simplex_points: simplex_funvals.append(self.funvals[simplex_point]) simplex_mean_funval = np.mean(simplex_funvals, axis=0) simplex_dfs = simplex_mean_funval - simplex_funvals print(simplex_dfs) simplex_dfs_norms = self.error_norm(simplex_dfs) simplex_error[simplex_id] = max(simplex_dfs_norms) return simplex_error def add_vertex(self, vertex): self.vertices.resize( (self.vertices.shape[0] + 1, self.vertices.shape[1])) self.vertices[self.vertices.shape[0] - 1, :] = vertex self.triangulation.add_points(np.reshape(vertex, (1, len(vertex)))) def run(self, max_vertices=sys.maxsize): while len(self.vertices) < max_vertices: iteration_begin = time.time() if (self.vertices.shape[0] != len(self.funvals)): self.funvals.append( self.target( self.rescale_parameters( self.vertices[len(self.funvals) - 1, :]))) iteration_end = time.time() self.iteration_times.append( {'target_evaluation': iteration_end - iteration_begin}) else: dfs = self.error_estimator() error_estimator_time = time.time() max_simplices_ids = np.argwhere(dfs == np.max(dfs)) max_simplices_measures = np.empty(max_simplices_ids.shape) print((np.argmax(dfs), np.max(dfs))) #max_simplices_centroids = np.empty((max_simplices_ids.size, len(self.parameters))) for max_simplex_id, simplex_id in enumerate(max_simplices_ids): simplex_vertices = self.triangulation.simplices[ simplex_id, :] simplex_vertices_coordinates = self.triangulation.points[ simplex_vertices, :] max_simplices_measures[max_simplex_id] = np.linalg.det( simplex_vertices_coordinates[0, 1:, :] - simplex_vertices_coordinates[0, 0, :] ) / math.factorial(len(self.parameters)) max_simplex_vertices = self.triangulation.simplices[ max_simplices_ids[np.argmax(max_simplices_measures )], :].ravel() simplex_edge_lengths = np.empty( (len(max_simplex_vertices), len(max_simplex_vertices))) for simplex_vertex1_id, simplex_vertex1 in enumerate( max_simplex_vertices): for simplex_vertex2_id, simplex_vertex2 in enumerate( max_simplex_vertices): simplex_edge_lengths[ simplex_vertex1_id, simplex_vertex2_id] = np.linalg.norm( self.triangulation.points[simplex_vertex1, :] - self.triangulation.points[simplex_vertex2, :]) print( self.rescale_parameters( self.triangulation.points[simplex_vertex1])) print( np.real((self.funvals[simplex_vertex1][1] - self.funvals[simplex_vertex1][0]))) simplex_vertex1_id, simplex_vertex2_id = np.unravel_index( [np.argmax(simplex_edge_lengths.ravel())], simplex_edge_lengths.shape) simplex_vertex1 = max_simplex_vertices[simplex_vertex1_id] simplex_vertex2 = max_simplex_vertices[simplex_vertex2_id] simplex_chooser_time = time.time() #print ((simplex_vertex1[0], simplex_vertex2[0], simplex_edge_lengths[simplex_vertex1_id, simplex_vertex2_id], np.max(dfs))) median = 0.5 * ( self.triangulation.points[simplex_vertex1[0], :] + self.triangulation.points[simplex_vertex2[0], :]) self.add_vertex(median) self.funvals.append( self.target(self.rescale_parameters(median))) iteration_end_time = time.time() centroid = np.mean( self.triangulation.points[max_simplex_vertices, :], axis=0) self.add_vertex(centroid) self.funvals.append( self.target(self.rescale_parameters(centroid))) self.iteration_times.append({ 'error_estimator': -iteration_begin + error_estimator_time, 'simplex_chooser': -error_estimator_time + simplex_chooser_time, 'target_evaluation': iteration_end_time - error_estimator_time })
def triangulatePoly(coords,addPoints=False,iterations=2,debug=False): """Triangulates a polygon with given coords using a Delaunay triangulation. Uses http://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Delaunay.html#scipy.spatial.Delaunay to calculate triangulation, then filters the triangles actually lying within the polygon. Args: coords (list): List of (x,y)-coordinates of corners. Keyword Args: addPoints (bool): Allow incremental addition of points. iterations (int): Number of iterations of additional point adding. debug (boo): Print debugging messages. Returns: tuple: Tuple containing: * triFinal (list): List of found triangles. * coordsTri (list): List of vertex coordinates. """ #Bookkeeping list triFinal=[] #Triangulate tri=Delaunay(coords,incremental=addPoints) #Backup original coordinates coordsOrg=list(coords) if debug: print "Found ", len(tri.simplices.copy()), "triangles in initial call." #Incrementally refine triangulation if addPoints: for i in range(iterations): mids=[] for j in range(len(tri.simplices)): mid=getCenterOfMass(coords[tri.simplices.copy()[j]]) mids.append(mid) coords=np.asarray(list(coords)+mids) tri.add_points(mids,restart=True) if debug: print "Found ", len(tri.simplices.copy()), "triangles after iterations." #Remember assigment of points by traingulation function coordsTri=tri.points midsIn=[] midsOut=[] triOutIdx=[] triInIdx=[] for i in range(len(tri.simplices)): # Get COM of triangle mid=getCenterOfMass(coordsTri[tri.simplices.copy()[i]]) #Check if triangle is inside original polygon if checkInsidePolyVec(mid[0],mid[1],coordsOrg): triFinal.append(tri.simplices.copy()[i]) midsIn.append(mid) triInIdx.append(i) else: triOutIdx.append(i) midsOut.append(mid) if debug: print "Removed ", len(tri.simplices.copy())-len(triFinal), "triangles through COM criteria." print "Returning ", len(triFinal), "triangles." return triFinal,coordsTri
class Delaunay(Sampler): """ Delaunay Class. Inherits from the Sampler class and augments pick and update with the mechanics of the Delanauy triangulation method Attributes ---------- triangulation : scipy.spatial.qhull.Delaunay The Delaunay triangulation model object simplex_cache : dict Cached values of simplices for Delaunay triangulation explore_priority : float The priority of exploration against exploitation See Also -------- Sampler : Base Class """ name = 'Delaunay' def __init__(self, lower, upper, explore_priority=0.0001): """ Initialise the Delaunay class. .. note :: Currently only supports rectangular type restrictions on the parameter space Parameters ---------- lower : array_like Lower or minimum bounds for the parameter space upper : array_like Upper or maximum bounds for the parameter space explore_priority : float, optional The priority of exploration against exploitation """ Sampler.__init__(self, lower, upper) self.triangulation = None # Delaunay model self.simplex_cache = {} # Pre-computed values of simplices self.explore_priority = explore_priority def update(self, uid, y_true): """ Update a job with its observed value. Parameters ---------- uid : str A hexadecimal ID that identifies the job to be updated y_true : float The observed value corresponding to the job identified by 'uid' Returns ------- int Index location in the data lists 'Delaunay.X' and 'Delaunay.y' corresponding to the job being updated """ return self._update(uid, y_true) def pick(self): """ Pick the next feature location for the next observation to be taken. This uses the recursive Delaunay subdivision algorithm. Returns ------- numpy.ndarray Location in the parameter space for the next observation to be taken str A random hexadecimal ID to identify the corresponding job """ n = len(self.X) # -- note that we are assuming the points in X are not reordered by # the scipy Delaunay implementation n_corners = 2**self.dims if n < n_corners + 1: # Bootstrap with a regular sampling strategy to get it started xq = grid_sample(self.lower, self.upper, n) yq_exp = [0.] else: X = self.X() # calling returns the value as an array y = self.y() virtual = self.virtual_flag() # Otherwise, recursive subdivide the edges with the Delaunay model if not self.triangulation: self.triangulation = ScipyDelaunay(X, incremental=True) # Weight by hyper-volume simplices = [tuple(s) for s in self.triangulation.vertices] cache = self.simplex_cache def get_value(s): # Computes the sample value as: # hyper-volume of simplex * variance of values in simplex ind = list(s) value = (np.var(y[ind]) + self.explore_priority) * \ np.linalg.det((X[ind] - X[ind[0]])[1:]) if not np.max(virtual[ind]): cache[s] = value return value # Mostly the simplices won't change from call to call - cache! sample_value = [ cache[s] if s in cache else get_value(s) for s in simplices ] # alternatively, a nicely vectorised computation might work here # profile and check what the bottleneck is # Extract the points in the highest value simplex simplex_indices = list(simplices[np.argmax(sample_value)]) simplex = X[simplex_indices] simplex_v = y[simplex_indices] # Weight the position in this simplex based on value deviation eps = 1e-3 weight = eps + np.abs(simplex_v - np.mean(simplex_v)) weight /= np.sum(weight) xq = np.sum(weight * simplex, axis=0) # dot yq_exp = np.sum(weight * simplex_v, axis=0) self.triangulation.add_points(xq[np.newaxis, :]) # incremental uid = Sampler._assign(self, xq, yq_exp) return xq, uid
class AdaptiveParametricSpaceMapper: def __init__(self, parameters, target, error_norm): self.parameters = parameters self.target = target self.vertices = np.empty((0, len(parameters)), dtype=float) self.funvals = [] self.error_norm = error_norm self.create_initial_mesh() self.iteration_times = [] def rescale_parameters(self, values): rescaled = np.ndarray(values.shape) for parameter_id in range(len(self.parameters)): pmin = self.parameters[parameter_id][1] pmax = self.parameters[parameter_id][2] rescaled[parameter_id] = pmin+(pmax-pmin)*values[parameter_id] return rescaled def rescale_parameters_multiple(self, values): rescaled = np.ndarray(values.shape) for parameter_id in range(len(self.parameters)): pmin = self.parameters[parameter_id][1] pmax = self.parameters[parameter_id][2] rescaled[:,parameter_id] = pmin+(pmax-pmin)*values[:,parameter_id] return rescaled def inverse_rescale_parameters_multiple(self, values): rescaled = np.ndarray(values.shape) for parameter_id in range(len(self.parameters)): pmin = self.parameters[parameter_id][1] pmax = self.parameters[parameter_id][2] rescaled[:,parameter_id] = (values[:,parameter_id]-pmin)/(pmax-pmin) return rescaled # initial mesh is a n-parallelipiped def create_initial_mesh(self): self.vertices = np.empty((2**len(self.parameters)+1,len(self.parameters)), dtype=np.float) for vertex_id in range(2**len(self.parameters)): for parameter_id in range(len(self.parameters)): if vertex_id % (2**(parameter_id+1)) >= 2**parameter_id: self.vertices[vertex_id, parameter_id] = 1 else: self.vertices[vertex_id, parameter_id] = 0 self.vertices[2**len(self.parameters), :] = np.mean(self.vertices[:2**len(self.parameters),:], axis=0) #grids1d = [] #for parameter_id in range(len(self.parameters)): # grids1d.append(np.linspace(0, 1, 3)) #gridsnd = np.asarray(np.meshgrid(*(tuple(grids1d)))) #gridsnd = np.reshape(gridsnd, (len(self.parameters), 3**len(self.parameters))) #self.vertices = gridsnd.T self.triangulation = Delaunay(np.asarray(self.vertices), incremental=True) self.funvals = [] def error_estimator(self): nsimplex = self.triangulation.simplices.shape[0] simplex_error = np.empty((nsimplex,)) for simplex_id in range(nsimplex): simplex_points = self.triangulation.simplices[simplex_id, :] simplex_funvals = [] for simplex_point in simplex_points: simplex_funvals.append(self.funvals[simplex_point]) simplex_mean_funval = np.mean(simplex_funvals, axis=0) simplex_dfs = simplex_mean_funval - simplex_funvals print(simplex_dfs) simplex_dfs_norms = self.error_norm(simplex_dfs) simplex_error[simplex_id] = max(simplex_dfs_norms) return simplex_error def add_vertex(self, vertex): self.vertices.resize((self.vertices.shape[0]+1, self.vertices.shape[1])) self.vertices[self.vertices.shape[0]-1, :] = vertex self.triangulation.add_points(np.reshape(vertex, (1, len(vertex)))) def run(self, max_vertices=sys.maxsize): while len(self.vertices)<max_vertices: iteration_begin = time.time() if (self.vertices.shape[0] != len(self.funvals)): self.funvals.append(self.target(self.rescale_parameters(self.vertices[len(self.funvals)-1,:]))) iteration_end = time.time() self.iteration_times.append({'target_evaluation': iteration_end-iteration_begin}) else: dfs = self.error_estimator() error_estimator_time = time.time() max_simplices_ids = np.argwhere(dfs==np.max(dfs)) max_simplices_measures = np.empty(max_simplices_ids.shape) print((np.argmax(dfs),np.max(dfs))) #max_simplices_centroids = np.empty((max_simplices_ids.size, len(self.parameters))) for max_simplex_id, simplex_id in enumerate(max_simplices_ids): simplex_vertices = self.triangulation.simplices[simplex_id, :] simplex_vertices_coordinates = self.triangulation.points[simplex_vertices,:] max_simplices_measures[max_simplex_id] = np.linalg.det(simplex_vertices_coordinates[0,1:,:]-simplex_vertices_coordinates[0,0,:])/math.factorial(len(self.parameters)) max_simplex_vertices = self.triangulation.simplices[max_simplices_ids[np.argmax(max_simplices_measures)],:].ravel() simplex_edge_lengths = np.empty((len(max_simplex_vertices), len(max_simplex_vertices))) for simplex_vertex1_id, simplex_vertex1 in enumerate(max_simplex_vertices): for simplex_vertex2_id, simplex_vertex2 in enumerate(max_simplex_vertices): simplex_edge_lengths[simplex_vertex1_id, simplex_vertex2_id] = np.linalg.norm(self.triangulation.points[simplex_vertex1,:] - self.triangulation.points[simplex_vertex2,:]) print (self.rescale_parameters(self.triangulation.points[simplex_vertex1])) print (np.real((self.funvals[simplex_vertex1][1]-self.funvals[simplex_vertex1][0]))) simplex_vertex1_id, simplex_vertex2_id = np.unravel_index([np.argmax(simplex_edge_lengths.ravel())], simplex_edge_lengths.shape) simplex_vertex1 = max_simplex_vertices[simplex_vertex1_id] simplex_vertex2 = max_simplex_vertices[simplex_vertex2_id] simplex_chooser_time = time.time() #print ((simplex_vertex1[0], simplex_vertex2[0], simplex_edge_lengths[simplex_vertex1_id, simplex_vertex2_id], np.max(dfs))) median = 0.5*(self.triangulation.points[simplex_vertex1[0],:] + self.triangulation.points[simplex_vertex2[0],:]) self.add_vertex(median) self.funvals.append(self.target(self.rescale_parameters(median))) iteration_end_time = time.time() centroid = np.mean(self.triangulation.points[max_simplex_vertices,:], axis=0) self.add_vertex(centroid) self.funvals.append(self.target(self.rescale_parameters(centroid))) self.iteration_times.append({'error_estimator': -iteration_begin + error_estimator_time, 'simplex_chooser': -error_estimator_time + simplex_chooser_time, 'target_evaluation': iteration_end_time - error_estimator_time})
max_p2 = e[1] max_v = v #sample center of this edge center = edge_center(max_p1, max_p2) P = dataGrid.grid_num(int(center[0]), int(center[1])) if P in S or P < 1 or P >= dataGrid.size: cell = np.random.choice(range(1, dataGrid.size + 1), 1) while cell[0] in S: cell = np.random.choice(range(1, dataGrid.size + 1), 1) P = cell[0] M[P - 1] = dataGrid.data_at_loc(P)[:, 1] S.add(P) x, y = dataGrid.coord(P) tri.add_points([(x - 1, y - 1)]) update_edge_values() #Plotting full_data = interpolateData(M, 3, dataGrid) exp_data = clipSimilarityMatrix(getSimilarityMatrix(full_data, dataGrid)) ax[0, 1].imshow(exp_data) ax[1, 1].imshow(true_data) measured_points = np.zeros(dataGrid.dims) for s in S: x, y = dataGrid.coord(s) measured_points[x - 1, y - 1] = 1 ax[1, 0].imshow(measured_points)
class AdaptiveMesh(object): """ Implementation of an adaptive mesh for a given mapping f:domain->image. We start from a Delaunay triangulation in the domain of f. This grid will be distorted in the image space. We refine the mesh by subdividing large or broken triangles. This process can be iterated, e.g. wehn a triangle is cut multiple times (use threshold for minimal size of triangle in domain space). Points outside of the domain (e.g. raytrace fails) should be mapped to image point (np.nan,np.nan) and are handled separately. ToDo: add unit tests """ def __init__(self, initial_domain, mapping): """ Initialize mesh, mapping and image points. initial_domain ... 2d array of shape (nPoints,2) mapping ... function image=mapping(domain) that accepts a list of domain points and returns corresponding image points """ from scipy.spatial import Delaunay assert (initial_domain.ndim == 2 and initial_domain.shape[1] == 2) self.initial_domain = initial_domain self.mapping = mapping # triangulation of initial domain self.__tri = Delaunay(initial_domain, incremental=True) self.simplices = self.__tri.simplices # calculate distorted grid self.initial_image = self.mapping(self.initial_domain) assert (self.initial_image.ndim == 2) assert (self.initial_image.shape == (self.initial_domain.shape[0], 2)) # current domain and image during refinement and for plotting self.domain = self.initial_domain self.image = self.initial_image # initial domain area self.initial_domain_area = np.sum(self.get_area_in_domain()) def get_mesh(self): """ return triangles and points in domain and image space domain,image: coordinate array of shape (nPoints,2) simplices: index array for vertices of each triangle, shape (nTriangles,3) Returns: (domain,image,simplices) """ return self.domain, self.image, self.simplices def plot_triangulation(self, skip_triangle=None): """ plot current triangulation of adaptive mesh in domain and image space Parameters ---------- skip_triangle : function mask=skip_triangle(simplices), optional function that accepts a ndarray of simplices of shape (nTriangles, 3) and returns a flag for each triangle indicating that it should not be drawn Return ------ handle to new matplotlib figure """ simplices = self.simplices.copy() if skip_triangle is not None: skip = skip_triangle(simplices) skipped_simplices = simplices[skip] simplices = simplices[~skip] fig, (ax1, ax2) = plt.subplots(2) ax1.set_title("Sampling + Triangulation in Domain") if skip_triangle is not None and np.sum(skip) > 0: ax1.triplot(self.domain[:, 0], self.domain[:, 1], skipped_simplices, 'k:') if len(simplices) > 0: ax1.triplot(self.domain[:, 0], self.domain[:, 1], simplices, 'b-') ax1.plot(self.initial_domain[:, 0], self.initial_domain[:, 1], 'r.') ax2.set_title("Sampling + Triangulation in Image") if len(simplices) > 0: ax2.triplot(self.image[:, 0], self.image[:, 1], simplices, 'b-') ax2.plot(self.initial_image[:, 0], self.initial_image[:, 1], 'r.') # if aspect is close to 1 force aspect to be 1 if 1. / 4 < ax1.get_data_ratio() < 4: ax1.set_aspect('equal') if 1. / 4 < ax2.get_data_ratio() < 4: ax2.set_aspect('equal') return fig def get_area_in_domain(self, simplices=None): """ calculate signed area of given simplices in domain space simplices ... (opt) list of simplices, shape (nTriangles,3) Returns: 1d vector of size nTriangles containing the signed area of each triangle (positive: ccw orientation, negative: cw orientation of vertices) """ if simplices is None: simplices = self.simplices x, y = self.domain[simplices].T # See http://geomalgorithms.com/a01-_area.html#2D%20Polygons return 0.5 * ((x[1] - x[0]) * (y[2] - y[0]) - (x[2] - x[0]) * (y[1] - y[0])) def get_area_in_image(self, simplices=None): """ calculate signed area of given simplices in image space (see get_area_in_domain()) """ if simplices is None: simplices = self.simplices x, y = self.image[simplices].T # See http://geomalgorithms.com/a01-_area.html#2D%20Polygons return 0.5 * ((x[1] - x[0]) * (y[2] - y[0]) - (x[2] - x[0]) * (y[1] - y[0])) def find_broken_triangles( self, simplices=None, lthresh=None, ): """ identify triangles that are cut in image space or include invalid vertices Parameters ---------- simplices : ndarray of ints, shape (nTriangles,3), optional Indices of the points forming the simplices in the triangulation. lthresh : float, optional absolute threshold for longest side of broken triangle Returns ------- bBroken: boolean vector of length nTriangles indicates, if triangle is broken """ if simplices is None: simplices = self.simplices # x and y coordinates for each vertex in each triangle triangles = self.image[simplices] # calculate maximum of (squared) length of two sides of each triangle # (X[0]-X[1])**2 + (Y[0]-Y[1])**2; (X[1]-X[2])**2 + (Y[1]-Y[2])**2 max_lensq = np.max(np.sum(np.diff(triangles, axis=1)**2, axis=2), axis=1) # default: mark triangle as broken, if max side is 3 times larger than median value if lthresh is None: lthresh = 3 * np.sqrt(np.nanmedian(max_lensq)) # valid triangles: all sides smaller than lthresh, none of its vertices invalid (np.nan) bValid = max_lensq < lthresh**2 return ~bValid # Note: differs from (max_lensq >= lthresh**2), if some vertices are invalid! def find_skinny_triangles(self, simplices=None, rthresh=5, Athresh=1e-10): """ Identify triangles that deviate strongly from regular triangle (have very small angles) Parameters ---------- simplices : ndarray of ints, shape (nTriangles,3), optional Indices of the points forming the simplices in the triangulation. rthresh : float, optional relative threshold, specifies, how much smaller (area) a triangle can be compared to the corresponding regular triangle Athresh : float, optional area in domain threshold (relative to total domain area) Returns ------- bSkinny: boolean vector of length nTriangles indicates, if triangle is skinny """ if simplices is None: simplices = self.simplices # x and y coordinates for each vertex in each triangle, shape (nTriangles,3,2) triangles = self.image[simplices] # calculate (squared) length of each edge in triangle, shape (nTriangles,3) # (X[0]-X[1])**2 + (Y[0]-Y[1])**2; (X[1]-X[2])**2 + (Y[1]-Y[2])**2, (X[2]-X[0])**2 + (Y[2]-Y[0])**2 lensq = np.sum(np.diff(triangles[:, [0, 1, 2, 0]], axis=1)**2, axis=2) # calculate (signed) area of triangles x, y = triangles.T area = 0.5 * ((x[1] - x[0]) * (y[2] - y[0]) - (x[2] - x[0]) * (y[1] - y[0])) # skinny triangles: area of triangle is much smaller (by factor rthresh) # than the area of regular triangle sqrt(3)/4*maxlensq ~ 0.433*maxlensq bSkinny = np.abs(area) < (0.433 / rthresh) * np.nanmax(lensq, axis=1) # note: nan's in area are not catched so far bSkinny &= np.abs( area) / self.initial_domain_area > Athresh # triangle is large return bSkinny def refine_skinny_triangles_complicated(self, skip_triangle=None, rthresh=5, scale_sampling=0.5, bPlot=False): """ subdivide skinny triangles in the image mesh (have very small angles) Parameters ---------- skip_triangle: function mask=skip_triangle(simplices), optional function, that accepts a list of simplices of shape (nTriangles, 3) and returns a flag for each triangle indicating if it is ignored rthresh : float, optional relative threshold, specifies, how much smaller (area) a triangle can be compared to the corresponding regular triangle scale_sampling : float, optional increasing scale_sampling will increase the number of subdivisions, a typical range is between 0.5 (default) and 1 bPlot : boolean if True, the triangulation including the skinny triangles is shown Returns -------- number of points added to the triangulation Note ---- Artefacts occur at the boundary of the split triangles, if the neighboring triangle is not split (very thin, overlapping triangles). Separation should maybe be done differently (using Delaunay triangulation, just splitting largest side by two☺) See Shewchuk, Delaunay Refinement Algorithms for Triangular Mesh Generation http://www.cs.berkeley.edu/~jrs/papers/2dj.pdf """ bSkinny = self.find_skinny_triangles(self.simplices, rthresh=rthresh) if skip_triangle is not None: bSkinny &= ~skip_triangle(self.simplices) if np.sum(bSkinny) == 0: return # nothing to do simplices = self.simplices[bSkinny] # shape (nTriangles,3) triangles = self.image[simplices] # shape (nTriangles,3,2) nPointsOrigMesh = self.image.shape[0] # find largest side of triangle in image space -> named as CA len_edge = np.sqrt( np.sum(np.diff(triangles[:, [0, 1, 2, 0]], axis=1)**2, axis=2)) # shape (nTriangles,3) indC = np.argmax(len_edge, axis=1) area = np.abs(self.get_area_in_image(simplices)) # iterate over all triangles and subdivide them # # notation for skinny triangle (oriented ccw) # C . # /| CA: longest side of triangle # / | # / | # A /___| B # # subdivision of skinny triangle: # we add nCB points along CB, nBA points along BA # and nCB+nBA+1 points along CA and create new triangles # as indicated below for nCB=3, nBA=1. # # p0 p1 p2 p3 p4 p5 # .______.______.______B______.______A # / \ / \ / \ / \ / \ / # / \ / \ / \ / \ / \ / # C_____\/_____\/_____\/_____\/_____\/ # q0 q1 q2 q3 q4 q5 # # New triangles (all oriented ccw): # lower triangles: (q_i, q_{i+1}, p_i) for i=0,...,nCB+nBA # upper triangles: (p_i, q_{i+1}, p_{i+1}) for i=0,...,nCB+nBA # # Special case: nCB=0=nBA # one lower and one upper triangle is created new_domain_points = [] new_simplices = [] for k, simplex in enumerate(simplices): # vertices of simplex in domain space, ordered such that # edge CA is largest side of triangle in image space vertices = self.domain[simplex] # shape (3,2); ind_sort = (indC[k] + np.arange(3)) % 3 # index array for sorting vertices as C,A,B C, A, B = vertices[ind_sort] ca, ba, cb = len_edge[k, ind_sort] assert ca >= ba and ca >= cb, "unexpected error in naming of skinny triangle" # estimate number of subdivisions (from ratio of heigt h_CA of triangle ABC vs CB and CA) hCA = 2 * area[k] / ca nCB = int(np.floor(cb / hCA * 0.86 * scale_sampling)) # Note: cb>h_CA, i.e. nCB would be always >1 without factor 0.8 nBA = int(np.floor(ba / hCA * 0.86 * scale_sampling)) # 0.86 ~ sqrt(3)/2, height in regular triangle nCA = nCB + nBA + 1 #print k,nCB,nBA # offset for indices of points p and q in list of domain_points p = nPointsOrigMesh + len(new_domain_points) q = nPointsOrigMesh + len(new_domain_points) + nCA + 1 # create new sampling points for x in np.arange(1, nCB + 2) / ( nCB + 1.): # [1/n, 2/n, ..., 1], at least [1,] new_domain_points.append((1 - x) * C + x * B) # p0, ..., p_nCB for x in np.arange(1, nBA + 2) / ( nBA + 1.): # [1/n, 2/n, ..., 1], at least [1,] new_domain_points.append((1 - x) * B + x * A) # p_{nCB+1}, ..., p_nCA for x in np.arange(0, nCA + 1) / ( nCA + 1.): # [0, 1/n, ..., (n-1)/n], at least [0,0.5] new_domain_points.append((1 - x) * C + x * A) # q_0, ..., q_nCA; # create new simplices (see figure above) new_simplices.extend([(q + i, q + i + 1, p + i) for i in range(0, nCA)]) # lower triangles new_simplices.extend([(p + i, q + i + 1, p + i + 1) for i in range(0, nCA)]) # upper triangles # update points in mesh (points are no longer unique!) logging.debug("refining_skinny_triangles(): adding %d points" % len(new_domain_points)) new_domain_points = np.asarray(new_domain_points) new_image_points = self.mapping(new_domain_points) self.domain = np.vstack((self.domain, new_domain_points)) self.image = np.vstack((self.image, new_image_points)) if bPlot: from matplotlib.collections import PolyCollection fig = self.plot_triangulation() fig.suptitle("DEBUG: refine_skinny_triangles()") ax1, ax2 = fig.axes params = dict(facecolors='r', edgecolors='none', alpha=0.3) ax1.add_collection(PolyCollection(self.domain[simplices], **params)) ax2.add_collection(PolyCollection(self.image[simplices], **params)) ax2.plot(new_image_points[:, 0], new_image_points[:, 1], 'k.') # sanity check that total area did not change after segmentation old = np.sum(np.abs(self.get_area_in_domain(simplices))) new = np.sum(np.abs(self.get_area_in_domain(new_simplices))) assert (abs((old - new) / old) < 1e-10 ) # segmentation of triangle has no holes/overlaps # update list of simplices return self.__add_new_simplices(np.asarray(new_simplices), bSkinny) def refine_skinny_triangles(self, skip_triangle=None, rthresh=5, Athresh=1e-10, scale_sampling=0.5, bPlot=False): """ subdivide skinny triangles in the image mesh (have very small angles) Parameters ---------- skip_triangle: function mask=skip_triangle(simplices), optional function, that accepts a list of simplices of shape (nTriangles, 3) and returns a flag for each triangle indicating if it is ignored rthresh : float, optional relative threshold, specifies, how much smaller (area) a triangle can be compared to the corresponding regular triangle Athresh : float, optional area in domain threshold (relative to total domain area) scale_sampling : float, optional increasing scale_sampling will increase the number of subdivisions, a typical range is between 0.5 (default) and 1 bPlot : boolean if True, the triangulation including the skinny triangles is shown Returns -------- number of points added to the triangulation Note ---- ToDo: remove duplicate points !! See Shewchuk, Delaunay Refinement Algorithms for Triangular Mesh Generation http://www.cs.berkeley.edu/~jrs/papers/2dj.pdf """ # check if mesh is still a Delaunay mesh if self.__tri is None: raise RuntimeError( 'Mesh is no longer a Delaunay mesh. Subdivision not implemented for this case.' ) bSkinny = self.find_skinny_triangles(self.simplices, rthresh=rthresh, Athresh=Athresh) if skip_triangle is not None: bSkinny &= ~skip_triangle(self.simplices) if np.sum(bSkinny) == 0: return # nothing to do simplices = self.simplices[bSkinny] # shape (nTriangles,3) triangles = self.image[simplices] # shape (nTriangles,3,2) nTriangles = triangles.shape[0] # identify the shortest edge of the triangle in image space (not cut) lensq = np.sum(np.diff(triangles[:, [0, 1, 2, 0]], axis=1)**2, axis=2) # shape (nTriangles,3) min_edge = np.argmin(lensq, axis=1) # shape (nTriangles) # find point as C (opposit to min_edge) and calculate midpoint on CA and CB indC = min_edge - 1 A,B,C,new_domain_points,new_image_points = \ self.__resample_edges_of_triangle(simplices,indC,x=(0.5,)) # unique domain_points new_domain_points = np.vstack( set(map(tuple, new_domain_points.reshape(2 * nTriangles, 2)))) new_image_points = self.mapping(new_domain_points) if bPlot: from matplotlib.collections import PolyCollection fig = self.plot_triangulation() fig.suptitle("DEBUG: refine_skinny_triangles()") ax1, ax2 = fig.axes params = dict(facecolors='r', edgecolors='none', alpha=0.3) ax1.add_collection(PolyCollection(self.domain[simplices], **params)) ax1.plot(new_domain_points[:, 0], new_domain_points[:, 1], 'k.') ax2.add_collection(PolyCollection(self.image[simplices], **params)) ax2.plot(new_image_points[:, 0], new_image_points[:, 1], 'k.') # update triangulation logging.debug("refining_skinny_triangles(): adding %d points" % (new_domain_points.shape[0])) self.image = np.vstack((self.image, new_image_points)) self.domain = np.vstack((self.domain, new_domain_points)) self.__tri.add_points(new_domain_points) self.simplices = self.__tri.simplices return 2 * nTriangles def refine_large_triangles(self, is_large): """ subdivide large triangles in the image mesh Parameters ---------- is_large : function, mask=is_large(triangles) function, which accepts a list of simplices of shape (nTriangles, 3) and returns a flag for each triangle indicating if it should be subdivided Returns -------- number of points added to the triangulation Note ---- Additional points are added at the center of gravity of large triangles and the Delaunay triangulation is recalculated. Edge flips can occur. This procedure is suboptimal, as it produces skinny triangles. """ # check if mesh is still a Delaunay mesh if self.__tri is None: raise RuntimeError( 'Mesh is no longer a Delaunay mesh. Subdivision not implemented for this case.' ) ind = is_large(self.simplices) if np.sum(ind) == 0: return # nothing to do # add center of gravity for critical triangles new_domain_points = np.sum(self.domain[self.simplices[ind]], axis=1) / 3 # shape (nTriangles,2) # remove invalid points (coordinates are nan) # new_domain_points = new_domain_points[~np.any(np.isnan(new_domain_points),axis=1)] # update triangulation self.__tri.add_points(new_domain_points) logging.debug("refining_large_triangles(): adding %d points" % (new_domain_points.shape[0])) # calculate image points and update data new_image_points = self.mapping(new_domain_points) self.image = np.vstack((self.image, new_image_points)) self.domain = np.vstack((self.domain, new_domain_points)) # remove degenerated triangles (p1,p2 identical to A or B) => area is 0 simplices = self.__tri.simplices area = self.get_area_in_domain(simplices) degenerated = np.abs(area / self.initial_domain_area) < 1e-10 assert (np.all(area[~degenerated] > 0)) # by construction all triangles are oriented ccw self.simplices = simplices[~degenerated] # remove degenerate triangles return new_domain_points.shape[0] def refine_broken_triangles(self, is_broken, nDivide=10, bPlot=False, bPlotTriangles=[0]): """ subdivide triangles which contain discontinuities in the image mesh or invalid vertices is_broken ... function mask=is_broken(triangles) that accepts a list of simplices of shape (nTriangles, 3) and returns a flag for each triangle indicating if it should be subdivided nDivide ... (opt) number of subdivisions of each side of broken triangle bPlot ... (opt) plot sampling and selected points for debugging bPlotTriangles (opt) list of triangle indices for which segmentation should be shown returns: number of new triangles Note: The resulting mesh will be no longer a Delaunay mesh (identical points might be present, circumference rule not guaranteed). Mesh functions, that need this property (like refine_large_triangles()) will not work after calling this function. """ broken = is_broken(self.simplices) # shape (nSimplices) simplices = self.simplices[broken] # shape (nTriangles,3) triangles = self.image[simplices] # shape (nTriangles,3,2) # check if any of the triangles has an invalid vertex (x or y coordinate is np.nan) bInvalidVertex = np.any(np.isnan(triangles), axis=2) # shape (nTriangles,3) if np.sum(bInvalidVertex) > 0: raise RuntimeError( "Mesh contains invalid points. Call Mesh.refine_invalid_triangles() first." ) # check if subdivision is needed at all nTriangles = np.sum(broken) if nTriangles == 0: return 0 # noting to do! nPointsOrigMesh = self.image.shape[0] # add new simplices: # segmentation of each broken triangle is generated in a cyclic manner, # starting with isolated point C and the two closest new sampling points # in image space, p1 + p2), continues with p3,p4,A,B. # # C # /\ # / \ \\\ largest segments of triangle in image space # p1 * * p2 * new sampling points # ....///....\\\.............. discontinuity # p3 * * p4 # / \ new triangles: # /____________\ (C,p1,p2), isolated point + closest two new points # A B (p1,p3,p2),(p2,p3,p4) new broken triangles, only between new sampling points # (p4,p3,A), (p4,A,B): rest # # Note: one has to pay attention, that C,p1,p3,A are located on same side # of the triangle, otherwise the partition will fail! # identify the shortest edge of the triangle in image space (not cut) lensq = np.sum(np.diff(triangles[:, [0, 1, 2, 0]], axis=1)**2, axis=2) # shape (nTriangles,3) min_edge = np.argmin(lensq, axis=1) # shape (nTriangles) # find point as C (opposit to min_edge) and resample CA and CB indC = min_edge - 1 A, B, C, domain_points, image_points = self.__resample_edges_of_triangle( simplices, indC, nDivide=nDivide) # shape (nDivide,2,nTriangles,2) # determine indices of broken segments (largest elements in CA and CB) len_segments = np.sum(np.diff(image_points, axis=0)**2, axis=-1) # shape (nDivide-1,2,nTriangle) largest_segments = np.argmax(len_segments, axis=0) # shape (2,nTriangle) edge_points = np.asarray((largest_segments, largest_segments + 1)) # shape (2,2,nTriangle) # set points p1 ... p4 for segmentation of triangle # see http://stackoverflow.com/questions/15660885/correctly-indexing-a-multidimensional-numpy-array-with-another-array-of-indices idx_tuple = (edge_points[..., np.newaxis], ) + tuple( np.ogrid[:2, :nTriangles, :2]) new_domain_points = domain_points[idx_tuple] new_image_points = image_points[idx_tuple] # shape (2,2,nTriangle,2), indicating iDistance,iEdge,iTriangle,(x/y) # update points in mesh (points are no longer unique!) logging.debug("refining_broken_triangles(): adding %d points" % (4 * nTriangles)) self.image = np.vstack((self.image, new_image_points.reshape(-1, 2))) self.domain = np.vstack((self.domain, new_domain_points.reshape(-1, 2))) if bPlot: from matplotlib.collections import PolyCollection fig = self.plot_triangulation(skip_triangle=is_broken) fig.suptitle("DEBUG: refine_broken_triangles()") ax1, ax2 = fig.axes #params = dict(facecolors='r', edgecolors='none', alpha=0.3); #ax1.add_collection(PolyCollection(self.domain[simplices],**params)); ax1.plot(domain_points[..., 0].flat, domain_points[..., 1].flat, 'k.', label='test points') ax1.plot(new_domain_points[..., 0].flat, new_domain_points[..., 1].flat, 'g.', label='selected points') ax1.legend(loc=0) ax2.plot(image_points[..., 0].flat, image_points[..., 1].flat, 'k.') ax2.plot(new_image_points[..., 0].flat, new_image_points[..., 1].flat, 'g.', label='selected points') # indices for points p1 ... p4 in new list of points self.domain # (offset by number of points in the original mesh!) # Note: by construction, the order of p1 ... p4 corresponds exactly to the order # shown above (first tuple contains points closest to C, # first on CA, then on CB, second tuple beyond the discontinuity) (p1, p2), (p3, p4) = np.arange(4 * nTriangles).reshape( 2, 2, nTriangles) + nPointsOrigMesh # shape (nTriangles,) # construct the five triangles from points t1 = np.vstack((C, p1, p2)) # shape (3,nTriangles) t2 = np.vstack((p1, p3, p2)) t3 = np.vstack((p2, p3, p4)) t4 = np.vstack((p3, A, p4)) t5 = np.vstack((p4, A, B)) new_simplices = np.hstack((t1, t2, t3, t4, t5)).T # shape (5*nTriangles,3), reshape as (5,nTriangles,3) to obtain subdivision of each triangle # DEBUG subdivision of triangles if bPlot: ax1.add_collection( PolyCollection(self.domain[t1.T], edgecolor='none', facecolor='b', alpha=0.3)) ax1.add_collection( PolyCollection(self.domain[t2.T], edgecolor='none', facecolor='r', alpha=0.3)) ax1.add_collection( PolyCollection(self.domain[t3.T], edgecolor='none', facecolor='y', alpha=0.3)) ax1.add_collection( PolyCollection(self.domain[t4.T], edgecolor='none', facecolor='g', alpha=0.3)) ax1.add_collection( PolyCollection(self.domain[t5.T], edgecolor='none', facecolor='k', alpha=0.3)) for t in bPlotTriangles: # select index of triangle to look at BCA = [B[t], C[t], A[t]] subdiv = [C[t], p1[t], p2[t], p3[t], p4[t], A[t]] pt = self.domain[BCA] ax1.plot(pt[..., 0], pt[..., 1], 'g') pt = self.image[BCA] ax2.plot(pt[..., 0], pt[..., 1], 'g') pt = self.domain[subdiv] ax1.plot(pt[..., 0], pt[..., 1], 'r') pt = self.image[subdiv] ax2.plot(pt[..., 0], pt[..., 1], 'r') # sanity check that total area did not change after segmentation old = np.sum(np.abs(self.get_area_in_domain(simplices))) new = np.sum(np.abs(self.get_area_in_domain(new_simplices))) assert (abs((old - new) / old) < 1e-10 ) # segmentation of triangle has no holes/overlaps # update list of simplices return self.__add_new_simplices(new_simplices, broken) def refine_invalid_triangles(self, nDivide=10, bPlot=False, bPlotTriangles=[0]): """ subdivide triangles which have one or two invalid vertices (x or y coordinate are np.nan) nDivide ... (opt) number of subdivisions of each side of triangle bPlot ... (opt) plot sampling and selected points for debugging bPlotTriangles (opt) list of triangle indices for which segmentation should be shown returns: number of new triangles Note: This function might also reuse refine_broken_triangles(), if we replace NaN's by a very large but finit number. However it might be less clean. Note: The resulting mesh will be no longer a Delaunay mesh (identical points might be present, circumference rule n ot guaranteed) and the total area in domain is reduced. """ vertices = self.image[self.simplices] # shape (nSimplices,3,2) bInvalidVertex = np.any(np.isnan(vertices), axis=2) # shape (nSimplices,3) if ~np.any(bInvalidVertex): return 0 # all valid: nothing to do if np.all(bInvalidVertex): # all invalid: can do nothing logging.warning('all rays are invalid') return 0 # we consider three cases: # 1. one vertex is invalid (generate two new triangles) # 2. two vertices are invalid (generate one new triangle) # 3. all vertices are invalid (triangle is skipped) # all triangles with only valid vertices are unchanged nInvalidVertices = np.sum(bInvalidVertex, axis=1) # shape (nSimplices) new_simplices = [] ind_case1 = nInvalidVertices == 1 new_simplices.extend( self.__subdivide_triangles_with_one_invalid_vertex( ind_case1, nDivide=nDivide)) ind_case2 = nInvalidVertices == 2 new_simplices.extend( self.__subdivide_triangles_with_two_invalid_vertices( ind_case2, nDivide=nDivide)) new_simplices = np.reshape(new_simplices, (-1, 3)) # update list of simplices bReplace = nInvalidVertices > 0 # includes case 1, 2 and 3 return self.__add_new_simplices(new_simplices, bReplace) def __subdivide_triangles_with_one_invalid_vertex(self, bInvalid, nDivide=10): """ case 1: one point is invalid (chosen as point C) adds new points p1 and p2 to mesh and returns new simplices C x x x x invalid points x x o new triangle vertices (first valid from C) x x p1 o o p2 / \ new triangles: /___________\ (p1,A,p2),(A,B,p2) A B """ if ~np.any(bInvalid): return [] # nothing to do simplices = self.simplices[bInvalid] # shape (nTriangles,3) triangles = self.image[simplices] # shape (nTriangles,3,2) nTriangles = triangles.shape[0] nPointsOrigMesh = self.image.shape[0] # find invalid point as C (index on first axis) and resample CA and CB indC = np.where(np.any(np.isnan(triangles), axis=-1))[1] A, B, C, domain_points, image_points = self.__resample_edges_of_triangle( simplices, indC, nDivide=nDivide) # shape (nDivide,2,nTriangles,2) assert (np.all(np.any(np.isnan(self.image[C]), axis=-1))) # all points C should be invalid # iterate over all triangles and subdivide them new_domain_points = [] new_image_points = [] new_simplices = [] for k in range(nTriangles): # find index of first valid point p1 on CA and p2 on CB ind = np.any(np.isnan(image_points[:, :, k]), axis=-1) # shape (nDivide,2) p1 = np.where(~ind[:, 0])[0][0] # first valid point on CA p2 = np.where(~ind[:, 1])[0][0] # first valid ponit on CB new_domain_points.extend( (domain_points[p1, 0, k, :], domain_points[p2, 1, k, :])) new_image_points.extend( (image_points[p1, 0, k, :], image_points[p2, 1, k, :])) # calculate index for points p1 and p2 in self.domain = [self.domain, new_domain_points] P1 = nPointsOrigMesh + 2 * k P2 = P1 + 1 new_simplices.extend(((P1, A[k], P2), (A[k], B[k], P2))) # update points in mesh (points are no longer unique!) logging.debug("refine_invalid_triangles(case1): adding %d points" % (2 * nTriangles)) self.image = np.vstack( (self.image, np.reshape(new_image_points, (2 * nTriangles, 2)))) self.domain = np.vstack( (self.domain, np.reshape(new_domain_points, (2 * nTriangles, 2)))) return np.reshape(new_simplices, (2 * nTriangles, 3)) def __subdivide_triangles_with_two_invalid_vertices( self, bInvalid, nDivide=10): """ case 2: two points are invalid (chosen as points A and B) C /\ / \ x invalid points / \ o new triangle vertices (last valid from C) / \ p1 o o p2 x x new triangle: xxxxxxxxxxxxxx (p1,p2,C) A B """ if ~np.any(bInvalid): return [] # nothing to do simplices = self.simplices[bInvalid] # shape (nTriangles,3) triangles = self.image[simplices] # shape (nTriangles,3,2) nTriangles = triangles.shape[0] nPointsOrigMesh = self.image.shape[0] # find valid point as C (index on first axis) and resample CA and CB indC = np.where(~np.any(np.isnan(triangles), axis=-1))[1] A, B, C, domain_points, image_points = self.__resample_edges_of_triangle( simplices, indC, nDivide=nDivide) # shape (nDivide,2,nTriangles,2) assert (np.all(np.any(np.isnan(self.image[A]), axis=-1))) # all points A should be invalid assert (np.all(np.any(np.isnan(self.image[B]), axis=-1))) # all points B should be invalid # iterate over all triangles and subdivide them new_domain_points = [] new_image_points = [] new_simplices = [] for k in range(nTriangles): # find index of first valid point p1 on CA and p2 on CB ind = np.any(np.isnan(image_points[:, :, k]), axis=-1) # shape (nDivide,2) p1 = np.where(~ind[:, 0])[0][-1] # last valid point on CA p2 = np.where(~ind[:, 1])[0][-1] # last valid ponit on CB new_domain_points.extend( (domain_points[p1, 0, k, :], domain_points[p2, 1, k, :])) new_image_points.extend( (image_points[p1, 0, k, :], image_points[p2, 1, k, :])) # calculate index for points p1 and p2 in self.domain = [self.domain, new_domain_points] P1 = nPointsOrigMesh + 2 * k P2 = P1 + 1 new_simplices.append((P1, P2, C[k])) # update points in mesh (points are no longer unique!) logging.debug("refine_invalid_triangles(case2): adding %d points" % (2 * nTriangles)) self.image = np.vstack( (self.image, np.reshape(new_image_points, (2 * nTriangles, 2)))) self.domain = np.vstack( (self.domain, np.reshape(new_domain_points, (2 * nTriangles, 2)))) return np.reshape(new_simplices, (nTriangles, 3)) def __resample_edges_of_triangle(self, simplices, indC, x=None, nDivide=10): """ generate dense sampling on edges CA and CB on given simplices Parameters ---------- simplices : ndarray of shape (nTriangles,3) vertex indices of triangles that should be resampled indC : vector of length nTriangles vertex number (mod 3) that should be used as point C x : vector of floats, optional indicates position of sampling points nDivide : integer, optional (only active if x=None) number of sampling points on CA and CB Returns ------- A,B,C : vector of ints, length (nTriangles) indices of points A,B,C domain_points : ndarray of shape (nDivide,2,nTriangle,2) sampling points along CA,CB in domain, indices are (iPoint,iSide,iTriangle,xy) image_points : ndarray of shape (nDivide,2,nTriangle,2) sampling points along CA,CB in image """ # handle optional arguments (sampling along CA and CB) if x is None: x = np.linspace(0, 1, nDivide, endpoint=True) else: x = np.asarray(x) nDivide = x.size # get indices of points ABC as shown above (C is isolated point) nTriangles = simplices.shape[0] ind_triangle = np.arange(nTriangles) C = simplices[ind_triangle, (indC) % 3] A = simplices[ind_triangle, (indC + 1) % 3] B = simplices[ind_triangle, (indC - 1) % 3] # create dense sampling along C->B and C->A in domain space CA = np.outer(1 - x, self.domain[C]) + np.outer(x, self.domain[A]) CB = np.outer(1 - x, self.domain[C]) + np.outer(x, self.domain[B]) # map sampling on CA and CB to image space domain_points = np.hstack((CA, CB)).reshape(nDivide, 2, nTriangles, 2) image_points = self.mapping(domain_points.reshape(-1, 2)).reshape( nDivide, 2, nTriangles, 2) return A, B, C, domain_points, image_points def __add_new_simplices(self, new_simplices, bReplace): """ add list of new simplices to Mesh and Replace old simplices indicated by boolean array perform sanity checks beforehand new_simplices ... shape(nTriangles,3) bReplace ... shape(self.simplices.shape[0]) returns: number of added triangles """ # remove degenerated triangles (p1,p2 identical to A or B) => area is 0 area = self.get_area_in_domain(new_simplices) degenerated = np.abs(area / self.initial_domain_area) < 1e-10 new_simplices = new_simplices[~degenerated] # remove degenerate triangles assert (np.all(area[~degenerated] > 0)) # by construction all triangles are oriented ccw # update simplices in mesh self.__tri = None # delete initial Delaunay triangulation self.simplices = np.vstack((self.simplices[~bReplace], new_simplices)) # no longer Delaunay return new_simplices.shape[0]