#! /usr/bin/python3 # Last edited on 2026-11-02 14:10:22 by stolfi # Like {bitemplate_match_with_py_funcs.py} but offloads the computation # to an external C program. import os, sys, re from sys import stderr as err from math import inf, isfinite, floor import datetime as dtt from process_funcs import run_command, bash import bitemplate_funcs as btf import bimatching_funcs as bmf # For tests: import bitemplate_match_test_funcs as bmtestf def match_bitemplate(text0, text1, bitemp, eva_per_hanzi, debug_level): # Matches two given strings {text0,text1} to a bitemplate {bitemp}. If # there is at least one possible bimatching, chooses the one with the # lowest badness score. Otherwise returns an infinite badness score # and no bimatching. # # Returns the bimatching {opt_bimatch} that yielded the smallest # {score}, as well as its score. # # If there are no possible bimatchings, returns {None,+inf}. # # See {match_bitemplate} in {bitemplate_match_with_py_funcs.py} for # the definitions of "bitemplate", "bipattern", "variant", "rung" # and "bimatching". # # This version uses an external C program to actually compute the optimum # bimatching. The badness score of each hit is taken from {bitmep}. # The badness score of each gap is defined in that C program, and may vary # somwehat from the score computed by the python version of this procedure. # debug_all = (text1[:15] == "pokararkeeyokee") # debug_all = (text1[:13] == "poarkeeodaiin") debug_top = debug_level >= 4 # debug_tableau = debug_level >= 8 # debug_plane = debug_level >= 6 # debug_vertex = debug_level >= 10 # debug_edge = debug_level >= 20 verbose = True nt = (len(text0), len(text1),) # Lengths of the texts. nr = len(bitemp) # Number of bipatterns. nh = nr; ng = nh + 1; ns = ng + nh # Number of hits, gaps, segs. assert ns >= 1 if debug_top: err.write(f"!~ enter match_bitemplate\n") err.write(f"!~ {nr = } {nh = } {ng = } {ns = }\n") err.write(f"!~ {nt[0] = :3d} {text0 = !r}\n") err.write(f"!~ {nt[1] = :3d} {text1 = !r}\n") bimatch = None; score = +inf # Get the candidate hits for the template {bitemp}: hcand_lists = btf.get_hit_candidate_lists(bitemp, text0, text1) if hcand_lists is not None and len(hcand_lists[nh-1]) > 0: # Write the candidate lists to a temp file: cdate = dtt.datetime.now(dtt.UTC).isoformat(timespec='seconds', sep='-') cdate = cdate.replace("+00:00", "") prefix_data = f"/tmp/{cdate}-hcands" prefix_result = f"/tmp/{cdate}-bimatch" bash("rm -f /tmp/*-hcands*.* /tmp/*-bimatch*.*") write_hcand_lists(prefix_data, hcand_lists) # Call the external program: prog = "optimal_bimatch" command = [ prog, "-textSizes", f"{len(text0)}", f"{len(text1)}", "-hits", f"{nh}", "-sizeScale", f"{eva_per_hanzi:.5f}", "-debugLevel", f"{debug_level}", "-inPrefix", prefix_data, "-outPrefix", prefix_result ]; err.write(f"{command = !r}\n") run_command(command) # Read the bimatch: bimatch, score = read_bimatch(prefix_result, bitemp, nt) bmf.check_bimatch(bimatch, bitemp, text0, text1) if debug_top: err.write(f"!~ FINAL {score = :.8f}\n") if debug_top: err.write("!~ exit match_bitemplate\n") return bimatch, score # ---------------------------------------------------------------------- def write_hcand_lists(prefix_data, hcand_lists): nh = len(hcand_lists) fname_data = f"{prefix_data}.txt" err.write(f"writing data file \"{fname_data}\" ...\n") wr = open(fname_data, "w") wr.write(f"{nh:3d}\n") for ih in range(nh): hcands = hcand_lists[ih] nch = len(hcands) wr.write(f"{ih:3d} {nch:3d}\n") for kc in range(nch): ini0, fin0,ini1, fin1, pena, ivar = hcands[kc] ipena = int(floor(1000*pena+ 0.5)) wr.write(f"{ini0:3d} {fin0:3d} {ini1:3d} {fin1:3d} {ipena:5d} {ivar:5d}\n") wr.close() return # ---------------------------------------------------------------------- def read_bimatch(prefix_result, bitemp, nt): # Reads a bimatching created by the external program {optimal_bimatch}. # Returns the bimatching and the score. nh = len(bitemp) ng = nh+1 ns = ng + nh fname_result = f"{prefix_result}.txt" err.write(f"reading results file \"{fname_result}\" ...\n") rd = open(fname_result, "r") line = rd.readline().strip() m = re.fullmatch(r"cost *[=] *([-+]?[0-9]+)", line); assert m is not None iscore = int(m.group(1)) line = rd.readline().strip() m = re.fullmatch(r"ns *[=] *([0-9]+)", line); assert m is not None ns1 = int(m.group(1)) if ns1 == 0: # Failed to find a bimatching: assert iscore == -1, f"inconsistent {iscore = }"; bimatch = None; score = +inf; else: assert iscore >= 0, f"bad {iscore = }"; assert ns1 == ns, f"wrong bimatch size {ns1 = } should be {nh = }" score = iscore/1000 bimatch = [ ] pref_pat = r"rung *([0-9]+) +(gap|hit) *([0-9]+)" rung_pat = r"([0-9]+)[:]([0-9]+) +([0-9]+)[:]([0-9]+) +([0-9]+) +([-]?[0-9]+)" rung_pat = re.compile(pref_pat + " *[=] *" + rung_pat) for ks in range(ns): rung = read_rung(rd, ks, rung_pat, bitemp, nt) if (ks % 2) == 1: bimatch.append(rung) rd.close() return bimatch, score # ---------------------------------------------------------------------- def read_rung(rd, ks, rung_pat, bitemp, nt): # Reads from {rd} a line with rung {ks} of a bimatch, using # the pattern {rung_pat}. Assumes {bitemp} is the bitemplate # and {nt[0..1]} are the sizes of the two texts. line = rd.readline() assert line != "", "premature end-of-file" line = line.strip() m = rung_pat.fullmatch(line); assert m is not None ksr = int(m.group(1)); assert ksr == ks, f"seq error {ks = } {ksr = }" stype = m.group(2); assert stype == ("gap","hit")[ks % 2], f"bad rung type '{stype}'" igh = int(m.group(3)); assert igh == ks//2, f"bad gap/hit index {igh = }" ini0 = int(m.group(4)); fin0 = int(m.group(5)); assert 0 <= ini0 and ini0 <= fin0 and fin0 <= nt[0], f"invalid seg 0 {ini0}:{fin0}" ini1 = int(m.group(6)); fin1 = int(m.group(7)); assert 0 <= ini1 and ini1 <= fin1 and fin1 <= nt[1], f"invalid seg 1 {ini1}:{fin1}" icost = int(m.group(8)); cost = icost/1000 ivar = int(m.group(9)); bpvar = None if (ks % 2) == 0: # Gap rung: assert 0 <= icost, f"invalid segment cost {icost = }" assert ivar == -1, f"invalid pattern variant index {ivar = }" bpvar = None else: # Hit rung: ih = igh bipat = bitemp[ih] bpvar = bipat[ivar] assert 0 <= ivar and ivar < len(bipat), f"invalid pattern variant index {ivar = }" pena, pat0, pat1 = bpvar assert abs(cost - pena) < 1.0e-3, f"invalid segment cost {icost = } {cost = } {pena = }" rung = (ini0, fin0, ini1, fin1, bpvar) return rung # ---------------------------------------------------------------------- def match_test_func(text0, text1, bitemp, un1_per_un0, debug_level): bimatch, score = match_bitemplate(text0, text1, bitemp, un1_per_un0, debug_level) return bimatch, score # ---------------------------------------------------------------------- if len(sys.argv) > 1 and sys.argv[1] == "BTMCF.TEST": bmtestf.all_tests(match_test_func, 20)