diff --git a/common/concertina_lib.py b/common/concertina_lib.py index cd95c89..073e493 100644 --- a/common/concertina_lib.py +++ b/common/concertina_lib.py @@ -176,7 +176,11 @@ def __init__(self, config, engine, display_mode='colab', iterations=None): self.all_actions = {a["name"] for a in self.config} self.complete_actions = set() self.running_actions = set() - assert display_mode in ('colab', 'terminal', 'colab-text', 'silent'), ( + self.show_only_running = False + if os.getenv('LOGICA_TERMINAL_ONELINE', 'no') == 'yes': + self.show_only_running = True + assert display_mode in ('colab', 'terminal', + 'colab-text', 'silent'), ( 'Unrecognized display mode: %s' % display_mode) self.display_mode = display_mode self.display_id = self.GetDisplayId() @@ -293,6 +297,13 @@ def AsArtGraph(): extra_lines = self.ProgressBar().split('\n') return AsArtGraph().GetPicture(updating=updating, extra_lines=extra_lines) + def ShowRunning(self, updating): + nodes, edges = self.AsNodesAndEdges() + running = [n for n in nodes if n.startswith('\033[1m')] + if not running: + return '*' + return '[%d / %d] ' % (len(self.complete_actions), + len(self.all_actions)) + running[0] def AsNodesAndEdges(self): """Nodes and edges to display in terminal.""" @@ -405,14 +416,18 @@ def UpdateDisplay(self, final=False): self.display_update_period = min(0.5, self.display_update_period * 1.2) if (now - self.recent_display_update_seconds < self.display_update_period and - not final): + not final and + not self.show_only_running): # Avoid frequent display updates slowing down execution. return self.recent_display_update_seconds = now if self.display_mode == 'colab': update_display(self.AsGraphViz(), display_id=self.display_id) elif self.display_mode == 'terminal': - print(self.AsTextPicture(updating=True)) + if self.show_only_running: + print(self.ShowRunning(updating=True)) + else: + print(self.AsTextPicture(updating=True)) elif self.display_mode == 'colab-text': update_display( self.StateAsSimpleHTML(), diff --git a/examples/graph/tgdk/benchmark_and_collect.py b/examples/graph/tgdk/benchmark_and_collect.py new file mode 100755 index 0000000..2c8e043 --- /dev/null +++ b/examples/graph/tgdk/benchmark_and_collect.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Run all TC/SG benchmarks on Logica and Nemo, collect times into CSV + ASCII table.""" + +import csv +import os +import re +import resource +import subprocess +import sys +import time + + +BENCHMARKS = [ + # (problem, dataset, csv_file) + ("TC", "g1k", "g1k.csv"), + ("TC", "g2k", "g2k.csv"), + ("TC", "g3k", "g3k.csv"), + ("TC", "g4k", "g4k.csv"), + ("TC", "g5k", "g5k.csv"), + ("SG", "tree7", "tree7.csv"), + ("SG", "tree8", "tree8.csv"), + ("SG", "tree9", "tree9.csv"), + ("SG", "tree10", "tree10.csv"), + ("SG", "tree11", "tree11.csv"), + ("SG", "tree12", "tree12.csv"), +] + + +LOGICA_TEMPLATES = { + "TC": '''@Ground(G); +G(a, b) :- `("{csv}")`(a, b); + +@Recursive(TC, ∞, stop: Stop); +TC(a, b) distinct :- G(a, b); +TC(a, c) distinct :- TC(a, b), G(b, c); + +OldN() += 1 :- TC(); +Stop() :- OldN() == Sum{{1 :- TC()}}; + +N() += 1 :- TC(a, b); +''', + "SG": '''G(a, b) :- `("{csv}")`(a, b); + +@Recursive(SG, -1, stop: Done); +SG(x, y) distinct :- G(a, x), G(a, y); +SG(x, y) distinct :- SG(a, b), G(a, x), G(b, y); +PrevSG(x, y) :- SG(x, y); +Done() :- Sum{{ 1 :- SG(x, y) }} == Sum{{ 1 :- PrevSG(x, y) }}; + +N() += 1 :- SG(x, y); +''', +} + +NEMO_TEMPLATES = { + "TC": '''@import edge :- csv{{resource="{csv}", ignore_headers=true}}. + +TC(?A, ?B) :- edge(?A, ?B). +TC(?A, ?C) :- TC(?A, ?B), edge(?B, ?C). + +N(#count(?A, ?B)) :- TC(?A, ?B). + +@export N :- csv{{resource="n.csv"}}. +''', + "SG": '''@import tree :- csv{{resource="{csv}", ignore_headers=true, format=(string,string)}}. + +SG(?X, ?Y) :- tree(?A, ?X), tree(?A, ?Y). +SG(?X, ?Y) :- SG(?A, ?B), tree(?A, ?X), tree(?B, ?Y). + +N(#count(?X, ?Y)) :- SG(?X, ?Y). + +@export N :- csv{{resource="n.csv"}}. +''', +} + + +def generate_programs(problem, dataset, csv_file): + """Write _.l and .nemo files from templates.""" + base = f"{problem.lower()}_{dataset}" + l_file = f"{base}.l" + nemo_file = f"{base}.nemo" + with open(l_file, "w") as f: + f.write(LOGICA_TEMPLATES[problem].format(csv=csv_file)) + with open(nemo_file, "w") as f: + f.write(NEMO_TEMPLATES[problem].format(csv=csv_file)) + return l_file, nemo_file + + +def run_timed(cmd): + """Run a command, return (wall, user, sys, stdout, stderr).""" + r0 = resource.getrusage(resource.RUSAGE_CHILDREN) + t0 = time.time() + proc = subprocess.run(cmd, capture_output=True, text=True) + wall = time.time() - t0 + r1 = resource.getrusage(resource.RUSAGE_CHILDREN) + user = r1.ru_utime - r0.ru_utime + sys_t = r1.ru_stime - r0.ru_stime + return wall, user, sys_t, proc.stdout, proc.stderr + + +def parse_logica_n(stdout): + """Extract the N value from Logica's artistic_table output.""" + # Look for a number inside a table row like "| 12345 |" + for line in stdout.splitlines(): + m = re.match(r"\|\s*(\d+)\s*\|", line) + if m: + return int(m.group(1)) + return None + + +def parse_nemo_n(results_path="results/n.csv"): + """Nemo writes N to results/n.csv (one number per file).""" + try: + with open(results_path) as f: + line = f.readline().strip().strip('"') + return int(line) + except (FileNotFoundError, ValueError): + return None + + +def run_logica(l_file): + cmd = ["python3", "logica/logica.py", l_file, "run_in_terminal", "N"] + wall, user, sys_t, out, err = run_timed(cmd) + n = parse_logica_n(out) + return wall, user + sys_t, n + + +def run_nemo(nemo_file): + cmd = ["nemo", nemo_file, "--overwrite-results"] + wall, user, sys_t, out, err = run_timed(cmd) + n = parse_nemo_n() + return wall, user + sys_t, n + + +def ascii_table(rows, header): + """Render rows as +---+---+ style table.""" + all_rows = [header] + [[str(c) for c in r] for r in rows] + widths = [max(len(r[i]) for r in all_rows) for i in range(len(header))] + sep = "+" + "+".join("-" * (w + 2) for w in widths) + "+" + def fmt(r): + return "| " + " | ".join(c.ljust(w) for c, w in zip(r, widths)) + " |" + lines = [sep, fmt(header), sep] + for r in all_rows[1:]: + lines.append(fmt(r)) + lines.append(sep) + return "\n".join(lines) + + +def main(): + os.chdir(os.path.dirname(os.path.abspath(__file__))) + os.makedirs("results", exist_ok=True) + + rows = [] + for problem, dataset, csv_file in BENCHMARKS: + print(f"=== {problem} {dataset} ===", flush=True) + + l_file, nemo_file = generate_programs(problem, dataset, csv_file) + print(f" Generated: {l_file}, {nemo_file}", flush=True) + + print(f" Logica: {l_file}", flush=True) + l_wall, l_cpu, l_n = run_logica(l_file) + print(f" wall={l_wall:.2f}s cpu={l_cpu:.2f}s N={l_n}", flush=True) + + print(f" Nemo: {nemo_file}", flush=True) + n_wall, n_cpu, n_n = run_nemo(nemo_file) + print(f" wall={n_wall:.2f}s cpu={n_cpu:.2f}s N={n_n}", flush=True) + + rows.append([ + problem, dataset, + f"{l_wall:.2f}", f"{l_cpu:.2f}", + f"{n_wall:.2f}", f"{n_cpu:.2f}", + l_n if l_n is not None else "?", + n_n if n_n is not None else "?", + ]) + + header = ["Problem", "Dataset", + "Logica wall", "Logica CPU", + "Nemo wall", "Nemo CPU", + "Logica N", "Nemo N"] + + with open("benchmark_results.csv", "w", newline="") as f: + w = csv.writer(f) + w.writerow(header) + w.writerows(rows) + + table = ascii_table(rows, header) + with open("benchmark_results.txt", "w") as f: + f.write(table + "\n") + + print() + print(table) + print() + print("Wrote benchmark_results.csv and benchmark_results.txt") + + +if __name__ == "__main__": + main() diff --git a/examples/graph/tgdk/benchmark_results.txt b/examples/graph/tgdk/benchmark_results.txt new file mode 100644 index 0000000..b0844fc --- /dev/null +++ b/examples/graph/tgdk/benchmark_results.txt @@ -0,0 +1,15 @@ ++---------+---------+-------------+------------+-----------+----------+-----------+-----------+ +| Problem | Dataset | Logica wall | Logica CPU | Nemo wall | Nemo CPU | Logica N | Nemo N | ++---------+---------+-------------+------------+-----------+----------+-----------+-----------+ +| TC | g1k | 1.71 | 9.06 | 3.35 | 3.35 | 1000000 | 1000000 | +| TC | g2k | 2.93 | 37.81 | 14.87 | 14.87 | 4000000 | 4000000 | +| TC | g3k | 4.53 | 66.86 | 36.40 | 36.40 | 9000000 | 9000000 | +| TC | g4k | 6.55 | 105.75 | 70.53 | 70.52 | 16000000 | 16000000 | +| TC | g5k | 9.91 | 162.22 | 116.35 | 116.34 | 24995000 | 24995000 | +| SG | tree7 | 1.13 | 4.00 | 0.02 | 0.02 | 17506 | 17506 | +| SG | tree8 | 1.40 | 4.48 | 0.11 | 0.11 | 106907 | 106907 | +| SG | tree9 | 1.98 | 6.46 | 0.99 | 0.99 | 672411 | 672411 | +| SG | tree10 | 2.50 | 15.77 | 5.93 | 5.93 | 4263436 | 4263436 | +| SG | tree11 | 6.00 | 85.04 | 38.10 | 38.10 | 25802317 | 25802317 | +| SG | tree12 | 24.99 | 453.25 | 276.70 | 276.69 | 161827886 | 161827886 | ++---------+---------+-------------+------------+-----------+----------+-----------+-----------+ diff --git a/examples/graph/tgdk/sg_tree7.l b/examples/graph/tgdk/sg_tree7.l new file mode 100644 index 0000000..bcd8e20 --- /dev/null +++ b/examples/graph/tgdk/sg_tree7.l @@ -0,0 +1,9 @@ +G(a, b) :- `("tree7.csv")`(a, b); + +@Recursive(SG, -1, stop: Done); +SG(x, y) distinct :- G(a, x), G(a, y); +SG(x, y) distinct :- SG(a, b), G(a, x), G(b, y); +PrevSG(x, y) :- SG(x, y); +Done() :- Sum{ 1 :- SG(x, y) } == Sum{ 1 :- PrevSG(x, y) }; + +N() += 1 :- SG(x, y); diff --git a/examples/graph/tgdk/sg_tree7.nemo b/examples/graph/tgdk/sg_tree7.nemo new file mode 100644 index 0000000..24cdeb8 --- /dev/null +++ b/examples/graph/tgdk/sg_tree7.nemo @@ -0,0 +1,8 @@ +@import tree :- csv{resource="tree7.csv", ignore_headers=true, format=(string,string)}. + +SG(?X, ?Y) :- tree(?A, ?X), tree(?A, ?Y). +SG(?X, ?Y) :- SG(?A, ?B), tree(?A, ?X), tree(?B, ?Y). + +N(#count(?X, ?Y)) :- SG(?X, ?Y). + +@export N :- csv{resource="n.csv"}. diff --git a/examples/graph/tgdk/tc_g1k.l b/examples/graph/tgdk/tc_g1k.l new file mode 100644 index 0000000..97430be --- /dev/null +++ b/examples/graph/tgdk/tc_g1k.l @@ -0,0 +1,11 @@ +@Ground(G); +G(a, b) :- `("g1k.csv")`(a, b); + +@Recursive(TC, ∞, stop: Stop); +TC(a, b) distinct :- G(a, b); +TC(a, c) distinct :- TC(a, b), G(b, c); + +OldN() += 1 :- TC(); +Stop() :- OldN() == Sum{1 :- TC()}; + +N() += 1 :- TC(a, b); diff --git a/examples/graph/tgdk/tc_g1k.nemo b/examples/graph/tgdk/tc_g1k.nemo new file mode 100644 index 0000000..7536f41 --- /dev/null +++ b/examples/graph/tgdk/tc_g1k.nemo @@ -0,0 +1,8 @@ +@import edge :- csv{resource="g1k.csv", ignore_headers=true}. + +TC(?A, ?B) :- edge(?A, ?B). +TC(?A, ?C) :- TC(?A, ?B), edge(?B, ?C). + +N(#count(?A, ?B)) :- TC(?A, ?B). + +@export N :- csv{resource="n.csv"}.