figure.py

7.1 kB · python · 207 lines

1import os23# STYLE45FONT = "Helvetica,Arial,sans-serif"6# PALETTE7INK = "#000000"8MUTED = "#8e8e93"9PALE = "#bababf"10PAPER = "#ffffff"11CONCEPT = {12    "gasket": "#00cad8",13    "carpet": "#008cff",14    "sponge": "#6768fa",15    "prime": "#ff325a",16    "bound": "#ff8f2c",17    "window": "#ff3d40",18    "control": "#8e8e93",19}20PALETTE = {21    "black": "#000000",22    "white": "#ffffff",23    "red": "#ff3d40",24    "red-light": "#ff9d95",25    "red-dark": "#a80016",26    "orange": "#ff8f2c",27    "orange-light": "#ffc093",28    "orange-dark": "#a25400",29    "yellow": "#ffd100",30    "yellow-light": "#ffe591",31    "yellow-dark": "#9e8100",32    "green": "#32cc58",33    "green-light": "#5eee79",34    "green-dark": "#007f2c",35    "mint": "#00d1bb",36    "mint-light": "#48efd8",37    "mint-dark": "#008173",38    "teal": "#00cad8",39    "teal-light": "#48e9f7",40    "teal-dark": "#007c85",41    "cyan": "#1ec9f3",42    "cyan-light": "#86e2ff",43    "cyan-dark": "#007c98",44    "blue": "#008cff",45    "blue-light": "#84bdff",46    "blue-dark": "#00559f",47    "indigo": "#6768fa",48    "indigo-light": "#9ea9ff",49    "indigo-dark": "#3c2abc",50    "purple": "#d332e9",51    "purple-light": "#f08aff",52    "purple-dark": "#870097",53    "pink": "#ff325a",54    "pink-light": "#ff9a9f",55    "pink-dark": "#a50030",56    "brown": "#b18462",57    "brown-light": "#dfaf8c",58    "brown-dark": "#754c2b",59    "gray": "#8e8e93",60    "gray-light": "#bababf",61    "gray-dark": "#56565a",62}63# PALETTE END64STROKE = 1.265THIN = 0.666MARGIN = 366768def esc(s):69    return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")7071def header(width, height):72    return ['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" width="%d" height="%d">' % (width, height, width, height), '<rect width="%d" height="%d" fill="%s"/>' % (width, height, PAPER)]7374def text(x, y, s, size=11, fill=INK, anchor="start", weight=None):75    w = ' font-weight="%s"' % weight if weight else ""76    return '<text x="%.1f" y="%.1f" font-family="%s" font-size="%d" fill="%s" text-anchor="%s"%s>%s</text>' % (x, y, FONT, size, fill, anchor, w, esc(s))7778def line(x1, y1, x2, y2, stroke=INK, width=STROKE):79    return '<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%.1f"/>' % (x1, y1, x2, y2, stroke, width)8081def rect(x, y, w, h, fill, opacity=1.0):82    return '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="%s" opacity="%.2f"/>' % (x, y, w, h, fill, opacity)8384def circle(x, y, r, fill):85    return '<circle cx="%.1f" cy="%.1f" r="%.1f" fill="%s"/>' % (x, y, r, fill)8687def save(parts, path):88    with open(path, "w") as handle:89        handle.write("\n".join(parts + ["</svg>"]) + "\n")9091# STYLE END9293# DRAW9495PAIRS = [(45, 105), (61, 121), (78, 102), (94, 118)]9697def maps():98    out = []99    for flip in range(2):100        for turn in range(4):101            m = []102            for f in range(9):103                r, c = divmod(f, 3)104                if flip:105                    r, c = c, r106                for _ in range(turn):107                    r, c = c, 2 - r108                m.append(r * 3 + c)109            if m not in out:110                out.append(m)111    return out112113def pair_table(side, group):114    cells = side * side115    index = [-1] * (cells * cells)116    count = 0117    for j in range(cells):118        for l in range(j, cells):119            if index[j * cells + l] != -1:120                continue121            for m in group:122                index[m[j] * cells + m[l]] = count123                index[m[l] * cells + m[j]] = count124            count += 1125    return index, count126127def big_maps():128    out = []129    for flip in range(2):130        for turn in range(4):131            m = []132            for f in range(81):133                r, c = divmod(f, 9)134                if flip:135                    r, c = c, r136                for _ in range(turn):137                    r, c = c, 8 - r138                m.append(r * 9 + c)139            if m not in out:140                out.append(m)141    return out142143def census(points, index, cells, count):144    out = [0] * count145    for a in range(len(points)):146        row = points[a] * cells147        for b in range(a, len(points)):148            out[index[row + points[b]]] += 1149    return out150151def kronecker(bits):152    base = [j for j in range(9) if bits >> j & 1]153    return sorted(((p // 3) * 3 + s // 3) * 9 + ((p % 3) * 3 + s % 3) for p in base for s in base)154155def grid(x, y, unit, bits):156    parts = []157    for f in range(9):158        r, c = divmod(f, 3)159        fill = CONCEPT["carpet"] if bits >> f & 1 else PAPER160        parts.append(rect(x + c * unit, y + r * unit, unit, unit, fill))161    for k in range(4):162        parts.append(line(x, y + k * unit, x + 3 * unit, y + k * unit, PALE, THIN))163        parts.append(line(x + k * unit, y, x + k * unit, y + 3 * unit, PALE, THIN))164    return parts165166def main():167    group = maps()168    index, classes = pair_table(3, group)169    assert classes == 11, classes170    index2, classes2 = pair_table(9, big_maps())171    assert classes2 == 461, classes2172    unit = 16173    width, height = 560, 322174    row = 62175    top = 46176    parts = header(width, height)177    parts.append(text(MARGIN, 26, "Four pairs of designs with the same pair census, and what level 2 sees", size=11, weight="bold"))178    for i, (a, b) in enumerate(PAIRS):179        y = top + i * row180        first = census([j for j in range(9) if a >> j & 1], index, 9, classes)181        second = census([j for j in range(9) if b >> j & 1], index, 9, classes)182        assert first == second, (a, b)183        parts += grid(MARGIN, y, unit, a)184        parts.append(text(MARGIN + 3 * unit + 10, y + 2 * unit - 3, "=", size=14, fill=MUTED))185        parts += grid(MARGIN + 3 * unit + 26, y, unit, b)186        base = MARGIN + 6 * unit + 46187        tall = 3 * unit188        top_count = max(first) or 1189        for c, n in enumerate(first):190            w = 9191            h = tall * n / top_count192            parts.append(rect(base + c * (w + 3), y + tall - h, w, h, CONCEPT["bound"] if n else PALE))193        moved = sum(1 for x, z in zip(census(kronecker(a), index2, 81, classes2), census(kronecker(b), index2, 81, classes2)) if x != z)194        parts.append(line(base - 4, y + tall + 1, base + 11 * 12 - 3, y + tall + 1, PALE, THIN))195        parts.append(line(base + 140, y, base + 140, y + tall + 1, PALE, THIN))196        parts.append(rect(base + 150, y + tall - tall * moved / classes2, 9, tall * moved / classes2, CONCEPT["prime"]))197        parts.append(line(base + 146, y + tall + 1, base + 163, y + tall + 1, PALE, THIN))198        parts.append(text(base + 170, y + tall - 1, "%d of the 461 level-2 counts differ" % moved, size=9, fill=INK))199        parts.append(text(MARGIN, y + tall + 12, "codes %d and %d, fill %d" % (a, b, bin(a).count("1")), size=9, fill=MUTED))200    parts.append(text(MARGIN, height - 12, "left: the two designs. middle: their 11 shared level-1 class counts. right: the level-2 counts that part them.", size=9, fill=MUTED))201    out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "figures")202    os.makedirs(out, exist_ok=True)203    save(parts, os.path.join(out, "homometric.svg"))204    print("drew figures/homometric.svg, 4 pairs checked against the 11 and 461 class tables")205206if __name__ == "__main__":207    main()