Przeglądaj źródła

Merge pull request #751 from BitSecret/main

[毕业设计] GPSAgent - 结合FormalGeo形式化系统与Agent的几何问题自动求解器
Sizhou Chen 1 miesiąc temu
rodzic
commit
9f88cdd2b3

Plik diff jest za duży
+ 105 - 0
Co-creation-projects/BitSecret-GPSAgent/README.md


BIN
Co-creation-projects/BitSecret-GPSAgent/architecture.png


+ 161 - 0
Co-creation-projects/BitSecret-GPSAgent/main.ipynb

@@ -0,0 +1,161 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Geometry Problem Solving Agent\n",
+    "\n",
+    "> 结合 FormalGeo 与 Agent 的几何问题形式化自动求解器\n",
+    "\n",
+    "本 Notebook 演示如何使用 RAVS 框架求解几何问题。"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 导入必要的模块\n",
+    "import sys\n",
+    "import os\n",
+    "\n",
+    "# 添加项目路径\n",
+    "sys.path.append(\"../src\")\n",
+    "os.chdir(\"../src/ravs\")\n",
+    "\n",
+    "from agent_loop import main"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 1. 单题求解示例\n",
+    "\n",
+    "求解编号为 1 的几何问题"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 求解单个题目\n",
+    "result = main(\n",
+    "    test_pids=[1],          # 题目编号,范围 1-7000\n",
+    "    log_path=\"../../outputs/log/test.json\",  # 日志保存路径\n",
+    "    model_names=['Deepseek'],  # 使用的 LLM\n",
+    "    max_epoch=50,            # 最大交互轮次\n",
+    "    max_context=80000,       # 最大上下文长度\n",
+    "    solve_again=False,       # 失败后是否重试\n",
+    "    debug_mode=True          # 是否输出详细日志\n",
+    ")\n",
+    "\n",
+    "print(f\"求解状态: {result}\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 2. 批量求解示例\n",
+    "\n",
+    "批量求解编号为 1-5 的几何问题"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 批量求解\n",
+    "results = main(\n",
+    "    test_pids=list(range(1, 6)),  # 批量求解 1-5 题\n",
+    "    log_path=\"../../outputs/log/batch_test.json\",\n",
+    "    model_names=['Deepseek'],\n",
+    "    max_epoch=50,\n",
+    "    max_context=80000,\n",
+    "    solve_again=True,\n",
+    "    debug_mode=False\n",
+    ")\n",
+    "\n",
+    "print(\"批量求解完成!\")\n",
+    "print(f\"结果: {results}\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 3. 多模型并行求解\n",
+    "\n",
+    "使用多个 LLM 模型并行处理不同题目"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 多模型并行(每个模型名称对应一个进程)\n",
+    "results = main(\n",
+    "    test_pids=[1, 2, 3, 4],\n",
+    "    log_path=\"../../outputs/log/multi_model_test.json\",\n",
+    "    model_names=['Deepseek', 'Deepseek', 'BaiLian'],  # 3 个进程\n",
+    "    max_epoch=50,\n",
+    "    max_context=80000,\n",
+    "    solve_again=True,\n",
+    "    debug_mode=False\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 4. 绘制统计图表\n",
+    "\n",
+    "绘制论文中的统计图(需要先运行过求解)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from chart import main as chart_main\n",
+    "\n",
+    "# 生成统计图表\n",
+    "chart_main()\n",
+    "print(\"图表已保存至 outputs/ 目录\")"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.12.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}

+ 7 - 0
Co-creation-projects/BitSecret-GPSAgent/requirements.txt

@@ -0,0 +1,7 @@
+openai==2.21.0
+dotenv==0.9.9
+matplotlib==3.10.8
+numpy==2.3.5
+sympy==1.14.0
+func-timeout==4.3.5
+hello-agents==1.0.0

+ 312 - 0
Co-creation-projects/BitSecret-GPSAgent/src/gps/agent_loop.py

@@ -0,0 +1,312 @@
+from symbolic_solver import SymbolicSolver
+from utils import parse_gdl, parse_cdl, load_json, save_json, get_theorems, make_train_val_test_split
+from multiprocessing import Process, Queue
+from dotenv import load_dotenv
+from hello_agents import HelloAgentsLLM
+import time
+import os
+import json
+import random
+import warnings
+
+warnings.filterwarnings("ignore")
+debug = False
+load_dotenv()
+
+
+def dprint(msg):
+    if debug:
+        print(msg)
+
+
+class Agent:
+    def __init__(self, api_key, base_url, model_name):
+        self.hello_agents_llm = HelloAgentsLLM(model=model_name, api_key=api_key, base_url=base_url)
+        self.model_name = model_name
+        self.history = []
+        self.memory = []
+        self.context_length = 0
+        self.timing = time.time()
+
+    def run(self, time_sleep=15, max_epoch=6):
+        dprint(f'⏳ 正在调用{self.model_name}...\n')
+        epoch = 0
+        response = '{' + f'"thinking":"尝试调用{max_epoch}次模型,均发生异常。","action":"finish()"' + '}'
+        while epoch < max_epoch:
+            epoch += 1
+            try:
+                response = self.hello_agents_llm.invoke(
+                    messages=self.memory,
+                    response_format={'type': 'json_object'}
+                ).content
+                if len(response) == 0:
+                    max_epoch += 1
+                    raise Exception('模型输出内容为空,服务器负载过大,不计入调用次数。')
+            except Exception as e:
+                if epoch < max_epoch:
+                    dprint(f'❌ 第({epoch}/{max_epoch})次调用模型时发生异常:{repr(e)}。{time_sleep}s后重试...')
+                    time.sleep(time_sleep)
+                else:
+                    response = '{' + f'"thinking":"尝试调用{max_epoch}次模型,均发生异常。","action":"finish()"' + '}'
+                    dprint(f'❌ 第({epoch}/{max_epoch})次调用模型时发生异常:{repr(e)}。')
+            else:
+                break
+
+        self.add_memory(role='assistant', content=response)
+
+        return response
+
+    def add_memory(self, role, content):
+        self.context_length += len(content)
+
+        if role == 'system':
+            dprint(f'📋 System (contex={self.context_length}, timing={round(time.time() - self.timing, 3)}s):')
+            dprint(content + '\n')
+        elif role == 'user':
+            if content.startswith('调用工具时发生错误'):
+                dprint(f'❌ Tool (contex={self.context_length}, timing={round(time.time() - self.timing, 3)}s):')
+                dprint(content + '\n')
+            elif content.startswith('工具执行结果'):
+                dprint(f'🛠️ Tool (contex={self.context_length}, timing={round(time.time() - self.timing, 3)}s):')
+                dprint(content + '\n')
+            else:
+                dprint(f'🙋 User (contex={self.context_length}, timing={round(time.time() - self.timing, 3)}s):')
+                dprint(content + '\n')
+        else:
+            dprint(f'🤖 Assistant (contex={self.context_length}, timing={round(time.time() - self.timing, 3)}s):')
+            dprint(content + '\n')
+
+        self.memory.append({"role": role, "content": content})
+
+    def summarize(self, user_prompt, summary):
+        self.history.append(self.memory)
+        self.memory = self.memory[:1]  # 清空记忆
+        self.context_length = len(self.memory[0]['content'])
+        dprint('----------------------------------------------------------------------------------------------------\n')
+        self.add_memory('user', user_prompt)
+        self.add_memory('assistant', summary)
+
+    def save_history(self, filename):
+        save_json(
+            data={
+                'timing': time.time() - self.timing,
+                'model_name': self.model_name,
+                'history': self.history + [self.memory]
+            },
+            filename=filename
+        )
+
+
+def get_system_prompt(gdl):
+    relation_prompt = []
+    for relation in gdl['Relations']:
+        relation_prompt.append(
+            relation + ':' + gdl['Relations'][relation]['geometric_constraints']
+        )
+    attribution_prompt = []
+    for attribution in gdl['Attributions']:
+        if attribution in {'XOfPoint(A)', 'YOfPoint(A)'}:
+            continue
+        attribution_prompt.append(
+            attribution + ':' + gdl['Attributions'][attribution]['sym']
+        )
+    theorem_prompt = []
+    theorems = get_theorems()
+    for theorem in gdl['Theorems']:
+        if theorem.split('(')[0] not in theorems:
+            continue
+        theorem_prompt.append(
+            theorem + ':' + gdl['Theorems'][theorem]['premises'] + '->' + gdl['Theorems'][theorem]['conclusion']
+        )
+
+    with open('../../datasets/system_prompt.txt', 'r', encoding='utf-8') as f:
+        system_prompt = f.read()
+        system_prompt = system_prompt.replace('{relation}', '\n'.join(relation_prompt))
+        system_prompt = system_prompt.replace('{attribution}', '\n'.join(attribution_prompt))
+        system_prompt = system_prompt.replace('{theorem}', '\n'.join(theorem_prompt))
+
+    return system_prompt
+
+
+def get_summarize_prompt():
+    with open('../../datasets/summarize_prompt.txt', 'r', encoding='utf-8') as f:
+        summarize_prompt = f.read()
+    return summarize_prompt
+
+
+def parse_response(response):
+    response = json.loads(response)
+    tool_name, args = response['action'].split('(', 1)
+    if tool_name == 'summarize':
+        args = response['thinking']
+    else:
+        args = args[:-1]
+    return tool_name, args
+
+
+def solve(api_key, base_url, model_name, max_epoch, max_context, problem_id, debug_mode):
+    if debug_mode:
+        global debug
+        debug = True
+
+    dprint(f"📋 调用'{model_name}'求解问题 {problem_id} (max_epoch={max_epoch}, max_context={max_context}) ...\n")
+    timing = time.time()
+    epoch_count = 0
+    agent = Agent(api_key=api_key, base_url=base_url, model_name=model_name)
+
+    try:
+        gdl = load_json('../../datasets/gdl.json')
+        cdl = load_json(f'../../datasets/problems/{problem_id}.json')
+        solver = SymbolicSolver(parse_gdl(gdl), parse_cdl(cdl))
+
+        agent.add_memory(role='system', content=get_system_prompt(gdl))
+        agent.add_memory(role='user', content=solver.state())
+
+        try:
+            while epoch_count < max_epoch:
+                epoch_count += 1
+
+                response = agent.run()
+
+                try:  # tool calls
+                    tool_name, args = parse_response(response)
+                    if tool_name == 'apply':
+                        tool_call = '工具执行结果:\n' + solver.apply(args)
+                    elif tool_name == 'decompose':
+                        tool_call = '工具执行结果:\n' + solver.decompose(args)
+                    elif tool_name == 'find_fact':
+                        tool_call = '工具执行结果:\n' + solver.find_fact(args)
+                    elif tool_name == 'find_goal':
+                        tool_call = '工具执行结果:\n' + solver.find_goal(args)
+                    elif tool_name == 'check':
+                        tool_call = '工具执行结果:\n' + solver.check()
+                    elif tool_name == 'summarize':
+                        agent.summarize(solver.state(), args)
+                        continue
+                    elif tool_name == 'finish':
+                        break
+                    else:
+                        raise Exception(f'工具未定义: {tool_name}.')
+                except Exception as e:
+                    tool_call = f"调用工具时发生错误:{repr(e)}"
+
+                agent.add_memory(role='user', content=tool_call)
+
+                if solver.status_of_goal[0] == 1:
+                    agent.add_memory(role='user', content='检测到问题已求解,自动结束。')
+                    break
+
+                if agent.context_length > max_context:
+                    agent.add_memory(role='user', content=get_summarize_prompt())
+
+        except KeyboardInterrupt:
+            agent.add_memory(role='user', content="用户主动介入中断(KeyboardInterrupt)。")
+
+        if solver.status_of_goal[0] == 1:
+            result = 'solved'
+            agent.add_memory(role='user', content="求解结束:成功✅")
+        elif epoch_count >= max_epoch:
+            result = 'timeout'
+            agent.add_memory(role='user', content="求解结束:超时❌")
+        else:
+            result = 'unsolved'
+            agent.add_memory(role='user', content="求解结束:失败❌")
+
+    except Exception as e:
+        result = 'error'
+        agent.add_memory(role='user', content=f"智能体执行期间发生异常:{repr(e)}")
+        agent.add_memory(role='user', content="求解结束:异常❌")
+
+    agent.save_history(f'../../outputs/agent/solving_history_{problem_id}.json')
+
+    return result, epoch_count, time.time() - timing
+
+
+def multiprocess_solve(task_queue, reply_queue, api_key, base_url, model_name, max_epoch, max_context, debug_mode):
+    while not task_queue.empty():
+        problem_id = task_queue.get()
+        # reply_queue.put((os.getpid(), "start", time.time(), (problem_id, model_name)))
+        result, epoch_count, timing = solve(
+            api_key, base_url, model_name, max_epoch, max_context, problem_id, debug_mode
+        )
+        reply_queue.put((os.getpid(), "end", time.time(), (problem_id, model_name, result, epoch_count, timing)))
+
+
+def main(test_pids, log_path, model_names, max_epoch, max_context, solve_again, debug_mode):
+    log = {"total": test_pids, "solved": {}, "unsolved": {}, "timeout": {}, "error": {}}
+    if os.path.exists(log_path):
+        log = load_json(log_path)
+        if solve_again:
+            log["unsolved"] = {}
+            log["timeout"] = {}
+            log["error"] = {}
+
+    problem_ids = []
+    for problem_id in test_pids:
+        if str(problem_id) in log["solved"]:
+            continue
+        if str(problem_id) in log["unsolved"]:
+            continue
+        if str(problem_id) in log["timeout"]:
+            continue
+        if str(problem_id) in log["error"]:
+            continue
+        problem_ids.append(problem_id)
+    random.shuffle(problem_ids)
+
+    task_queue = Queue()
+    for problem_id in problem_ids:
+        task_queue.put(problem_id)
+
+    all_process = []
+    reply_queue = Queue()
+    for model_name in model_names:
+        if task_queue.empty():
+            break
+        process = Process(
+            target=multiprocess_solve,
+            args=(
+                task_queue, reply_queue, os.getenv(f'{model_name}_API_KEY'), os.getenv(f'{model_name}_BASE_URL'),
+                os.getenv(f'{model_name}_MODEL_ID'), max_epoch, max_context, debug_mode
+            )
+        )
+        process.start()
+        all_process.append(process)
+
+    output_format = '{0:<15}{1:<8}{2:<23}{3:<10}'
+    print(output_format.format('process_id', 'flag', 'time', 'info'))
+    while True:
+        try:
+            if not reply_queue.empty():  # directly calling .get() will block process
+                process_id, flag, log_time, info = reply_queue.get()
+                log_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(log_time))
+                if flag == 'start':
+                    problem_id, model_name = info
+                    info = f"Use '{model_name}' solve problem {problem_id}."
+                else:
+                    problem_id, model_name, result, epoch_count, timing = info
+                    log[result][problem_id] = {"epoch": epoch_count, "timing": timing}
+                    save_json(log, log_path)
+                    info = (f"'{model_name}' solve problem {problem_id} end: "
+                            f"result='{result}', epoch={epoch_count}, timing={round(timing, 3)}s.")
+                print(output_format.format(process_id, flag, log_time, info))
+        except BaseException as e:
+            print(f"多线程求解过程中发生异常'{repr(e)}',关闭所有子进程({len(all_process)})后结束。")
+            for process in all_process:
+                if process.is_alive():
+                    process.kill()
+                    process.join(timeout=0.5)
+                print(f'已关闭子进程 {process.pid}')
+            exit(0)
+
+
+if __name__ == '__main__':
+    main(
+        test_pids=make_train_val_test_split()['test'],
+        log_path="../../outputs/log/log_pssr_agent.json",
+        model_names=['Deepseek'],
+        max_epoch=50,
+        max_context=80000,
+        solve_again=True,
+        debug_mode=True
+    )

+ 476 - 0
Co-creation-projects/BitSecret-GPSAgent/src/gps/chart.py

@@ -0,0 +1,476 @@
+import json
+import os
+import matplotlib.pyplot as plt
+import matplotlib.patches as mpatches
+import numpy as np
+from matplotlib.ticker import FuncFormatter
+
+path_agent_history = '../../outputs/agent/'
+filename_log_pssr = '../../outputs/log/log_pssr_agent.json'
+
+
+def load_json(filename):
+    """打开json文件并解析成dict"""
+    with open(filename, "r", encoding="utf-8") as f:
+        return json.load(f)
+
+
+def save_json(data, filename):
+    """将dict存储为json文件"""
+    filename_bk = filename + '.bk'
+    with open(filename_bk, "w", encoding="utf-8") as f:
+        json.dump(data, f, ensure_ascii=False, indent=2)
+    if os.path.exists(filename):
+        os.remove(filename)
+    os.rename(filename_bk, filename)
+
+
+def get_problem_level():
+    """返回问题problem_id到问题难度的映射"""
+    map_pid_to_level = {}
+    for problem_id in load_json(filename_log_pssr)['total']:
+        theorem_length = len(load_json(f'../../datasets/problems/{problem_id}.json')['theorem_seqs'])
+        if theorem_length > 12:
+            map_pid_to_level[problem_id] = 6
+        else:
+            map_pid_to_level[problem_id] = int(theorem_length / 2) + theorem_length % 2
+
+    return map_pid_to_level
+
+
+def get_avg_context_len():
+    """
+    按照问题难度,统计平均上下文长度。每个问题的上下文长度是 solving_history_pid.json 文件中,所有content长度的和; len(content)
+    此外,还要分为 已求解的问题 和 其他问题
+    """
+    log = load_json(filename_log_pssr)
+    map_pid_to_level = get_problem_level()
+    solved_pids = {int(k) for k in log['solved']}
+
+    # {level: [total_len, count]}
+    solved_accum = {l: [0, 0] for l in range(1, 7)}
+    others_accum = {l: [0, 0] for l in range(1, 7)}
+
+    for pid in log['total']:
+        level = map_pid_to_level.get(pid)
+        if level is None:
+            continue
+        hist = load_json(f'{path_agent_history}solving_history_{pid}.json')
+        ctx_len = sum(
+            len(str(msg.get('content', '')))
+            for round_msgs in hist.get('history', [])
+            if isinstance(round_msgs, list)
+            for msg in round_msgs
+        )
+        accum = solved_accum if pid in solved_pids else others_accum
+        accum[level][0] += ctx_len
+        accum[level][1] += 1
+
+    # dict: key 为 problem_level; value 为 avg_context_length
+    avg_context_len_solved = {
+        l: (solved_accum[l][0] / solved_accum[l][1] if solved_accum[l][1] > 0 else None)
+        for l in range(1, 7)
+    }
+    # unsolved + timeout + error; 如果当前等级的问题没有,则key 为 None
+    avg_context_length_others = {
+        l: (others_accum[l][0] / others_accum[l][1] if others_accum[l][1] > 0 else None)
+        for l in range(1, 7)
+    }
+
+    return avg_context_len_solved, avg_context_length_others
+
+
+def get_avg_epoch():
+    """
+        按照问题难度,统计平均交互次数。每个问题的交互次数存储在 solving_history_pid.json 文件中。
+        此外,还要分为 已求解的问题 和 其他问题
+    """
+    log = load_json(filename_log_pssr)
+    map_pid_to_level = get_problem_level()
+
+    # {level: [total_epoch, count]}
+    solved_accum = {l: [0, 0] for l in range(1, 7)}
+    others_accum = {l: [0, 0] for l in range(1, 7)}
+
+    for cat in ('solved', 'unsolved', 'timeout', 'error'):
+        accum = solved_accum if cat == 'solved' else others_accum
+        for pid_str, info in log[cat].items():
+            level = map_pid_to_level.get(int(pid_str))
+            if level is None:
+                continue
+            accum[level][0] += info['epoch']
+            accum[level][1] += 1
+    # dict: key 为 problem_level; value 为 avg_epoch
+    avg_epoch_solved = {
+        l: (solved_accum[l][0] / solved_accum[l][1] if solved_accum[l][1] > 0 else None)
+        for l in range(1, 7)
+    }
+    # unsolved + timeout + error; 如果当前等级的问题没有,则key 为 None
+    avg_epoch_others = {
+        l: (others_accum[l][0] / others_accum[l][1] if others_accum[l][1] > 0 else None)
+        for l in range(1, 7)
+    }
+
+    return avg_epoch_solved, avg_epoch_others
+
+
+def get_tool_call():
+    """
+    按照问题难度,统计所有工具的平均调用次数。需要解析每个问题solving_history_pid.json 文件中 role 为 assistance 的消息
+    当json解析出错时,记为error
+    """
+    tool_keys = ['apply', 'decompose', 'find', 'check', 'error']
+    log = load_json(filename_log_pssr)
+    map_pid_to_level = get_problem_level()
+    solved_pids = {int(k) for k in log['solved']}
+
+    # {level: {tool: total_count}}, {level: problem_count}
+    solved_count = {l: {t: 0 for t in tool_keys} for l in range(1, 7)}
+    others_count = {l: {t: 0 for t in tool_keys} for l in range(1, 7)}
+    solved_n = {l: 0 for l in range(1, 7)}
+    others_n = {l: 0 for l in range(1, 7)}
+
+    for pid in log['total']:
+        level = map_pid_to_level.get(pid)
+        if level is None:
+            continue
+        hist = load_json(f'{path_agent_history}solving_history_{pid}.json')
+        is_solved = pid in solved_pids
+        count = solved_count[level] if is_solved else others_count[level]
+
+        for round_msgs in hist.get('history', []):
+            if not isinstance(round_msgs, list):
+                continue
+            for msg in round_msgs:
+                if msg.get('role') != 'assistant':
+                    continue
+                try:
+                    parsed = json.loads(str(msg.get('content', '')))
+                    tool = parsed.get('action', '').split('(')[0].strip()
+
+                    if tool in ['find_fact', 'find_goal']:
+                        tool = 'find'
+
+                    if tool in tool_keys:
+                        count[tool] += 1
+                except Exception:
+                    count['error'] += 1
+
+        if is_solved:
+            solved_n[level] += 1
+        else:
+            others_n[level] += 1
+
+    # dict: key 为 problem_level; value 为 平均tool_call次数
+    avg_tool_call_solved = {
+        l: ({t: solved_count[l][t] / solved_n[l] for t in tool_keys} if solved_n[l] > 0 else None)
+        for l in range(1, 7)
+    }
+    # unsolved + timeout + error; 如果当前等级的问题没有,则key 为 None
+    avg_tool_call_others = {
+        l: ({t: others_count[l][t] / others_n[l] for t in tool_keys} if others_n[l] > 0 else None)
+        for l in range(1, 7)
+    }
+
+    return avg_tool_call_solved, avg_tool_call_others
+
+
+def draw_figure():
+    """
+    结合上述三个数据画图
+    """
+    avg_context_len_solved, avg_context_length_others = get_avg_context_len()
+    avg_epoch_solved, avg_epoch_others = get_avg_epoch()
+    avg_tool_call_solved, avg_tool_call_others = get_tool_call()
+
+    levels = [1, 2, 3, 4, 5, 6]
+    tool_keys = ['apply', 'decompose', 'find', 'check', 'error']
+    n_tools = len(tool_keys)
+
+    # 全局设置
+    plt.rcParams.update({
+        'font.family': 'serif',
+        'font.size': 10,
+        'axes.linewidth': 1.0,
+        'xtick.direction': 'out',
+        'ytick.direction': 'out',
+        'xtick.major.size': 4,
+        'ytick.major.size': 4,
+        'figure.dpi': 150,
+    })
+
+    # 柱状图色板
+    tool_colors = [
+        '#55A868', '#5DA5DA', '#9970AB', '#E6AB02', '#E7298A',
+    ]
+
+    bar_width = 0.2
+    level_spacing = n_tools * bar_width + 0.2
+    x_centers = np.arange(len(levels)) * level_spacing
+
+    fig, ax_bar = plt.subplots(figsize=(10, 4.2))
+    ax_ctx = ax_bar.twinx()
+    ax_epoch = ax_bar.twinx()
+
+    ax_ctx.yaxis.set_label_position('left')
+    ax_ctx.yaxis.tick_left()
+    ax_bar.yaxis.set_visible(False)
+
+    # 顶部封边
+    ax_bar.spines['top'].set_visible(True)
+    ax_ctx.spines['top'].set_visible(False)
+    ax_epoch.spines['top'].set_visible(False)
+
+    # --- 发散柱状图 ---
+    for i, tool in enumerate(tool_keys):
+        sv = [avg_tool_call_solved[l][tool] if avg_tool_call_solved[l] is not None else 0 for l in levels]
+        ov = [avg_tool_call_others[l][tool] if avg_tool_call_others[l] is not None else 0 for l in levels]
+        x_pos = x_centers + (i - n_tools / 2 + 0.5) * bar_width
+
+        bars_s = ax_bar.bar(x_pos, [-v for v in sv], width=bar_width,
+                            color=tool_colors[i], edgecolor='white', linewidth=0.3, zorder=2)
+        bars_o = ax_bar.bar(x_pos, ov, width=bar_width,
+                            color=tool_colors[i], edgecolor='white', linewidth=0.3,
+                            hatch='////', alpha=0.75, zorder=2)
+
+        # 柱子向下 (Solved) 的文本
+        for bar, val in zip(bars_s, sv):
+            ax_epoch.text(bar.get_x() + bar.get_width() / 2, -val - 0.05,
+                          f'{val:.1f}', ha='center', va='top', fontsize=6,
+                          color='#333333', fontfamily='sans-serif',
+                          fontweight='bold',
+                          zorder=10,
+                          transform=ax_bar.transData)
+
+        # 柱子向上 (Failed / Others) 的文本
+        for bar, val in zip(bars_o, ov):
+            ax_epoch.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.05,
+                          f'{val:.1f}', ha='center', va='bottom', fontsize=6,
+                          color='#333333', fontfamily='sans-serif',
+                          fontweight='bold',
+                          zorder=10,
+                          transform=ax_bar.transData)
+
+    ax_bar.axhline(0, color='#333333', linewidth=0.8, zorder=3)
+    ax_bar.set_xticks(x_centers)
+    ax_bar.set_xticklabels([f'Level {l}' for l in levels], fontsize=10, fontweight='bold')
+
+    # --- 折线 ---
+    def plot_line(ax, solved_dict, others_dict, color, label_s, label_o,
+                  marker_s='o', marker_o='s'):
+        s_pts = [(x_centers[j], v)
+                 for j, (l, v) in enumerate(zip(levels, [solved_dict.get(l) for l in levels]))
+                 if v is not None]
+        o_pts = [(x_centers[j], v)
+                 for j, (l, v) in enumerate(zip(levels, [others_dict.get(l) for l in levels]))
+                 if v is not None]
+        h1 = h2 = None
+        if s_pts:
+            xs, vs = zip(*s_pts)
+            h1, = ax.plot(xs, vs, color=color, linestyle='-', linewidth=1.8,
+                          marker=marker_s, markersize=6, markeredgecolor='white',
+                          markeredgewidth=0.8, label=label_s, zorder=5)
+        if o_pts:
+            xo, vo = zip(*o_pts)
+            h2, = ax.plot(xo, vo, color=color, linestyle='--', linewidth=1.8,
+                          marker=marker_o, markersize=6, markeredgecolor='white',
+                          markeredgewidth=0.8, label=label_o, zorder=5)
+        return h1, h2
+
+    h_ctx_s, h_ctx_o = plot_line(ax_ctx, avg_context_len_solved, avg_context_length_others,
+                                 '#1A6FAF', 'Context Length (Solved)', 'Context Length (Failed)')
+    ax_ctx.set_ylabel('Avg. Context Length', fontsize=11, color='black', labelpad=6, fontweight='bold')
+    ax_ctx.tick_params(axis='y', labelcolor='black', labelsize=9)
+    ax_ctx.spines['left'].set_edgecolor('black')
+
+    def format_k(x, pos):
+        return f'{x / 1000:g}k' if x >= 1000 else f'{x:g}'
+
+    ax_ctx.yaxis.set_major_formatter(FuncFormatter(format_k))
+
+    h_ep_s, h_ep_o = plot_line(ax_epoch, avg_epoch_solved, avg_epoch_others,
+                               '#C0392B', 'Avg. Epoch (Solved)', 'Avg. Epoch (Failed)',
+                               marker_s='^', marker_o='v')
+    ax_epoch.set_ylabel('Avg. Epoch', fontsize=11, color='black', labelpad=6, fontweight='bold')
+    ax_epoch.tick_params(axis='y', colors='black', labelsize=9)
+    ax_epoch.spines['right'].set_edgecolor('black')
+
+    # --- 图例 ---
+    tool_patches = [mpatches.Patch(facecolor=tool_colors[i], edgecolor='#555555',
+                                   linewidth=0.5, label=tool_keys[i])
+                    for i in range(n_tools)]
+    line_handles = [h for h in [h_ctx_s, h_ctx_o, h_ep_s, h_ep_o] if h is not None]
+
+    all_handles = tool_patches + line_handles
+    n_cols = 5
+    ordered_handles = [h for i in range(n_cols) for h in all_handles[i::n_cols]]
+
+    legend = ax_bar.legend(
+        handles=ordered_handles,
+        fontsize=8,
+        loc='lower left',
+        bbox_to_anchor=(0, 1.05, 1, 0.1),
+        mode="expand",
+        ncol=n_cols,
+        framealpha=0.9,
+        edgecolor='#CCCCCC',
+        borderpad=0.6,
+        borderaxespad=0.
+    )
+    for text in legend.get_texts():
+        text.set_fontweight('bold')
+
+    # 如果图例有标题,也加粗
+    if legend.get_title():
+        legend.get_title().set_fontweight('bold')
+
+    plt.tight_layout()
+    plt.savefig('../../outputs/fig-statistics.pdf', bbox_inches='tight')
+    plt.show()
+
+
+def draw_table(level=6, span=2, latex=True, show_complete=False):
+    filenames = {
+        'Backward-DFS': 'log_pssr_formalgeo7k-bw-dfs.json',  # symbolic solver
+        'Backward-RS': 'log_pssr_formalgeo7k-bw-rs.json',
+        'Backward-BFS': 'log_pssr_formalgeo7k-bw-bfs.json',
+        'Forward-DFS': 'log_pssr_formalgeo7k-fw-dfs.json',
+        'Forward-BFS': 'log_pssr_formalgeo7k-fw-bfs.json',
+        'Forward-RS': 'log_pssr_formalgeo7k-fw-rs.json',
+
+        'Kimi-K2': 'log_pssr_kimi-k2.json',  # neural solver
+        'DeepSeek v3': 'log_pssr_deepseek-v3.json',
+        'GPT-5 mini': [64.79, 74.11, 63.30, 64.66, 53.50, 53.23, 41.46],
+        'Qwen3-VL': [65.93, 74.53, 65.43, 72.18, 50.96, 41.94, 36.67],
+        'Doubao seed 1.8': [69.14, 74.11, 69.15, 71.43, 64.33, 50.00, 51.67],
+        'GPT-5.2': [73.14, 80.38, 73.40, 74.81, 63.06, 59.68, 46.67],
+        'Claude4.5 Sonnet': [75.79, 84.55, 73.94, 76.32, 67.52, 64.52, 48.33],
+
+        'T5-small': 'log_pssr_t5-small_bs20_timeout600.json',  # neural-symbolic solver (training-based)
+        'BART-base': 'log_pssr_bart-base_bs20_timeout600.json',
+        'Inter-GPS': 'log_pssr_intergps.json',
+        'DualGeoSolver': 'log_pssr_dualgeosolver_bs10_timeout600.json',
+        'NGS': 'log_pssr_ngs_bs10_timeout600.json',
+        'FGeo-DRL': 'log_pssr_fgeodrl.json',
+        'FGeo-TP': [80.86, 96.43, 85.44, 76.12, 62.26, 48.88, 29.55],
+        'FGeo-ISRL': 'log_pssr_res_bdrl.json',
+        'HyperGNet': 'log_pssr_hypergnet_TTT_bs5_gb_tm600.json',
+        'NSS': 'log_pssr_nss_FFFF_bs5_tm600.json',
+
+        'Pri-TPG': [89.29, 99.16, 96.28, 87.92, 77.07, 66.13, 30.00],  # neural-symbolic solver (training-free)
+        'Ours': 'log_pssr_agent.json'
+    }
+    last_methods = ["Forward-RS", "Claude4.5 Sonnet", "NSS", 'Ours']
+
+    problem_level = {}  # map problem_id to level
+    level_map = {}  # map t_length to level (start from 0)
+    for i in range(level):
+        for j in range(span):
+            level_map[i * span + j + 1] = i + 1
+    save_json({'info': 'map theorem_length to problem level.', 'map': level_map},
+              '../../outputs/log/log_level_map.json')
+    for pid in range(7000):
+        pid += 1
+        t_length = len(load_json(f'../../datasets/problems/{pid}.json')['theorem_seqs'])
+        problem_level[pid] = level_map[t_length] if t_length <= level * span else level
+
+    method_name_max_len = max([len(m) for m in filenames.keys()] + [6]) + 1
+
+    outputs = []
+    if not show_complete:
+        head = ['Method' + "".join([" "] * (method_name_max_len - 6)),
+                'Total', 'L1   ', 'L2   ', 'L3   ', 'L4   ', 'L5   ', 'L6   ']
+        line = ''.join(['-'] * (7 * 8 + method_name_max_len))
+    else:
+        head = ['Method' + "".join([" "] * (method_name_max_len - 6)),
+                '  A  ', '  T  ', 'Total', 'L1   ', 'L2   ', 'L3   ', 'L4   ', 'L5   ', 'L6   ']
+        line = ''.join(['-'] * (9 * 8 + method_name_max_len))
+
+    if latex:
+        print(' & '.join(head))
+        outputs.append(' & '.join(head))
+    else:
+        print(' | '.join(head))
+        outputs.append(' | '.join(head))
+    print(line)
+    outputs.append(line)
+
+    for method in filenames.keys():  # pssr_log
+        lines = [method + "".join([" "] * (method_name_max_len - len(method)))]
+
+        if isinstance(filenames[method], list):
+            lines.extend(['  -  ', '  -  '])
+            for r in filenames[method]:
+                lines.append(str(r))
+                lines[-1] = lines[-1] + ' ' * (5 - len(lines[-1]))
+        else:
+            pssr_log = load_json(f"../../outputs/log/{filenames[method]}")
+
+            GT = (len(pssr_log["solved"]) + len(pssr_log["unsolved"]) +  # 事实求解成功率,分母为已求解的题目
+                  len(pssr_log["timeout"]) + len(pssr_log["error"]))
+            lines.append(str(round(GT / len(pssr_log["total"]) * 100, 2)))
+            lines[-1] = lines[-1] + ' ' * (5 - len(lines[-1]))
+            lines.append(str(round(len(pssr_log["solved"]) / GT * 100, 2)))
+            lines[-1] = lines[-1] + ' ' * (5 - len(lines[-1]))
+
+            total_level_count = [0 for _ in range(level + 1)]  # [total, l1, l2, ...]
+            solved_level_count = [0 for _ in range(level + 1)]
+            for pid in pssr_log["total"]:
+                total_level_count[0] += 1
+                total_level_count[problem_level[pid]] += 1
+                if str(pid) in pssr_log["solved"]:
+                    solved_level_count[0] += 1
+                    solved_level_count[problem_level[pid]] += 1
+            # print()
+            # print(total_level_count)
+            # print(solved_level_count)
+            for i in range(level + 1):
+                if total_level_count[i] == 0:
+                    lines.append('Nan')
+                else:
+                    lines.append(str(round(solved_level_count[i] / total_level_count[i] * 100, 2)))
+
+                lines[-1] = lines[-1] + ' ' * (5 - len(lines[-1]))
+
+        if not show_complete:
+            lines = [lines[0]] + lines[3:]
+
+        if latex:
+            print(' & '.join(lines))
+            outputs.append(' & '.join(lines))
+        else:
+            print(' | '.join(lines))
+            outputs.append(' | '.join(lines))
+
+        if method in last_methods:
+            print(line)
+            outputs.append(line)
+
+    with open('../../outputs/tab-main_results.txt', 'w', encoding='utf-8') as f:
+        f.write('\n'.join(outputs))
+
+
+def lmm_call_statistic():
+    data = {'solved': [], 'unsolved': []}
+    log = load_json('../../outputs/log/log_pssr_agent.json')
+
+    for filename in os.listdir('../../outputs/agent'):
+        count = 0
+        for history in load_json(f'../../outputs/agent/{filename}')['history']:
+            for msg in history:
+                if msg['role'] == 'assistant':
+                    count += 1
+        pid = filename.split('.')[0].split('_')[-1]
+        if pid in log['solved']:
+            data['solved'].append(count)
+        else:
+            data['unsolved'].append(count)
+
+    print('solved', sum(data['solved']) / len(data['solved']))
+    print('unsolved', sum(data['unsolved']) / len(data['unsolved']))
+
+
+if __name__ == '__main__':
+    draw_figure()
+    draw_table()
+    lmm_call_statistic()

+ 1544 - 0
Co-creation-projects/BitSecret-GPSAgent/src/gps/symbolic_solver.py

@@ -0,0 +1,1544 @@
+from itertools import combinations
+from utils import replace_paras, parse_fact, replace_expr, _satisfy_algebraic
+from utils import _anti_parse_operation, _anti_parse_fact
+from sympy import symbols, nonlinsolve, FiniteSet, EmptySet
+from func_timeout import func_timeout, FunctionTimedOut
+
+special_theorem = {
+    'bisector_of_angle_property_line_ratio', 'right_triangle_property_pythagorean',
+    'circle_property_circular_power_chord_and_chord', 'circle_property_circular_power_tangent_and_segment_line',
+    'circle_property_circular_power_segment_and_segment_line'
+}
+
+
+class SymbolicSolver:
+    def __init__(self, parsed_gdl, parsed_cdl, timeout=5):
+        self.parsed_gdl = parsed_gdl
+        self.parsed_cdl = parsed_cdl
+        self.timeout = timeout
+
+        # forward related
+        self.facts = []  # fact_id -> (predicate, instance, {premise_id}, operation_id)
+        self.fact_id = {}  # (predicate, instance) -> fact_id
+        self.predicate_to_fact_instances = {}  # predicate -> [fact_instance]
+
+        # backward related
+        self.goals = []  # goal_id -> (predicate, instance, father_id, operation_id)
+        self.status_of_goal = []  # goal_id -> int (0: not check, 1: solved, -1: skip or unsolved)
+        self.goal_ids = {}  # (predicate, instance) -> {goal_id}
+        self.premise_ids_of_goal = {}  # sub_goal_id -> {premise_id}
+        self.sub_operations = {}  # goal_id -> {sub_goal_operation_id}
+        self.predicate_to_goal_instances = {}  # predicate -> [goal_instance]
+
+        # forward and backward
+        self.operations = []  # operation_id -> (operation_type, operation_predicate, operation_instance)
+        self.operation_groups = []  # operation_id -> {fact_id} or {goal_id}
+        self.theorem_instances = {}  # theorem_name -> {(theorem_paras, premises, conclusion)}
+
+        # algebraic system
+        self.points = {}  # point_sym -> point_value
+        self.sym_to_value = {}  # sym -> value
+        self.sym_to_sym = {}  # multiple_sym -> unified_sym
+        self.sym_to_syms = {}  # unified_sym -> {multiple_sym}
+        self.equations = {}  # group_id -> ((simplified_eq), ({premise_id}), {sym})
+        self.group_count = 0  # generate group_id
+        self.simplified_algebraic_goal = {}  # goal_id -> (simplified_eq, {premise_id}, {dependent_sym}, {group_id})
+        self.solved_target_cache = {}  # target_expr -> (status, {premise_id})
+        self.attempted_equations_cache = set()  # {(target_dependent_equation)}
+
+        # init problem
+        self._construct()
+
+    def _construct(self):
+        # 1. init problem
+        for predicate in list(self.parsed_gdl['Presets']) + list(self.parsed_gdl['Relations']):
+            if predicate == 'Eq':
+                continue
+            self.predicate_to_fact_instances[predicate] = []
+            self.predicate_to_goal_instances[predicate] = []
+        self.predicate_to_fact_instances['Eq'] = []
+        self.predicate_to_goal_instances['Eq'] = []
+
+        # 2. add construction cdl
+        premise_ids = set()
+        operation_id = self._add_operation(('Preset', 'init_construction', None))
+        for predicate, instance in self.parsed_cdl['construction_cdl']:
+            fact_id, _ = self._add_fact(predicate, instance, (), operation_id)
+            if fact_id is not None:
+                premise_ids.add(fact_id)
+
+        # 3. add point's coordinate
+        for point in self.parsed_cdl['points']:
+            self.points[symbols(f'{point}.x')] = self.parsed_cdl['points'][point][0]
+            self.points[symbols(f'{point}.y')] = self.parsed_cdl['points'][point][1]
+
+        # 4. topological extend
+        shapes = set()
+        collinears = set()
+        collinears_raw = set()
+        extend_constructions = {
+            'Point': set(),
+            'Line': set(),
+            'PointOnLine': set(),
+            'Angle': set(),
+            'Triangle': set(),
+            'Quadrilateral': set(),
+            'Circle': set(),
+            'PointOnCircle': set(),
+            'DoublePointsOnCircle': set(),
+            'TriplePointsOnCircle': set(),
+            'QuadruplePointsOnCircle': set()
+        }
+
+        # 4.1 Collinear extend
+        for instance in self.predicate_to_fact_instances['Collinear']:
+            for point in instance:  # add points
+                extend_constructions['Point'].add((point,))
+
+            collinears_raw.add(instance)
+            collinears_raw.add(instance[::-1])
+            for a, b in combinations(instance, 2):
+                extend_constructions['Line'].add((a, b))
+                extend_constructions['Line'].add((b, a))
+            for a, b, c in combinations(instance, 3):
+                extend_constructions['PointOnLine'].add((b, a, c))
+                extend_constructions['PointOnLine'].add((b, c, a))
+                collinears.add((a, b, c))
+                collinears.add((c, b, a))
+
+        # 4.2 Cocircular extend
+        for instance in self.predicate_to_fact_instances['Cocircular']:
+            extend_constructions['Circle'].add((instance[0],))
+            circle = instance[0]
+            points = instance[1:]
+            for point in points:
+                extend_constructions['Point'].add((point,))
+                extend_constructions['PointOnCircle'].add((point, circle))
+            if len(points) >= 2:
+                for a, b in combinations(points, 2):
+                    extend_constructions['DoublePointsOnCircle'].add((a, b, circle))
+                    extend_constructions['DoublePointsOnCircle'].add((b, a, circle))
+            if len(points) >= 3:
+                for a, b, c in combinations(points, 3):
+                    extend_constructions['TriplePointsOnCircle'].add((a, b, c, circle))
+                    extend_constructions['TriplePointsOnCircle'].add((b, c, a, circle))
+                    extend_constructions['TriplePointsOnCircle'].add((c, a, b, circle))
+            if len(points) >= 4:
+                for a, b, c, d in combinations(points, 4):
+                    extend_constructions['QuadruplePointsOnCircle'].add((a, b, c, d, circle))
+                    extend_constructions['QuadruplePointsOnCircle'].add((b, c, d, a, circle))
+                    extend_constructions['QuadruplePointsOnCircle'].add((c, d, a, b, circle))
+                    extend_constructions['QuadruplePointsOnCircle'].add((d, a, b, c, circle))
+
+        # 4.3 Shape extend (combination)
+        jigsaw_unit = {}  # shape's jigsaw
+        shape_unit = []  # mini shape unit
+        for instance in self.predicate_to_fact_instances['Shape']:  # Shape
+            for i in range(len(instance)):  # add point, line, and angles
+                if len(instance[i]) == 1:  # point
+                    extend_constructions['Point'].add((instance[i],))
+                elif len(instance[i]) == 2:  # line
+                    extend_constructions['Point'].add((instance[i][0],))
+                    extend_constructions['Point'].add((instance[i][1],))
+                    extend_constructions['Line'].add(tuple(instance[i]))
+                    extend_constructions['Line'].add(tuple(instance[i][::-1]))
+                    j = (i + 1) % len(instance)  # add init angle
+                    if len(instance[j]) == 2:
+                        extend_constructions['Angle'].add((instance[i][0], instance[i][1], instance[j][1]))
+                else:  # arc
+                    extend_constructions['Point'].add((instance[i][1],))
+                    extend_constructions['Point'].add((instance[i][2],))
+
+            multiple_forms = {instance}
+            for bias in range(1, len(instance)):  # all forms
+                multiple_form = tuple([instance[(i + bias) % len(instance)] for i in range(len(instance))])
+                multiple_forms.add(multiple_form)
+
+            shapes.update(multiple_forms)
+            for shape in multiple_forms:
+                jigsaw_unit[shape] = multiple_forms
+                shape_unit.append(shape)
+        shape_comb = shape_unit
+        jigsaw_comb = jigsaw_unit
+        while len(shape_comb):
+            shape_comb_new = []
+            jigsaw_comb_new = {}
+            for unit in shape_unit:
+                for comb in shape_comb:
+                    if len(unit) == 0 or len(comb) == 0:
+                        continue
+                    if len(unit[-1]) != len(comb[0]):  # has same sides?
+                        continue
+                    elif len(unit[-1]) == 3:  # is arc and same?
+                        if unit[-1] != comb[0]:
+                            continue
+                    else:
+                        if unit[-1] != comb[0][::-1]:  # is line and same?
+                            continue
+
+                    if unit in jigsaw_comb[comb]:  # comb is combined from unit
+                        continue
+
+                    same_length = 1  # number of same sides
+                    mini_length = len(unit) if len(unit) < len(comb) else len(comb)  # mini length
+                    while same_length < mini_length:
+                        if len(unit[- same_length - 1]) != len(comb[same_length]):  # all arcs or all lines
+                            break
+                        elif len(unit[- same_length - 1]) == 3:  # arc
+                            if unit[- same_length - 1] != comb[same_length]:
+                                break
+                        else:  # line
+                            if unit[- same_length - 1] != comb[same_length][::-1]:
+                                break
+
+                        same_length += 1
+
+                    new_shape = list(unit[0:len(unit) - same_length])  # diff sides in polygon1
+                    new_shape += list(comb[same_length:len(comb)])  # diff sides in polygon2
+
+                    if not len(new_shape) == len(set(new_shape)):  # ensure no ring
+                        continue
+
+                    new_shape = tuple(new_shape)
+                    if new_shape in shapes:
+                        continue
+
+                    all_sides = ""
+                    for item in new_shape:  # remove circle center point
+                        if len(item) == 3:
+                            item = item[1:]
+                        all_sides += item
+                    checked = True
+                    for point in all_sides:
+                        if all_sides.count(point) > 2:
+                            checked = False
+                            break
+                    if not checked:  # ensure no holes
+                        continue
+
+                    if new_shape in shapes:
+                        continue
+
+                    multiple_forms = {new_shape}
+                    for bias in range(1, len(new_shape)):  # all forms
+                        multiple_form = tuple([new_shape[(i + bias) % len(new_shape)] for i in range(len(new_shape))])
+                        multiple_forms.add(multiple_form)
+                    shapes.update(multiple_forms)
+
+                    new_shape_jigsaw = jigsaw_unit[unit] | jigsaw_comb[comb]
+                    for shape in multiple_forms:
+                        jigsaw_comb_new[shape] = new_shape_jigsaw
+                        shape_comb_new.append(shape)
+
+            shape_comb = shape_comb_new
+            jigsaw_comb = jigsaw_comb_new
+
+        # 4.4 Angle expand (combination)
+        angle_unit = list(extend_constructions['Angle'])
+        jigsaw_unit = {}
+        for angle in angle_unit:
+            jigsaw_unit[angle] = {angle}
+        angle_comb = angle_unit  # combination angle
+        jigsaw_comb = jigsaw_unit  # angle's jigsaw
+        while len(angle_comb):
+            angle_comb_new = []
+            jigsaw_comb_new = {}
+            for unit in angle_unit:
+                for comb in angle_comb:
+
+                    if unit in jigsaw_comb[comb]:  # comb is combined from unit
+                        continue
+
+                    if not (unit[1] == comb[1] and unit[2] == comb[0] and unit[0] != comb[2]):  # ensure adjacent
+                        continue
+
+                    if (unit[0], unit[1], comb[2]) in extend_constructions['Angle'] or \
+                            (unit[0], comb[2], unit[1]) in extend_constructions['Angle'] or \
+                            (comb[2], unit[0], unit[1]) in extend_constructions['Angle']:
+                        continue
+
+                    new_angle = (unit[0], unit[1], comb[2])
+
+                    if not len(new_angle) == len(set(new_angle)):  # ensure same points
+                        continue
+
+                    if new_angle in extend_constructions['Angle']:
+                        continue
+                    extend_constructions['Angle'].add(new_angle)
+
+                    new_angle_jigsaw = jigsaw_unit[unit] | jigsaw_comb[comb]
+                    jigsaw_comb_new[new_angle] = new_angle_jigsaw
+                    angle_comb_new.append(new_angle)
+
+            angle_comb = angle_comb_new
+            jigsaw_comb = jigsaw_comb_new
+
+        # 4.5 add angle, triangle, and quadrilateral
+        for shape in shapes:
+            # print(shape)
+            shape = list(shape)
+
+            for i in range(len(shape)):  # add angles
+                j = (i + 1) % len(shape)
+                if not (len(shape[i]) == 2 and len(shape[j]) == 2 and shape[i][1] == shape[j][0]):
+                    continue
+                extend_constructions['Angle'].add((shape[i][0], shape[i][1], shape[j][1]))
+
+            i = 0
+            has_arc = False
+            while i < len(shape):
+                if len(shape[i]) != 2:
+                    has_arc = True
+                    break
+                j = (i + 1) % len(shape)
+                if (shape[i][0], shape[i][1], shape[j][1]) in collinears:
+                    shape[i] = shape[i][0] + shape[j][1]
+                    shape.pop(j)
+                    continue
+                i += 1
+
+            if has_arc or len(shape) not in {3, 4}:  # only care about triangle and quadrilateral
+                continue
+
+            valid = True
+            i = 0
+            while i < len(shape):
+                if shape[i][1] != shape[(i + 1) % len(shape)][0]:
+                    valid = False
+                    break
+                i += 1
+
+            if not valid:
+                continue
+
+            polygon = tuple([item[0] for item in shape])
+            if len(polygon) == 3:
+                extend_constructions['Triangle'].add(polygon)
+            else:
+                extend_constructions['Quadrilateral'].add(polygon)
+
+        # 4.6 Angle expand (ABC -> CBA)
+        for angle in list(extend_constructions['Angle']):
+            extend_constructions['Angle'].add((angle[2], angle[1], angle[0]))
+
+        # 4.7 Angle from collinear extend
+        for instance in self.predicate_to_fact_instances['Collinear']:
+            for a, b, c in combinations(instance, 3):
+                extend_constructions['Angle'].add((a, b, c))
+                extend_constructions['Angle'].add((c, b, a))
+
+        # 4.8 Angle collinear expand (set same angle to same sym)
+        for angle in list(extend_constructions['Angle']):
+            if angle == ('D', 'B', 'G'):
+                pass
+            if symbols(''.join(angle) + '.ma') in self.sym_to_sym:
+                continue
+            a, v, b = angle
+            a_points = {a}  # Points collinear with a and on the same side with a
+            b_points = {b}
+            for collinear in collinears_raw:
+                if v not in collinear:
+                    continue
+                if a in collinear:
+                    if collinear.index(v) < collinear.index(a):  # .....V...A..
+                        i = collinear.index(v) + 1
+                        while i < len(collinear):
+                            a_points.add(collinear[i])
+                            i += 1
+                    else:  # ...A.....V...
+                        i = 0
+                        while i < collinear.index(v):
+                            a_points.add(collinear[i])
+                            i += 1
+                if b in collinear:
+                    if collinear.index(v) < collinear.index(b):  # .....V...B..
+                        i = collinear.index(v) + 1
+                        while i < len(collinear):
+                            b_points.add(collinear[i])
+                            i += 1
+                    else:  # ...B.....V...
+                        i = 0
+                        while i < collinear.index(v):
+                            b_points.add(collinear[i])
+                            i += 1
+
+            sym = symbols(''.join(angle) + '.ma')
+            self.sym_to_syms[sym] = {sym}
+            for a_point in a_points:
+                for b_point in b_points:
+                    angle = (a_point, v, b_point)
+                    extend_constructions['Angle'].add(angle)
+                    multiple_sym = symbols(''.join(angle) + f'.ma')
+                    self.sym_to_sym[multiple_sym] = sym
+                    self.sym_to_syms[sym].add(multiple_sym)
+
+        # 4.9 add extended constructions
+        operation_id = self._add_operation(('Preset', 'extend_construction', None))
+        for predicate in extend_constructions:
+            for instance in extend_constructions[predicate]:
+                self._add_fact(predicate, instance, premise_ids, operation_id)
+
+        # 5.Add facts
+        operation_id = self._add_operation(('Preset', 'init_fact', None))
+        for predicate, instance in self.parsed_cdl['relation_cdl']:
+            if not self._pass_geometric_constraints(predicate, instance):
+                raise Exception(f'EE check not passed when add init fact {(predicate, instance)}.')
+            if (predicate, instance) in self.fact_id:
+                continue
+            fact_id, _ = self._add_fact(predicate, instance, (), operation_id)
+            if fact_id is None:
+                raise Exception(f'Error when add init fact {(predicate, instance)}.')
+
+        # 6.Set goal
+        init_goal_operation_id = self._add_operation(('Preset', 'init_goal', None))
+        goal_ids = self._add_goals([self.parsed_cdl['goal_cdl']], None, init_goal_operation_id)
+        if goal_ids is None:
+            raise Exception(f"Error when set init goal {self.parsed_cdl['goal_cdl']}.")
+        self._check_goals(goal_ids)
+
+    def _add_fact(self, predicate, instance, premise_ids, operation_id):
+        if predicate == 'Eq':
+            instance = self._adjust_expr(instance)
+            if instance is None or len(instance.free_symbols) == 0:
+                return None, set()
+
+        if (predicate, instance) in self.fact_id:
+            return None, set()
+
+        fact_id = len(self.facts)
+        self.facts.append((predicate, instance, set(premise_ids), operation_id))
+        self.fact_id[(predicate, instance)] = fact_id
+        self.predicate_to_fact_instances[predicate].append(instance)
+        self.operation_groups[operation_id].add(fact_id)
+
+        goal_ids = set()
+
+        # auto expand
+        if predicate in self.parsed_gdl['FactAutoExpand']:
+            expand_operation_id = self._add_operation(('Preset', 'fact_auto_expand', None))
+            replace = dict(zip(self.parsed_gdl['FactAutoExpand'][predicate]['paras'], instance))
+            for expand_predicate, expand_instance in self.parsed_gdl['FactAutoExpand'][predicate]['expand']:
+                if expand_predicate == 'Eq':
+                    expand_instance = replace_expr(expand_instance, replace)
+                else:
+                    expand_instance = replace_paras(expand_instance, replace)
+                _, expand_goal_ids = self._add_fact(expand_predicate, expand_instance, {fact_id}, expand_operation_id)
+                goal_ids.update(expand_goal_ids)
+
+        if predicate != 'Eq':
+            if (predicate, instance) in self.goal_ids:
+                goal_ids.update(self.goal_ids[(predicate, instance)])
+            return fact_id, goal_ids
+
+        if self.operations[operation_id] == ('Preset', 'solve_eq', None):
+            return fact_id, set()
+
+        new_simplified_eqs = [instance]
+        new_premise_ids_list = [{fact_id}]
+        new_syms = set()
+
+        # replace solved sym with its value
+        for sym in instance.free_symbols:
+            if sym in self.sym_to_value:
+                new_simplified_eqs[0] = new_simplified_eqs[0].subs(sym, self.sym_to_value[sym])
+                new_premise_ids_list[0].add(self.fact_id[('Eq', sym - self.sym_to_value[sym])])
+            else:
+                new_syms.add(sym)
+
+        if len(new_syms) == 0:  # no unsolved sym
+            return fact_id, set()
+
+        # print(new_syms)
+
+        # merge equations group
+        deleted_group_ids = set()
+        for group_id in self.equations:
+            simplified_eqs, premise_ids_list, syms = self.equations[group_id]
+            if len(new_syms & syms) > 0:
+                deleted_group_ids.add(group_id)
+                new_simplified_eqs.extend(simplified_eqs)
+                new_premise_ids_list.extend(premise_ids_list)
+                new_syms.update(syms)
+
+        # print("self.equations:", self.equations)
+        # print("instance:", instance)
+        # print("deleted_group_ids:", deleted_group_ids)
+        for group_id in deleted_group_ids:  # delete old groups
+            del self.equations[group_id]
+
+        goal_ids = set()  # influenced sub_goals
+        for goal_id in self.simplified_algebraic_goal:
+            if len(new_syms & self.simplified_algebraic_goal[goal_id][2]) > 0:
+                goal_ids.add(goal_id)
+        # print("goal_ids:", goal_ids)
+        # print()
+        # solve equations
+        new_syms = sorted(list(new_syms), key=str)
+        new_simplified_eqs = sorted(new_simplified_eqs, key=str)
+        solved_values = {}
+
+        try:
+            solutions = func_timeout(timeout=self.timeout, func=nonlinsolve, args=(new_simplified_eqs, new_syms))
+            # print(new_simplified_eqs)
+            # print(new_syms)
+            # print(solutions)
+            # print()
+
+            # print(solutions)
+            if solutions is not EmptySet and type(solutions) is FiniteSet and len(solutions) > 0:
+                solutions = list(solutions)
+                for i in range(len(solutions))[::-1]:  # remove the negative solutions
+                    for j in range(len(new_syms)):
+                        if len(solutions[i][j].free_symbols) > 0:  # skip unsolved sym
+                            continue
+                        if '.' not in str(new_syms[j]):  # skip free symbols
+                            continue
+                        try:
+                            if _satisfy_algebraic['L'](solutions[i][j]):
+                                solutions.pop(i)
+                                break
+                        except BaseException as e:
+                            pass
+
+                for j in range(len(new_syms)):
+                    if len(solutions[0][j].free_symbols) != 0:  # no numeric solution
+                        continue
+
+                    same = True
+                    for i in range(1, len(solutions)):
+                        if not _satisfy_algebraic['Eq'](solutions[i][j] - solutions[0][j]):
+                            same = False
+                            break
+                    if not same:  # numeric solution not same in every solved result
+                        continue
+
+                    try:
+                        float(solutions[0][j])
+                    except BaseException:
+                        pass
+                    else:
+                        solved_values[new_syms[j]] = solutions[0][j]  # save solved value
+
+            # print(solutions)
+            # print()
+        except BaseException:
+            pass
+
+        # split equations group
+        if len(solved_values) == 0:  # no solved value
+            self.equations[self.group_count] = (tuple(new_simplified_eqs), tuple(new_premise_ids_list), set(new_syms))
+            self.group_count += 1
+            return fact_id, goal_ids
+
+        operation_id = self._add_operation(('Preset', 'solve_eq', None))  # add the solved values
+        premise_ids = set()
+        for new_premise_ids in new_premise_ids_list:
+            premise_ids.update(new_premise_ids)
+        for sym in solved_values:
+            instance = sym - solved_values[sym]
+            self.sym_to_value[sym] = solved_values[sym]
+            self._add_fact('Eq', instance, premise_ids, operation_id)
+
+        for i in range(len(new_simplified_eqs)):  # replace sym with it's solved value
+            for sym in new_simplified_eqs[i].free_symbols:
+                if sym in solved_values:
+                    new_simplified_eqs[i] = new_simplified_eqs[i].subs(sym, solved_values[sym])
+                    new_premise_ids_list[i].add(self.fact_id[('Eq', sym - solved_values[sym])])
+
+        while len(new_simplified_eqs) > 0:
+            if len(new_simplified_eqs[0].free_symbols) == 0:  # no unsolved sym, skip
+                new_simplified_eqs.pop(0)
+                new_premise_ids_list.pop(0)
+                continue
+
+            simplified_eqs = [new_simplified_eqs.pop(0)]
+            premise_ids_list = [new_premise_ids_list.pop(0)]
+            syms = set(simplified_eqs[0].free_symbols)
+            update = True
+            while update:
+                update = False
+                for i in range(len(new_simplified_eqs))[::-1]:
+                    if len(syms & new_simplified_eqs[i].free_symbols) > 0:
+                        simplified_eqs.append(new_simplified_eqs.pop(i))
+                        premise_ids_list.append(new_premise_ids_list.pop(i))
+                        syms.update(simplified_eqs[-1].free_symbols)
+                        update = True
+            self.equations[self.group_count] = (tuple(simplified_eqs), tuple(premise_ids_list), set(syms))
+            self.group_count += 1
+
+        return fact_id, goal_ids
+
+    def _add_operation(self, operation):
+        operation_id = len(self.operations)
+        self.operations.append(operation)
+        self.operation_groups.append(set())
+        return operation_id
+
+    def _adjust_expr(self, expr):
+        """添加代数型的fact或goal时,都要先调整expr,替换统一的符号表示,并调整为首项不为负号."""
+        for sym in list(expr.free_symbols):
+            if sym in self.sym_to_sym:
+                replace_sym = self.sym_to_sym[sym]
+                if sym != replace_sym:
+                    expr = expr.subs(sym, replace_sym)
+            elif '.' not in str(sym):  # free symbols
+                self.sym_to_sym[sym] = sym
+                self.sym_to_syms[sym] = {sym}
+            else:
+                entities, attr = str(sym).split('.')
+                replace = dict(zip(self.parsed_gdl['Attributions'][attr]['paras'], entities))
+
+                for predicate, paras in self.parsed_gdl['Attributions'][attr]['geometric_constraints']:
+                    if (predicate, replace_paras(paras, replace)) not in self.fact_id:
+                        # print(expr)
+                        # print(self.predicate_to_fact_instances[predicate])
+                        # print((predicate, replace_paras(paras, replace)))
+                        # print()
+                        return None
+
+                self.sym_to_sym[sym] = sym
+                self.sym_to_syms[sym] = {sym}
+                for paras in self.parsed_gdl['Attributions'][attr]['multiple_forms']:
+                    multiple_sym = symbols(''.join(replace_paras(paras, replace)) + '.' + attr)
+                    self.sym_to_sym[multiple_sym] = sym
+                    self.sym_to_syms[sym].add(multiple_sym)
+
+        if expr != 0 and str(expr)[0] == '-':
+            expr = -expr
+
+        return expr
+
+    def _pass_constraints(self, geometric_premises, algebraic_premises, algebraic_constraints, replace):
+        premise_ids = set()
+
+        # check geometric premises
+        for predicate, paras in geometric_premises:
+            fact = (predicate, replace_paras(paras, replace))
+            # print(f"check {str(fact)} {str(fact in self.fact_id)}")
+            if fact not in self.fact_id:
+                return False, f"前提'{_anti_parse_fact(fact)}'不满足。", None
+            premise_ids.add(self.fact_id[fact])
+
+        # check algebraic constraint of dependent entity
+        for algebraic_relation, expr in algebraic_constraints:
+            expr = replace_expr(expr, replace)
+            # print(f"check {str(expr)} {str(_satisfy_algebraic[algebraic_relation](expr, self.points))}")
+            if not _satisfy_algebraic[algebraic_relation](expr, self.points):
+                expr = str(expr).replace(' ', '')
+                return False, f"代数约束'{expr}'不满足。", None
+
+        # check algebraic premises
+        for expr in algebraic_premises:
+            expr = replace_expr(expr, replace)
+
+            status, algebraic_premise_ids = self._pass_algebraic_premise(expr)
+            # print(f"check {str(expr)} {str(status == 1)}")
+            if status != 1:
+                fact = ('Eq', expr)
+                return False, f"前提'{_anti_parse_fact(fact)}'不满足。", None
+
+            premise_ids.update(algebraic_premise_ids)
+
+        return True, "通过约束", premise_ids
+
+    def _pass_algebraic_premise(self, expr):
+        """return status, premise_ids
+        status=1 solved
+        status=-1 unsolved
+        status=0 no solution
+        """
+        expr = self._adjust_expr(expr)
+        if expr is None:
+            return -1, set()
+
+        if ('Eq', expr) in self.fact_id:  # expr in self.facts
+            return 1, {self.fact_id[('Eq', expr)]}
+
+        premise_ids = set()
+        for sym in list(expr.free_symbols):
+            if sym in self.sym_to_value:
+                premise_ids.add(self.fact_id[('Eq', sym - self.sym_to_value[sym])])
+                expr = expr.subs(sym, self.sym_to_value[sym])
+
+        if len(expr.free_symbols) == 0:
+            if _satisfy_algebraic['Eq'](expr):
+                return 1, premise_ids
+            return -1, premise_ids
+
+        if expr in self.solved_target_cache:
+            status, cache_premise_ids = self.solved_target_cache[expr]
+            return status, cache_premise_ids | premise_ids
+
+        target_sym = symbols('t')
+        equations = [target_sym - expr]
+        syms = set(expr.free_symbols)
+        for group_id in self.equations:
+            if len(self.equations[group_id][2] & expr.free_symbols) > 0:
+                equations.extend(self.equations[group_id][0])
+                for eq_premise_ids in self.equations[group_id][1]:
+                    premise_ids.update(eq_premise_ids)
+                syms.update(self.equations[group_id][2])
+        syms = [target_sym] + sorted(list(syms), key=str)
+        equations = sorted(equations, key=str)
+
+        equations_tuple = tuple(equations)
+        if equations_tuple in self.attempted_equations_cache:
+            return 0, None
+        self.attempted_equations_cache.add(equations_tuple)
+
+        try:
+            equation_solutions = func_timeout(
+                timeout=self.timeout,
+                func=nonlinsolve,
+                args=(equations, syms)
+            )
+            # print(expr)
+            # print(equations, syms)
+            # print(equation_solutions)
+        except FunctionTimedOut:
+            return 0, None
+        except Exception:
+            return 0, None
+
+        # print(equation_solutions is EmptySet)
+        # print(type(equation_solutions) is not FiniteSet)
+        if equation_solutions is EmptySet or type(equation_solutions) is not FiniteSet:
+            return 0, None
+
+        equation_solutions = list(equation_solutions)
+        for i in range(len(equation_solutions))[::-1]:  # remove the negative solutions
+            for j in range(len(syms)):
+                if len(equation_solutions[i][j].free_symbols) > 0:  # skip unsolved sym
+                    continue
+                if '.' not in str(syms[j]):  # skip free symbols
+                    continue
+                try:
+                    if _satisfy_algebraic['L'](equation_solutions[i][j]):
+                        equation_solutions.pop(i)
+                        break
+                except Exception as e:
+                    pass
+
+        for solved_value in equation_solutions:  # in every solution, the solved value of target_sym must be 0
+            try:  # try to convert to float
+                float(solved_value[0])
+            except Exception as e:
+                # print(solved_value[0])
+                # print(repr(e))
+                return 0, None
+
+            if not _satisfy_algebraic['Eq'](solved_value[0]):
+                self.solved_target_cache[expr] = (-1, premise_ids)
+                return -1, premise_ids
+
+        self.solved_target_cache[expr] = (1, premise_ids)
+        return 1, premise_ids
+
+    def _add_conclusion(self, theorem_gdl, replace, premise_ids, operation_id):
+        # print(replace)
+        predicate, instance = theorem_gdl['conclusion']
+        if predicate == "Eq":
+            instance = replace_expr(instance, replace)
+        else:
+            instance = replace_paras(instance, replace)
+        return self._add_fact(predicate, instance, premise_ids, operation_id)
+
+    def _add_goals(self, goals, father_id, operation_id):
+        for predicate, instance in goals:
+            if predicate == 'Eq':
+                # print(instance)
+                instance = self._adjust_expr(instance)
+                # print(instance)
+                # print()
+                if instance is None:
+                    return None
+            if self._ancestor_has_goal(predicate, instance, father_id):  # ensure ancestor no sub_goal
+                return None
+            if father_id is not None:
+                for father_sub_operation_id in self.sub_operations[father_id]:  # ensure father no same sub_goal
+                    if self.operations[father_sub_operation_id] == self.operations[operation_id]:
+                        return None
+
+        goal_ids = set()
+        for predicate, instance in goals:  # add goal
+            if predicate == 'Eq':
+                instance = self._adjust_expr(instance)
+            goal_id = len(self.goals)
+            self.goals.append((predicate, instance, father_id, operation_id))
+            # print(goal_id, (predicate, instance), len(self.goals))
+            self.status_of_goal.append(0)
+            if (predicate, instance) not in self.goal_ids:
+                self.goal_ids[(predicate, instance)] = {goal_id}
+            else:
+                self.goal_ids[(predicate, instance)].add(goal_id)
+            self.sub_operations[goal_id] = set()
+            if father_id is not None:
+                self.sub_operations[father_id].add(operation_id)
+            self.predicate_to_goal_instances[predicate].append(instance)
+            self.operation_groups[operation_id].add(goal_id)
+            if predicate == 'Eq':
+                self.simplified_algebraic_goal[goal_id] = (instance, set(), set(), set())
+            goal_ids.add(goal_id)
+
+            # auto expand
+            if predicate in self.parsed_gdl['GoalAutoExpand']:
+                replace = dict(zip(self.parsed_gdl['GoalAutoExpand'][predicate]['paras'], instance))
+                for expand_predicate, expand_instance in self.parsed_gdl['GoalAutoExpand'][predicate]['expand']:
+                    expand_instance = replace_paras(expand_instance, replace)
+                    if not self._pass_geometric_constraints(expand_predicate, expand_instance):
+                        continue
+                    expand_operation_id = self._add_operation(('Preset', 'goal_auto_decompose', None))
+                    expand_goal_ids = self._add_goals(
+                        [(expand_predicate, expand_instance)], goal_id, expand_operation_id
+                    )
+                    if expand_goal_ids is not None:
+                        goal_ids.update(expand_goal_ids)
+
+        # print(goal_ids)
+        return goal_ids
+
+    def _ancestor_has_goal(self, check_predicate, check_instance, goal_id):
+        if goal_id is None:
+            return False
+
+        predicate, instance, father_id, _ = self.goals[goal_id]
+        if predicate == check_predicate and instance == check_instance:
+            return True
+
+        return self._ancestor_has_goal(check_predicate, check_instance, father_id)
+
+    def _pass_geometric_constraints(self, predicate, instance):
+        goal = _anti_parse_fact((predicate, instance))
+        if predicate == 'Eq':  # eq
+            for sym in instance.free_symbols:
+                sym = str(sym)
+                if '.' not in sym:
+                    continue
+                instance, attr = sym.split('.')
+                attr_gdl = self.parsed_gdl["Attributions"][attr]
+                replace = dict(zip(attr_gdl['paras'], instance))
+                for ee_check_predicate, ee_check_paras in attr_gdl['geometric_constraints']:
+                    ee_check_instance = tuple(replace_paras(ee_check_paras, replace))
+                    if (ee_check_predicate, ee_check_instance) not in self.fact_id:
+                        entity = _anti_parse_fact((ee_check_predicate, ee_check_instance))
+                        return False, f"目标{goal}实体存在性检查未通过,不存在依赖实体{entity}。"
+
+        elif predicate in self.parsed_gdl["Presets"]:  # Presets
+            if (predicate, instance) in self.fact_id:
+                return True, 'pass'
+            else:
+                entity = _anti_parse_fact((predicate, instance))
+                return False, f"目标{goal}实体存在性检查未通过,不存在依赖实体{entity}。"
+
+        else:  # relation
+            relation_gdl = self.parsed_gdl["Relations"][predicate]
+            replace = dict(zip(relation_gdl['paras'], instance))
+            for ee_check_predicate, ee_check_paras in relation_gdl['geometric_constraints']:
+                ee_check_instance = replace_paras(ee_check_paras, replace)
+                if (ee_check_predicate, ee_check_instance) not in self.fact_id:
+                    entity = _anti_parse_fact((ee_check_predicate, ee_check_instance))
+                    return False, f"目标{goal}实体存在性检查未通过,不存在依赖实体{entity}。"
+        return True, 'pass'
+
+    def _pass_algebraic_constraints(self, theorem_gdl, replace):
+        for gpl_one_term in theorem_gdl['premises_gpl']:
+            for algebraic_relation, expr in gpl_one_term['algebraic_constraints']:
+                expr = replace_expr(expr, replace)
+                if not _satisfy_algebraic[algebraic_relation](expr, self.points):
+                    expr = str(expr).replace(' ', '')
+                    return False, f"代数约束'{expr}'不满足。",
+        return True, "约束通过"
+
+    def _find_father_ids(self, predicate, instance):
+        # print('goal:', predicate, instance)
+        father_ids = []
+        if predicate != 'Eq':
+            if (predicate, instance) not in self.goal_ids:
+                return []
+            for goal_id in self.goal_ids[(predicate, instance)]:
+                if self.status_of_goal[goal_id] == 0:
+                    father_ids.append(goal_id)
+        else:  # algebraic goal
+            for goal_id in self.simplified_algebraic_goal:
+                # print(self.simplified_algebraic_goal[goal_id])
+                if self.status_of_goal[goal_id] != 0:
+                    continue
+                if len(self.simplified_algebraic_goal[goal_id][2] & instance.free_symbols) == 0:
+                    continue
+                father_ids.append(goal_id)
+
+        return father_ids
+
+    def _generate_sub_goals(self, theorem_gdl, replace):
+        sub_goals = []
+
+        for gpl_one_term in theorem_gdl['premises_gpl']:
+            predicate, paras = gpl_one_term['product'][:2]
+            instance = tuple(replace_paras(paras, replace))
+            passed, result = self._pass_geometric_constraints(predicate, instance)
+            if not passed:
+                return None, "构造子目标失败," + result
+            sub_goals.append((predicate, instance))
+
+            for predicate, paras in gpl_one_term['geometric_premises']:
+                instance = tuple(replace_paras(paras, replace))
+                passed, result = self._pass_geometric_constraints(predicate, instance)
+                if not passed:
+                    return None, "构造子目标失败," + result
+                sub_goals.append((predicate, instance))
+
+            for expr in gpl_one_term['algebraic_premises']:
+                instance = replace_expr(expr, replace)
+                fact = _anti_parse_fact(('Eq', instance))
+                instance = self._adjust_expr(instance)
+                if instance is None:  # not ask len(instance.free_symbols) > 0
+                    return None, f"构造子目标失败,子目标{fact}非法。"
+                passed, result = self._pass_geometric_constraints('Eq', instance)
+                if not passed:
+                    return None, "构造子目标失败," + result
+                sub_goals.append(('Eq', instance))
+
+        return sub_goals, 'passed'
+
+    def _set_status(self, goal_id, status):
+        """
+        1: 向下传递给所有children-1,同时向上检查一层,如果所有兄弟都是 1,就应用定理
+        -1:横向传递给所有兄弟、向下传递给所有children
+        """
+        all_sub_goal_solved = False
+        if self.status_of_goal[goal_id] == 0:
+            _, _, father_id, operation_id = self.goals[goal_id]
+
+            if status == 1:
+                self.status_of_goal[goal_id] = 1
+                all_sub_goal_solved = True
+                for bro_goal_id in self.operation_groups[operation_id]:
+                    if self.status_of_goal[bro_goal_id] != 1:
+                        all_sub_goal_solved = False
+                        break
+
+            else:  # status == -1
+                self.status_of_goal[goal_id] = -1
+                for bro_goal_id in self.operation_groups[operation_id]:
+                    self._set_status(bro_goal_id, -1)
+
+            if goal_id in self.sub_operations:
+                for child_operation_id in self.sub_operations[goal_id]:
+                    for child_goal_id in self.operation_groups[child_operation_id]:
+                        self._set_status(child_goal_id, -1)
+
+        return all_sub_goal_solved
+
+    def _check_goals(self, goal_ids):
+        if len(goal_ids) == 0:
+            return
+
+        goal_ids = list(goal_ids)
+        # print(goal_ids)
+
+        for goal_id in goal_ids:
+            if self.status_of_goal[goal_id] != 0:
+                continue
+            all_sub_goal_solved = False
+            predicate, instance, father_id, operation_id = self.goals[goal_id]
+
+            if self.goals[goal_id][0] == 'Eq':  # algebraic goal
+                instance = self.simplified_algebraic_goal[goal_id][0]
+                # print(instance)
+                status, premise_ids = self._pass_algebraic_premise(instance)
+                # print(status, premise_ids)
+                if status == 1:  # has solution, update status
+                    all_sub_goal_solved = self._set_status(goal_id, 1)
+                    premise_ids.update(self.simplified_algebraic_goal[goal_id][1])
+                    self.premise_ids_of_goal[goal_id] = premise_ids
+                elif status == -1:  # has solution, update status
+                    self._set_status(goal_id, -1)
+                    premise_ids.update(self.simplified_algebraic_goal[goal_id][1])
+                    self.premise_ids_of_goal[goal_id] = premise_ids
+                else:  # no solution, simplify expr
+                    premise_ids = self.simplified_algebraic_goal[goal_id][1]
+                    for sym in list(instance.free_symbols):
+                        if sym in self.sym_to_value:
+                            instance = instance.subs(self.sym_to_value)
+                            premise_ids.add(self.fact_id[('Eq', sym - self.sym_to_value[sym])])
+                    dependent_syms = set(instance.free_symbols)
+                    group_ids = set()
+                    for group_id in self.equations:
+                        if len(dependent_syms & self.equations[group_id][2]) > 0:
+                            dependent_syms.update(self.equations[group_id][2])
+                            group_ids.add(group_id)
+                    self.simplified_algebraic_goal[goal_id] = (instance, premise_ids, dependent_syms, group_ids)
+
+            else:  # geometric goal
+                # print(f'check={(predicate, instance) in self.fact_id}', (predicate, instance))
+                if (predicate, instance) in self.fact_id:
+                    self.premise_ids_of_goal[goal_id] = {self.fact_id[(predicate, instance)]}
+                    all_sub_goal_solved = self._set_status(goal_id, 1)
+                elif predicate in self.parsed_gdl['Presets']:
+                    self._set_status(goal_id, -1)
+
+            if all_sub_goal_solved and father_id is not None and self.operations[operation_id][2] is not None:
+                premise_ids = set()
+                for bro_goal_id in self.operation_groups[operation_id]:
+                    premise_ids.update(self.premise_ids_of_goal[bro_goal_id])
+                _, theorem_name, theorem_paras = self.operations[operation_id]
+                operation_id = self._add_operation(('Apply', theorem_name, theorem_paras))
+                theorem_gdl = self.parsed_gdl['Theorems'][theorem_name]
+                replace = dict(zip(theorem_gdl['paras'], theorem_paras))
+                fact_id, new_goal_ids = self._add_conclusion(theorem_gdl, replace, premise_ids, operation_id)
+                if fact_id is not None:
+                    goal_ids.extend(new_goal_ids)
+
+    def _run_gpl(self, theorem_gpl):
+        paras = []
+        instances = [[]]
+        premise_ids = [[]]
+
+        for gpl_one_term in theorem_gpl:
+            product = gpl_one_term['product']  # (predicate, paras, inherent_same_index, mutual_same_index, added_index)
+            geometric_premises = gpl_one_term['geometric_premises']  # [(predicate, paras)]
+            algebraic_premises = gpl_one_term['algebraic_premises']  # [expr]
+            algebraic_constraints = gpl_one_term['algebraic_constraints']  # [(relation_type, expr)]
+
+            new_instances = []
+            new_premise_ids = []
+            paras.extend([product[1][j] for j in product[4]])
+            for k in range(len(instances)):
+                instance = instances[k]
+                for product_instance in self.predicate_to_fact_instances[product[0]]:
+                    # check inherent same index constraint
+                    passed = True
+                    for i, j in product[2]:
+                        if product_instance[i] != product_instance[j]:
+                            passed = False
+                            break
+                    if not passed:
+                        continue
+
+                    # check mutual same index constraint
+                    passed = True
+                    for i, j in product[3]:
+                        if instance[i] != product_instance[j]:
+                            passed = False
+                            break
+                    if not passed:
+                        continue
+
+                    # constrained cartesian product: add different letter
+                    new_instance = list(instance)
+                    new_instance.extend([product_instance[j] for j in product[4]])
+
+                    replace = dict(zip(paras, new_instance))
+
+                    # check constraints
+                    passed, result, constraints_premise_id = self._pass_constraints(
+                        geometric_premises, algebraic_premises, algebraic_constraints, replace
+                    )
+                    if not passed:
+                        continue
+
+                    new_premise_id = list(premise_ids[k])
+                    new_premise_id.append(self.fact_id[(product[0], product_instance)])
+                    new_premise_id.extend(constraints_premise_id)
+
+                    new_instances.append(new_instance)
+                    new_premise_ids.append(new_premise_id)
+
+            instances = new_instances
+            premise_ids = new_premise_ids
+
+        return paras, instances, premise_ids
+
+    def show(self):
+        operation_ids = set()
+        goal_related_operation_ids = set()
+        if len(self.goals) > 0 and self.status_of_goal[0] == 1:
+            goal_related_premise_ids = list(self.premise_ids_of_goal[0])
+        else:
+            goal_related_premise_ids = []
+        for fact_id in goal_related_premise_ids:
+            goal_related_operation_ids.add(self.facts[fact_id][3])
+            for new_fact_id in self.facts[fact_id][2]:
+                if new_fact_id not in goal_related_premise_ids:
+                    goal_related_premise_ids.append(new_fact_id)
+        goal_related_premise_ids = set(goal_related_premise_ids)
+
+        pf = '{0:<15}{1:<45}{2:<40}{3:<15}{4:<100}'
+        pfu = '\033[32m' + pf + '\033[0m'
+        for predicate in self.predicate_to_fact_instances:
+            if len(self.predicate_to_fact_instances[predicate]) == 0:
+                continue
+
+            if predicate in self.parsed_gdl['Presets']:
+                print(f'\033[34mPreset - {predicate}:\033[0m')
+            else:
+                print(f'\033[34mRelation - {predicate}:\033[0m')
+            print('\033[34m' + pf.format(
+                'fact_id', 'instance', 'premise_ids', 'operation_id', 'operation') + '\033[0m')
+            for instance in self.predicate_to_fact_instances[predicate]:
+                fact_id = self.fact_id[(predicate, instance)]
+                _, _, premise_ids, operation_id = self.facts[fact_id]
+                operation_ids.add(operation_id)
+                operation = _anti_parse_operation(self.operations[operation_id])
+                if predicate != 'Eq':
+                    instance = '(' + ','.join(instance) + ')'
+                else:
+                    instance = str(instance).replace(' ', '')
+                premise_ids = '{' + ','.join([str(item) for item in sorted(list(premise_ids))]) + '}'
+                if fact_id not in goal_related_premise_ids:
+                    print(pf.format(fact_id, instance, premise_ids, operation_id, operation))
+                else:
+                    print(pfu.format(fact_id, instance, premise_ids, operation_id, operation))
+            print()
+
+        if len(self.sym_to_syms) > 0:
+            sym_pf = '{0:<50}{1:<15}{2:<50}{3:<10}{4:<20}'
+            sym_pfu = '\033[32m' + sym_pf + '\033[0m'
+            print('\033[33mAlgebraic System - Symbols:\033[0m')
+            print('\033[33m' + sym_pf.format(
+                'attribution', 'sym', 'multiple_forms', 'fact_id', 'value') + '\033[0m')
+            for sym in self.sym_to_syms:
+                if '.' in str(sym):
+                    entities, attr = str(sym).split('.')
+                    predicate = self.parsed_gdl['Attributions'][attr]['name']
+                    instance = ",".join(list(entities))
+                    attr = f'{predicate}({instance})'
+                    multiple_forms = '(' + ', '.join([str(item) for item in self.sym_to_syms[sym]]) + ')'
+                else:
+                    attr = f'Free({str(sym)})'
+                    multiple_forms = '(' + str(sym) + ')'
+
+                if sym in self.sym_to_value:
+                    fact_id = self.fact_id[('Eq', sym - self.sym_to_value[sym])]
+                    value = str(self.sym_to_value[sym])
+                else:
+                    fact_id = 'None'
+                    value = "None"
+
+                if fact_id != 'None' and fact_id in goal_related_premise_ids:
+                    print(sym_pfu.format(attr, str(sym), multiple_forms, fact_id, value))
+                else:
+                    print(sym_pf.format(attr, str(sym), multiple_forms, fact_id, value))
+            print()
+
+        if len(self.equations) > 0:
+            eq_groups_pf = '{0:<10}{1:<45}{2:<35}{3:<15}'
+            print('\033[33mAlgebraic System - Equation groups:\033[0m')
+            print('\033[33m' + eq_groups_pf.format(
+                'group_id', 'simplified_eq', 'premise_ids', 'free_symbols') + '\033[0m')
+            for group_id in self.equations:
+                for i in range(len(self.equations[group_id][0])):
+                    simplified_eq = str(self.equations[group_id][0][i]).replace(' ', '')
+                    premise_ids = ','.join([str(item) for item in sorted(list(self.equations[group_id][1][i]))])
+                    premise_ids = '{' + premise_ids + '}'
+                    free_symbols = ', '.join([str(item) for item in self.equations[group_id][0][i].free_symbols])
+                    free_symbols = '(' + free_symbols + ')'
+                    print(eq_groups_pf.format(group_id, simplified_eq, premise_ids, free_symbols))
+                print()
+
+        if len(self.simplified_algebraic_goal) > 0:
+            algebraic_goal_pf = '{0:<10}{1:<45}{2:<35}{3:<15}{4:<30}'
+            print('\033[33mAlgebraic System - Algebraic Goals:\033[0m')
+            print('\033[33m' + algebraic_goal_pf.format(
+                'goal_id', 'simplified_eq', 'premise_ids', 'group_ids', 'dependent_syms', ) + '\033[0m')
+            for goal_id in self.simplified_algebraic_goal:
+                simplified_eq, premise_ids, dependent_syms, group_ids = self.simplified_algebraic_goal[goal_id]
+                simplified_eq = str(simplified_eq).replace(' ', '')
+                premise_ids = ','.join([str(item) for item in sorted(list(premise_ids))])
+                premise_ids = '{' + premise_ids + '}'
+                dependent_syms = ','.join([str(item) for item in sorted(list(dependent_syms), key=str)])
+                dependent_syms = '{' + dependent_syms + '}'
+                group_ids = ','.join([str(item) for item in sorted(list(group_ids))])
+                group_ids = '{' + group_ids + '}'
+                print(algebraic_goal_pf.format(goal_id, simplified_eq, premise_ids, group_ids, dependent_syms))
+            print()
+
+        goal_pf = '{0:<10}{1:<40}{2:<40}{3:<10}{4:<10}{5:<35}{6:<15}{7:<100}'
+        goal_pfs = '\033[32m' + goal_pf + '\033[0m'
+        goal_pfu = '\033[31m' + goal_pf + '\033[0m'
+        print("\033[35mGoals:\033[0m")
+        print('\033[35m' + goal_pf.format('goal_id', 'predicate', 'instance', 'father_id', 'status',
+                                          'premise_ids', 'operation_id', 'operation') + '\033[0m')
+        last_operation_id = self.goals[0][3]
+        for goal_id in range(len(self.goals)):
+            predicate, instance, father_id, operation_id = self.goals[goal_id]
+            if last_operation_id != operation_id:
+                print()
+            last_operation_id = operation_id
+            if predicate != 'Eq':
+                instance = '(' + ','.join(instance) + ')'
+            else:
+                instance = str(instance).replace(' ', '')
+            father_id = str(father_id)
+            status = self.status_of_goal[goal_id]
+            operation_ids.add(operation_id)
+            operation = _anti_parse_operation(self.operations[operation_id])
+            if status == 1:
+                premise_ids = ','.join([str(item) for item in sorted(list(self.premise_ids_of_goal[goal_id]))])
+                premise_ids = '{' + premise_ids + '}'
+            else:
+                premise_ids = '{}'
+
+            if status == 1:
+                print(goal_pfs.format(goal_id, predicate, instance, father_id, status, premise_ids,
+                                      operation_id, operation))
+            elif status == -1:
+                print(goal_pfu.format(goal_id, predicate, instance, father_id, status, premise_ids,
+                                      operation_id, operation))
+            else:
+                print(goal_pf.format(goal_id, predicate, instance, father_id, status, premise_ids,
+                                     operation_id, operation))
+        print()
+
+        if len(self.theorem_instances) > 0:
+            theorem_instance_pf = '{0:<50}{1:<100}'
+            print('\033[35mTheorem instances:\033[0m')
+            print('\033[35m' + theorem_instance_pf.format('theorem_name', 'theorem_instances') + '\033[0m')
+            for t_name in self.theorem_instances:
+                # t_instances = [f"({','.join(item)})" for item in self.theorem_instances[t_name][0]]
+                # t_instances = f"[{', '.join(t_instances)}]"
+                print(theorem_instance_pf.format(t_name, str(self.theorem_instances[t_name])))
+
+        operation_pf = '{0:<15}{1:<50}'
+        operation_pfu = '\033[32m' + operation_pf + '\033[0m'
+        print('\033[36mOperations:\033[0m')
+        print('\033[36m' + operation_pf.format('operation_id', 'operation') + '\033[0m')
+        for operation_id in range(len(self.operations)):
+            if operation_id not in operation_ids:
+                continue
+            operation = _anti_parse_operation(self.operations[operation_id])
+            if operation_id in goal_related_operation_ids:
+                print(operation_pfu.format(operation_id, operation))
+            else:
+                print(operation_pf.format(operation_id, operation))
+        print()
+
+    def _get_update(self, old_fact_id, old_goal_id, old_goal_status):
+        result = []
+
+        new_fact_ids = [i for i in range(old_fact_id, len(self.facts))]
+        if len(new_fact_ids) > 0:
+            result.append('新推导出的条件:')
+            for fact_id in new_fact_ids:
+                result.append(_anti_parse_fact((self.facts[fact_id][0], self.facts[fact_id][1])))
+
+        new_goal_ids = [i for i in range(old_goal_id, len(self.goals))]
+        if len(new_goal_ids) > 0:
+            result.append(
+                '新分解得到的子目标及其原目标为(括号内数字表示目标状态,0表示此目标待求解,1表示此目标已求解,-1表示此目标不可能实现):'
+            )
+            for goal_id in new_goal_ids:
+                goal = _anti_parse_fact((self.goals[goal_id][0], self.goals[goal_id][1]))
+                goal = goal + f'({self.status_of_goal[goal_id]}), 父目标为'
+                father_goal_id = self.goals[goal_id][2]
+                father_goal = _anti_parse_fact((self.goals[father_goal_id][0], self.goals[father_goal_id][1]))
+                father_goal = father_goal + f'({self.status_of_goal[father_goal_id]})'
+                result.append(goal + father_goal)
+
+        updated_goal_ids = [goal_id for goal_id in range(old_goal_id)
+                            if old_goal_status[goal_id] != self.status_of_goal[goal_id]]
+        if len(updated_goal_ids) > 0:
+            result.append(
+                '部分目标的状态更新为(括号内数字表示目标状态,0表示此目标待求解,1表示此目标已求解,-1表示此目标不可能实现):'
+            )
+            for goal_id in updated_goal_ids:
+                goal = _anti_parse_fact((self.goals[goal_id][0], self.goals[goal_id][1]))
+                goal = goal + f'({self.status_of_goal[goal_id]})'
+                result.append(goal)
+
+        return '\n'.join(result)
+
+    def _parse_theorem(self, theorem):
+        try:
+            theorem_name, theorem_paras = parse_fact(theorem.replace(' ', ''))
+        except Exception as e:
+            e_msg = (f"Error '{repr(e)}' occurred while parsing the theorem '{theorem}'. "
+                     f"The theorem format is incorrect.")
+            raise Exception(e_msg)
+
+        if theorem_name not in self.parsed_gdl["Theorems"]:
+            e_msg = f"Unknown theorem name: '{theorem_name}'."
+            raise Exception(e_msg)
+
+        error_paras = set([char for char in theorem_paras if not char.isupper()])
+        if len(error_paras) > 0:
+            e_msg = (f"Theorem parameters must be uppercase letters and , only. "
+                     f"The current theorem contains invalid characters '{str(error_paras)}'.")
+            raise Exception(e_msg)
+
+        if len(theorem_paras) != 0 and len(theorem_paras) != len(self.parsed_gdl["Theorems"][theorem_name]['paras']):
+            e_msg = (f"'{theorem}' has wrong number of parameters "
+                     f"(expected {len(self.parsed_gdl["Theorems"][theorem_name]['paras'])}).")
+            raise Exception(e_msg)
+
+        if len(theorem_paras) == 0:
+            theorem_paras = None
+
+        return theorem_name, theorem_paras
+
+    def apply(self, theorem):
+        old_fact_id = len(self.facts)
+        old_goal_id = len(self.goals)
+        old_goal_status = self.status_of_goal.copy()
+        theorem_name, theorem_paras = self._parse_theorem(theorem)
+
+        if theorem_paras is not None:
+            theorem_gdl = self.parsed_gdl['Theorems'][theorem_name]
+            replace = dict(zip(theorem_gdl['paras'], theorem_paras))
+            premise_ids = set()
+            for gpl_one_term in theorem_gdl['premises_gpl']:  # run gdl with theorem parameter
+                product = gpl_one_term['product']
+                algebraic_constraints = gpl_one_term['algebraic_constraints']
+                geometric_premises = gpl_one_term['geometric_premises']
+                algebraic_premises = gpl_one_term['algebraic_premises']
+                predicate = product[0]
+                instance = replace_paras(product[1], replace)
+                if (predicate, instance) not in self.fact_id:  # verification mode, not cartesian product
+                    result = f"定理'{theorem}'应用失败,前提'{_anti_parse_fact((predicate, instance))}'不满足。"
+                    return result
+
+                premise_ids.add(self.fact_id[(predicate, instance)])
+
+                # check constraints
+                passed, result, constraints_premise_ids = self._pass_constraints(
+                    geometric_premises, algebraic_premises, algebraic_constraints, replace)
+                if not passed:
+                    return f"定理'{theorem}'应用失败," + result
+                premise_ids.update(constraints_premise_ids)
+
+            # add operation
+            operation_id = self._add_operation(('Apply', theorem_name, theorem_paras))
+
+            # add conclusions
+            fact_id, goal_ids = self._add_conclusion(theorem_gdl, replace, premise_ids, operation_id)
+            if fact_id is None:
+                return f"定理'{theorem}'所有前提已满足,但添加结论失败。结论可能已经存在,或者结论未通过合法性检查。"
+
+            self._check_goals(goal_ids)
+
+            result = f"定理'{theorem}'执行成功,以下为问题的状态更新。\n"
+            return result + self._get_update(old_fact_id, old_goal_id, old_goal_status)
+
+        else:
+            if theorem_name in special_theorem:
+                msg = f"When using the 'apply' tool with theorem '{theorem_name}', theorem parameters must be added."
+                raise Exception(msg)
+
+            if ('perimeter' in theorem_name or 'area' in theorem_name or
+                    'similar' in theorem_name or 'congruent' in theorem_name):
+                msg = ("When the theorem name contains 'perimeter', 'area', 'similar' and 'congruent', "
+                       "theorem parameters must be added.")
+                raise Exception(msg)
+
+            all_goal_ids = set()
+            theorem_gdl = self.parsed_gdl['Theorems'][theorem_name]
+            paras, instances, premise_ids = self._run_gpl(theorem_gdl['premises_gpl'])
+            for i in range(len(instances)):
+                replace = dict(zip(paras, instances[i]))
+
+                # add operation
+                theorem_paras = replace_paras(theorem_gdl['paras'], replace)
+                operation_id = self._add_operation(('Apply', theorem_name, theorem_paras))
+
+                # add conclusions
+                fact_id, goal_ids = self._add_conclusion(theorem_gdl, replace, premise_ids[i], operation_id)
+                all_goal_ids.update(goal_ids)
+
+            self._check_goals(all_goal_ids)
+
+            if len(all_goal_ids) == 0:
+                return f"定理'{theorem_name}'执行成功,但没有推导出新的结论。"
+
+            result = f"定理'{theorem_name}'执行成功,以下为问题的状态更新。\n"
+            return result + self._get_update(old_fact_id, old_goal_id, old_goal_status)
+
+    def decompose(self, theorem):
+        old_fact_id = len(self.facts)
+        old_goal_id = len(self.goals)
+        old_goal_status = self.status_of_goal.copy()
+        theorem_name, theorem_paras = self._parse_theorem(theorem)
+
+        if theorem_paras is None:
+            raise ValueError("Tool 'decompose' only accepts theorems with parameters!")
+
+        theorem_gdl = self.parsed_gdl['Theorems'][theorem_name]
+        replace = dict(zip(theorem_gdl['paras'], theorem_paras))
+        predicate, instance = theorem_gdl['conclusion']  # generate conclusion
+        if predicate == "Eq":
+            instance = self._adjust_expr(replace_expr(instance, replace))
+            if instance is None or len(instance.free_symbols) == 0:
+                return f"使用定理'{theorem}'分解目标{_anti_parse_fact((predicate, instance))}失败,目标无需分解或非法。"
+        else:
+            instance = tuple(replace_paras(instance, replace))
+        goal = _anti_parse_fact((predicate, instance))
+
+        passed, result = self._pass_algebraic_constraints(theorem_gdl, replace)  # ac checks
+        if not passed:
+            return f"使用定理'{theorem}'分解目标{goal}失败," + result
+
+        passed, result = self._pass_geometric_constraints(predicate, instance)  # ee checks
+        if not passed:
+            return f"使用定理'{theorem}'分解目标{goal}失败," + result
+
+        father_ids = self._find_father_ids(predicate, instance)  # find father nodes
+        if len(father_ids) == 0:
+            return f"使用定理'{theorem}'分解目标{goal}失败,待分解目标不存在。"
+
+        sub_goals, result = self._generate_sub_goals(theorem_gdl, replace)  # generate sub_goals
+        if sub_goals is None:
+            return f"使用定理'{theorem}'分解目标{goal}失败," + result
+
+        all_goal_ids = set()
+        for father_id in father_ids:  # add sub_goals
+            operation_id = self._add_operation(('Decompose', theorem_name, theorem_paras))
+            goal_ids = self._add_goals(sub_goals, father_id, operation_id)
+            if goal_ids is not None:
+                all_goal_ids.update(goal_ids)
+
+        if len(all_goal_ids) == 0:
+            return f"使用定理'{theorem}'分解目标{goal}失败,新分解的子目标不能是原目标的父目标。"
+
+        self._check_goals(all_goal_ids)
+
+        result = f"使用定理'{theorem}'分解目标{goal}成功,以下为问题的状态更新。\n"
+        return result + self._get_update(old_fact_id, old_goal_id, old_goal_status)
+
+    def find_fact(self, relation):
+        if relation not in self.predicate_to_fact_instances:
+            msg = f"Unknown relation type '{relation}'."
+            raise Exception(msg)
+
+        if len(self.predicate_to_fact_instances) == 0:
+            return relation + f"类型的关系列表为空,当前问题暂时未推导出{relation}关系。"
+
+        if relation == 'Eq':
+            result = []
+            if len(self.equations) > 0:
+                result.append("按照方程变量是否相交来分组,得到的代数方程组(所有方程省略'=0'、组序号可能不连续):")
+                for group_id in self.equations:
+                    eqs = [str(eq).replace(' ', '') for eq in self.equations[group_id][0]]
+                    result.append(f'Group {group_id}: ' + ', '.join(eqs))
+            if len(self.sym_to_value) > 0:
+                result.append("以下是所有已经求解出值的变量:")
+                result.append(str(self.sym_to_value))
+            return '\n'.join(result)
+        else:
+            instances = []
+            for instance in self.predicate_to_fact_instances[relation]:
+                instances.append('(' + ','.join(instance) + ')')
+            result = relation + ': ' + ', '.join(instances)
+            return result
+
+    def find_goal(self, relation):
+        if relation not in self.predicate_to_goal_instances:
+            msg = f"Unknown relation type '{relation}'."
+            raise Exception(msg)
+
+        if len(self.predicate_to_goal_instances) == 0:
+            return relation + f"类型的目标列表为空,当前问题暂时未分解出{relation}目标。"
+
+        results = [
+            f"以下为{relation}类型目标和状态(括号内数字表示目标状态,0表示此目标待求解,1表示此目标已求解,-1表示此目标不可能实现):"
+        ]
+        for instance in self.predicate_to_goal_instances[relation]:
+            goal = _anti_parse_fact((relation, instance))
+            for goal_id in self.goal_ids[(relation, instance)]:
+                _, _, father_id, _ = self.goals[goal_id]
+                if father_id is None:
+                    results.append(goal + f'({self.status_of_goal[goal_id]})' + ', 初始目标')
+                else:
+                    father_goal = _anti_parse_fact((self.goals[father_id][0], self.goals[father_id][1]))
+                    father_goal = father_goal + f'({self.status_of_goal[father_id]})'
+                    results.append(goal + f'({self.status_of_goal[goal_id]})' + ', 父目标为' + father_goal)
+        return '\n'.join(results)
+
+    def state(self):
+        result = ["当前问题的状态描述如下所示:", "几何图形的结构信息描述:"]
+        for predicate in self.predicate_to_fact_instances:
+            if predicate not in {'Shape', 'Collinear', 'Cocircular'}:
+                continue
+            if len(self.predicate_to_fact_instances[predicate]) == 0:
+                continue
+            instances = []
+            for instance in self.predicate_to_fact_instances[predicate]:
+                instances.append(_anti_parse_fact((predicate, instance)))
+            result.append(', '.join(instances))
+
+        result.append("几何问题的初始已知条件:")
+        instances = []
+        for fact_id in range(len(self.facts)):
+            predicate, instance, premise_ids, operation_id = self.facts[fact_id]
+            if predicate in {'Shape', 'Collinear', 'Cocircular'}:
+                continue
+            if len(premise_ids) > 0:
+                continue
+            instances.append(_anti_parse_fact((predicate, instance)))
+        result.append(', '.join(instances))
+
+        result.append(
+            "几何问题的求解目标和状态(括号内数字表示目标状态,0表示此目标待求解,1表示此目标已求解,-1表示此目标不可能实现):"
+        )
+        predicate, instance, _, _ = self.goals[0]
+        result.append(_anti_parse_fact((predicate, instance)) + f'({self.status_of_goal[0]})')
+
+        result.append("解析几何图形的结构信息得到的实体:")
+        for predicate in self.predicate_to_fact_instances:
+            if predicate not in self.parsed_gdl['Presets'] or predicate in {'Shape', 'Collinear', 'Cocircular', 'Eq'}:
+                continue
+            if len(self.predicate_to_fact_instances[predicate]) == 0:
+                continue
+            instances = []
+            for instance in self.predicate_to_fact_instances[predicate]:
+                instances.append('(' + ','.join(instance) + ')')
+            result.append(predicate + ': ' + ', '.join(instances))
+
+        result.append("按条件类型列出的所有已知条件:")
+        for predicate in self.predicate_to_fact_instances:
+            if predicate in self.parsed_gdl['Presets']:
+                continue
+            if len(self.predicate_to_fact_instances[predicate]) == 0:
+                continue
+            instances = []
+            if predicate == 'Eq':
+                for instance in self.predicate_to_fact_instances[predicate]:
+                    instances.append(str(instance).replace(' ', ''))
+            else:
+                for instance in self.predicate_to_fact_instances[predicate]:
+                    instances.append('(' + ','.join(instance) + ')')
+            result.append(predicate + ': ' + ', '.join(instances))
+
+        result.append(self.find_fact('Eq'))  # 代数关系
+
+        result.append(
+            "初始目标和所有分解得到的目标(括号内数字表示目标状态,0表示此目标待求解,1表示此目标已求解,-1表示此目标不可能实现):"
+        )
+        goal_group = {}  # {(father_id, operation_id): [goal_id]}
+
+        for goal_id in range(len(self.goals)):
+            predicate, instance, father_id, operation_id = self.goals[goal_id]
+            if (father_id, operation_id) in goal_group:
+                goal_group[(father_id, operation_id)].append(goal_id)
+            else:
+                goal_group[(father_id, operation_id)] = [goal_id]
+
+        for father_id, operation_id in goal_group:
+            goals = []
+            for goal_id in goal_group[(father_id, operation_id)]:
+                goal = _anti_parse_fact((self.goals[goal_id][0], self.goals[goal_id][1]))
+                goals.append(goal + f'({self.status_of_goal[goal_id]})')
+            goal = '&'.join(goals)
+
+            if father_id is None:
+                result.append(goal + ', 初始目标')
+            else:
+                father_goal = _anti_parse_fact((self.goals[father_id][0], self.goals[father_id][1]))
+                father_goal = father_goal + f'({self.status_of_goal[father_id]})'
+                result.append(goal + ', 父目标为' + father_goal)
+
+        return '\n'.join(result)
+
+    def check(self):
+        goal = _anti_parse_fact((self.goals[0][0], self.goals[0][1]))
+        if self.status_of_goal[0] == 1:
+            return f'问题初始目标{goal}已完成,求解成功,你需要调用finish()工具结束解题过程。'
+        return f'问题初始目标{goal}未完成,请继续求解。'

+ 964 - 0
Co-creation-projects/BitSecret-GPSAgent/src/gps/utils.py

@@ -0,0 +1,964 @@
+import json
+from sympy import symbols, sympify, log, atan2, pi
+from pprint import pprint
+import re
+import string
+import random
+import time
+from copy import deepcopy
+import pickle
+import os
+
+
+def load_json(filename):
+    with open(filename, "r", encoding="utf-8") as f:
+        return json.load(f)
+
+
+def save_json(data, filename):
+    filename_bk = filename + '.bk'
+    with open(filename_bk, "w", encoding="utf-8") as f:
+        json.dump(data, f, ensure_ascii=False, indent=2)
+    if os.path.exists(filename):
+        os.remove(filename)
+    os.rename(filename_bk, filename)
+
+
+def show_json(dict_data):
+    pprint(dict_data, sort_dicts=False, compact=True)
+    print()
+
+
+def load_pickle(filename):
+    with open(filename, "rb") as f:
+        data = pickle.load(f)
+    return data
+
+
+def save_pickle(data, filename):
+    with open(filename, "wb") as f:
+        pickle.dump(data, f)
+
+
+def debug_execute(func, debug_execute_args):
+    timing = time.time()
+    result = func(*debug_execute_args)
+    msg = (f"func: {func.__name__}, args: {str(debug_execute_args)}, return: {str(result)}, "
+           f"take: {round(time.time() - timing, 4)}s.")
+    if isinstance(result, bool):
+        if result:
+            print(f"\033[32m{msg}\033[0m")
+        else:
+            print(f"\033[31m{msg}\033[0m")
+    else:
+        print(msg)
+
+
+def parse_fact(s):
+    """
+    Parse s to get predicate name and paras.
+    >> parse_geo_predicate('Predicate(A,B,C)')
+    ('Predicate', ['A', 'B', 'C'])
+    """
+    predicate_name, paras = s.split("(")
+    paras = paras[:-1].replace(",", "")
+    return predicate_name, tuple(paras)
+
+
+def parse_expr(s):
+    """
+    Parse str expression to sympy expression.
+    Args:
+        s (str): Algebra relation and expression. The components include algebra relation types,
+        algebraic operations, the symbolic representations of measures and constants. Such as:
+        'Eq(Sub(A.y,Add(Mul(l.k,A.x),l.b)))', 'Value(Mul(Sub(C.x,B.x),Sub(A.y,B.y)))'.
+
+    Returns:
+        parsed_s (tuple): Algebra relation type and instance of sympy expression. Such as:
+        ('Eq', -A.x*l.k + A.y - l.b), ('Value', (A.y - B.y)*(-B.x + C.x)).
+    """
+    predicate, expr_str = s.split("(", 1)
+    expr_str = expr_str[:-1]
+
+    if '(' not in expr_str:  # such as 'Eq(lk.ma)'
+        return predicate, symbols(expr_str)
+
+    i = 0
+    j = 0
+    stack = []
+    while j < len(expr_str):
+        if expr_str[j] == "(":
+            stack.append(expr_str[i:j])
+            stack.append(expr_str[j])
+            i = j + 1
+        elif expr_str[j] == ",":
+            if i < j:
+                stack.append(expr_str[i: j])
+                i = j + 1
+            else:
+                i = i + 1
+        elif expr_str[j] == ")":
+            if i < j:
+                stack.append(expr_str[i: j])
+                i = j + 1
+            else:
+                i = i + 1
+
+            paras = []
+            while True:
+                para = stack.pop()
+                if para == "(":
+                    break
+                if type(para) is str:
+                    if '.' in para:
+                        para = symbols(para)  # symbol representation of measure
+                    else:
+                        para = sympify(para.replace('{', '(').replace('}', ')'))  # constant, free symbols, or expr
+                paras.append(para)
+            paras = paras[::-1]
+
+            operation = stack.pop()
+
+            if operation == 'Add':
+                result = paras[0]
+                for p in paras[1:]:
+                    result += p
+            elif operation == 'Sub':
+                result = paras[0] - paras[1]
+            elif operation == 'Mul':
+                result = paras[0]
+                for p in paras[1:]:
+                    result *= p
+            elif operation == 'Div':
+                result = paras[0] / paras[1]
+            elif operation == 'Pow':
+                result = paras[0] ** paras[1]
+            elif operation == 'Log':
+                result = log(paras[0])
+            elif operation == 'Ma':
+                a_x, a_y, b_x, b_y, c_x, c_y = paras
+                BA = (a_x - b_x, a_y - b_y)  # vector BA
+                BC = (c_x - b_x, c_y - b_y)  # vector BC
+                angle_BA = atan2(BA[1], BA[0])  # (-π, π]
+                angle_BC = atan2(BC[1], BC[0])  # (-π, π]
+                result = (angle_BA - angle_BC) % (2 * pi)  # clockwise
+            else:
+                e_msg = f"Unknown operation '{operation}' in s '{s}'."
+                raise Exception(e_msg)
+
+            stack.append(result)
+
+        j = j + 1
+
+    if len(stack) > 1:
+        e_msg = f"Syntax error in s '{s}': missing ')'?"
+        raise Exception(e_msg)
+
+    return predicate, stack.pop()
+
+
+def replace_paras(paras, replace):
+    replaced_paras = [replace[p] for p in paras]
+    return tuple(replaced_paras)
+
+
+def replace_expr(expr, replace):
+    """Replace instances according to the replacement mapping.
+
+    Args:
+        expr (sympy_expr): instance of sympy expression. Such as -A.x*l.k + A.y - l.b.
+        replace (dict): Keys are the old entity and values are the new entity. Such As {'A': 'B', 'l': 'k'}.
+
+    Returns:
+        replaced_expr: Replaced expr. Such as -B.x*k.k + B.y - k.b.
+    """
+    replace_old_to_temp = {}
+    replace_temp_to_new = {}
+    for sym_old in expr.free_symbols:
+        entities_old, attr = str(sym_old).split('.')
+
+        sym_temp = symbols("".join([e + "'" for e in entities_old]) + '.' + attr)
+        replace_old_to_temp[sym_old] = sym_temp
+
+        sym_new = symbols("".join([replace[e] for e in entities_old]) + '.' + attr)
+        replace_temp_to_new[sym_temp] = sym_new
+
+    expr = expr.subs(replace_old_to_temp).subs(replace_temp_to_new)
+
+    return expr
+
+
+def parse_disjunctive(s):
+    if len(s) == 0:
+        return []
+    return s.split('&')
+
+
+def parse_gdl(gdl):
+    parsed_gdl = {
+        'Presets': {},
+        'Relations': {},
+        'Attributions': {},
+        'sym_to_attr': {},
+        'Theorems': {},
+        'FactAutoExpand': {},
+        'GoalAutoExpand': {}
+    }
+
+    for preset in gdl['Presets']:
+        preset_name, preset_paras = parse_fact(preset)
+        parsed_gdl['Presets'][preset_name] = {
+            'paras': preset_paras
+        }
+
+    for relation in gdl['Relations']:
+        relation_name, relation_paras = parse_fact(relation)
+        geometric_constraints = []
+        for geometric_constraint in parse_disjunctive(gdl['Relations'][relation]['geometric_constraints']):
+            name, paras = parse_fact(geometric_constraint)
+            geometric_constraints.append((name, paras))
+        parsed_gdl['Relations'][relation_name] = {
+            'paras': relation_paras,
+            'geometric_constraints': tuple(geometric_constraints)
+        }
+
+    for attr in gdl['Attributions']:
+        attr_name, attr_paras = parse_fact(attr)
+        geometric_constraints = []
+        for geometric_constraint in parse_disjunctive(gdl['Attributions'][attr]['geometric_constraints']):
+            name, paras = parse_fact(geometric_constraint)
+            geometric_constraints.append((name, paras))
+        multiple_forms = []
+        for multi in parse_disjunctive(gdl['Attributions'][attr]['multiple_forms']):
+            _, multi_paras = parse_fact(multi)
+            multiple_forms.append(multi_paras)
+        parsed_gdl['Attributions'][gdl['Attributions'][attr]['sym']] = {
+            'name': attr_name,
+            'paras': attr_paras,
+            'geometric_constraints': tuple(geometric_constraints),
+            'multiple_forms': tuple(multiple_forms)
+        }
+
+    for theorem in gdl['Theorems']:
+        _parse_one_theorem(theorem, gdl, parsed_gdl)
+
+    for common_sense in gdl['CommonSense']:
+        _parse_one_common_sense(common_sense, gdl, parsed_gdl)
+
+    return parsed_gdl
+
+
+def get_theorems():
+    useful_theorems = set()
+    for pid in make_train_val_test_split()['test']:
+        for theorem in load_json(f'../../datasets/problems/{pid}.json')['theorem_seqs']:
+            useful_theorems.add(theorem.split('(')[0])
+    # all_theorems = set(parse_gdl(load_json('../../datasets/gdl.json'))['Theorems'])
+    # print(f'All: {len(all_theorems)}, Useful: {len(useful_theorems)}, Useless: {len(all_theorems - useful_theorems)}')
+    return useful_theorems
+
+
+def _parse_one_common_sense(common_sense, gdl, parsed_gdl):
+    if gdl['CommonSense'][common_sense]['conclusion'].startswith('Eq('):
+        premise_predicate, premise_paras = parse_fact(gdl['CommonSense'][common_sense]['premises'])
+        conclusion_predicate, conclusion_expr = parse_expr(gdl['CommonSense'][common_sense]['conclusion'])
+
+        if premise_predicate in parsed_gdl['FactAutoExpand']:
+            replace = dict(zip(premise_paras, parsed_gdl['FactAutoExpand'][premise_predicate]['paras']))
+            conclusion_expr = replace_expr(conclusion_expr, replace)
+            parsed_gdl['FactAutoExpand'][premise_predicate]['expand'] = tuple(
+                list(parsed_gdl['FactAutoExpand'][premise_predicate]['expand']) +
+                [(conclusion_predicate, conclusion_expr)]
+            )
+
+        else:
+            parsed_gdl['FactAutoExpand'][premise_predicate] = {
+                'paras': premise_paras,
+                'expand': ((conclusion_predicate, conclusion_expr),)
+            }
+    else:
+        premise_predicate, premise_paras = parse_fact(gdl['CommonSense'][common_sense]['premises'])
+        conclusion_predicate, conclusion_paras = parse_fact(gdl['CommonSense'][common_sense]['conclusion'])
+
+        if premise_predicate in parsed_gdl['FactAutoExpand']:
+            replace = dict(zip(premise_paras, parsed_gdl['FactAutoExpand'][premise_predicate]['paras']))
+            premise_paras = parsed_gdl['FactAutoExpand'][premise_predicate]['paras']
+            conclusion_paras = replace_paras(conclusion_paras, replace)
+            parsed_gdl['FactAutoExpand'][premise_predicate]['expand'] = tuple(
+                list(parsed_gdl['FactAutoExpand'][premise_predicate]['expand']) +
+                [(conclusion_predicate, conclusion_paras)]
+            )
+
+        else:
+            parsed_gdl['FactAutoExpand'][premise_predicate] = {
+                'paras': premise_paras,
+                'expand': ((conclusion_predicate, conclusion_paras),)
+            }
+
+        if len(set(premise_paras) - set(conclusion_paras)) != 0:
+            return
+
+        if conclusion_predicate in parsed_gdl['GoalAutoExpand']:
+            replace = dict(zip(conclusion_paras, parsed_gdl['GoalAutoExpand'][conclusion_predicate]['paras']))
+            premise_paras = replace_paras(premise_paras, replace)
+            parsed_gdl['GoalAutoExpand'][conclusion_predicate]['expand'] = tuple(
+                list(parsed_gdl['GoalAutoExpand'][conclusion_predicate]['expand']) +
+                [(premise_predicate, premise_paras)]
+            )
+
+        else:
+            parsed_gdl['GoalAutoExpand'][conclusion_predicate] = {
+                'paras': conclusion_paras,
+                'expand': ((premise_predicate, premise_paras),)
+            }
+
+
+def _parse_one_theorem(theorem, gdl, parsed_gdl):
+    theorem_name, theorem_paras = parse_fact(theorem)
+
+    geometric_constraints = []  # (predicate, paras)
+    geometric_premises = []  # (predicate, paras)
+    algebraic_premises = []  # (expr, paras)
+    algebraic_constraints = []  # (relation_type, expr, paras)
+
+    for premise in parse_disjunctive(gdl['Theorems'][theorem]['premises']):
+        if premise.startswith('Eq('):
+            _, expr = parse_expr(premise)
+            paras = []
+            for sym in expr.free_symbols:
+                paras.extend(list(str(sym).split('.')[0]))
+            algebraic_premises.append((expr, paras))
+        else:
+            premise_name, premise_paras = parse_fact(premise)
+            geometric_premises.append((premise_name, premise_paras))
+
+            if premise_name in parsed_gdl['Presets']:
+                geometric_constraints.append((premise_name, premise_paras))
+            else:
+                replace = dict(zip(parsed_gdl['Relations'][premise_name]['paras'], premise_paras))
+                for predicate, paras in parsed_gdl['Relations'][premise_name]['geometric_constraints']:
+                    paras = replace_paras(paras, replace)
+                    geometric_constraints.append((predicate, paras))
+
+    for constraint in parse_disjunctive(gdl['Theorems'][theorem]['algebraic_constraints']):
+        algebra_relation, expr = parse_expr(constraint)
+        paras = [str(sym).split('.')[0] for sym in expr.free_symbols]
+        algebraic_constraints.append((algebra_relation, expr, paras))
+
+    entities_gpl = _get_gpl(geometric_constraints, [], algebraic_constraints, theorem_paras)
+    premises_gpl = _get_gpl(geometric_premises, algebraic_premises, algebraic_constraints, theorem_paras)
+
+    # parse theorem conclusions
+    if gdl['Theorems'][theorem]['conclusion'].startswith('Eq('):
+        _, expr = parse_expr(gdl['Theorems'][theorem]['conclusion'])
+        conclusion = ('Eq', expr)
+    else:
+        conclusion_name, conclusion_paras = parse_fact(gdl['Theorems'][theorem]['conclusion'])
+        conclusion = (conclusion_name, conclusion_paras)
+    # print(gdl['Theorems'][theorem])
+    parsed_gdl['Theorems'][theorem_name] = {
+        'paras': theorem_paras,
+        'circle': set(gdl['Theorems'][theorem]['circle']),
+        'entities_gpl': entities_gpl,
+        'premises_gpl': premises_gpl,
+        'conclusion': conclusion
+    }
+
+
+def _get_gpl(geometric_premises, algebraic_premises, algebraic_constraints, theorem_paras):
+    geometric_premises = list(geometric_premises)  # (predicate, paras)
+    algebraic_premises = list(algebraic_premises)  # (expr, paras)
+    algebraic_constraints = list(algebraic_constraints)  # (relation_type, expr, paras)
+
+    # adjust the execution order
+    products = []
+    added_paras = set()
+
+    # map para to geometric_premises
+    paras_to_geometric_premises = {}
+    for premise_name, premise_paras in geometric_premises:
+        for p in list(set(premise_paras)):
+            if p not in paras_to_geometric_premises:
+                paras_to_geometric_premises[p] = [(premise_name, premise_paras)]
+            else:
+                paras_to_geometric_premises[p].append((premise_name, premise_paras))
+
+    # add geometric_premise to product, entity p only exist in those geometric_premise
+    for p in paras_to_geometric_premises:
+        if len(paras_to_geometric_premises[p]) == 1 and paras_to_geometric_premises[p][0] not in products:
+            products.append(paras_to_geometric_premises[p][0])
+            geometric_premises.remove(paras_to_geometric_premises[p][0])
+            added_paras.update(paras_to_geometric_premises[p][0][1])
+
+    # for the remaining geometric_premise, select a portion to add to product, according to:
+    # 1. the number of not added entities in it paras
+    # 2. the number of paras
+    # print(products)
+    # print(paras_to_geometric_premises)
+    # print(added_paras)
+    # print()
+    while len(added_paras) < len(theorem_paras):
+        # print(added_paras)
+        # print(theorem_paras)
+        # print(theorem_geometric_premises)
+        max_index = 0
+        max_not_added_paras_len = len(set(geometric_premises[0][1]) - added_paras)
+        max_paras_len = len(geometric_premises[0][1])
+
+        for i in range(1, len(geometric_premises)):
+            not_added_paras_len = len(set(geometric_premises[i][1]) - added_paras)
+            paras_len = len(geometric_premises[i][1])
+
+            if not_added_paras_len > max_not_added_paras_len or (
+                    not_added_paras_len == max_not_added_paras_len and paras_len > max_paras_len):
+                max_index = i
+                max_not_added_paras_len = not_added_paras_len
+                max_paras_len = paras_len
+
+        products.append(geometric_premises[max_index])
+        added_paras.update(geometric_premises[max_index][1])
+        geometric_premises.pop(max_index)
+
+    # sort product according to the number of its paras
+    products.sort(key=len, reverse=True)
+
+    gpl = []
+    added_paras = []
+    for predicate, paras in products:
+        inherent_same_index = []
+        for i in range(len(paras)):
+            for j in range(i + 1, len(paras)):
+                if paras[i] == paras[j]:
+                    inherent_same_index.append((i, j))
+        mutual_same_index = []
+        for i in range(len(added_paras)):
+            for j in range(len(paras)):
+                if added_paras[i] == paras[j]:
+                    mutual_same_index.append((i, j))
+        added_index = []
+        for j in range(len(paras)):
+            if paras[j] not in added_paras:
+                added_index.append(j)
+                added_paras.append(paras[j])
+
+        geometric_premise = _get_geometric_premise(geometric_premises, added_paras)  # (predicate, paras)
+        algebraic_premise = _get_algebraic_premise(algebraic_premises, added_paras)  # (expr)
+        algebraic_constraint = _get_algebraic_constraint(algebraic_constraints, added_paras)  # (relation_type, expr)
+
+        gpl.append({
+            "product": (predicate, paras, tuple(inherent_same_index), tuple(mutual_same_index), tuple(added_index)),
+            "geometric_premises": geometric_premise,
+            "algebraic_premises": algebraic_premise,
+            "algebraic_constraints": algebraic_constraint
+        })
+
+    if len(geometric_premises) > 0 or len(algebraic_premises) > 0 or len(algebraic_constraints) > 0:
+        e_msg = f"There exist unadded constraints."
+        raise Exception(e_msg)
+
+    return tuple(gpl)
+
+
+def _get_algebraic_constraint(algebraic_constraints, added_paras):
+    algebraic_constraint = []  # (relation_type, expr, paras)
+    for i in range(len(algebraic_constraints))[::-1]:
+        ac_check_type, ac_check_expr, ac_check_paras = algebraic_constraints[i]
+        if len(set(ac_check_paras) - set(added_paras)) == 0:
+            algebraic_constraint.append(algebraic_constraints[i])
+            algebraic_constraints.pop(i)
+    # sort according to the number of paras
+    algebraic_constraint = sorted(algebraic_constraint, key=lambda x: (len(x[2]), len(set(x[2]))), reverse=True)
+    algebraic_constraint = tuple([(relation_type, expr) for relation_type, expr, _ in algebraic_constraint])
+    return algebraic_constraint
+
+
+def _get_geometric_premise(geometric_premises, added_paras):
+    geometric_premise = []  # (predicate, paras)
+    for i in range(len(geometric_premises))[::-1]:
+        geometric_premises_predicate, geometric_premises_paras = geometric_premises[i]
+        if len(set(geometric_premises_paras) - set(added_paras)) == 0:
+            geometric_premise.append(geometric_premises[i])
+            geometric_premises.pop(i)
+    # sort according to the number of paras
+    geometric_premise = tuple(sorted(geometric_premise, key=lambda x: (len(x[1]), len(set(x[1]))), reverse=True))
+    return geometric_premise
+
+
+def _get_algebraic_premise(algebraic_premises, added_paras):
+    algebraic_premise = []  # (expr, paras)
+    for i in range(len(algebraic_premises))[::-1]:
+        algebraic_premises_expr, algebraic_premises_paras = algebraic_premises[i]
+        if len(set(algebraic_premises_paras) - set(added_paras)) == 0:
+            algebraic_premise.append(algebraic_premises[i])
+            algebraic_premises.pop(i)
+    algebraic_premise = sorted(algebraic_premise, key=lambda x: (len(x[1]), len(set(x[1]))), reverse=True)
+    algebraic_premise = tuple([expr for expr, _ in algebraic_premise])
+    return algebraic_premise
+
+
+def parse_cdl(cdl):
+    construction_cdl = []
+    for one_cdl in cdl['construction_cdl']:
+        if one_cdl.startswith("Shape"):
+            predicate, paras = one_cdl.split('(')
+            paras = tuple(paras[:-1].split(','))
+        elif one_cdl.startswith('Collinear'):
+            predicate, paras = one_cdl.split('(')
+            paras = tuple(paras[:-1])
+        else:
+            predicate, paras = one_cdl.split('(')
+            paras = tuple(paras[:-1].replace(',', ''))
+        construction_cdl.append((predicate, paras))
+
+    points = {}
+    for point in cdl['points']:
+        points[point] = tuple(cdl['points'][point])
+
+    relation_cdl = []
+    for one_cdl in cdl['text_cdl'] + cdl['image_cdl']:
+        if one_cdl.startswith('Eq('):
+            fact = parse_expr(one_cdl)
+        else:
+            fact = parse_fact(one_cdl)
+
+        if fact not in relation_cdl:
+            relation_cdl.append(fact)
+
+    if cdl['goal_cdl'].startswith('Eq('):
+        goal_cdl = parse_expr(cdl['goal_cdl'])
+    else:
+        goal_cdl = parse_fact(cdl['goal_cdl'])
+
+    parsed_cdl = {
+        'problem_id': cdl['problem_id'],
+        'construction_cdl': tuple(construction_cdl),
+        'points': points,
+        'relation_cdl': tuple(relation_cdl),
+        'goal_cdl': goal_cdl
+    }
+
+    # for predicate, instance in parsed_cdl['relation_cdl']:
+    #     if predicate == 'Eq':
+    #         for sym in instance.free_symbols:
+    #             print(f'{str(sym)}: ', sym == symbols(str(sym)))
+
+    return parsed_cdl
+
+
+def get_used_theorems():
+    used_theorems = set()
+    for pid in range(7000):
+        pid += 1
+        for theorem in load_json(f'../../datasets/problems/{pid}.json')['theorem_seqs']:
+            used_theorems.add(theorem.split('(')[0])
+
+    return sorted(list(used_theorems))
+
+
+expr_letters = tuple(  # letters in algebraic expr
+    ['+', '-', '**', '*', '/', 'sqrt', 'number', 'pi', '(', ')'] +
+    sorted(['.' + attr_sym for attr_sym in parse_gdl(load_json('../../datasets/gdl.json'))['Attributions'].keys()])
+)
+
+theorem_letters = tuple(  # theorem letters (theorem vocab)
+    ['solve_eq'] + get_used_theorems()
+    # sorted(list(parse_gdl(load_json('../../datasets/gdl.json'))['Theorems'].keys()))
+)
+
+state_letters = tuple(  # letters in serialized problem state
+    ['padding'] +
+    list(expr_letters) +  # letters in algebraic expr
+    [  # delimiter letter
+        ',', '&', '|',  # split facts
+        '<construction>',  # construction
+        '<init_fact>', '<premise>', '<apply_theorem>', '<conclusion>',  # forward
+        '<init_goal>', '<goal>', '<decompose>', '<sub_goals>'  # backward
+    ] +
+    sorted([r for r in parse_gdl(load_json('../../datasets/gdl.json'))['Presets'].keys()]) +  # Predicate
+    sorted([r for r in parse_gdl(load_json('../../datasets/gdl.json'))['Relations'].keys()]) +  # Predicate
+    list(string.ascii_letters) +  # parameters
+    list(theorem_letters)  # # theorem letters (theorem vocab)
+)
+
+
+def _anti_parse_operation(operation):
+    operation_type, operation_predicate, operation_instance = operation
+    if operation_type == 'Preset':
+        return 'Preset: ' + operation_predicate
+    elif operation_type == 'Apply':
+        return 'Apply: ' + operation_predicate + '(' + ','.join(operation_instance) + ')'
+    elif operation_type == 'Decompose':
+        return 'Decompose: ' + operation_predicate + '(' + ','.join(operation_instance) + ')'
+    else:
+        raise Exception(f"Unknown operation type '{operation_type}'.")
+
+
+def _serialize_fact(predicate, instance):
+    if predicate == 'Eq':
+        # print(instance)
+        serialized_expr = ['Eq']
+        expr = str(instance).replace(' ', '')  # remove ' '
+
+        for matched in re.findall(r'\d+\.*\d*', expr):  # replace number with 'nums'
+            expr = expr.replace(matched, 'number', 1)
+
+        i = 0
+        while i < len(expr):  # serialize
+            added = False
+            for matched_part in expr_letters:  # expr letters
+                if expr[i:].startswith(matched_part):
+                    serialized_expr.append(matched_part)
+                    i = i + len(matched_part)
+                    added = True
+                    break
+            if not added:  # entity letters
+                serialized_expr.append(expr[i])
+                i = i + 1
+        # print(serialized_expr)
+        # print()
+        return serialized_expr
+    else:
+        return [predicate] + list(instance)
+
+
+def _serialize_operation(operation):
+    operation_type, operation_predicate, operation_instance = operation
+    if operation_type == 'Preset':
+        return [operation_predicate]
+    elif operation_type == 'Apply':
+        return [operation_predicate] + list(operation_instance)
+    elif operation_type == 'Decompose':
+        return [operation_predicate] + list(operation_instance)
+    else:
+        raise Exception(f"Unknown operation type '{operation_type}'.")
+
+
+def _anti_parse_fact(fact):
+    predicate, instance = fact
+    if predicate == 'Eq':
+        return f"Eq({str(instance).replace(' ', '')})"
+    else:
+        return f"{predicate}({','.join(instance)})"
+
+
+precision = 15
+chop = 1e-10
+
+
+def _satisfy_eq(expr, sym_to_value=None):
+    try:
+        if sym_to_value is None:
+            return expr.evalf(n=precision, chop=chop) == 0
+        return expr.subs(sym_to_value).evalf(n=precision, chop=chop) == 0
+    except Exception:
+        return False
+
+
+def _satisfy_g(expr, sym_to_value=None):
+    try:
+        if sym_to_value is None:
+            return expr.evalf(n=precision, chop=chop) > 0
+        return expr.subs(sym_to_value).evalf(n=precision, chop=chop) > 0
+    except Exception:
+        return False
+
+
+def _satisfy_geq(expr, sym_to_value=None):
+    try:
+        if sym_to_value is None:
+            return expr.evalf(n=precision, chop=chop) >= 0
+        return expr.subs(sym_to_value).evalf(n=precision, chop=chop) >= 0
+    except Exception:
+        return False
+
+
+def _satisfy_l(expr, sym_to_value=None):
+    try:
+        if sym_to_value is None:
+            return expr.evalf(n=precision, chop=chop) < 0
+        return expr.subs(sym_to_value).evalf(n=precision, chop=chop) < 0
+    except Exception:
+        return False
+
+
+def _satisfy_leq(expr, sym_to_value=None):
+    try:
+        if sym_to_value is None:
+            return expr.evalf(n=precision, chop=chop) <= 0
+        # print('Leq')
+        # print(expr)
+        # print(sym_to_value)
+        # print(expr.subs(sym_to_value))
+        # print(expr.subs(sym_to_value).evalf(n=precision, chop=chop))
+        # print((expr / pi * 180).subs(sym_to_value).evalf(n=precision, chop=chop))
+        # print(expr.subs(sym_to_value).evalf(n=precision, chop=chop) <= 0)
+        # print()
+        return expr.subs(sym_to_value).evalf(n=precision, chop=chop) <= 0
+    except Exception:
+        return False
+
+
+def _satisfy_ueq(expr, sym_to_value=None):
+    try:
+        if sym_to_value is None:
+            return expr.evalf(n=precision, chop=chop) != 0
+        return expr.subs(sym_to_value).evalf(n=precision, chop=chop) != 0
+    except Exception:
+        return False
+
+
+_satisfy_algebraic = {'Eq': _satisfy_eq, 'G': _satisfy_g, 'Geq': _satisfy_geq,
+                      'L': _satisfy_l, 'Leq': _satisfy_leq, 'Ueq': _satisfy_ueq}
+
+
+def get_theorem_seqs(problem):
+    theorem_seqs = []
+    goal_related_premise_ids = list(problem.premise_ids_of_goal[0])
+    goal_related_operation_ids = set()
+    for fact_id in goal_related_premise_ids:
+        goal_related_operation_ids.add(problem.facts[fact_id][3])
+        for new_fact_id in problem.facts[fact_id][2]:
+            if new_fact_id not in goal_related_premise_ids:
+                goal_related_premise_ids.append(new_fact_id)
+
+    for operation_id in range(len(problem.operations)):
+        if operation_id not in goal_related_operation_ids:
+            continue
+        operation_type, operation_predicate, operation_instance = problem.operations[operation_id]
+        if operation_type != 'Apply':
+            continue
+
+        theorem_seqs.append(operation_predicate + '(' + ','.join(operation_instance) + ')')
+
+    return theorem_seqs
+
+
+def get_cleaned_theorem_seqs(problem_initial, theorem_seqs):
+    theorem_seqs = deepcopy(theorem_seqs)
+    for i in range(len(theorem_seqs))[::-1]:  # try delete theorem i
+        problem = deepcopy(problem_initial)
+        for j in range(len(theorem_seqs)):  # not apply theorem i
+            if j == i:
+                continue
+            problem.apply(theorem_seqs[j])
+
+        if problem.status_of_goal[0] == 1:  # theorem i can delete
+            theorem_seqs.pop(i)
+
+    return theorem_seqs
+
+
+def get_dag(applied_theorems, edges):
+    n = len(applied_theorems)  # 这里去重的代码,还能再优化下
+    closure = [[False] * n for _ in range(n)]
+
+    for head, tail in edges:
+        closure[applied_theorems.index(head)][applied_theorems.index(tail)] = True
+
+    for k in range(n):
+        for i in range(n):
+            for j in range(n):
+                if closure[i][k] and closure[k][j]:
+                    closure[i][j] = True
+
+    for i in range(n):
+        for j in range(n):
+            if closure[i][j]:
+                for k in range(n):
+                    if k != i and k != j and closure[i][k] and closure[k][j]:
+                        if (applied_theorems[i], applied_theorems[j]) in edges:
+                            edges.remove((applied_theorems[i], applied_theorems[j]))
+                        break
+    dag = {
+        'in_degree': {},
+        'out_degree': {},
+        'edges': []
+    }
+    for theorem in applied_theorems:
+        dag['in_degree'][theorem] = 0
+        dag['out_degree'][theorem] = 0
+    for head, tail in edges:
+        dag['in_degree'][tail] += 1
+        dag['out_degree'][head] += 1
+    dag['edges'] = edges
+
+    return dag
+
+
+def get_forward_dag(problem_initial, theorem_seqs):
+    theorem_seqs = deepcopy(theorem_seqs)
+    previous_problem = deepcopy(problem_initial)
+    applied_theorems = []
+    edges = []
+    while len(theorem_seqs) > 0:
+        for i in range(len(theorem_seqs))[::-1]:
+            problem = deepcopy(previous_problem)
+            if not problem.apply(theorem_seqs[i]):  # check whether theorem i can apply under previous theorems
+                continue
+
+            dependent_theorems = deepcopy(applied_theorems)
+            for j in range(len(dependent_theorems))[::-1]:  # check whether theorem j is dependent
+                problem = deepcopy(problem_initial)
+                for k in range(len(dependent_theorems)):  # not apply theorem k=j
+                    if k == j:
+                        continue
+                    problem.apply(dependent_theorems[k])
+
+                if problem.apply(theorem_seqs[i]):  # still can apply theorem i after delete theorem j
+                    dependent_theorems.pop(j)
+
+            check_theorem = theorem_seqs.pop(i)
+            applied_theorems.append(check_theorem)
+            previous_problem.apply(check_theorem)
+            for dependent_theorem in dependent_theorems:
+                edges.append((dependent_theorem, check_theorem))
+
+    return get_dag(applied_theorems, edges)
+
+
+def get_backward_dag(problem_initial, theorem_seqs):
+    theorem_seqs = deepcopy(theorem_seqs)
+    previous_problem = deepcopy(problem_initial)
+    applied_theorems = []
+    edges = []
+    while len(theorem_seqs) > 0:
+        for i in range(len(theorem_seqs))[::-1]:
+            problem = deepcopy(previous_problem)
+            if not problem.decompose(theorem_seqs[i]):  # check whether theorem i can apply under previous theorems
+                continue
+
+            dependent_theorems = deepcopy(applied_theorems)
+            for j in range(len(dependent_theorems))[::-1]:  # check whether theorem j is dependent
+                problem = deepcopy(problem_initial)
+                for k in range(len(dependent_theorems)):  # not apply theorem k=j
+                    if k == j:
+                        continue
+                    problem.decompose(dependent_theorems[k])
+
+                if problem.decompose(theorem_seqs[i]):  # still can apply theorem i after delete theorem j
+                    dependent_theorems.pop(j)
+
+            check_theorem = theorem_seqs.pop(i)
+            applied_theorems.append(check_theorem)
+            previous_problem.decompose(check_theorem)
+            for dependent_theorem in dependent_theorems:
+                edges.append((dependent_theorem, check_theorem))
+
+    return get_dag(applied_theorems, edges)
+
+
+def inverse_parse_theorem(theorem):
+    operation_type, operation_predicate, operation_instance = theorem
+    if operation_type == 'Preset':
+        return operation_predicate
+    else:
+        return operation_predicate + '(' + ','.join(operation_instance) + ')'
+
+
+def inverse_parse_cdl(predicate, instance):
+    if predicate in _satisfy_algebraic.keys():
+        return predicate + '(' + str(instance).replace(' ', '') + ')'
+    else:
+        return predicate + '(' + ','.join(instance) + ')'
+
+
+def get_meta_hypertree(problem):
+    """
+    Generate meta hypertree message for downstream task.
+    :return nodes: all nodes, {node_id: node_name}, such as {1: 'Equation(ll_ab-1)'}
+    :return edges: all edges, {edge_id: edge_name}, such as {1: "extended"}
+    :return free_nodes: nodes not in hypertree but in prerequisite, [node_id], such as [1, 2, 3]
+    :return target_node_id: target node id, such as 1
+    :return hypertree: {((tail_node_ids), edge_id): (tail_node_ids))}, such as {((1, 2, 3), 1): (4, 5))}
+    """
+    group = {}  # (premise, theorem): [_id], used for building hyper graph.
+    cdl = {}  # _id: anti_parsed_cdl, user for getting cdl by id.
+    init_nodes = []  # [_id], id of prerequisite.
+    tree_nodes = []  # [_id], id of tree nodes.
+    target_node_id = None
+
+    for fact_id in range(len(problem.facts)):
+        predicate, instance, premise_ids, operation_id = problem.facts[fact_id]
+        premise_ids = tuple(sorted(list(premise_ids)))
+        theorem = inverse_parse_theorem(problem.operations[operation_id])
+
+        if theorem == "extend_construction":  # 不需要这些节点
+            continue
+
+        cdl[fact_id] = inverse_parse_cdl(predicate, instance)
+
+        if theorem in {'init_construction', 'init_fact'}:  # root nodes
+            init_nodes.append(fact_id)
+            continue
+
+        if (premise_ids, theorem) not in group:
+            group[(premise_ids, theorem)] = [fact_id]
+        else:
+            group[(premise_ids, theorem)].append(fact_id)
+
+    if len(problem.goals) > 0 and problem.status_of_goal[0] == 1:
+        predicate, instance, _, _ = problem.goals[0]
+        if predicate == 'Eq' and (predicate, instance) not in problem.fact_id:
+            target_node_id = len(problem.facts)
+            cdl[target_node_id] = predicate + '(' + str(instance).replace(' ', '') + ')'
+            premise_ids = tuple(sorted(list(problem.premise_ids_of_goal[0])))
+            group[(premise_ids, 'solve_eq')] = [target_node_id]
+        else:
+            target_node_id = problem.fact_id[(predicate, instance)]
+
+    # for cdl_key in cdl.keys():
+    #     print(cdl_key, cdl[cdl_key])
+    # print()
+    #
+    # for group_key in group:
+    #     print(group_key, group[group_key])
+    # print()
+
+    edges = {-2: "none", -1: "self"}
+    tree = {}
+    for premise, theorem in group:
+        conclusion = group[(premise, theorem)]
+        edge_id = len(edges)
+        edges[edge_id] = theorem
+
+        adjust_premise = []
+        for fact_id in premise:
+            if fact_id in cdl:
+                adjust_premise.append(fact_id)
+            else:
+                _, _, premise_ids, _ = problem.facts[fact_id]
+                adjust_premise.extend(premise_ids)
+        adjust_premise = sorted(list(set(adjust_premise)))
+
+        tree_nodes += adjust_premise
+        tree_nodes += conclusion
+        tree[(tuple(adjust_premise), edge_id)] = conclusion
+
+    nodes = {}
+    for node_id in sorted(list(set(tree_nodes + init_nodes))):
+        nodes[node_id] = cdl[node_id]
+
+    free_nodes = sorted(list(set(init_nodes) - set(tree_nodes)))
+
+    return nodes, edges, free_nodes, target_node_id, tree
+
+
+def make_train_val_test_split(random_seed=0, data_split=(4, 1, 1)):
+    filename = "../../outputs/log/log_data_problem_split.json"
+    if os.path.exists(filename):
+        return load_json(filename)
+
+    problem_ids = list(range(1, 7001))
+    random.Random(random_seed).shuffle(problem_ids)
+
+    train, val, test = data_split
+    train_problem_ids = sorted(problem_ids[:int(7000 * train / (train + val + test))])
+    val_problem_ids = sorted(problem_ids[int(7000 * train / (train + val + test)):
+                                         int(7000 * (train + val) / (train + val + test))])
+    test_problem_ids = sorted(problem_ids[int(7000 * (train + val) / (train + val + test)):])
+    problem_split = {"train": train_problem_ids, "val": val_problem_ids, "test": test_problem_ids}
+
+    print(f"train: {len(train_problem_ids)}, val: {len(val_problem_ids)}, test: {len(test_problem_ids)}")
+    save_json(problem_split, filename)
+
+    return problem_split

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików