import argparse
import json
import os
import subprocess
import sys
import time
import urllib.request
import urllib.parse
from pathlib import Path
from typing import List, Dict, Optional, Any, Set
sys.path.insert(0, '/home/feanor/Projects/geographdb-core/geographdb-py/python')
import geographdb as gdb
OUTPUT_DIR = Path("/home/feanor/Projects/geographdb-core/research_agent/output")
SEEN_PAPERS_FILE = OUTPUT_DIR / "seen_papers.json"
ALL_FINDINGS_FILE = OUTPUT_DIR / "all_findings.jsonl"
def load_seen_papers() -> Set[str]:
if SEEN_PAPERS_FILE.exists():
try:
data = json.loads(SEEN_PAPERS_FILE.read_text())
return set(data.get("titles", []))
except (json.JSONDecodeError, KeyError):
return set()
return set()
def save_seen_papers(titles: Set[str]):
SEEN_PAPERS_FILE.parent.mkdir(parents=True, exist_ok=True)
SEEN_PAPERS_FILE.write_text(json.dumps({"titles": sorted(titles)}, indent=2))
def append_finding(record: Dict[str, Any]):
ALL_FINDINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(ALL_FINDINGS_FILE, "a") as f:
f.write(json.dumps(record, default=str) + "\n")
def load_all_findings() -> List[Dict[str, Any]]:
if not ALL_FINDINGS_FILE.exists():
return []
findings = []
with open(ALL_FINDINGS_FILE) as f:
for line in f:
line = line.strip()
if line:
try:
findings.append(json.loads(line))
except json.JSONDecodeError:
pass
return findings
QUERY_SETS = {
1: {
"name": "Tensor Networks & Compression",
"queries": [
"cat:cs.LG AND (\"matrix product state\" OR \"tensor train\" OR \"MPS\" OR \"MPO\") AND (\"attention\" OR \"transformer\" OR \"KV cache\")",
"cat:cs.LG AND (\"low-rank\" OR \"SVD truncation\" OR \"compression\") AND (\"language model\" OR \"inference\")",
],
},
2: {
"name": "Differential Geometry & Manifolds",
"queries": [
"cat:math.DG AND (\"Ricci curvature\" OR \"manifold learning\")",
"cat:cs.LG AND (\"Cartan moving frame\" OR \"differential geometry\" OR \"information geometry\")",
],
},
3: {
"name": "Topological Data Analysis",
"queries": [
"cat:math.AT AND (\"persistent homology\" OR \"topological data analysis\")",
"cat:cs.LG AND (\"Betti numbers\" OR \"barcode\" OR \"topological\") AND (\"neural\" OR \"deep learning\")",
],
},
4: {
"name": "Information Geometry & Optimization",
"queries": [
"cat:cs.LG AND (\"natural gradient\" OR \"Fisher information metric\" OR \"information geometry\")",
"cat:stat.ML AND (\"KL divergence\" OR \"Wasserstein distance\") AND (\"optimization\" OR \"gradient\")",
],
},
5: {
"name": "Spatiotemporal & Dynamic Graphs",
"queries": [
"cat:cs.DS AND (\"temporal graph\" OR \"dynamic graph\" OR \"spatiotemporal\") AND (\"algorithm\" OR \"pathfinding\")",
"cat:cs.LG AND (\"time-aware\" OR \"4D graph\" OR \"temporal path\") AND (\"neural network\" OR \"GNN\")",
],
},
6: {
"name": "Combinatorial & Discrete Methods",
"queries": [
"cat:cs.DS AND (\"percolation\" OR \"phase transition\" OR \"community detection\")",
"cat:cs.LG AND (\"sparse attention\" OR \"block sparse\" OR \"combinatorial optimization\") AND (\"transformer\" OR \"LLM\")",
],
},
}
MODULE_MAP = {
"kv_cache_mps": {
"keywords": ["kv cache", "key value", "attention cache", "low-rank", "svd", "compression"],
"bench": "bench_kv_cache_mps",
},
"kv_frame_codec": {
"keywords": ["frame", "codec", "differential", "cartan", "moving frame", "transition"],
"bench": "bench_kv_frame_codec",
},
"mpo": {
"keywords": ["matrix product operator", "mpo", "tensor operator", "tensor network"],
"bench": "bench_mpo",
},
"mps": {
"keywords": ["matrix product state", "mps", "tensor train"],
"bench": "bench_mps",
},
"sparse_attn": {
"keywords": ["sparse attention", "sparsity", "pruning attention", "block sparse"],
"bench": "bench_sparse_attn",
},
"ricci": {
"keywords": ["ricci", "curvature", "manifold", "geometry"],
"bench": "bench_ricci",
},
"percolation": {
"keywords": ["percolation", "phase transition", "threshold", "connectivity"],
"bench": "bench_percolation",
},
"persistence": {
"keywords": ["persistent homology", "topological", "barcode", "betti"],
"bench": "bench_persistence",
},
"astar": {
"keywords": ["a*", "pathfinding", "shortest path", "route"],
"bench": "bench_astar",
},
"four_d": {
"keywords": ["4d", "spatiotemporal", "temporal graph", "time-aware"],
"bench": "bench_four_d",
},
"natural_grad": {
"keywords": ["natural gradient", "fisher information", "information geometry"],
"bench": "bench_natural_grad",
},
"infogeo": {
"keywords": ["information geometry", "kl divergence", "metric tensor"],
"bench": "bench_infogeo",
},
}
def search_arxiv(query: str, max_results: int = 10) -> List[Dict[str, Any]]:
encoded = urllib.parse.quote(query)
url = f"http://export.arxiv.org/api/query?search_query={encoded}&max_results={max_results}&sortBy=submittedDate&sortOrder=descending"
try:
req = urllib.request.Request(url, headers={"User-Agent": "geographdb-research/1.0"})
with urllib.request.urlopen(req, timeout=20) as resp:
data = resp.read().decode("utf-8")
except Exception as e:
print(f" arXiv search failed: {e}")
return []
import xml.etree.ElementTree as ET
try:
root = ET.fromstring(data)
except ET.ParseError:
return []
ns = {"atom": "http://www.w3.org/2005/Atom"}
entries = []
for entry in root.findall("atom:entry", ns):
title = entry.find("atom:title", ns)
summary = entry.find("atom:summary", ns)
published = entry.find("atom:published", ns)
id_elem = entry.find("atom:id", ns)
if title is not None and summary is not None:
year = 2024
if published is not None and published.text:
try:
year = int(published.text[:4])
except ValueError:
pass
entries.append({
"title": (title.text or "").strip().replace("\n", " "),
"abstract": (summary.text or "").strip().replace("\n", " ")[:800],
"year": year,
"url": id_elem.text if id_elem is not None else "",
"source": "arxiv",
})
return entries
def discover_formulas(query_set_id: int) -> List[Dict[str, Any]]:
print("\n" + "=" * 70)
print("PHASE 1: DISCOVER")
print("=" * 70)
query_set = QUERY_SETS.get(query_set_id, QUERY_SETS[1])
print(f"Query Set {query_set_id}: {query_set['name']}")
all_entries = []
for query in query_set["queries"]:
print(f"\n🔍 Query: {query[:80]}...")
entries = search_arxiv(query, max_results=10)
print(f" Found {len(entries)} papers")
all_entries.extend(entries)
time.sleep(3)
seen = set()
unique = []
for e in all_entries:
key = e["title"].lower()[:100]
if key not in seen:
seen.add(key)
unique.append(e)
historically_seen = load_seen_papers()
new_papers = []
for e in unique:
key = e["title"].lower()[:100]
if key in historically_seen:
print(f" ⏭ SKIP (already seen): {e['title'][:60]}...")
else:
new_papers.append(e)
historically_seen.add(key)
save_seen_papers(historically_seen)
print(f"\n📊 Total unique papers this query: {len(unique)}")
print(f"📊 New papers (never seen): {len(new_papers)}")
print(f"📊 Skipped (already seen): {len(unique) - len(new_papers)}")
print(f"📊 Total historically tracked: {len(historically_seen)}")
return new_papers
def map_paper_to_module(paper: Dict[str, Any]) -> Optional[str]:
text = (paper["title"] + " " + paper["abstract"]).lower()
best_module = None
best_score = 0
for module, info in MODULE_MAP.items():
score = sum(1 for kw in info["keywords"] if kw in text)
if score > best_score:
best_score = score
best_module = module
return best_module if best_score >= 1 else None
def map_papers(papers: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
print("\n" + "=" * 70)
print("PHASE 2: MAP")
print("=" * 70)
mapped = []
for p in papers:
module = map_paper_to_module(p)
if module:
p["mapped_module"] = module
mapped.append(p)
print(f" ✓ {p['title'][:60]}... -> {module}")
else:
p["mapped_module"] = None
print(f" ✗ {p['title'][:60]}... -> no match")
print(f"\n📊 Mapped: {len(mapped)}/{len(papers)}")
return papers
def run_cargo_tests(module: str) -> Dict[str, Any]:
result = {
"tests_passed": 0,
"tests_total": 0,
"tests_failed": 0,
"success": False,
"output": "",
}
try:
proc = subprocess.run(
["cargo", "test", "--lib", module],
cwd="/home/feanor/Projects/geographdb-core",
capture_output=True,
text=True,
timeout=120,
)
output = proc.stdout + proc.stderr
result["output"] = output[-2000:]
import re
match = re.search(r"test result: ok\.\s+(\d+) passed;\s+(\d+) failed", output)
if match:
result["tests_passed"] = int(match.group(1))
result["tests_failed"] = int(match.group(2))
result["tests_total"] = result["tests_passed"] + result["tests_failed"]
result["success"] = result["tests_failed"] == 0
else:
match = re.search(r"test result: FAILED\.\s+(\d+) passed;\s+(\d+) failed", output)
if match:
result["tests_passed"] = int(match.group(1))
result["tests_failed"] = int(match.group(2))
result["tests_total"] = result["tests_passed"] + result["tests_failed"]
result["success"] = False
else:
result["tests_total"] = 0
result["success"] = proc.returncode == 0
except subprocess.TimeoutExpired:
result["output"] = "TIMEOUT after 120s"
result["success"] = False
except Exception as e:
result["output"] = f"ERROR: {e}"
result["success"] = False
return result
def bench_module(module: str) -> Optional[Dict[str, Any]]:
if module == "astar":
return bench_astar()
elif module == "four_d":
return bench_four_d()
elif module == "scc":
return bench_scc()
elif module in ("kv_cache_mps", "kv_frame_codec", "mpo", "mps", "sparse_attn",
"ricci", "percolation", "persistence", "natural_grad", "infogeo"):
return None
else:
return None
def bench_astar() -> Dict[str, Any]:
import random
results = []
for n in [10, 20, 30, 50]:
graph = gdb.Graph4D()
for i in range(n):
for j in range(n):
node = gdb.GraphNode4D(
id=i * n + j, x=float(i), y=float(j), z=0.0,
begin_ts=0, end_ts=1000,
)
graph.add_node(node)
for i in range(n):
for j in range(n):
idx = i * n + j
if i < n - 1:
graph.add_edge(idx, (i + 1) * n + j, 1.0, 0, 1000)
if j < n - 1:
graph.add_edge(idx, i * n + (j + 1), 1.0, 0, 1000)
ctx = gdb.TraversalContext4D(time_start=0, time_end=1000)
_ = gdb.py_astar_find_path_4d(graph, 0, n * n - 1, ctx)
times = []
for _ in range(5):
start = time.perf_counter()
path = gdb.py_astar_find_path_4d(graph, 0, n * n - 1, ctx)
elapsed = (time.perf_counter() - start) * 1000
times.append(elapsed)
avg_time = sum(times) / len(times)
results.append({
"n": n,
"nodes": n * n,
"path_len": len(path) if path else 0,
"avg_ms": round(avg_time, 3),
"min_ms": round(min(times), 3),
"max_ms": round(max(times), 3),
})
return {
"benchmark": "astar_grid",
"results": results,
}
def bench_four_d() -> Dict[str, Any]:
results = []
for n in [10, 20, 30]:
graph = gdb.Graph4D()
for i in range(n):
for j in range(n):
node = gdb.GraphNode4D(
id=i * n + j, x=float(i), y=float(j), z=0.0,
begin_ts=0, end_ts=1000,
)
graph.add_node(node)
for i in range(n):
for j in range(n):
idx = i * n + j
if i < n - 1:
graph.add_edge(idx, (i + 1) * n + j, 1.0, 0, 1000)
if j < n - 1:
graph.add_edge(idx, i * n + (j + 1), 1.0, 0, 1000)
ctx = gdb.TraversalContext4D(time_start=0, time_end=1000)
_ = gdb.py_fastest_temporal_path_4d(graph, 0, n * n - 1, ctx)
times = []
for _ in range(5):
start = time.perf_counter()
path = gdb.py_fastest_temporal_path_4d(graph, 0, n * n - 1, ctx)
elapsed = (time.perf_counter() - start) * 1000
times.append(elapsed)
avg_time = sum(times) / len(times)
results.append({
"n": n,
"nodes": n * n,
"path_len": len(path) if path else 0,
"avg_ms": round(avg_time, 3),
"min_ms": round(min(times), 3),
"max_ms": round(max(times), 3),
})
return {
"benchmark": "fastest_temporal_path_grid",
"results": results,
}
def bench_scc() -> Dict[str, Any]:
results = []
for n in [10, 20, 30, 50]:
graph = gdb.Graph4D()
for i in range(n):
for j in range(n):
node = gdb.GraphNode4D(
id=i * n + j, x=float(i), y=float(j), z=0.0,
begin_ts=0, end_ts=1000,
)
graph.add_node(node)
for i in range(n):
for j in range(n):
idx = i * n + j
if i < n - 1:
graph.add_edge(idx, (i + 1) * n + j, 1.0, 0, 1000)
if j < n - 1:
graph.add_edge(idx, i * n + (j + 1), 1.0, 0, 1000)
ctx = gdb.TraversalContext4D(time_start=0, time_end=1000)
times = []
for _ in range(5):
start = time.perf_counter()
sccs = gdb.py_strongly_connected_components_4d(graph, ctx)
elapsed = (time.perf_counter() - start) * 1000
times.append(elapsed)
avg_time = sum(times) / len(times)
results.append({
"n": n,
"nodes": n * n,
"scc_count": len(sccs) if sccs else 0,
"avg_ms": round(avg_time, 3),
"min_ms": round(min(times), 3),
"max_ms": round(max(times), 3),
})
return {
"benchmark": "scc_grid",
"results": results,
}
def validate_paper(paper: Dict[str, Any]) -> Dict[str, Any]:
module = paper.get("mapped_module")
if not module:
return {
"paper_title": paper["title"],
"module": None,
"tests": {"tests_passed": 0, "tests_total": 0, "success": False},
"benchmark": None,
"accepted": False,
"reason": "No module mapping",
}
print(f"\n Validating: {paper['title'][:50]}...")
print(f" Module: {module}")
test_results = run_cargo_tests(module)
print(f" Tests: {test_results['tests_passed']}/{test_results['tests_total']} passed")
bench_results = bench_module(module)
if bench_results:
print(f" Benchmark: {bench_results['benchmark']}")
for r in bench_results["results"]:
print(f" n={r.get('n', '?')}: {r.get('avg_ms', '?')}ms avg")
else:
print(f" Benchmark: No Python FFI benchmark available (Rust tests only)")
accepted = test_results["success"] and test_results["tests_total"] > 0
status = "✓ ACCEPTED" if accepted else "✗ REJECTED"
print(f" {status}")
return {
"paper_title": paper["title"],
"module": module,
"tests": test_results,
"benchmark": bench_results,
"accepted": accepted,
"reason": "Tests passed" if accepted else "Tests failed or no tests found",
}
def validate_papers(papers: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
print("\n" + "=" * 70)
print("PHASE 3: VALIDATE")
print("=" * 70)
results = []
for p in papers:
if p.get("mapped_module"):
result = validate_paper(p)
results.append(result)
accepted = sum(1 for r in results if r["accepted"])
print(f"\n📊 Validated: {len(results)}, Accepted: {accepted}")
return results
def combine_results(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
print("\n" + "=" * 70)
print("PHASE 4: COMBINE")
print("=" * 70)
accepted = [r for r in results if r["accepted"]]
if len(accepted) < 2:
print(" Not enough validated modules for meaningful combinations")
return []
unique_modules = sorted(set(r["module"] for r in accepted))
combinations = []
for i, a in enumerate(unique_modules):
for b in unique_modules[i + 1:]:
combo = {
"module_a": a,
"module_b": b,
"note": "Both modules have passing GeoMetriDB tests. Combined use depends on application.",
}
combinations.append(combo)
print(f" {a} + {b}: both pass tests")
print(f"\n📊 Distinct module pairs: {len(combinations)}")
return combinations
def prune_combinations(combinations: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
print("\n" + "=" * 70)
print("PHASE 5: PRUNE")
print("=" * 70)
print(f" All {len(combinations)} combinations are valid (no pruning needed)")
return combinations
def document_findings(
papers: List[Dict[str, Any]],
validations: List[Dict[str, Any]],
combinations: List[Dict[str, Any]],
query_set_id: int,
) -> str:
print("\n" + "=" * 70)
print("PHASE 6: DOCUMENT")
print("=" * 70)
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
query_set = QUERY_SETS.get(query_set_id, QUERY_SETS[1])
report = f"""# GeoMetriDB Research Agent Report
**Run Date:** {timestamp}
**Query Set:** {query_set_id} — {query_set['name']}
**Total Papers Found:** {len(papers)}
**Mapped to Modules:** {sum(1 for p in papers if p.get('mapped_module'))}
**Validated:** {len(validations)}
**Accepted:** {sum(1 for v in validations if v['accepted'])}
**Combinations:** {len(combinations)}
## Validated Papers
| Paper | Module | Tests | Status | Benchmark |
|-------|--------|-------|--------|-----------|
"""
for v in validations:
status = "✓" if v["accepted"] else "✗"
tests = f"{v['tests']['tests_passed']}/{v['tests']['tests_total']}"
bench = v["benchmark"]["benchmark"] if v["benchmark"] else "N/A"
report += f"| {v['paper_title'][:50]}... | {v['module']} | {tests} | {status} | {bench} |\n"
report += "\n## Test Details\n\n"
for v in validations:
report += f"### {v['paper_title'][:60]}...\n"
report += f"- **Module:** {v['module']}\n"
report += f"- **Tests:** {v['tests']['tests_passed']}/{v['tests']['tests_total']} passed\n"
report += f"- **Accepted:** {v['accepted']}\n"
report += f"- **Reason:** {v['reason']}\n"
if v["benchmark"]:
report += f"- **Benchmark:** {v['benchmark']['benchmark']}\n"
for r in v["benchmark"]["results"]:
report += f" - n={r.get('n', '?')}: {r.get('avg_ms', '?')}ms avg"
if "path_len" in r:
report += f", path_len={r['path_len']}"
if "scc_count" in r:
report += f", scc_count={r['scc_count']}"
report += "\n"
report += "\n"
report += "\n## Module Combinations\n\n"
if combinations:
report += "| Module A | Module B | Note |\n"
report += "|----------|----------|------|\n"
for c in combinations:
report += f"| {c['module_a']} | {c['module_b']} | {c['note']} |\n"
else:
report += "No combinations evaluated (insufficient validated modules).\n"
report += "\n## Raw Papers\n\n"
for p in papers:
module = p.get("mapped_module") or "unmapped"
report += f"- **{p['title'][:70]}** ({p['year']}) -> {module}\n"
report += f" Source: {p['url']}\n"
if p['abstract']:
report += f" Abstract: {p['abstract'][:250]}...\n"
report += "\n"
output_dir = Path("/home/feanor/Projects/geographdb-core/research_agent/output")
output_dir.mkdir(parents=True, exist_ok=True)
report_path = output_dir / "report.md"
report_path.write_text(report)
json_path = output_dir / "findings.json"
json_path.write_text(json.dumps({
"timestamp": timestamp,
"query_set_id": query_set_id,
"query_set_name": query_set["name"],
"papers": papers,
"validations": validations,
"combinations": combinations,
}, indent=2, default=str))
print(f" Report: {report_path}")
print(f" JSON: {json_path}")
return str(report_path)
def run_research_cycle(query_set_id: int) -> str:
print("=" * 70)
print("GEOMETRIDB AUTONOMOUS RESEARCH AGENT")
print("=" * 70)
print(f"Query Set: {query_set_id}")
t0 = time.time()
papers = discover_formulas(query_set_id)
papers = map_papers(papers)
validations = validate_papers(papers)
combinations = combine_results(validations)
pruned = prune_combinations(combinations)
report_path = document_findings(papers, validations, pruned, query_set_id)
if papers:
record = {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"query_set_id": query_set_id,
"query_set_name": QUERY_SETS.get(query_set_id, QUERY_SETS[1])["name"],
"new_papers_count": len(papers),
"validations_count": len(validations),
"accepted_count": sum(1 for v in validations if v["accepted"]),
"combinations_count": len(pruned),
"paper_titles": [p["title"] for p in papers],
}
append_finding(record)
print(f" Appended to historical findings log")
elapsed = time.time() - t0
print(f"\n{'=' * 70}")
print(f"CYCLE COMPLETE in {elapsed:.1f}s")
print(f"{'=' * 70}")
return report_path
def main():
parser = argparse.ArgumentParser(description="GeoMetriDB Autonomous Research Agent")
parser.add_argument("--query-set", type=int, default=1, help="Query set ID (1-6)")
parser.add_argument("--mode", choices=["once", "cron"], default="once", help="Run mode")
args = parser.parse_args()
if args.mode == "cron":
state_file = Path("/home/feanor/Projects/geographdb-core/research_agent/output/state.json")
if state_file.exists():
state = json.loads(state_file.read_text())
query_set = state.get("last_query_set", 0) + 1
if query_set > 6:
query_set = 1
else:
query_set = 1
state = {"last_query_set": query_set}
state_file.parent.mkdir(parents=True, exist_ok=True)
state_file.write_text(json.dumps(state))
all_findings = load_all_findings()
total_historical = len(all_findings)
report = run_research_cycle(query_set)
new_findings = load_all_findings()
new_runs = len(new_findings) - total_historical
print(f"\n{'=' * 70}")
print("CRON SUMMARY")
print(f"{'=' * 70}")
print(f"Historical runs tracked: {len(new_findings)}")
print(f"Total unique papers seen (all time): {len(load_seen_papers())}")
print(f"Next query set will be: {(query_set % 6) + 1}")
else:
report = run_research_cycle(args.query_set)
print(f"\nReport saved to: {report}")
if __name__ == "__main__":
main()