# Implementation of module {contact} # Last edited on 2021-10-31 04:40:01 by stolfi import contact import move import move_parms import path import block import hacks; from hacks import fbomb import rn import pyx from math import nan, inf, sqrt import sys class Contact_IMP: # The endpoints of the contact are {ct.pt[0]} and {ct.pt[1]}, in # arbitrary order. # # The contact is between traces {ctside[0]} and {ctside[1]}, # two distinct unoriented {Move} objects that are traces (not jumps). # # The field {ct.tcov} is a pair (2-tuple) of times, such # that {ct.tcov[isd]} is the time for the nozzle to go past the midpont of # the contact when tracing the move {ctside[isd]} in its native direction. def __init__(self, p0, p1, mv0, tcov0, mv1, tcov1): # Read-only fields: self.pts = (p0, p1) # Endpoints. self.side = (mv0, mv1) # {Move} objects on each side. self.tclim = +inf # Max allowed cooling time. self.tcov = (tcov0, tcov1) # {otcov[isd]] is fabtime from start of side {isd} to contact midpoint. self.name = None # Name of contact, for debugging. # Mutable fields: self.sdpaths = [set(), set()] # {side_paths[isd]} is a set of triples {(ph,dr,imv)} for the paths that contain {side[isd]}. self.blocks = [None, None] # Two {Block} objects that contain the sides of this contact. def make(p0, p1, omv0, omv1): assert hacks.is_point(p0); p0 = tuple(p0) # Make sure it is immutable. assert hacks.is_point(p1); p1 = tuple(p1) # Make sure it is immutable. mv0,dr0 = move.unpack(omv0); assert not move.is_jump(mv0) mv1,dr1 = move.unpack(omv1); assert not move.is_jump(mv1) assert mv0 != mv1 m = rn.mix(0.5, p0, 0.5, p1) tcov0 = move.cover_time(mv0, m) tcov1 = move.cover_time(mv1, m) ct = contact.Contact(p0, p1, mv0, tcov0, mv1, tcov1) # Set a default name: if move.has_name(mv0) and move.has_name(mv1): na0 = move.get_name(mv0) na1 = move.get_name(mv1) set_name(ct, "C(%s:%s)" % (na0,na1)) return ct def endpoints(ct): assert isinstance(ct, contact.Contact) return ct.pts def tcool_limit(ct): return ct.tclim def set_tcool_limit(ct, tclim): assert type(tclim) is float or type(tclim) is int, "invalid {tclim}" tclim = float(tclim) assert tclim >= 0.001, "{tclim} too small" ct.tclim = tclim def pmid(ct): return rn.mix(0.5, ct.pts[0], 0.5, ct.pts[1]) def side_move(ct, isd): return ct.side[isd] def side_moves(ct): return ct.side def side_tcov(ct, isd): return ct.tcov[isd] def which_side(omv, ct): mv, dr = move.unpack(omv) for isd in range(2): if ct.side[isd] == mv: return isd return None def from_moves(omv0, omv1, mvdir, szmin, rszmin, tol): if move.is_jump(omv0) or move.is_jump(omv1): return None mv0,dr0 = move.unpack(omv0) mv1,dr1 = move.unpack(omv1) if mv0 == mv1: return None p0, p1 = move.shared_border(omv0, omv1, mvdir, tol) if p0 == None: return None assert p1 != None dp = rn.dist(p0, p1) if dp == 0: return None if dp < szmin: return None L0 = move.length(omv0) L1 = move.length(omv1) if rszmin > 0 and dp < rszmin*min(L0,L1): return None ct = make(p0, p1, omv0, omv1) return ct # ---------------------------------------------------------------------- def from_move_lists(OMVS0, OMVS1, szmin, rszmin, tol, ydir): assert type(OMVS0) is tuple or type(OMVS0) is list assert type(OMVS1) is tuple or type(OMVS1) is list sys.stderr.write("{from_move_lists} OMVS0 = %d OMVS1 = %d\n" % (len(OMVS0),len(OMVS1))) MVPAIRS = set() # Sets of distinct sorted move pairs from the two lists CTS = [ ] for imv0 in range(len(OMVS0)): for imv1 in range(len(OMVS1)): omv0 = OMVS0[imv0] omv1 = OMVS1[imv1] omv0, omv1 = move.sort_by_midpoint(omv0, omv1, ydir) mv0, dr0 = move.unpack(omv0) mv1, dr1 = move.unpack(omv1) mvpair = (mv0,mv1) if mv0 != mv1 and not (mvpair in MVPAIRS): mvdir = None # Let the procedure chose a {mvdir} aligned with the moves themselves. ct = from_moves(mv0, mv1, mvdir, szmin, rszmin, tol) # Already sorted by {ydir} if ct != None: CTS.append(ct) MVPAIRS.add(mvpair) # Set their names, if not defined: for ict in range(len(CTS)): ct = CTS[ict] if not has_name(ct): set_name(ct, "C%d" % ict) return CTS # ---------------------------------------------------------------------- def from_paths(oph0, oph1, szmin, rszmin, tol, ydir): CTS = from_path_lists([oph0,], [oph1,], szmin, rszmin, tol, ydir) return CTS # ---------------------------------------------------------------------- def from_path_lists(OPHS0, OPHS1, szmin, rszmin, tol, ydir): # Collect the two sets of {Move} objects: OMVS = [set(), set()] for iph in range(2): for oph in (OPHS0,OPHS1)[iph]: for kmv in range(path.nelems(oph)): mvk, drk = move.unpack(path.elem(oph,kmv)) OMVS[iph].add(mvk) # Convert to list in a fixed order: OMVS[iph] = list(OMVS[iph]) OMVS[iph].sort(key = move.pini) # create the contacts: CTS = from_move_lists(OMVS[0], OMVS[1], szmin, rszmin, tol, ydir) # Add the contact-path information: for ct in CTS: for oph in OPHS0 + OPHS1: for imv in range(path.nelems(oph)): omv = path.elem(oph, imv) mv, dr = move.unpack(omv) for isd in range(2): if side_move(ct, isd) == mv: path.add_contact(oph, isd, ct) # It is a set, so no problem if repeated {ct}. add_side_path(ct, isd, oph, imv) # It is a set, so no problem if repeated {Path} object. return CTS # ---------------------------------------------------------------------- def from_blocks(bc0, bc1, szmin, rszmin, tol, ydir): # Collect the choices: OPHS = [None,None] for ibs in range(2): bci = (bc0, bc1)[ibs] nch = block.nchoices(bci) OPHS[ibs] = [ block.choice(bci,ich) for ich in range(nch) ] # Now get the contacts: CTS = from_path_lists(OPHS[0],OPHS[1], szmin,rszmin, tol, ydir) return CTS # ---------------------------------------------------------------------- def bbox(CTS): B = None for ct in CTS: B = rn.box_include_point(B, ct.pts[0]) B = rn.box_include_point(B, ct.pts[1]) return B # ---------------------------------------------------------------------- def path_ixcovs(oph, ct): ixs = [None, None] n = path.nelems(oph) for imv in range(n): omv = path.elem(oph, imv) mv, dr = move.unpack(omv) for isd in range(2): if side_move(ct, isd) == mv: assert ixs[isd] == None, "repeated move in path" ixs[isd] = imv return tuple(ixs) # ---------------------------------------------------------------------- def is_relevant(ct, BCS, ich): assert ich == None or type(ich) is int assert type(BCS) is list or type(BCS) is tuple assert isinstance(ct, contact.Contact) for bc in BCS: nch = nchoices(bc) if ich == None or ich < nch: choices = (ich,) if ich != None else range(nch) for jch in choices: oph = choice(bc, jch) ixs = path_ixcovs(oph, ct) if ixs[0] != None or ixs[1] != None: return True return False # ---------------------------------------------------------------------- def path_tcov(oph, imv, ct, isd): assert isinstance(ct, contact.Contact) ph, dr_ph = path.unpack(oph) # For the typechecking. if imv == None: tc = None else: mv = side(ct, isd) omv = path.elem(oph, imv) mv, dr = move.unpack(omv) if dr == 0: tc = path.tini(oph, imv) + ct.tcov[isd] else: tc = path.tfin(oph, imv) - ct.tcov[isd] return tc def path_tcovs(oph, ct): ixs = path_ixcovs(oph, ct) assert len(ixs) == 2 tcs = [ None, None ] for isd in range(2): imv = ixs[isd] if imv != None: omv = path.elem(oph, imv) mv, dr = move.unpack(omv) if dr == 0: tcs[isd] = path.tini(oph, imv) + ct.tcov[isd] else: tcs[isd] = path.tfin(oph, imv) - ct.tcov[isd] return tuple(tcs) # ---------------------------------------------------------------------- # PATHS ASSOCIATED TO CONTACT SIDE def clear_side_paths(ct, isd): assert isinstance(ct, contact.Contact) ct.sdpaths[isd] = set() # ---------------------------------------------------------------------- def add_side_path(ct, isd, oph, imv): assert isinstance(ct, contact.Contact) mv = ct.side[isd] if imv == None: imv = path.find_move(oph, mv) assert imv != None, "path does not cover side {isd} of {ct}" else: omv_c = path.elem(oph, imv) mv_c, dr_c = move.unpack(omv_c) assert mv_c == mv, "contact side is not the specified move" ph, dr = path.unpack(oph) ct.sdpaths[isd].add((ph,dr,imv)) # ---------------------------------------------------------------------- def get_side_paths(ct, isd): assert isinstance(ct, contact.Contact) return frozenset(ct.sdpaths[isd]) # ---------------------------------------------------------------------- def check_side_paths(CTS,OPHS,ydir): def showbug(oph, ct, msg): path.show(sys.stderr, " oph = ", oph, "\n", True, 0,0,0); sys.stderr.write("ct =\n"); show(sys.stderr, " ct = ", ct, "\n", 0); assert False, msg # .................................................................... # Check from {get.side_paths} PHS_org = set((path.unpack(oph))[0] for oph in OPHS) for ct in CTS: for isd in range(2): mvi = side_move(ct, isd) for phj, drj, imvj in get_side_paths(ct, isd): if not (phj in PHS_org): showbug(phj, ct, "contact refer to path not in {OPHS}") if not ct in path.get_contacts(phj,isd): showbug(phj, ct, "contact not in contacts of its attached path") ophj = path.spin(phj, drj) mvk, drk = move.unpack(path.elem(ophj, imvj)) if mvk != mvi: showbug(ophj, ct, "atached path/move do not match contact's side") # Check from {path.get_contacts}: CTS_org = set(CTS) for oph in OPHS: ph, dr = path.unpack(oph) for isd in range(2): for ct in path.get_contacts(oph, isd): if not ct in CTS_org: showbug(oph, ct, "path refers to contact not in {CTS}") mvi = side_move(ct, isd) imv = path.find_move(oph, mvi) if imv == None: showbug(oph, ct, "side of attached contact does not occur in path") if not (ph, dr, imv) in get_side_paths(ct, isd): showbug(oph, ct, "attached path/moves do not include a path of {OPHS}") if ydir != None: # Check Y-order of moves: for ct in CTS: mv0 = contact.side_move(ct, 0) mv1 = contact.side_move(ct, 1) # Compute move midpoints: pm0 = rn.mix(0.5, move.pini(mv0), 0.5, move.pfin(mv0)) pm1 = rn.mix(0.5, move.pini(mv1), 0.5, move.pfin(mv1)) # Check order of projections: assert rn.dot(pm0, ydir) <= rn.dot(pm1, ydir) return # ---------------------------------------------------------------------- # COOLING ESTIMATORS def tcool_closed(ct, tcs): # Returns the cooling time ratio of {ct} in some path {oph} that closes {ct}, # given the corresponding cover times {tcs[0..1]}. Fails if either of # the times is {None}. assert type(tcs) is list or type(tcs) is tuple assert len(tcs) == 2 assert tcs[0] != None and tcs[1] != None tc = abs(tcs[0] - tcs[1]) return tc # ---------------------------------------------------------------------- def tcool(oph, ct): tcs = path_tcovs(oph, ct) if tcs[0] == None or tcs[1] == None: tc = None else: tc = tcool_closed(ct, tcs) return tc # ---------------------------------------------------------------------- def est_rcool_closed_by_path(ct, i0, oph0, tc0, oph1, tc1, mp_jump): # Returns a lower bound for the cooling time ratio of {ct} in some path that includes {oph0} and {oph1}, # in that order and direction; where {oph0} and {oph1} cover sides {i0} and {1-i0} of {ct}, respectively. # # Assumes that {tc0} is the fabtime of {oph0} from the midpoint of # {ct} to the end of {oph0}, and {tc1} is the fabtime of {oph1} from # the beginning to the midpoint of {ct}. Fails if either is {None}. tc_lim = tcool_limit(ct) if tc_lim == +inf: return 0 use_links = True tconn_est = path.connection_time(oph0, oph1, use_links, mp_jump) tcool_min = tc0 + tconn_est + tc1 rcool_min = tcool_min/tc_lim return rcool_min # ---------------------------------------------------------------------- def est_rcool_closed_by_other_paths(ct, oph, isd, tc, mp_jump, quick): # Assumes that the oriented path {oph} covers side {isd} of {ct}. # Returns a lower bound for the cooling time ratio of {ct} among all # paths {P} that start with {oph} and include some paths {oph1} among # the paths that have been associated to side {1-isd} of {ct}. The # procedure fails if there are no such paths. # # Assumes that {tc} (which must not be {None}) is the fabtime of # {oph} from the midpoint of {ct} to the end of {oph}. # # The {quick} parameter has the same meaning as in {est_rcool}. tc_lim = tcool_limit(ct) if tc_lim == +inf: return 0 if quick and tc > tc_lim: return +inf # Time to finish {oph} already exceeds limit. # Compute the min fabtime {tc1} to close {ct} with some choice of {bc} after finishing {oph}: OPHS1 = get_side_paths(ct, 1-isd) rcool_min = +inf for oph1 in OPHS1: tcs1 = path_tcovs(oph1, ct) assert tcs1[isd] == None and tcs1[1-isd] != None tc1 = tcs1[1-isd] rcool_min = min(rcool_min, est_rcool_closed_by_path(ct, isd, oph, tc, oph1, tc1, mp_jump)) assert rcool_min < +inf, "no paths in {get_side_paths} closes {ct}" return rcool_min # ---------------------------------------------------------------------- def est_rcool(oph, ct, mp_jump, quick): tclim = tcool_limit(ct) if tclim == +inf: rc = 0 else: tcs = path_tcovs(oph, ct) assert type(tcs) is list or type(tcs) is tuple assert len(tcs) == 2 if tcs[0] != None and tcs[1] != None: # Contact {ct} is closed by {oph}, the exact cooling time is known: tc = tcool_closed(ct, tcs) rc = tc/tclim elif tcs[0] != None or tcs[1] != None: # Only one side of {ct} is covered bt {oph}. # Get the fabtime from {ct} to the end of {oph}: isd = 0 if tcs[0] != None else 1 tc0 = path.fabtime(oph) - tcs[isd] # Compute the cloosing time ratio for the best choice of {bc} rc = est_rcool_closed_by_other_paths(ct, oph, isd, tc0, mp_jump, quick) else: rc = 0 return rc # ---------------------------------------------------------------------- def est_max_rcool(oph, CTS, mp_jump, quick): assert type(CTS) is list or type(CTS) is tuple est_max_rc = 0 for ct in CTS: rc_ct = est_rcool(oph, ct, mp_jump, quick) est_max_rc = max(est_max_rc, rc_ct) if quick and est_max_rc > 1: est_max_rc = +inf; break return est_max_rc # ---------------------------------------------------------------------- def plot_to_files(fname, CTS, clr, dashpat, ext, OPHS, CLRS, rwd, wd_axes, tics, arrows): assert type(CTS) is list or type(CTS) is tuple assert type(OPHS) is list or type(OPHS) is tuple assert len(OPHS) > 0 # Compute the plot's bounding box: B = path.bbox(OPHS) B = rn.box_join(B, bbox(CTS)) dp = None # No frame, because it may confuse with contour: c, szx, szy = hacks.make_canvas(hacks.round_box(B,0.5), None, None, False, True, 1, 1) # Plot the paths: axes = True dots = True arrows_ph = True matter = True path.plot_standard(c, OPHS, None, None, CLRS, rwd, wd_axes, axes, dots, arrows_ph, matter) # Plot the contacts: wd_ct = 1.5*wd_axes sz_tics = wd_ct if tics else 0 arrows_ct = arrows for ct in CTS: plot_single(c, ct, None, clr, dashpat, ext, wd=wd_ct, sz_tic=sz_tics, arrow=arrows_ct) hacks.write_plot(c, fname) return # ---------------------------------------------------------------------- def plot_single(c, ct, dp, clr, dashpat, ext, wd, sz_tic, arrow): p = ct.pts[0] q = ct.pts[1] u, distpq = rn.dir(rn.sub(q, p)) if distpq < 1.0e-6: if ext != 0: sys.stderr.write("!! warning: cannot extend zero-length contact") # Perturb the points slightly: peps = 0.01*wd p = (p[0]-peps, p[1]-peps) q = (q[0]+peps, q[1]+peps) elif ext != 0: p = rn.mix(1,p, -ext,u) q = rn.mix(1,q, +ext,u) hacks.plot_line(c, p, q, dp, clr, wd, dashpat) # Should we plot a transversal tic or arrowhead? if sz_tic == None: sz_tic = 0 # Simplification. if sz_tic > 0 or arrow: # Plot the transversal tic or arrowhead: m = rn.mix(0.5, p, 0.5, q) # Midpoint. u = get_perp_dir(m, ct.side[0], ct.side[1]) sz_arrow = 3*wd if arrow else 0 # We need a tic with a certain min size for the arrowhead: sz_tic = max(sz_tic, 0.80*sz_arrow) a = rn.mix(1.0, m, -0.5*sz_tic, u) b = rn.mix(1.0, m, +0.5*sz_tic, u) sty_basic = [ pyx.style.linecap.round, clr, ] sty_tic = sty_basic if sz_arrow > 0: # Add the arrowhead to the tic. arrowpos = 0.5 # Position of arrow on transversal line. wd_arrow = sz_arrow/5 # Linewidth for stroking the arrowhead (guess). sty_arrow = sty_basic + [ pyx.deco.stroked([pyx.style.linewidth(wd_arrow), pyx.style.linejoin.round]), pyx.deco.filled([]) ] sty_tic = sty_tic + \ [ pyx.deco.earrow(sty_arrow, size=sz_arrow, constriction=None, pos=arrowpos, angle=35) ] sys.stderr.write("sz_arrow = %.3f wd_arrow = %3f sz_tic = %.3f\n" % (sz_arrow, wd_arrow, sz_tic)) sty_tic = sty_tic + [ pyx.style.linewidth(wd), ] if dp != None: sty_tic.append(pyx.trafo.translate(dp[0], dp[1])) c.stroke(pyx.path.line(a[0], a[1], b[0], b[1]), sty_tic) return # ---------------------------------------------------------------------- def get_perp_dir(m, omv0, omv1): # Returns the direction from trace {mv0} towards trace{mv1} # at the point {m}, assumed to be the midpoint of a contact # between them. sys.stderr.write("m = ( %.3f %.3f )\n" % ( m[0], m[1],)) assert hacks.is_point(m) mv0, dr0 = move.unpack(omv0) mv1, dr1 = move.unpack(omv1) assert mv0 != mv1, "both sides on same move?" a = [None,None] for imv in range(2): mvi = (mv0,mv1)[imv] p0i, p1i = move.endpoints(mvi) r = min(1, max(0, rn.pos_on_line(p0i, p1i, m))) # Nearest rel pos in move to {m} a[imv] = rn.mix(1-r, p0i, r, p1i) assert a[0] != a[1] sys.stderr.write("a = ( %.3f %.3f ) ( %.3f %.3f )\n" % ( a[0][0], a[0][1], a[1][0], a[1][1],)) u, da = rn.dir(rn.sub(a[1],a[0])) return u # ---------------------------------------------------------------------- def has_name(ct): return ct.name != None # ---------------------------------------------------------------------- def get_name(ct): assert isinstance(ct, contact.Contact) name = ct.name if name == None: name = "C?" return name # ---------------------------------------------------------------------- def set_name(ct, name): assert type(name) is str ct.name = name return # ---------------------------------------------------------------------- def tag_names(CTS, tag): if tag != None and tag != "": assert type(tag) is str for ct in CTS: ct.name = tag + get_name(ct) return # ---------------------------------------------------------------------- def compare(ct0, ct1, tol, die): # Check endpoints: ends0 = endpoints(ct0) ends1 = endpoints(ct1) for ipt in range(2): if not hacks.same_point(ends0[ipt], ends1[ipt], tol): return fbomb(die) for isd in range(2): # Check moves on side {isd}: omv0 = side_move(ct0, isd) omv1 = side_move(ct1, isd) if not hacks.same_point(move.pini(omv0), move.pini(omv1), tol): return fbomb(die) if not hacks.same_point(move.pfin(omv0), move.pfin(omv1), tol): return fbomb(die) # Check paths on side {isd} OPHS0 = get_side_paths(ct0, isd) OPHS1 = get_side_paths(ct1, isd) assert len(OPHS0) == len(OPHS1) # ??? Should compare the paths ??? return True # ---------------------------------------------------------------------- def show(wr, pref, ct, suff, wna): if pref != None: wr.write(pref) wr.write("%-*s" % (wna,get_name(ct))) # Endpoints: pts = endpoints(ct) for ipt in range(2): pti = pts[ipt] wr.write(" ( %8.3f, %8.3f )" % (pti[0], pti[1])) tclim = tcool_limit(ct) xtclim = "" if tclim == +inf else ("%.1f" % tclim) wr.write(" %5s" % xtclim) # Side moves and side paths: for isd in range(2): wr.write(" ") mvi = ct.side[isd] wr.write(move.get_name(mvi)) SDPSi = contact.get_side_paths(ct, isd) if len(SDPSi) > 0: wr.write("@{") sep = '' for phi, dri, imvi in SDPSi: ophi = path.spin(phi,dri) wr.write("%s%s" % (sep, path.get_name(ophi))) mvj, drj = move.unpack(path.elem(ophi,imvi)) xrev = "~" if drj == 1 else "" wr.write("[%s%d]" % (xrev,imvi)) if mvi != mvj: sys.stderr.write("\n") move.show(sys.stderr, "mvi = ", mvi, "\n", 0) path.show(sys.stderr, "ophi = ", ophi, "\n", True, 0,0,0) move.show(sys.stderr, "mvj = ", mvj, "\n", 0) assert mvj == mvi, "inconsistent move index in side path" sep = ',' wr.write("}") if suff != None: wr.write(suff) return # ---------------------------------------------------------------------- def show_list(wr, pref, CTS, suff): assert type(CTS) is list or type (CTS) is tuple nct = len(CTS) if nct == 0: return wna = 4 # Width of "name" column; min 4 because of the header. for ct in CTS: wna = max(wna, len(get_name(ct))) wix = len(str(nct-1)) # Num digits in index. wr.write("\n") # Write header: wpt = 22 # Width of a point coordinates column. wr.write("%*s%*s %-*s %*s %*s %*s sides\n" % (len(pref),'',wix,"k",wna,"name",wpt,"pini",wpt,"pfin",5,"tclim")) wr.write("%*s%s %s %s %s %s --------------\n" % (len(pref),'',"-"*wix,"-"*wna,"-"*wpt,"-"*wpt,"-"*5)) # Write contacts: for kct in range(len(CTS)): ct = CTS[kct] if pref != None: wr.write(pref) wr.write("%*d " % (wix,kct)) show(wr, None, ct, suff, wna) wr.write("\n") wr.write("\n") return # ----------------------------------------------------------------------