def at_same_location(V, now): """Checks whether all vertices are more or less at same location of first vertex in the list """ P = [v.position_at(now) for v in V] logging.debug(P) p = P[0] for o in P[1:]: if not (near_zero(p[0] - o[0]) and near_zero(p[1] - o[1])): return False return True
def replace_kvertex(t, v, newv, now, direction, queue, immediate): """Replace kinetic vertex at incident triangles Returns fan of triangles that were replaced """ logging.debug("replace_kvertex, start at: {0} [{1}] dir: {2}".format(id(t), t.info, direction)) fan = [] first = True while t is not None: # assert t.stops_at is None, "{}: {}".format( # id(t), [id(n) for n in t.neighbours]) logging.debug(" @ {} [{}]".format(id(t), t.info)) # FIXME: # if we have an event with the same time as now, # we should actually handle it logging.debug(t.event) if t.event is not None and near_zero(now - t.event.time): logging.debug(near_zero(now - t.event.time)) logging.debug(""" SAME SAME TIME... ARE WE PARALLEL? """) if t.event.tp == 'flip': logging.debug(t.neighbours[t.event.side[0]]) # -- can have become None # raw_input # if t.neighbours.count(None) < 2: logging.error('Error with current event -- as we do not handle flip now, we run the risk of inconsistency -- in fan: {0} $'.format(t.event)) # raw_input('paused $$') side = t.vertices.index(v) fan.append(t) t.vertices[side] = newv logging.debug( "Placed vertex #{} [{}] (inf fast? {}) at side {} of triangle {} [{}]".format( # id(newv), #repr(newv), id(newv), newv.info, newv.inf_fast, side, id(t), t.info # , repr(t) )) if newv.inf_fast and t.event is not None: # infinitely fast queue.discard(t.event) if t.event in immediate: immediate.remove(t.event) else: # vertex moves with normal speed replace_in_queue(t, now, queue, immediate) t = t.neighbours[direction(side)] return tuple(fan)
def eig_2x2(a, b, c, d): """Calculate eigenvalues and vectors of 2x2 matrix A, where: | a b | A = | | | c d | T = a + d D = a * d - b * c Eigenvalues: L1 = T/2 + (T**2/4 - D)**1/2 L2 = T/2 - (T**2/4 - D)**1/2 If c is not zero, then the eigenvectors are: | L1-d | | L2-d | | | , | | | c | | c | If b is not zero, then the eigenvectors are: | b | | b | | | , | | | L1-a | | L2-a | If both b and c are zero, then the eigenvectors are: | 1 | | 0 | | | , | | | 0 | | 1 | Note, the matrix is row-based! """ T = a + d D = a * d - b * c tmp = 0.25 * T**2 - D tmp = math.sqrt(max(0, tmp)) # prevent negative square root L1 = 0.5 * T + tmp L2 = 0.5 * T - tmp if not near_zero(c): # != 0 eig_vecs = (L1 - d, c), (L2 - d, c) elif not near_zero(b): # != 0 eig_vecs = (b, L1 - a), (b, L2 - a) else: eig_vecs = (1.0, 0.0), (0.0, 1.0) eig_vals = map(abs, [L1, L2]) # is taking the absolute value needed? return eig_vals, eig_vecs
def bisector(u1, u2): """Based on two unit vectors perpendicular to the wavefront, get the bisector The magnitude of the bisector vector represents the speed in which a vertex has to move to keep up (stay at the intersection of) the 2 wavefront edges """ direction = add(u1, u2) logging.debug(" direction: {}".format(direction)) d, acos_d = angle_unit(u1, u2) # if all(map(near_zero, direction)) or near_zero(acos_d - math.pi): # if all(map(near_zero, direction)) or (near_zero(acos_d - math.pi) or d < math.cos(math.radians(179.9999))): logging.debug( " vectors cancel each other out / angle ~180° -> parallel wavefront!" ) return (0, 0) #raise ValueError("parallel wavefront") ### logging.debug(" unit(direction): {}".format(unit(direction))) alpha = 0.5 * math.pi + 0.5 * acos_d logging.debug(" degrees(alpha): {}°".format(math.degrees(alpha))) magnitude = math.sin(alpha) logging.debug(" magnitude: {}".format(magnitude)) # print magnitude bisector = div(unit(direction), magnitude) logging.debug(" bisector: {}".format(bisector)) return bisector
def check_active_triangles_orientation(tri, now): for t in tri: if t is None: continue if t.stops_at is None: pos = [t.vertices[side].position_at(now) for side in range(3)] ori = orient2d(*pos) if all( isinstance(t.vertices[side], KineticVertex) for side in range(3)): assert ori >= 0 or near_zero( ori ), "triangle ({} [{}]) is not turning CCW @ t={}".format( id(t), t.info, now) else: assert ori <= 0 or near_zero( ori ), "inf-triangle ({} [{}]) is not turning CW @ t={}".format( id(t), t.info, now)
def intersection_type(self): # solve intersection # one, other = self.one, self.other assert len(one.w) == 2 assert len(other.w) == 2 # == 3 possible outcomes in 2D: # # 0. overlapping lines - always intersecting in a line # 1. crossing - point2 # 2. parallel - no intersection # (a1, b1), c1 = one.w, one.b (a2, b2), c2 = other.w, other.b denom = a1 * b2 - a2 * b1 # print(f'denom {denom}') # if denom == 0: # ^FIXME: use near_zero ? if near_zero(denom) == True: x1 = a1 * c2 - a2 * c1 x2 = b1 * c2 - b2 * c1 # print(f'cross1 {x1}') # print(f'cross2 {x2}') # if (x1 == 0) and (x2 == 0): # ^FIXME: use near_zero ? if near_zero(x1) == True and near_zero(x2) == True: # overlapping lines, always intersecting in this configuration self.result = self.one return LineLineIntersectionResult.LINE else: # parallel lines, but not intersecting in this configuration self.result = None return LineLineIntersectionResult.NO_INTERSECTION else: # crossing lines num1 = b1 * c2 - b2 * c1 num2 = a2 * c1 - a1 * c2 # print(denom, num1, num2) x, y, w = num1, num2, denom xw = x / w yw = y / w # print(f'point2 {xw}, {yw}') self.result = (xw, yw) return LineLineIntersectionResult.POINT
def check_bisector_direction(triangle, side, time): """Asserts that orientation of bisector is straight or turning left wrt unconstrained edge on its left""" v0, v1 = triangle.vertices[ccw(side)], triangle.vertices[cw(side)] pos2 = add(v1.position_at(time), v1.velocity) pos0, pos1 = v0.position_at(time), v1.position_at(time) ori = orient2d(pos0, pos1, pos2) assert ori > 0 or near_zero( ori ), "v0, v1: {}, {} | orientation: {} | positions: {}; {}; {}".format( v0.info, v1.info, orient2d(pos0, pos1, pos2), pos0, pos1, pos2) # left / straight
def regress(pts): """Return list of rotated points. The points are rotated in such a way that the direction of largest spread after rotation coincides with the positive x-axis, and the mean of the points is transformed to (0,0). If the original points formed a straight line, then the rotated points should have y-values that are all near zero. """ # split points in list of x- and y-values xs, ys = split(pts) # adjust x- and y-values (subtract the mean) _, xsadj = adjust(xs) _, ysadj = adjust(ys) # calculate variances (spread) in x- and y-direction # If one of the two variances is (nearly) 0, # we short circuit our logic # -> points then have to be on a straight line # (either horizontal or vertical) sx = variance(xsadj, m=0) if near_zero(sx): # == 0 return ysadj, xsadj sy = variance(ysadj, m=0) if near_zero(sy): # == 0 return xsadj, ysadj # calculate covariance sxy = cov(xsadj, ysadj, 0, 0) # get list of eigenvalues and vectors # these are sorted based on size of eigenvalues eig_sorted = sorted(zip(*eig_2x2(sx, sxy, sxy, sy)), reverse=True) vecs = [vec for _, vec in eig_sorted] newxs, newys = [], [] for pt in zip(xsadj, ysadj): # no need to transpose the vectors as they are xnew = vecs[0][0] * pt[0] + vecs[0][1] * pt[1] ynew = vecs[1][0] * pt[0] + vecs[1][1] * pt[1] newxs.append(xnew) # residuals?! # These should be squared distances to regression line that was fitted newys.append(ynew) return newxs, newys
def test_bump(self): """2 vertices that bump into each other half way""" u = KineticVertex() u.origin = (0., 0.) u.velocity = (1., 1.) v = KineticVertex() v.origin = (10., 10.) v.velocity = (-1., -1.) time = collapse_time_edge(u, v) assert time is not None dist = u.distance2_at(v, time) assert near_zero(dist)
def show_all_times(times, o, d, a): print "" print times for time in times: pa = o.position_at(time) pb = d.position_at(time) pc = a.position_at(time) collapse = near_zero(orient2d(pa, pb, pc)) print time, collapse if collapse: dists = [ o.distance2_at(d, time), d.distance2_at(a, time), a.distance2_at(o, time) ] if all_close(dists, abs_tol=1e-8): print "point", pa, pb, pc avg = [] for i in range(2): avg.append(sum(map(lambda x: x[i], (pa, pb, pc))) / 3.) print " avg ", tuple(avg) else: print "line", pa, pb, pc # to a segment, two separate points # 1 distance is near_zero! # pa+pb = pc # pa+pc = pb # pb+pc = pa X = all_close_clusters([x for x, y in (pa, pb, pc)]) Y = all_close_clusters([y for x, y in (pa, pb, pc)]) print " ", len(X) print " ", len(Y) print " ", X print " ", Y # FIXME: by looking at the distances and knowledge on what # side is what, we should be able to reliable decide what # is the event that happens to this triangle! if len(X) == 3 or len(Y) == 3: print "flip/split", dists else: assert len(X) == 2 or len(Y) == 2 print "collapse", dists # # to a line, three separate points # no distance is near_zero! # -> could be flip or split event print ""
def angle_unit(v1, v2): """angle between 2 *unit* vectors does not compute the norm(v1)*norm(v2), as it is assumed to be 1 """ d = dot(v1, v2) if d > 1.0 or d < -1.0: logging.debug("dot not in [-1, 1] -- clamp") d = max(-1.0, min(1.0, d)) acos_d = math.acos(d) logging.debug(" d : {}".format(d)) logging.debug(" acos(d) : {}".format(acos_d)) logging.debug(" › near zero? {}".format(near_zero(acos_d - math.pi))) logging.debug(" degrees(acos(d)): {}°".format(math.degrees(acos_d))) logging.debug(" › d < cos(179.999)? {}".format( d < math.cos(math.radians(179.9999)))) return d, acos_d
def test_crossing(self): """2 vertices crossing each other""" u = KineticVertex() u.origin = (0., 0.) u.velocity = (1., 1.) v = KineticVertex() v.origin = (10., 0.) v.velocity = (-1., 1.) time = collapse_time_edge(u, v) # with open("/tmp/cpa.csv","w") as fh: # for i, inc in enumerate(range(100, 150), start=1): # val = inc / 1000. # u.position_at(val) # v.position_at(val) # print >>fh, val,";", sqrt(u.distance2_at(v, val)) dist = u.distance2_at(v, time) assert near_zero(dist)
def check_direct(event): """ check the direct neighbours whether they will collapse """ current = event.triangle seen = set([current]) visit = [current] also = [] # FIXME: maybe we should use 'geometric inliers' to the support line here # this way we could also resolve 'flip event loops' while visit: current = visit.pop() for n in current.neighbours: if n is not None and n.event is not None and n not in seen: seen.add(n) if near_zero(n.event.time - event.time): also.append(n) visit.append(n) # elif n is not None and n.event is None: # # not sure -- should we visit 'uncollapsable' triangles to check if their neighbours can collapse? # seen.add(n) # visit.append(n) if also: logging.debug([n.info for n in also])
def check_direct(event): """ check the direct neighbours whether they will collapse """ current = event.triangle seen = set([current]) visit = [current] also = [] # FIXME: maybe we should use 'geometric inliers' to the support line here # this way we could also resolve 'flip event loops' while visit: current = visit.pop() for n in current.neighbours: if n is not None and n.event is not None and n not in seen: seen.add(n) if near_zero(n.event.time - event.time): also.append(n) visit.append(n) # elif n is not None and n.event is None: # # not sure -- should we visit 'uncollapsable' triangles to check if their neighbours can collapse? # seen.add(n) # visit.append(n) events = [event] + [n.event for n in also] if events and all([_.tp == 'flip' for _ in events]): logging.debug( "Only flip events, picking flip event with longest side, should guarantee progress" ) logging.debug(events) # # only flip events happening "now", pick event with longest side dist_events = [] for evt in events: # compute distance of side to flip orig, dest = Edge(evt.triangle, evt.side[0]).segment dist = orig.distance2_at(dest, evt.time) dist_events.append((dist, evt)) # take event with longest side dist_events.sort(reverse=True) return dist_events[0][1] else: return event
def compute_new_kvertex(ul, ur, now, sk_node, info, internal, pause=False): """Based on the two wavefront directions and time t=now, compute the velocity and position at t=0 and return a new kinetic vertex Returns: KineticVertex """ kv = KineticVertex() kv.info = info kv.starts_at = now kv.start_node = sk_node kv.internal = internal logging.debug('/=-= New vertex: {} [{}] =-=\\'.format(id(kv), kv.info)) logging.debug('bisector calc') logging.debug(' ul: {}'.format(ul)) logging.debug(' ur: {}'.format(ur)) logging.debug(' sk_node.pos: {}'.format(sk_node.pos)) # FIXME: only in pause mode! from grassfire.vectorops import angle_unit, add import math logging.debug(' >>> {}'.format(angle_unit(ul.w, ur.w))) u1, u2 = ul.w, ur.w direction = add(u1, u2) logging.debug(" direction: {}".format(direction)) d, acos_d = angle_unit(u1, u2) # FIXME: replace with new api: line.at_time(0).visualize() // line.at_time(t).visualize() if pause: with open('/tmpfast/support_lines.wkt', 'w') as fh: from grassfire.inout import interactive_visualize fh.write("wkt\tlr\toriginal") fh.write("\n") # -- bisector line -- # b = ul.bisector(ur) b = ul.translated(mul(ul.w,now)).bisector( ur.translated(mul(ur.w,now)) ) bperp = b.perpendicular(sk_node.pos) for l, lr, original in zip([ul, ur, ul.translated(mul(ul.w,now)), ur.translated(mul(ur.w,now)), b, bperp], ["l", "r", "l", "r", "b", "b"], [True, True, False, False, None, None]): fh.write(l.at_time(0).visualize()) fh.write("\t") fh.write(lr) fh.write("\t") fh.write(str(original)) fh.write("\n") # check for parallel wavefronts if all(map(near_zero, direction)) or near_zero(acos_d - math.pi) or d < math.cos(math.radians(179.999999)): logging.debug(" OVERRULED - vectors cancel each other out / angle ~180° -> parallel wavefront!") bi = (0, 0) else: from grassfire.line2d import LineLineIntersector, make_vector, LineLineIntersectionResult intersect = LineLineIntersector(ul, ur) tp = intersect.intersection_type() if tp == LineLineIntersectionResult.NO_INTERSECTION: bi = (0, 0) elif tp == LineLineIntersectionResult.POINT: pos_at_t0 = intersect.result ul_t = ul.translated(ul.w) ur_t = ur.translated(ur.w) intersect_t = LineLineIntersector(ul_t, ur_t) assert intersect_t.intersection_type() == 1 bi = make_vector(end=intersect_t.result, start=pos_at_t0) elif tp == LineLineIntersectionResult.LINE: # this would mean original overlapping wavefronts... # -> parallel at left / right of a kvertex # FIXME: would it be possible here to get to original position that defined the line? bi = tuple(ul.w[:]) neg_velo = mul(mul(bi, -1.0), now) pos_at_t0 = add(sk_node.pos, neg_velo) kv.velocity = bi #was: bisector(ul, ur) logging.debug(' kv.velocity: {}'.format(kv.velocity)) from grassfire.vectorops import norm magn_v = norm(kv.velocity) logging.debug(' magnitude of velocity: {}'.format(magn_v)) logging.debug('\=-= New vertex =-=/') # if magn_v > 1000000: # logging.debug(" OVERRULED - super fast vertex angle ~180° -> parallel wavefront!") # kv.velocity = (0,0) # compute where this vertex would have been at time t=0 # we set this vertex as infinitely fast, if velocity in one of the # directions is really high, or when the bisectors of adjacent # wavefronts cancel each other out if kv.velocity == (0, 0): ### or abs(kv.velocity[0]) > 100 or abs(kv.velocity[1]) > 100: kv.inf_fast = True kv.origin = sk_node.pos else: # neg_velo = mul(mul(kv.velocity, -1.0), now) # pos_at_t0 = add(sk_node.pos, neg_velo) kv.origin = pos_at_t0 kv.ul = ul kv.ur = ur return kv
def stop_kvertices(V, step, now, pos=None): """ Stop a list of kinetic vertices *V* at time *now*, creating a new node. If one of the vertices was already stopped before, at a node, use that skeleton node Returns tuple of (new node, False) in case all vertices are stopped for the first time, otherwise it returns (node, True) to indicate that were already stopped once. """ # precondition: # the kinetic vertices that we are stopping should be # at more or less the same location # assert at_same_location(V, now) sk_node = None logging.debug("stopping kinetic vertices, @t:={}".format(now)) for v in V: logging.debug(" - kv #{} [{}] inf-fast:={}".format(id(v), v.info, v.inf_fast)) for v in V: stopped = v.stops_at is not None time_close = near_zero(v.starts_at - now) logging.debug("Vertex starts at same time as now: {}".format(time_close)) logging.debug("Kinetic vertex is not stopped: {}".format(stopped)) # vertex already stopped if stopped: logging.debug("Stop_node of vertex") sk_node = v.stop_node elif time_close: logging.debug("Time close") # FIXME: for the parallel case this code is problematic # as start and end point will be at same time assert not stopped sk_node = v.start_node else: v.stops_at = now if sk_node is not None: logging.debug("Skeleton node already there") for v in V: v.stop_node = sk_node v.stops_at = now # FIXME: it is not always true that vertices stopped this way # are at the same location (they are close, but because of # numerical issues can be on slightly different location # assert at_same_location([v, sk_node], now) is_new_node = False else: if pos is None: logging.debug("Make new skeleton node") l = [v.position_at(now) for v in V] ct = len(l) sumx, sumy = 0., 0. for x, y in l: sumx += x sumy += y pos = sumx / ct, sumy / ct # for x, y in l: # dx, dy = near_zero(x - pos[0]), near_zero(y - pos[1]) # assert dx, x - pos[0] # assert dy, y - pos[1] else: logging.debug("Make new skeleton node - using external position: {}".format(pos)) sk_node = SkeletonNode(pos, step) for v in V: v.stop_node = sk_node is_new_node = True # post condition # all vertices do have a stop node and are stopped at a certain time for v in V: assert v.stop_node is not None assert v.is_stopped == True assert v.stops_at == now logging.debug("POINT({0[0]} {0[1]});sknode_new_pos".format(sk_node.pos)) # the geometric embedding of the vertex and its direction should correspond # only check when speed is relatively small ### if abs(v.velocity[0]) < 100 and abs(v.velocity[1]) < 100: ### d = dist( ### v.stop_node.position_at(v.stops_at), ### v.position_at(v.stops_at), ### ) ### assert is_close(d, 0.0, rel_tol=1e-2, abs_tol=1e-2, method="weak"), \ ### "mis-match between position of skeleton and position of kinetic vertex at time:" \ ### " {}; vertex {} [{}]; dist:={}, v.velocity={}".format(v.stops_at, id(v), v.info, d, v.velocity) # FIXME: # should all segments in the skeleton have length # or do we keep a topological tree of events (where nodes # can be embedded at same location) ??? # assert not at_same_location([v.start_node, v.stop_node], now), "stopped nodes should be different, but are not for {0}".format(id(v)) return sk_node, is_new_node
def handle_parallel_edge_event_3tri(t, e, pivot, now, step, skel, queue, immediate): logging.info("* parallel|even#3 :: tri>> #{} [{}]".format(id(t), t.info)) logging.debug('At start of handle_parallel_edge_event for 3 triangle') logging.debug("Edge with inf fast vertex collapsing! {0}".format(t.neighbours[e] is None)) # triangle is like: # *-------------------------* # `---------------*''''''' # we are collapsing the edge opposite of the inf fast pivot vertex # this assumes that v1 and v2 need to be on the same location!!! assert t.vertices.index(pivot) == e assert t.vertices[e] is pivot # assert pivot.inf_fast left_leg_idx = ccw(t.vertices.index(pivot)) left_leg = Edge(t, left_leg_idx) left_dist = dist(*map(lambda x: x.position_at(now), left_leg.segment)) v1 = t.vertices[ccw(e)] right_leg_idx = cw(t.vertices.index(pivot)) right_leg = Edge(t, right_leg_idx) right_dist = dist(*map(lambda x: x.position_at(now), right_leg.segment)) v2 = t.vertices[cw(e)] assert v1 is not pivot assert v2 is not pivot assert pivot in t.vertices assert v1 in t.vertices assert v2 in t.vertices from grassfire.vectorops import dot, norm logging.debug(v1.velocity) logging.debug(v2.velocity) magn_v1 = norm(v1.velocity) magn_v2 = norm(v2.velocity) logging.debug(' velocity magnitude: {}'.format([magn_v1, magn_v2])) dists = [left_dist, right_dist] logging.debug(' distances: {}'.format(dists)) dists_sub_min = [near_zero(_ - min(dists)) for _ in dists] logging.debug(dists_sub_min) # stop the non-infinite vertices at the same location # use the slowest moving vertex to determine the location if magn_v2 < magn_v1: sk_node, newly_made = stop_kvertices([v2], step, now) if newly_made: skel.sk_nodes.append(sk_node) v1.stop_node = sk_node v1.stops_at = now else: sk_node, newly_made = stop_kvertices([v1], step, now) if newly_made: skel.sk_nodes.append(sk_node) v2.stop_node = sk_node v2.stops_at = now # FIXME: # make edge between v1 and v2 # assert pivot.stop_node is None # assert pivot.stops_at is None #FIXME: wrong sk_node for pivot pivot.stop_node = sk_node pivot.stops_at = now # this is not necessary, is it? ## update_circ(pivot, v1, now) ## update_circ(v2, pivot, now) # we "remove" the triangle itself t.stops_at = now for kv in t.vertices: assert kv.stops_at is not None
def handle_parallel_fan(fan, pivot, now, direction, step, skel, queue, immediate, pause): """Dispatches to correct function for handling parallel wavefronts fan: list of triangles, sorted (in *direction* order) pivot: the vertex that is going infinitely fast now: time at which parallel event happens direction: how the fan turns skel: skeleton structure queue: event queue immediate: queue of events that should be dealt with when finished handling the current event pause: whether we should stop for interactivity """ if not fan: raise ValueError("we should receive a fan of triangles to handle them") return logging.debug(""" # --------------------------------------------------------- # -- PARALLEL event handler invoked -- # --------------------------------------------------------- """) if pause: logging.debug(" -- {}".format(len(fan))) logging.debug(" triangles in the fan: {}".format([(id(_), _.info) for _ in fan])) logging.debug(" fan turns: {}".format(direction)) logging.debug(" pivot: {} [{}]".format(id(pivot), pivot.info)) interactive_visualize(queue, skel, step, now) assert pivot.inf_fast first_tri = fan[0] last_tri = fan[-1] # special case, infinite fast vertex in *at least* 1 corner # no neighbours (3 wavefront edges) # -> let's collapse the edge opposite of the pivot if first_tri.neighbours.count(None) == 3: assert first_tri is last_tri #FIXME: is this true? dists = [] for side in range(3): edge = Edge(first_tri, side) d = dist(*map(lambda x: x.position_at(now), edge.segment)) dists.append(d) dists_sub_min = [near_zero(_ - min(dists)) for _ in dists] if near_zero(min(dists)) and dists_sub_min.count(True) == 1: logging.debug(dists_sub_min) logging.debug("Smallest edge collapses? {}".format(near_zero(min(dists)))) assert dists_sub_min.count(True) == 1 # assert dists_sub_min.index(True) == first_tri.vertices.index(pivot) side = dists_sub_min.index(True) pivot = first_tri.vertices[dists_sub_min.index(True)] handle_parallel_edge_event_even_legs(first_tri, first_tri.vertices.index(pivot), pivot, now, step, skel, queue, immediate) return else: handle_parallel_edge_event_3tri(first_tri, first_tri.vertices.index(pivot), pivot, now, step, skel, queue, immediate) return if first_tri is last_tri: assert len(fan) == 1 if direction is cw: left = fan[0] right = fan[-1] else: assert direction is ccw left = fan[-1] right = fan[0] left_leg_idx = ccw(left.vertices.index(pivot)) left_leg = Edge(left, left_leg_idx) if left.neighbours[left_leg_idx] is not None: logging.debug("inf-fast pivot, but not over wavefront edge? -- left side") left_dist = dist(*map(lambda x: x.position_at(now), left_leg.segment)) right_leg_idx = cw(right.vertices.index(pivot)) right_leg = Edge(right, right_leg_idx) if right.neighbours[right_leg_idx] is not None: logging.debug("inf-fast pivot, but not over wavefront edge? -- right side") right_dist = dist(*map(lambda x: x.position_at(now), right_leg.segment)) dists = [left_dist, right_dist] logging.debug(' distances: {}'.format(dists)) dists_sub_min = [near_zero(_ - min(dists)) for _ in dists] logging.debug(dists_sub_min) unique_dists = dists_sub_min.count(True) if unique_dists == 2: logging.debug("Equal sized legs") if len(fan) == 1: logging.debug("Calling handle_parallel_edge_event_even_legs for 1 triangle") handle_parallel_edge_event_even_legs(first_tri, first_tri.vertices.index(pivot), pivot, now, step, skel, queue, immediate) elif len(fan) == 2: # FIXME: even if the 2 wavefronts collapse and the sides are equal in size # some triangles can be partly inside the fan and partly outside # e.g. if quad collapses and 2 sides collapse onto each other, the spokes # can still stick out of the fan -> # result: triangles around these spokes should be glued together (be each others neighbours) # =======================+ # + \\\\\\\\\\\\\\\\\ # inf-----------------------------------------+ # + ///////////////// # =======================+ # handle_parallel_edge_event_even_legs(first_tri, first_tri.vertices.index(pivot), pivot, now, skel, queue, immediate) # handle_parallel_edge_event_even_legs(last_tri, last_tri.vertices.index(pivot), pivot, now, skel, queue, immediate) logging.debug("Calling handle_parallel_edge_event_even_legs for *multiple* triangles") # raise NotImplementedError('multiple triangles #{} in parallel fan that should be stopped'.format(len(fan))) all_2 = True for t in fan: # FIXME: left = cw / right = ccw seen from the vertex left_leg_idx = ccw(t.vertices.index(pivot)) left_leg = Edge(t, left_leg_idx) left_dist = dist(*map(lambda x: x.position_at(now), left_leg.segment)) right_leg_idx = cw(t.vertices.index(pivot)) right_leg = Edge(t, right_leg_idx) right_dist = dist(*map(lambda x: x.position_at(now), right_leg.segment)) dists = [left_dist, right_dist] dists_sub_min = [near_zero(_ - min(dists)) for _ in dists] logging.debug(" {}".format([left_dist, right_dist])) logging.debug(" {}".format(dists_sub_min)) unique_dists = dists_sub_min.count(True) if unique_dists != 2: all_2 = False # assert unique_dists == 2 if all_2 == True: for t in fan: handle_parallel_edge_event_even_legs(t, t.vertices.index(pivot), pivot, now, step, skel, queue, immediate) else: # not all edges in the fan have equal length, so first flip the triangles # before continue handling them # unpack the 2 triangles from the fan and flip them t0 = fan[0] t1 = fan[1] side0 = t0.neighbours.index(t1) side1 = t1.neighbours.index(t0) flip(t0, side0, t1, side1) # now if a triangle has inf-fast vertex, handle the wavefront collapse t0_has_inf_fast = [v.inf_fast for v in t0.vertices] t1_has_inf_fast = [v.inf_fast for v in t1.vertices] logging.debug(t0_has_inf_fast) logging.debug(t1_has_inf_fast) if True in t0_has_inf_fast: logging.debug("-- Handling t0 after flip event in parallel fan --") handle_parallel_edge_event_even_legs(t0, t0.vertices.index(pivot), pivot, now, step, skel, queue, immediate) if True in t1_has_inf_fast: logging.debug("-- Handling t1 after flip event in parallel fan --") handle_parallel_edge_event_even_legs(t1, t1.vertices.index(pivot), pivot, now, step, skel, queue, immediate) if pause: interactive_visualize(queue, skel, step, now) # raise NotImplementedError('multiple triangles in parallel fan that should be *flipped*') else: raise NotImplementedError('More than 2 triangles in parallel fan with 2 equal sized legs on the outside -- does this exist?') # post condition: all triangles in the fan are stopped # when we have 2 equal sized legs -- does not hold for Koch-rec-level-3 ??? if len(fan) == 1 or (len(fan) == 2 and all_2 == True): for t in fan: assert t.stops_at is not None else: # check what is the shortest of the two distances shortest_idx = dists_sub_min.index(True) if shortest_idx == 1: # right is shortest, left is longest logging.debug("CW / left wavefront at pivot, ending at v2, is longest") handle_parallel_edge_event_shorter_leg(right_leg.triangle, right_leg.side, pivot, now, step, skel, queue, immediate, pause) elif shortest_idx == 0: # left is shortest, right is longest logging.debug("CCW / right wavefront at pivot, ending at v1, is longest") handle_parallel_edge_event_shorter_leg(left_leg.triangle, left_leg.side, pivot, now, step, skel, queue, immediate, pause)
def choose_next_event(queue): """Choose a next event from the queue In case we have all flip events, we pick the longest edge to flip *Incorrect:* In case there are multiple events happening more or less at the same time we pick a non-flipping event first. -- This is not okay -> we do not handle items in their correct order, and hence this leads to problems """ # it = iter(queue) # first = next(it) # events = [first] # item = events[0] # queue.remove(item) # return item # # return the first item, sorted on type (non-flipping events first) # for item in sorted(events, key=sort_key): # break def sort_key(evt): """Key for sorting; first by event type, when these equal by time""" types = {'flip': 2, 'split': 1, 'edge': 0} # types = {'flip': 0, 'split': 1, 'edge': 2} # x = evt.triangle.vertices[0].position_at(evt.time)[0] # Do sort by event type (first split, than edge, than flip) # in case multiple same types, then sort on time, if time is exactly the same, we sort on info (stable id of kinetic triangle) # (otherwise we might miss events that should happen) return (types[evt.tp], evt.time, -evt.triangle.type, evt.triangle.info) it = iter(queue) first = next(it) if False: events = [first] for e in it: # if e.tp == "flip" and if near_zero(e.time - first.time): events.append(e) else: break logging.debug("Multiple Events to pick next event from ...") # logging.debug("\n - " + # "\n - ".join(map(str, events))) if len(events) == 1: # just one event happening now item = events[0] elif all([_.tp == 'flip' for _ in events]): logging.debug( "Only flip events, picking flip event with longest side, should guarantee progress" ) # # only flip events happening "now", pick event with longest side dist_events = [] for evt in events: # compute distance of side to flip orig, dest = Edge(evt.triangle, evt.side[0]).segment dist = orig.distance2_at(dest, evt.time) dist_events.append((dist, evt)) # take event with longest side dist_events.sort(reverse=True) item = dist_events[0][1] # item = events[0] else: ## pts = [] ## for evt in events: ## pts.extend([v.position_at(first.time) ## for v in evt.triangle.vertices]) ## on_straight_line = are_residuals_near_zero(pts) ## print pts ## for pt in pts: ## print "POINT({0[0]} {0[1]})".format(pt) ## metric = r_squared(pts) ## logging.debug('metric {0}'.format(metric)) ## # import numpy as np ## x = np.array([pt[0] for pt in pts]) ## y = np.array([pt[1] for pt in pts]) ## A = np.vstack([x, np.ones(len(x))]).T ## res = np.linalg.lstsq(A, y) ## print res # if not on_straight_line: # logging.debug('pick first event') # # points should be on straight line to exhibit infinite flipping # # if this is not the case, we return the first event # item = events[0] # else: # logging.debug('pick non-flip event (vertices on straight line)') ##item = events[0] # # return the first item, sorted on type (non-flipping events first) for item in sorted(events, key=sort_key): break ## FIXME: ## BUG: this code leads to problems in removing events from the queue ## my assumption is that: ## 1. we can navigate onto a triangle that already has been dealt with ## 2. the event on the triangle has not been updated properly (old event) def check_direct(event): """ check the direct neighbours whether they will collapse """ current = event.triangle seen = set([current]) visit = [current] also = [] # FIXME: maybe we should use 'geometric inliers' to the support line here # this way we could also resolve 'flip event loops' while visit: current = visit.pop() for n in current.neighbours: if n is not None and n.event is not None and n not in seen: seen.add(n) if near_zero(n.event.time - event.time): also.append(n) visit.append(n) # elif n is not None and n.event is None: # # not sure -- should we visit 'uncollapsable' triangles to check if their neighbours can collapse? # seen.add(n) # visit.append(n) events = [event] + [n.event for n in also] if events and all([_.tp == 'flip' for _ in events]): logging.debug( "Only flip events, picking flip event with longest side, should guarantee progress" ) logging.debug(events) # # only flip events happening "now", pick event with longest side dist_events = [] for evt in events: # compute distance of side to flip orig, dest = Edge(evt.triangle, evt.side[0]).segment dist = orig.distance2_at(dest, evt.time) dist_events.append((dist, evt)) # take event with longest side dist_events.sort(reverse=True) return dist_events[0][1] else: return event evt = first queue.remove(evt) # evt = check_direct(evt) # try: # queue.remove(evt) # except: # print(evt) # raise return evt