12 Commits

77 changed files with 2213 additions and 938 deletions

View File

@@ -1,18 +1,20 @@
# 潜进 (HeurAMS) - 启发式辅助记忆程序 # 潜进 (HeurAMS) - 启发式辅助记忆程序
## 概述 ## 概述
"潜进" (HeurAMS: Heuristic Auxiliary Memorizing Scheduler, 启发式记忆辅助调度器) 是为习题册, 古诗词, 及其他问答/记忆/理解型知识设计的多用途辅助记忆软件, 提供动态规划的优化记忆方案 "潜进" (HeurAMS: Heuristic Auxiliary Memorizing Scheduler, 启发式记忆辅助调度器) 是为习题册, 古诗词, 及其他问答/记忆/理解型知识设计的开放源代码多用途辅助记忆软件, 提供动态规划的优化记忆方案
## 关于此仓库 ## 关于此仓库
"潜进" 软件组项目包含多个子项目 本仓库为 "潜进" 软件组项目的核心部分, 包含核心功能模块以及基于 Textual 框架的基础用户界面(heurams.interface)实现
此仓库包含了 "潜进" 项目的核心和基于 Textual 的基本用户界面的实现 除了通过用户界面进行学习外, 你也可以在 Python 中导入 `heurams` 库, 使用其中实现的状态机, 算法迭代器和数据模型构建辅助记忆功能
本仓库在 AGPLv3 下开放源代码(详见 LICENSE 文件)
## 开发进程 ## 版本日志
- 0.0.x: 简易调度器实现与最小原型. - 0.0.x: 简易调度器实现与最小原型.
- 0.1.x: 命令行操作的调度器. - 0.1.x: 命令行操作的调度器.
- 0.2.x: 使用 Textual 构建富文本终端用户界面, 项目可行性验证, 采用 SM-2 原始算法, 评估方式为用户自评估原型. - 0.2.x: 使用 Textual 构建富文本终端用户界面; 项目可行性验证; 采用 SM-2 原始算法, 评估方式为用户自评估原型.
- 0.3.x: 简单的多文件项目, 创建了记忆内容/算法数据结构, 基于 SM-2 改进算法的自动复习测评评估. 重点设计古诗文记忆理解功能, 以及 TUI 界面实现, 简单的 TTS 集成. - 0.3.x: 简单的多文件项目; 创建了记忆内容/算法数据结构; 基于 SM-2 改进算法的自动复习测评评估; 重点设计古诗文记忆理解功能; TUI 界面改进; 简单的 TTS 集成.
- 0.4.x: 使用模块管理解耦设计, 增加文档与类型标注, 采用上下文设计模式的隐式依赖注入与遵从 IoC, 注册器设计的算法与功能实现, 支持其他调度算法模块 (SM-2, FSRS) 与谜题模块, 采用日志调试, 更新文件格式, 引入动态数据模式(宏驱动的动态内容生成), 与基于文件的策略调控, 更佳的用户数据处理, 加入模块化扩展集成, 将算法数据格式换为 json 提高性能, 采用 provider-service 抽象架构, 支持切换服务提供者, 整体兼容性改进. - 0.4.x: 开发目标转为多用途; 使用模块管理解耦设计; 增加文档与类型标注; 采用上下文设计模式的隐式依赖注入与遵从 IoC, 注册器设计的算法与功能实现; 支持其他调度算法模块 (SM-2, SM-18M 逆向工程实现, FSRS) 与谜题模块; 采用日志调试; 更新文件格式; 引入动态数据模式(宏驱动的动态内容生成), 与基于文件的策略调控; 更佳的用户数据处理; 加入模块化扩展集成; 将算法数据格式换为 json 提高性能; 采用 provider-service 抽象架构, 支持切换服务提供者; 整体兼容性改进.
- 0.5.x: 以仓库(repo)对象作为文件系统与运行时对象间的桥梁, 提高解耦性与性能; 使用具有列表 - 字典 API 同步特性的 "Lict" 对象作为 Repo 数据的内部存储; 将粒子对象作为纯运行时对象, 数据通过引用自动同步至 Repo, 减少负担; 实现声音形式回顾 "电台" 功能; 改进数据存储结构, 实现选择性持久化; 增强可配置性; 使用 Transitions 状态机库重新实现 reactor 状态机, 增强可维护性; 实现整体回顾记忆功能, 与队列式记忆功能并列; 加入状态机快照功能(基于 pickle), 使中断的记忆流程得以恢复; 增加"整体文章引用"功能, 实现从一篇长文本中摘取内容片段记忆并在原文中高亮查看的组织操作.
> 下一步? > 下一步?
> 使用 Flutter 构建酷酷的现代化前端, 增加云同步/文档源服务... > 使用 Flutter 构建酷酷的现代化前端, 增加云同步/文档源服务...
@@ -103,7 +105,7 @@ verify_ssl = true # SSL 证书验证
## 项目结构 ## 项目结构
### 架构图 ### 架构图(待更新 0.5.0)
以下 Mermaid 图展示了 HeurAMS 的主要组件及其关系: 以下 Mermaid 图展示了 HeurAMS 的主要组件及其关系:
@@ -164,7 +166,7 @@ graph TB
Algorithms --> Files Algorithms --> Files
``` ```
### 目录结构 ### 目录结构(待更新 0.5.0)
``` ```
src/heurams/ src/heurams/
├── __init__.py # 包入口点 ├── __init__.py # 包入口点

59
data/config/config.toml Normal file
View File

@@ -0,0 +1,59 @@
# [调试] 将更改保存到文件
persist_to_file = 1
# [调试] 覆写时间, 设为 -1 以禁用
daystamp_override = -1
timestamp_override = -1
# [调试] 一键通过
quick_pass = 1
# 对于每个项目的默认新记忆原子数量
scheduled_num = 8
# UTC 时间戳修正 仅用于 UNIX 日时间戳的生成修正, 单位为秒
timezone_offset = +28800 # 中国标准时间 (UTC+8)
[interface]
[interface.memorizor]
autovoice = true # 自动语音播放, 仅限于 recognition 组件
[algorithm]
default = "SM-2" # 主要算法; 可选项: SM-2, SM-15M, FSRS
[puzzles] # 谜题默认配置
[puzzles.mcq]
max_riddles_num = 2
[puzzles.cloze]
min_denominator = 3
[paths] # 相对于配置文件的 ".." (即工作目录) 而言 或绝对路径
data = "./data"
cache = "./data/cache"
config = "./data/config"
global = "./data/global"
repo = "./data/repo"
[services] # 定义服务到提供者的映射
audio = "playsound" # 可选项: playsound(通用), termux(仅用于支持 Android Termux), mpg123(TODO)
tts = "edgetts" # 可选项: edgetts
llm = "openai" # 可选项: openai
sync = "webdav" # 可选项: 留空, webdav
[providers.tts.edgetts] # EdgeTTS 设置
voice = "zh-CN-XiaoxiaoNeural" # 可选项: zh-CN-YunjianNeural (男声), zh-CN-XiaoxiaoNeural (女声)
[providers.llm.openai] # 与 OpenAI 相容的语言模型接口服务设置
url = ""
key = ""
[providers.sync.webdav] # WebDAV 同步设置
url = ""
username = ""
password = ""
remote_path = "/heurams/"
verify_ssl = true
[sync]

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,3 @@
title = "测试单元: 过秦论"
author = "__heurams__"
desc = "高考古诗文: 过秦论"

View File

@@ -0,0 +1,11 @@
["秦孝公据崤函之固, 拥雍州之地,"]
note = []
content = "秦孝公/据/崤函/之固/, 拥/雍州/之地,/"
translation = "秦孝公占据着崤山和函谷关的险固地势,拥有雍州的土地,"
keyword_note = {"据"="占据", "崤函"="崤山和函谷关", "雍州"="古代九州之一"}
["君臣固守以窥周室,"]
note = []
content = "君臣/固守/以窥/周室,/"
translation = "君臣牢固地守卫着,借以窥视周王室的权力,"
keyword_note = {"窥"="窥视"}

View File

@@ -0,0 +1,5 @@
schedule = ["quick_review", "recognition", "final_review"]
[phases]
quick_review = [["FillBlank", "1.0"], ["SelectMeaning", "0.5"], ["Recognition", "1.0"]]
recognition = [["Recognition", "1.0"]]
final_review = [["FillBlank", "0.7"], ["SelectMeaning", "0.7"], ["Recognition", "1.0"]]

View File

@@ -0,0 +1,17 @@
[annotation]
note = "笔记"
keyword_note = "关键词翻译"
translation = "语句翻译"
delimiter = "分隔符"
content = "内容"
tts_text = "文本转语音文本"
[common]
delimiter = "/"
tts_text = "eval:payload['content'].replace('/', '')"
[common.puzzles] # 谜题定义
# 我们称 "Recognition" 为 recognition 谜题的 alia
"Recognition" = { __origin__ = "recognition", __hint__ = "", primary = "eval:payload['content']", secondary = ["eval:payload['keyword_note']", "eval:payload['note']"], top_dim = ["eval:payload['translation']"] }
"SelectMeaning" = { __origin__ = "mcq", __hint__ = "eval:payload['content']", primary = "eval:payload['content']", mapping = "eval:payload['keyword_note']", jammer = "eval:list(payload['keyword_note'].values())", max_riddles_num = "eval:default['mcq']['max_riddles_num']", prefix = "选择正确项: " }
"FillBlank" = { __origin__ = "cloze", __hint__ = "", text = "eval:payload['content']", delimiter = "eval:nucleon['delimiter']", min_denominator = "eval:default['cloze']['min_denominator']"}

View File

@@ -1,23 +0,0 @@
# Nucleon 是 HeurAMS 软件项目使用的基于 TOML 的专有源文件格式, 版本 5
# 建议使用的 MIME 类型: application/vnd.xyz.imwangzhiyu.heurams-nucleon.v5+toml
[__metadata__]
[__metadata__.attribution] # 元信息
desc = "带有宏支持的空白模板"
[__metadata__.annotation] # 键批注
[__metadata__.formation] # 文件配置
#delimiter = "/"
#tts_text = "eval:nucleon['content'].replace('/', '')"
[__metadata__.orbital.puzzles] # 谜题定义
# 我们称 "Recognition" 为 recognition 谜题的 alia
#"Recognition" = { __origin__ = "recognition", __hint__ = "", primary = "eval:nucleon['content']", secondary = ["eval:nucleon['keyword_note']", "eval:nucleon['note']"], top_dim = ["eval:nucleon['translation']"] }
#"SelectMeaning" = { __origin__ = "mcq", __hint__ = "eval:nucleon['content']", mapping = "eval:nucleon['keyword_note']", jammer = "eval:nucleon['keyword_note']", max_riddles_num = "eval:default['mcq']['max_riddles_num']", prefix = "选择正确项: " }
#"FillBlank" = { __origin__ = "cloze", __hint__ = "", text = "eval:nucleon['content']", delimiter = "eval:metadata['formation']['delimiter']", min_denominator = "eval:default['cloze']['min_denominator']"}
[__metadata__.orbital.schedule] # 内置的推荐学习方案
#quick_review = [["FillBlank", "1.0"], ["SelectMeaning", "0.5"], ["recognition", "1.0"]]
#recognition = [["Recognition", "1.0"]]
#final_review = [["FillBlank", "0.7"], ["SelectMeaning", "0.7"], ["recognition", "1.0"]]

View File

@@ -31,12 +31,7 @@ max_riddles_num = 2
min_denominator = 3 min_denominator = 3
[paths] # 相对于配置文件的 ".." (即工作目录) 而言 或绝对路径 [paths] # 相对于配置文件的 ".." (即工作目录) 而言 或绝对路径
nucleon_dir = "./data/nucleon" data = "./data"
electron_dir = "./data/electron"
global_dir = "./data/global" # 全局数据路径, SM-15 等算法需要
orbital_dir = "./data/orbital"
cache_dir = "./data/cache"
template_dir = "./data/template"
[services] # 定义服务到提供者的映射 [services] # 定义服务到提供者的映射
audio = "playsound" # 可选项: playsound(通用), termux(仅用于支持 Android Termux), mpg123(TODO) audio = "playsound" # 可选项: playsound(通用), termux(仅用于支持 Android Termux), mpg123(TODO)

14
examples/jiebatest.py Normal file
View File

@@ -0,0 +1,14 @@
# encoding=utf-8
import jieba
# jieba.enable_paddle()# 启动paddle模式。 0.40版之后开始支持,早期版本不支持
strs = ["我来到北京清华大学", "乒乓球拍卖完了", "中国科学技术大学"]
# for str in strs:
# seg_list = jieba.cut(str,use_paddle=True) # 使用paddle模式
# print("Paddle Mode: " + '/'.join(list(seg_list)))
seg_list = jieba.cut("秦孝公据崤函之固, 拥雍州之地", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list)) # 精确模式
seg_list = jieba.cut("他来到了网易杭研大厦") # 默认是精确模式
print(", ".join(seg_list))

719
examples/repo.ipynb Normal file
View File

@@ -0,0 +1,719 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "51b89355",
"metadata": {},
"source": [
"# 演练场\n",
"此笔记本将带你了解 repomgr 与 particles 对象相关操作"
]
},
{
"cell_type": "markdown",
"id": "f5c49014",
"metadata": {},
"source": [
"# 从一个例子开始\n",
"## 了解文件结构\n",
"了解一下文件结构"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "a5ed9864",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[01;34m.\u001b[0m\n",
"├── \u001b[00mrepo.ipynb\u001b[0m\n",
"└── \u001b[01;34mtest_repo\u001b[0m\n",
" ├── \u001b[00malgodata.json\u001b[0m\n",
" ├── \u001b[00mmanifest.toml\u001b[0m\n",
" ├── \u001b[00mpayload.toml\u001b[0m\n",
" ├── \u001b[00mschedule.toml\u001b[0m\n",
" └── \u001b[00mtypedef.toml\u001b[0m\n",
"\n",
"2 directories, 6 files\n"
]
}
],
"source": [
"!tree # 了解文件结构"
]
},
{
"cell_type": "markdown",
"id": "4e10922b",
"metadata": {},
"source": [
"如果你先前运行了单元格, 请运行下面一格清理."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "9777730e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"zsh:1: no matches found: heurams.log*\n"
]
}
],
"source": [
"!rm -rf test_new_repo\n",
"!rm -rf heurams.log*"
]
},
{
"cell_type": "markdown",
"id": "058c098f",
"metadata": {},
"source": [
"## 导入模块\n",
"导入所需模块, 你会看到欢迎信息, 标示了库所使用的配置. \n",
"HeurAMS 在基础设施也使用配置文件实现隐式的依赖注入. "
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "bf1b00c8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"欢迎使用 HeurAMS 及其组件!\n",
"rootdir: /mnt/data/Devel/HeurAMS/HeurAMS/src/heurams\n",
"workdir: /mnt/data/Devel/HeurAMS/HeurAMS/examples\n",
"未能加载自定义用户配置\n"
]
}
],
"source": [
"import heurams.kernel.repolib as repolib # 这是 RepoLib 子模块, 用于管理和结构化 repo(中文含义: 仓库) 数据结构与本地文件间的联系\n",
"import heurams.kernel.particles as pt # 这是 Particles(中文含义: 粒子) 子模块, 用于运行时的记忆管理操作\n",
"from pathlib import (\n",
" Path,\n",
") # 这是 Python 的 Pathlib 模块, 用于表示文件路径, 在整个项目中, 都使用此模块表示路径"
]
},
{
"cell_type": "markdown",
"id": "ea1f68bb",
"metadata": {},
"source": [
"## 运行时检查\n",
"如你所见, repo 在文件系统内存储为一个文件夹. \n",
"因此在载入之前, 首先要检查这是否是一个合乎标准的 repo 文件夹. "
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "897b62d7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"这是一个 合规 的 repo!\n"
]
}
],
"source": [
"is_vaild = repolib.Repo.check_repodir(Path(\"./test_repo\"))\n",
"print(f\"这是一个 {'合规' if is_vaild else '不合规'} 的 repo!\")"
]
},
{
"cell_type": "markdown",
"id": "24a19991",
"metadata": {},
"source": [
"## 加载仓库\n",
"接下来, 正式加载 repo."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "708ae7e4",
"metadata": {},
"outputs": [],
"source": [
"test_repo = repolib.Repo.create_from_repodir(Path(\"./test_repo\"))"
]
},
{
"cell_type": "markdown",
"id": "474f8eb7",
"metadata": {},
"source": [
"## 导出为字典\n",
"作为一个数据容器, repo 相应地建立了导入和导出的功能. \n",
"我们刚刚从本地文件夹导入了一个 repo. \n",
"现在试试导出为一个字典."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "a11115fb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'algodata': [('君臣固守以窥周室,', {}), ('秦孝公据崤函之固, 拥雍州之地,', {})],\n",
" 'manifest': {'author': '__heurams__',\n",
" 'desc': '高考古诗文: 过秦论',\n",
" 'title': '测试单元: 过秦论'},\n",
" 'payload': [('君臣固守以窥周室,',\n",
" {'content': '君臣/固守/以窥/周室,/',\n",
" 'keyword_note': {'窥': '窥视'},\n",
" 'note': [],\n",
" 'translation': '君臣牢固地守卫着,借以窥视周王室的权力,'}),\n",
" ('秦孝公据崤函之固, 拥雍州之地,',\n",
" {'content': '秦孝公/据/崤函/之固/, 拥/雍州/之地,/',\n",
" 'keyword_note': {'崤函': '崤山和函谷关', '据': '占据', '雍州': '古代九州之一'},\n",
" 'note': [],\n",
" 'translation': '秦孝公占据着崤山和函谷关的险固地势,拥有雍州的土地,'})],\n",
" 'schedule': {'phases': {'final_review': [['FillBlank', '0.7'],\n",
" ['SelectMeaning', '0.7'],\n",
" ['Recognition', '1.0']],\n",
" 'quick_review': [['FillBlank', '1.0'],\n",
" ['SelectMeaning', '0.5'],\n",
" ['Recognition', '1.0']],\n",
" 'recognition': [['Recognition', '1.0']]},\n",
" 'schedule': ['quick_review', 'recognition', 'final_review']},\n",
" 'source': PosixPath('test_repo'),\n",
" 'typedef': {'annotation': {'content': '内容',\n",
" 'delimiter': '分隔符',\n",
" 'keyword_note': '关键词翻译',\n",
" 'note': '笔记',\n",
" 'translation': '语句翻译',\n",
" 'tts_text': '文本转语音文本'},\n",
" 'common': {'delimiter': '/',\n",
" 'tts_text': \"eval:payload['content'].replace('/', '')\"},\n",
" 'puzzles': {'FillBlank': {'__hint__': '',\n",
" '__origin__': 'cloze',\n",
" 'delimiter': \"eval:metadata['formation']['delimiter']\",\n",
" 'min_denominator': \"eval:default['cloze']['min_denominator']\",\n",
" 'text': \"eval:payload['content']\"},\n",
" 'Recognition': {'__hint__': '',\n",
" '__origin__': 'recognition',\n",
" 'primary': \"eval:payload['content']\",\n",
" 'secondary': [\"eval:payload['keyword_note']\",\n",
" \"eval:payload['note']\"],\n",
" 'top_dim': [\"eval:payload['translation']\"]},\n",
" 'SelectMeaning': {'__hint__': \"eval:payload['content']\",\n",
" '__origin__': 'mcq',\n",
" 'jammer': \"eval:list(payload['keyword_note'].values())\",\n",
" 'mapping': \"eval:payload['keyword_note']\",\n",
" 'max_riddles_num': \"eval:default['mcq']['max_riddles_num']\",\n",
" 'prefix': '选择正确项: ',\n",
" 'primary': \"eval:payload['content']\"}},\n",
" '古文句': {}}}\n"
]
}
],
"source": [
"test_repo_dic = test_repo.export_to_single_dict()\n",
"from pprint import pprint\n",
"\n",
"pprint(test_repo_dic)"
]
},
{
"cell_type": "markdown",
"id": "35a2e06f",
"metadata": {},
"source": [
"## 持久化与部分保存\n",
"如你所见, 所有内容被结构化地输出了! \n",
"\n",
"现在写回到文件夹! \n",
"\n",
"我们注意到, 并非所有的内容都要被修改. \n",
"我们可以只保存接受修改的一部分, 默认情况下, 是迭代的记忆数据(algodata). \n",
"这就是为什么我们一般不使用单个 json 或 toml 来存储 repo.\n",
"\n",
"persist_to_repodir 接受两个可选参数: \n",
"- save_list: 默认为 [\"algodata\"], 是要持久化的数据.\n",
"- source: 默认为原目录, 你也可以手动指定为其他文件夹(通过 Path)\n",
"\n",
"现在做一些演练, 我们将创建一个位于 test_new_repo 的\"克隆\", 此时我们!\n",
"除非文件夹已经存在, Repo 对象将会为你自动创建新文件夹."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "05eeaacc",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[01;34m.\u001b[0m\n",
"├── \u001b[00mheurams.log\u001b[0m\n",
"├── \u001b[00mrepo.ipynb\u001b[0m\n",
"├── \u001b[01;34mtest_new_repo\u001b[0m\n",
"│   ├── \u001b[00malgodata.json\u001b[0m\n",
"│   ├── \u001b[00mmanifest.toml\u001b[0m\n",
"│   ├── \u001b[00mpayload.toml\u001b[0m\n",
"│   ├── \u001b[00mschedule.toml\u001b[0m\n",
"│   └── \u001b[00mtypedef.toml\u001b[0m\n",
"└── \u001b[01;34mtest_repo\u001b[0m\n",
" ├── \u001b[00malgodata.json\u001b[0m\n",
" ├── \u001b[00mmanifest.toml\u001b[0m\n",
" ├── \u001b[00mpayload.toml\u001b[0m\n",
" ├── \u001b[00mschedule.toml\u001b[0m\n",
" └── \u001b[00mtypedef.toml\u001b[0m\n",
"\n",
"3 directories, 12 files\n"
]
}
],
"source": [
"test_repo.persist_to_repodir(\n",
" save_list=[\"schedule\", \"payload\", \"manifest\", \"typedef\", \"algodata\"],\n",
" source=Path(\"test_new_repo\"),\n",
")\n",
"!tree"
]
},
{
"cell_type": "markdown",
"id": "059d7bdf",
"metadata": {},
"source": [
"如你所见, test_new_repo 已被生成!"
]
},
{
"cell_type": "markdown",
"id": "4ef8925c",
"metadata": {},
"source": [
"# 数据结构\n",
"现在讲解 repo 的数据结构"
]
},
{
"cell_type": "markdown",
"id": "c19fed95",
"metadata": {},
"source": [
"## Lict 对象\n",
"Lict 对象集成了部分列表和字典的功能, 数据在这两种风格的 API 间都可用, 且修改是同步的. \n",
"Lict 默认情况下不会保存序列顺序, 而是在列表形式下, 自动按索引字符序排布, 详情请参阅源代码. \n",
"现在导入并初始化一个 Lict 对象:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "7e88bd7c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('age', 12), ('enemy', 'jerry'), ('name', 'tom')]\n",
"[('age', 12), ('enemy', 'jerry'), ('name', 'tom')]\n"
]
}
],
"source": [
"from heurams.utils.lict import Lict\n",
"\n",
"lct = Lict() # 空的\n",
"lct = Lict(initlist=[(\"name\", \"tom\"), (\"age\", 12), (\"enemy\", \"jerry\")]) # 基于列表\n",
"print(lct)\n",
"lct = Lict(initdict={\"name\": \"tom\", \"age\": 12, \"enemy\": \"jerry\"}) # 基于字典\n",
"print(lct)"
]
},
{
"cell_type": "markdown",
"id": "4d760bf9",
"metadata": {},
"source": [
"### 输出形式\n",
"lct 的\"官方\"输出形式是列表形式\n",
"你也可以选择输出字典形式"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "248f6cba",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'name': 'tom', 'age': 12, 'enemy': 'jerry'}\n"
]
}
],
"source": [
"print(lct.dicted_data)"
]
},
{
"cell_type": "markdown",
"id": "29dce184",
"metadata": {},
"source": [
"### dicted_data 属性与修改方式\n",
"dicted_data 属性是一个字典, 它自动同步来自 Lict 对象操作的修改.\n",
"一个注意事项: 不要直接修改 dicted_data, 这将不会触发同步 hook.\n",
"如果你一定要这样做, 请在完事后手动运行同步 hook.\n",
"推荐的修改方式是直接把 lct 当作一个字典"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "a0eb07a7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('age', 12), ('enemy', 'jerry'), ('name', 'tom')]\n",
"[('age', 12), ('enemy', 'jerry'), ('name', 'tom'), ('type', 'cat')]\n",
"[('age', 12), ('enemy', 'jerry'), ('is_human', False), ('name', 'tom'), ('type', 'cat')]\n"
]
}
],
"source": [
"# 由于 jupyter 的环境处理, 请不要重复运行此单元格, 如果想再看一遍, 请重启 jupyter 后再全部运行\n",
"\n",
"# 错误的方式\n",
"lct.dicted_data[\"type\"] = \"cat\"\n",
"print(lct) # 将不会同步修改\n",
"\n",
"# 不推荐, 但可用的方式\n",
"lct.dicted_data[\"type\"] = \"cat\"\n",
"lct._sync_based_on_dict()\n",
"print(lct)\n",
"\n",
"# 推荐方式\n",
"lct[\"is_human\"] = False\n",
"print(lct)"
]
},
{
"cell_type": "markdown",
"id": "2337d113",
"metadata": {},
"source": [
"### data 属性与修改方式\n",
"data 属性是一个列表, 它自动同步来自 Lict 对象操作的修改.\n",
"一个注意事项: 不要直接修改 data, 这将不会触发同步 hook, 并且可能破坏排序.\n",
"如果你一定要这样做, 请在完事后手动运行同步 hook 和 sort, 此处不演示.\n",
"推荐的修改方式是直接把 lct 当作一个列表, 且避免使用索引修改"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "0ab442d4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'age': 12, 'enemy': 'jerry', 'is_human': False, 'name': 'tom', 'type': 'cat', 'enemy_2': 'spike'}\n"
]
}
],
"source": [
"# 由于 jupyter 的环境处理, 请不要重复运行此单元格, 如果想再看一遍, 请重启 jupyter 后再全部运行\n",
"\n",
"# 唯一推荐方式\n",
"lct.append((\"enemy_2\", \"spike\"))\n",
"print(lct.dicted_data)"
]
},
{
"cell_type": "markdown",
"id": "a3383f59",
"metadata": {},
"source": [
"### 多面手\n",
"Lict 有一些很酷的功能\n",
"详情请看源文件\n",
"此处是一些例子"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "f3ca752f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('age', 12), ('enemy', 'jerry'), ('enemy_2', 'spike'), ('is_human', False), ('name', 'tom'), ('type', 'cat')]\n",
"{'age': 12, 'enemy': 'jerry', 'is_human': False, 'name': 'tom', 'type': 'cat', 'enemy_2': 'spike'}\n",
"------\n",
"('age', 12)\n",
"('enemy', 'jerry')\n",
"('enemy_2', 'spike')\n",
"('is_human', False)\n",
"('name', 'tom')\n",
"('type', 'cat')\n",
"6\n",
"('type', 'cat')\n",
"[('age', 12), ('enemy', 'jerry'), ('enemy_2', 'spike'), ('is_human', False), ('name', 'tom')]\n",
"('name', 'tom')\n",
"[('age', 12), ('enemy', 'jerry'), ('enemy_2', 'spike'), ('is_human', False)]\n",
"('is_human', False)\n",
"[('age', 12), ('enemy', 'jerry'), ('enemy_2', 'spike')]\n",
"('enemy_2', 'spike')\n",
"[('age', 12), ('enemy', 'jerry')]\n",
"('enemy', 'jerry')\n",
"[('age', 12)]\n",
"('age', 12)\n",
"[]\n"
]
},
{
"data": {
"text/plain": [
"Ellipsis"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"lct = Lict(\n",
" initdict={\n",
" \"age\": 12,\n",
" \"enemy\": \"jerry\",\n",
" \"is_human\": False,\n",
" \"name\": \"tom\",\n",
" \"type\": \"cat\",\n",
" \"enemy_2\": \"spike\",\n",
" }\n",
")\n",
"print(lct)\n",
"print(lct.dicted_data)\n",
"print(\"------\")\n",
"for i in lct:\n",
" print(i)\n",
"print(len(lct))\n",
"while len(lct) > 0:\n",
" print(lct.pop())\n",
" print(lct)\n",
"lct = Lict(\n",
" initdict={\n",
" \"age\": 12,\n",
" \"enemy\": \"jerry\",\n",
" \"is_human\": False,\n",
" \"name\": \"tom\",\n",
" \"type\": \"cat\",\n",
" \"enemy_2\": \"spike\",\n",
" }\n",
")\n",
"..."
]
},
{
"cell_type": "markdown",
"id": "2d6d3483",
"metadata": {},
"source": [
"关爱环境 从你我做起"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "773bf99c",
"metadata": {},
"outputs": [],
"source": [
"!rm -rf test_new_repo\n",
"!rm -rf heurams.log*"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "8645c5a2",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{ 'content': '君臣/固守/以窥/周室,/',\n",
" 'delimiter': '/',\n",
" 'keyword_note': {'窥': '窥视'},\n",
" 'note': [],\n",
" 'translation': '君臣牢固地守卫着,借以窥视周王室的权力,',\n",
" 'tts_text': '君臣固守以窥周室,'}\n",
"{ 'SM-2': { 'efactor': 2.5,\n",
" 'interval': 1,\n",
" 'is_activated': 1,\n",
" 'last_date': 20454,\n",
" 'last_modify': 1767274438.752494,\n",
" 'next_date': 20455,\n",
" 'real_rept': 1,\n",
" 'rept': 0}}\n",
"{ 'content': '秦孝公/据/崤函/之固/, 拥/雍州/之地,/',\n",
" 'delimiter': '/',\n",
" 'keyword_note': {'崤函': '崤山和函谷关', '据': '占据', '雍州': '古代九州之一'},\n",
" 'note': [],\n",
" 'translation': '秦孝公占据着崤山和函谷关的险固地势,拥有雍州的土地,',\n",
" 'tts_text': '秦孝公据崤函之固, 拥雍州之地,'}\n",
"{ 'SM-2': { 'efactor': 2.5,\n",
" 'interval': 1,\n",
" 'is_activated': 1,\n",
" 'last_date': 20454,\n",
" 'last_modify': 1767274438.7534873,\n",
" 'next_date': 20455,\n",
" 'real_rept': 1,\n",
" 'rept': 0}}\n",
"{ 'algodata': [ ( '君臣固守以窥周室,',\n",
" { 'SM-2': { 'efactor': 2.5,\n",
" 'interval': 1,\n",
" 'is_activated': 1,\n",
" 'last_date': 20454,\n",
" 'last_modify': 1767274438.752494,\n",
" 'next_date': 20455,\n",
" 'real_rept': 1,\n",
" 'rept': 0}}),\n",
" ( '秦孝公据崤函之固, 拥雍州之地,',\n",
" { 'SM-2': { 'efactor': 2.5,\n",
" 'interval': 1,\n",
" 'is_activated': 1,\n",
" 'last_date': 20454,\n",
" 'last_modify': 1767274438.7534873,\n",
" 'next_date': 20455,\n",
" 'real_rept': 1,\n",
" 'rept': 0}})],\n",
" 'manifest': { 'author': '__heurams__',\n",
" 'desc': '高考古诗文: 过秦论',\n",
" 'title': '测试单元: 过秦论'},\n",
" 'payload': [ ( '君臣固守以窥周室,',\n",
" { 'content': '君臣/固守/以窥/周室,/',\n",
" 'keyword_note': {'窥': '窥视'},\n",
" 'note': [],\n",
" 'translation': '君臣牢固地守卫着,借以窥视周王室的权力,'}),\n",
" ( '秦孝公据崤函之固, 拥雍州之地,',\n",
" { 'content': '秦孝公/据/崤函/之固/, 拥/雍州/之地,/',\n",
" 'keyword_note': { '崤函': '崤山和函谷关',\n",
" '据': '占据',\n",
" '雍州': '古代九州之一'},\n",
" 'note': [],\n",
" 'translation': '秦孝公占据着崤山和函谷关的险固地势,拥有雍州的土地,'})],\n",
" 'schedule': { 'phases': { 'final_review': [ ['FillBlank', '0.7'],\n",
" ['SelectMeaning', '0.7'],\n",
" ['Recognition', '1.0']],\n",
" 'quick_review': [ ['FillBlank', '1.0'],\n",
" ['SelectMeaning', '0.5'],\n",
" ['Recognition', '1.0']],\n",
" 'recognition': [['Recognition', '1.0']]},\n",
" 'schedule': [ 'quick_review',\n",
" 'recognition',\n",
" 'final_review']},\n",
" 'source': PosixPath('test_repo'),\n",
" 'typedef': { 'annotation': { 'content': '内容',\n",
" 'delimiter': '分隔符',\n",
" 'keyword_note': '关键词翻译',\n",
" 'note': '笔记',\n",
" 'translation': '语句翻译',\n",
" 'tts_text': '文本转语音文本'},\n",
" 'common': { 'delimiter': '/',\n",
" 'tts_text': \"eval:payload['content'].replace('/', \"\n",
" \"'')\"},\n",
" 'puzzles': { 'FillBlank': { '__hint__': '',\n",
" '__origin__': 'cloze',\n",
" 'delimiter': \"eval:metadata['formation']['delimiter']\",\n",
" 'min_denominator': \"eval:default['cloze']['min_denominator']\",\n",
" 'text': \"eval:payload['content']\"},\n",
" 'Recognition': { '__hint__': '',\n",
" '__origin__': 'recognition',\n",
" 'primary': \"eval:payload['content']\",\n",
" 'secondary': [ \"eval:payload['keyword_note']\",\n",
" \"eval:payload['note']\"],\n",
" 'top_dim': [ \"eval:payload['translation']\"]},\n",
" 'SelectMeaning': { '__hint__': \"eval:payload['content']\",\n",
" '__origin__': 'mcq',\n",
" 'jammer': \"eval:list(payload['keyword_note'].values())\",\n",
" 'mapping': \"eval:payload['keyword_note']\",\n",
" 'max_riddles_num': \"eval:default['mcq']['max_riddles_num']\",\n",
" 'prefix': '选择正确项: ',\n",
" 'primary': \"eval:payload['content']\"}},\n",
" '古文句': {}}}\n"
]
}
],
"source": [
"repo = repolib.Repo.create_from_repodir(Path(\"./test_repo\"))\n",
"for i in repo.ident_index:\n",
" n = pt.Nucleon.create_on_nucleonic_data(\n",
" nucleonic_data=repo.nucleonic_data_lict.get_itemic_unit(i)\n",
" )\n",
" e = pt.Electron.create_on_electonic_data(\n",
" electronic_data=repo.electronic_data_lict.get_itemic_unit(i)\n",
" )\n",
" e.activate()\n",
" e.revisor(5, True)\n",
" print(repr(n))\n",
" print(repr(e))\n",
"print(repo)"
]
}
],
"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.13.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

54
examples/simplemem.py Normal file
View File

@@ -0,0 +1,54 @@
import heurams.kernel.repolib as repolib
import heurams.kernel.particles as pt
from heurams.services.textproc import truncate
from pathlib import Path
import time
repo = repolib.Repo.create_from_repodir(Path("./test_repo"))
alist = list()
print(repo.ident_index)
for i in repo.ident_index:
n = pt.Nucleon.create_on_nucleonic_data(
nucleonic_data=repo.nucleonic_data_lict.get_itemic_unit(i)
)
e = pt.Electron.create_on_electonic_data(
electronic_data=repo.electronic_data_lict.get_itemic_unit(i)
)
print(n)
input()
a = pt.Atom(n, e, repo.orbitic_data)
alist.append(a)
#e.activate()
#e.revisor(5, True)
print(repr(a))
# print(repr(e))
print(repo)
input()
import heurams.kernel.reactor as rt
ph: rt.Phaser = rt.Phaser(alist)
print(ph)
pr: rt.Procession = ph.current_procession() # type: ignore
print(pr)
pr.forward()
print(pr)
pr.forward() # 如果过界了?
print(pr) # 静默设置状态 无报错
pr.forward()
print(pr)
pr = ph.current_procession() # type: ignore # 下一个队列
print(pr)
pr.forward()
print(pr)
pr.append() # 如果记忆失败了?
print(pr)
pr.forward()
pr.append() # 如果记忆失败了?
pr.append() # 如果记忆失败了?
pr.append() # 如果记忆失败了?
pr.append() # 如果记忆失败了?
pr.append() # 如果记忆失败了?
# 重复项目只会占据一个车尾
print(pr)
pr.forward()
print(pr)
pr = ph.current_procession() # type: ignore
print(pr)

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,3 @@
title = "测试单元: 过秦论"
author = "__heurams__"
desc = "高考古诗文: 过秦论"

View File

@@ -0,0 +1,11 @@
["秦孝公据崤函之固, 拥雍州之地,"]
note = []
content = "秦孝公/据/崤函/之固/, 拥/雍州/之地,/"
translation = "秦孝公占据着崤山和函谷关的险固地势,拥有雍州的土地,"
keyword_note = {"据"="占据", "崤函"="崤山和函谷关", "雍州"="古代九州之一"}
["君臣固守以窥周室,"]
note = []
content = "君臣/固守/以窥/周室,/"
translation = "君臣牢固地守卫着,借以窥视周王室的权力,"
keyword_note = {"窥"="窥视"}

View File

@@ -0,0 +1,5 @@
schedule = ["quick_review", "recognition", "final_review"]
[phases]
quick_review = [["FillBlank", "1.0"], ["SelectMeaning", "0.5"], ["Recognition", "1.0"]]
recognition = [["Recognition", "1.0"]]
final_review = [["FillBlank", "0.7"], ["SelectMeaning", "0.7"], ["Recognition", "1.0"]]

View File

@@ -0,0 +1,17 @@
[annotation]
note = "笔记"
keyword_note = "关键词翻译"
translation = "语句翻译"
delimiter = "分隔符"
content = "内容"
tts_text = "文本转语音文本"
[common]
delimiter = "/"
tts_text = "eval:payload['content'].replace('/', '')"
[common.puzzles] # 谜题定义
# 我们称 "Recognition" 为 recognition 谜题的 alia
"Recognition" = { __origin__ = "recognition", __hint__ = "", primary = "eval:payload['content']", secondary = ["eval:payload['keyword_note']", "eval:payload['note']"], top_dim = ["eval:payload['translation']"] }
"SelectMeaning" = { __origin__ = "mcq", __hint__ = "eval:payload['content']", primary = "eval:payload['content']", mapping = "eval:payload['keyword_note']", jammer = "eval:list(payload['keyword_note'].values())", max_riddles_num = "eval:default['mcq']['max_riddles_num']", prefix = "选择正确项: " }
"FillBlank" = { __origin__ = "cloze", __hint__ = "", text = "eval:payload['content']", delimiter = "eval:nucleon['delimiter']", min_denominator = "eval:default['cloze']['min_denominator']"}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "heurams" name = "heurams"
version = "0.4.3" version = "0.5.0"
description = "Heuristic Assisted Memory Scheduler" description = "Heuristic Assisted Memory Scheduler"
license = {file = "LICENSE"} license = {file = "LICENSE"}
classifiers = [ classifiers = [

View File

@@ -5,6 +5,7 @@
import pathlib import pathlib
from contextvars import ContextVar from contextvars import ContextVar
import shutil
from heurams.services.config import ConfigFile from heurams.services.config import ConfigFile
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
@@ -14,31 +15,39 @@ from heurams.services.logger import get_logger
# 数据文件路径规定: 以运行目录为准 # 数据文件路径规定: 以运行目录为准
rootdir = pathlib.Path(__file__).parent rootdir = pathlib.Path(__file__).parent
print(f"rootdir: {rootdir}") print(f"项目根目录: {rootdir}")
logger = get_logger(__name__) logger = get_logger(__name__)
logger.debug(f"项目根目录: {rootdir}") logger.debug(f"项目根目录: {rootdir}")
workdir = pathlib.Path.cwd() workdir = pathlib.Path.cwd()
print(f"workdir: {workdir}") print(f"工作目录: {workdir}")
logger.debug(f"工作目录: {workdir}") logger.debug(f"工作目录: {workdir}")
config_var: ContextVar[ConfigFile] = ContextVar(
"config_var", default=ConfigFile(rootdir / "default" / "config" / "config.toml") if pathlib.Path(workdir / "data" / "config" / "config_dev.toml").exists():
)
try:
config_var: ContextVar[ConfigFile] = ContextVar(
"config_var", default=ConfigFile(workdir / "config" / "config.toml")
) # 配置文件
print("已加载自定义用户配置")
logger.info("已加载自定义用户配置, 路径: %s", workdir / "config" / "config.toml")
except Exception as e:
print("未能加载自定义用户配置")
logger.warning("未能加载自定义用户配置, 错误: %s", e)
if pathlib.Path(workdir / "config" / "config_dev.toml").exists():
print("使用开发设置") print("使用开发设置")
logger.debug("使用开发设置") logger.debug("使用开发设置")
config_var: ContextVar[ConfigFile] = ContextVar( config_var: ContextVar[ConfigFile] = ContextVar(
"config_var", default=ConfigFile(workdir / "config" / "config_dev.toml") "config_var",
default=ConfigFile(workdir / "data" / "config" / "config_dev.toml"),
) )
# runtime_var: ContextVar = ContextVar('runtime_var', default=dict()) # 运行时共享数据 else:
try:
config_var: ContextVar[ConfigFile] = ContextVar(
"config_var",
default=ConfigFile(workdir / "data" / "config" / "config.toml"),
) # 配置文件
except Exception as e:
input("按下回车以创建新的配置文件, 或按下 Ctrl + C 以终止程序 ")
(workdir / "data" / "config").mkdir(parents=True, exist_ok=True)
(workdir / "data" / "config" / "config").unlink(missing_ok=True)
shutil.copy(
(rootdir / "default" / "config" / "config.toml"),
workdir / "data" / "config" / "config.toml",
)
finally:
config_var: ContextVar[ConfigFile] = ContextVar(
"config_var",
default=ConfigFile(workdir / "data" / "config" / "config.toml"),
) # 配置文件
class ConfigContext: class ConfigContext:

View File

@@ -31,12 +31,7 @@ max_riddles_num = 2
min_denominator = 3 min_denominator = 3
[paths] # 相对于配置文件的 ".." (即工作目录) 而言 或绝对路径 [paths] # 相对于配置文件的 ".." (即工作目录) 而言 或绝对路径
nucleon_dir = "./data/nucleon" data = "./data"
electron_dir = "./data/electron"
global_dir = "./data/global" # 全局数据路径, SM-15 等算法需要
orbital_dir = "./data/orbital"
cache_dir = "./data/cache"
template_dir = "./data/template"
[services] # 定义服务到提供者的映射 [services] # 定义服务到提供者的映射
audio = "playsound" # 可选项: playsound(通用), termux(仅用于支持 Android Termux), mpg123(TODO) audio = "playsound" # 可选项: playsound(通用), termux(仅用于支持 Android Termux), mpg123(TODO)

View File

@@ -6,8 +6,8 @@ from heurams.services.logger import get_logger
from .screens.about import AboutScreen from .screens.about import AboutScreen
from .screens.dashboard import DashboardScreen from .screens.dashboard import DashboardScreen
from .screens.nucreator import NucleonCreatorScreen
from .screens.precache import PrecachingScreen from .screens.precache import PrecachingScreen
from .screens.repocreator import RepoCreatorScreen
from .screens.synctool import SyncScreen from .screens.synctool import SyncScreen
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -17,9 +17,9 @@ def environment_check():
from pathlib import Path from pathlib import Path
logger.debug("检查环境路径") logger.debug("检查环境路径")
subdir = ['cache/voice', 'repo', 'global', 'config']
for i in config_var.get()["paths"].values(): for i in subdir:
i = Path(i) i = Path(config_var.get()['paths']['data']) / i
if not i.exists(): if not i.exists():
logger.info("创建目录: %s", i) logger.info("创建目录: %s", i)
print(f"创建 {i}") print(f"创建 {i}")
@@ -39,13 +39,13 @@ class HeurAMSApp(App):
("d", "toggle_dark", "切换色调"), ("d", "toggle_dark", "切换色调"),
("1", "app.push_screen('dashboard')", "仪表盘"), ("1", "app.push_screen('dashboard')", "仪表盘"),
("2", "app.push_screen('precache_all')", "缓存管理器"), ("2", "app.push_screen('precache_all')", "缓存管理器"),
("3", "app.push_screen('nucleon_creator')", "创建新单元"), ("3", "app.push_screen('repo_creator')", "创建新仓库"),
# ("4", "app.push_screen('synctool')", "同步工具"), # ("4", "app.push_screen('synctool')", "同步工具"),
("0", "app.push_screen('about')", "版本信息"), ("0", "app.push_screen('about')", "版本信息"),
] ]
SCREENS = { SCREENS = {
"dashboard": DashboardScreen, "dashboard": DashboardScreen,
"nucleon_creator": NucleonCreatorScreen, "repo_creator": RepoCreatorScreen,
"precache_all": PrecachingScreen, "precache_all": PrecachingScreen,
"synctool": SyncScreen, "synctool": SyncScreen,
"about": AboutScreen, "about": AboutScreen,

View File

@@ -7,8 +7,8 @@ from heurams.services.logger import get_logger
from .screens.about import AboutScreen from .screens.about import AboutScreen
from .screens.dashboard import DashboardScreen from .screens.dashboard import DashboardScreen
from .screens.nucreator import NucleonCreatorScreen
from .screens.precache import PrecachingScreen from .screens.precache import PrecachingScreen
from .screens.repocreator import RepoCreatorScreen
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python3 """关于界面"""
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import ScrollableContainer from textual.containers import ScrollableContainer
from textual.screen import Screen from textual.screen import Screen
@@ -19,7 +20,7 @@ class AboutScreen(Screen):
版本 {version.ver} {version.stage.capitalize()} 版本 {version.ver} {version.stage.capitalize()}
开发代号: {version.codename.capitalize()} 开发代号: {version.codename.capitalize()} {version.codename_cn}
一个基于启发式算法的开放源代码记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划. 一个基于启发式算法的开放源代码记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划.

View File

@@ -1,16 +1,17 @@
#!/usr/bin/env python3 """仪表盘界面"""
import pathlib import pathlib
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import ScrollableContainer from textual.containers import ScrollableContainer
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import (Button, Footer, Header, Label, ListItem, ListView, from textual.widgets import Button, Footer, Header, Label, ListItem, ListView, Static
Static)
import heurams.services.timer as timer import heurams.services.timer as timer
import heurams.services.version as version import heurams.services.version as version
from heurams.context import * from heurams.context import *
from heurams.kernel.particles import * from heurams.kernel.particles import *
from heurams.kernel.repolib import *
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from .about import AboutScreen from .about import AboutScreen
@@ -31,130 +32,102 @@ class DashboardScreen(Screen):
classes: str | None = None, classes: str | None = None,
) -> None: ) -> None:
super().__init__(name, id, classes) super().__init__(name, id, classes)
self.nextdates = {} self.repostat = {}
self.texts = {} self.title2dirname = {}
self.stay_enabled = {} self.title2repo = {}
self._load_data()
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
"""组合界面组件""" """组合界面组件"""
yield Header(show_clock=True) yield Header(show_clock=True)
yield ScrollableContainer( yield ScrollableContainer(
Label('欢迎使用 "潜进" 启发式辅助记忆调度器', classes="title-label"), Label('欢迎使用 "潜进" 启发式辅助记忆调度器', classes="title-label"),
Label(f"当前 UNIX 日时间戳: {timer.get_daystamp()}"), Label(f"当前 UNIX 日时间戳: {timer.get_daystamp()} (UTC+{config_var.get()["timezone_offset"] / 3600})"),
Label(f'时区修正: UTC+{config_var.get()["timezone_offset"] / 3600}'), Label(f"全局算法设置: {config_var.get()['algorithm']['default']}"),
Label(f"使用算法: {config_var.get()['algorithm']['default']}"), Label("选择待学习或待修改的项目:", classes="title-label"),
Label("选择待学习或待修改的记忆单元集:", classes="title-label"), ListView(id="repo-list", classes="repo-list-view"),
ListView(id="union-list", classes="union-list-view"), Label(f'"潜进" 启发式辅助记忆调度器 | 版本 {version.ver} '),
Label(
f'"潜进" 启发式辅助记忆调度器 | 版本 {version.ver} '
f"{version.codename.capitalize()} 2025"
),
) )
yield Footer() yield Footer()
def analyser(self, filename: str) -> dict: def _load_data(self):
"""分析文件状态以生成显示文本 self.repo_dirs = Repo.probe_vaild_repos_in_dir(
Path(config_var.get()["paths"]["data"]) / "repo"
)
for repo_dir in self.repo_dirs:
repo = Repo.create_from_repodir(repo_dir)
self._analyse_repo(repo)
Args: def _analyse_repo(self, repo: Repo):
filename: 要分析的文件名 dirname = repo.source.name # type: ignore
title = repo.manifest["title"]
Returns:
dict: 包含显示文本的字典,键为行号
"""
from heurams.kernel.particles.loader import load_electron, load_nucleon
result = {}
filestem = pathlib.Path(filename).stem
# 构建电子文件路径
electron_dir = config_var.get()["paths"]["electron_dir"]
electron_file_path = pathlib.Path(electron_dir) / f"{filestem}.json"
logger.debug(f"电子文件路径: {electron_file_path}")
# 确保电子文件存在
if not electron_file_path.exists():
electron_file_path.touch()
electron_file_path.write_text("{}")
# 加载电子数据
electron_dict = load_electron(path=electron_file_path)
logger.debug(f"电子数据: {electron_dict}")
# 分析电子状态
is_due = 0 is_due = 0
is_activated = 0 unit_sum = len(repo)
activated_sum = 0
nextdate = 0x3F3F3F3F nextdate = 0x3F3F3F3F
is_unfinished = unit_sum > activated_sum
for electron in electron_dict.values(): for i in repo.ident_index:
logger.debug(f"{electron}, 是否到期: {electron.is_due()}") nucleon = pt.Nucleon.create_on_nucleonic_data(
nucleonic_data=repo.nucleonic_data_lict.get_itemic_unit(i)
if electron.is_due(): )
is_due = 1 electron = pt.Electron.create_on_electonic_data(
electronic_data=repo.electronic_data_lict.get_itemic_unit(i)
)
if electron.is_activated(): if electron.is_activated():
is_activated = 1 activated_sum += 1
nextdate = min(nextdate, electron.nextdate()) if electron.is_due():
is_due = 1
# 检查是否需要更多复习 nextdate = min(nextdate, electron.nextdate())
nucleon_dir = config_var.get()["paths"]["nucleon_dir"] if is_unfinished:
nucleon_path = pathlib.Path(nucleon_dir) / f"{filestem}.toml" nextdate = min(nextdate, timer.get_daystamp())
nucleon_count = len(load_nucleon(nucleon_path)) need_to_study = is_due or is_unfinished
electron_count = len(electron_dict) prompt = f"{title}\0\n 进度: {activated_sum}/{unit_sum}\n {"需要学习" if need_to_study else "无需操作"}"
is_more = not (electron_count >= nucleon_count) stat = {
"is_due": is_due,
logger.debug(f"是否需要更多复习: {is_more}") "unit_sum": unit_sum,
"title": title,
# 更新状态 "activated_sum": activated_sum,
self.nextdates[filename] = nextdate "nextdate": nextdate,
self.stay_enabled[filename] = is_due or is_more "is_unfinished": is_unfinished,
"need_to_study": need_to_study,
# 构建返回结果 "prompt": prompt,
result[0] = f"{filename}\0" "dirname": dirname,
}
if not is_activated: self.repostat[dirname] = stat
result[1] = " 尚未激活" self.title2dirname[title] = dirname
else: self.title2repo[title] = repo
status_text = "需要复习" if is_due else "当前无需复习"
result[1] = f"下一次复习: {nextdate}\n{status_text}"
return result
def on_mount(self) -> None: def on_mount(self) -> None:
"""挂载组件时初始化""" """挂载组件时初始化"""
union_list_widget = self.query_one("#union-list", ListView) repo_list_widget = self.query_one("#repo-list", ListView)
probe = probe_all(0)
# 分析所有文件
for file in probe["nucleon"]:
self.texts[file] = self.analyser(file)
# 按下次复习时间排序 # 按下次复习时间排序
nucleon_files = sorted( repodirs = sorted(
probe["nucleon"], self.repo_dirs,
key=lambda f: self.nextdates[f], key=lambda f: self.repostat[f.name]["nextdate"],
reverse=True, reverse=True,
) )
repotitles = map(lambda f: self.repostat[f.name]["title"], repodirs)
# 填充列表 # 填充列表
if not probe["nucleon"]: if not repodirs:
union_list_widget.append( repo_list_widget.append(
ListItem( ListItem(
Static( Static(
"在 ./nucleon/ 中未找到任何内容源数据文件。\n" "在 ./data/repo/ 中未找到任何仓库.\n"
"放置文件后重启应用或者新建空的单元集。" "导入仓库后重启应用, 或者新建空的仓库."
) )
) )
) )
union_list_widget.disabled = True repo_list_widget.disabled = True
return return
for file in nucleon_files: for repotitle in repotitles:
text = self.texts[file] prompt = self.repostat[self.title2dirname[repotitle]]["prompt"]
list_item = ListItem(Label(f"{text[0]}\n{text[1]}")) list_item = ListItem(Label(prompt))
union_list_widget.append(list_item) repo_list_widget.append(list_item)
if not self.stay_enabled[file]: # if not self.stay_enabled[repodir]:
list_item.disabled = True # list_item.disabled = True
def on_list_view_selected(self, event) -> None: def on_list_view_selected(self, event) -> None:
"""处理列表项选择事件""" """处理列表项选择事件"""
@@ -164,43 +137,19 @@ class DashboardScreen(Screen):
selected_label = event.item.query_one(Label) selected_label = event.item.query_one(Label)
label_text = str(selected_label.renderable) label_text = str(selected_label.renderable)
if "未找到任何 .toml 文件" in label_text: if "未找到任何仓库" in label_text:
return return
# 提取文件名 # 提取文件名
selected_filename = pathlib.Path(label_text.partition("\0")[0].replace("*", "")) selected_repotitle = label_text.partition("\0")[0].replace("*", "")
selected_repo = self.title2repo[label_text.partition("\0")[0].replace("*", "")]
# 构建文件路径
nucleon_dir = config_var.get()["paths"]["nucleon_dir"]
electron_dir = config_var.get()["paths"]["electron_dir"]
nucleon_file_path = pathlib.Path(nucleon_dir) / selected_filename
electron_file_path = (
pathlib.Path(electron_dir) / f"{selected_filename.stem}.json"
)
# 跳转到准备屏幕 # 跳转到准备屏幕
self.app.push_screen(PreparationScreen(nucleon_file_path, electron_file_path)) self.app.push_screen(
PreparationScreen(
def on_button_pressed(self, event) -> None: selected_repo, self.repostat[self.title2dirname[selected_repotitle]]
"""处理按钮点击事件""" )
button_id = event.button.id )
if button_id == "new_nucleon_button":
from .nucreator import NucleonCreatorScreen
new_screen = NucleonCreatorScreen()
self.app.push_screen(new_screen)
elif button_id == "precache_all_button":
from .precache import PrecachingScreen
precache_screen = PrecachingScreen()
self.app.push_screen(precache_screen)
elif button_id == "about_button":
about_screen = AboutScreen()
self.app.push_screen(about_screen)
def action_quit_app(self) -> None: def action_quit_app(self) -> None:
"""退出应用程序""" """退出应用程序"""

View File

@@ -0,0 +1 @@
"""笔记界面"""

View File

@@ -0,0 +1 @@
"""整体式记忆工作界面"""

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python3 """队列式记忆工作界面"""
from enum import Enum, auto from enum import Enum, auto
from textual.app import ComposeResult from textual.app import ComposeResult
@@ -7,8 +8,8 @@ from textual.reactive import reactive
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, Static from textual.widgets import Button, Footer, Header, Label, Static
import heurams.kernel.evaluators as pz
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz
from heurams.context import config_var from heurams.context import config_var
from heurams.kernel.reactor import * from heurams.kernel.reactor import *
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
@@ -27,7 +28,7 @@ logger = get_logger(__name__)
class MemScreen(Screen): class MemScreen(Screen):
BINDINGS = [ BINDINGS = [
("q", "pop_screen", "返回"), ("q", "pop_screen", "返回"),
# ("p", "prev", "复习上一个"), ("p", "prev", "查看上一个"),
("d", "toggle_dark", ""), ("d", "toggle_dark", ""),
("v", "play_voice", "朗读"), ("v", "play_voice", "朗读"),
("0,1,2,3", "app.push_screen('about')", ""), ("0,1,2,3", "app.push_screen('about')", ""),
@@ -39,67 +40,61 @@ class MemScreen(Screen):
def __init__( def __init__(
self, self,
atoms: list, phaser: Phaser,
name: str | None = None, name = None,
id: str | None = None, id = None,
classes: str | None = None, classes = None,
) -> None: ) -> None:
super().__init__(name, id, classes) super().__init__(name, id, classes)
self.atoms = atoms self.phaser = phaser
for i in self.atoms: self.update_state()
i.do_eval()
self.phaser = Phaser(atoms)
# logger.debug(self.phaser.state)
self.procession: Procession = self.phaser.current_procession() # type: ignore
self.atom: pt.Atom = self.procession.current_atom
# logger.debug(self.phaser.state)
# self.procession.forward(1)
def on_mount(self):
self.load_puzzle()
pass
def puzzle_widget(self):
try:
logger.debug(self.phaser.state)
logger.debug(self.procession.cursor)
logger.debug(self.atom)
self.fission = Fission(self.atom, self.phaser.state)
puzzle_debug = next(self.fission.generate())
# logger.debug(puzzle_debug)
return shim.puzzle2widget[puzzle_debug["puzzle"]](
atom=self.atom, alia=puzzle_debug["alia"]
)
except (KeyError, StopIteration, AttributeError) as e:
logger.debug(f"调度展开出错: {e}")
return Static("无法生成谜题")
# logger.debug(shim.puzzle2widget[puzzle_debug["puzzle"]])
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header(show_clock=True) yield Header(show_clock=True)
with ScrollableContainer(): with ScrollableContainer():
yield Label(self._get_progress_text(), id="progress") yield Label(self._get_progress_text(), id="progress")
# self.mount(self.current_widget()) # type: ignore
yield ScrollableContainer(id="puzzle-container") yield ScrollableContainer(id="puzzle-container")
# yield Button("重新学习此单元", id="re-recognize", variant="warning")
yield Footer() yield Footer()
def update_state(self):
"""更新状态机"""
self.procession: Procession = self.phaser.current_procession() # type: ignore
self.atom: pt.Atom = self.procession.current_atom # type: ignore
def on_mount(self):
self.mount_puzzle()
self.update_display()
def puzzle_widget(self):
try:
self.fission = self.procession.get_fission()
puzzle = self.fission.get_current_puzzle()
# logger.debug(puzzle_debug)
return shim.puzzle2widget[puzzle["puzzle"]]( # type: ignore
atom=self.atom, alia=puzzle["alia"] # type: ignore
)
except (KeyError, StopIteration, AttributeError) as e:
logger.debug(f"调度展开出错: {e}")
return Static(f"无法生成谜题 {e}")
# logger.debug(shim.puzzle2widget[puzzle_debug["puzzle"]])
def _get_progress_text(self): def _get_progress_text(self):
return f"当前进度: {self.procession.process() + 1}/{self.procession.total_length()}" return f"当前进度: {self.procession.process() + 1}/{self.procession.total_length()}"
def update_display(self): def update_display(self):
"""更新进度显示"""
progress_widget = self.query_one("#progress") progress_widget = self.query_one("#progress")
progress_widget.update(self._get_progress_text()) # type: ignore progress_widget.update(self._get_progress_text()) # type: ignore
def load_puzzle(self): def mount_puzzle(self):
self.atom: pt.Atom = self.procession.current_atom """挂载当前谜题组件"""
container = self.query_one("#puzzle-container") container = self.query_one("#puzzle-container")
for i in container.children: for i in container.children:
i.remove() i.remove()
container.mount(self.puzzle_widget()) container.mount(self.puzzle_widget())
def load_finished_widget(self): def mount_finished_widget(self):
"""挂载已完成组件"""
container = self.query_one("#puzzle-container") container = self.query_one("#puzzle-container")
for i in container.children: for i in container.children:
i.remove() i.remove()
@@ -110,39 +105,6 @@ class MemScreen(Screen):
def on_button_pressed(self, event): def on_button_pressed(self, event):
event.stop() event.stop()
def watch_rating(self, old_rating, new_rating) -> None:
if self.procession == 0:
return
if new_rating == -1:
return
forwards = 1 if new_rating >= 4 else 0
self.rating = -1
logger.debug(f"试图前进: {"允许" if forwards else "禁止"}")
if forwards:
ret = self.procession.forward(1)
if ret == 0: # 若结束了此次队列
self.procession = self.phaser.current_procession() # type: ignore
if self.procession == 0: # 若所有队列都结束了
logger.debug(f"记忆进程结束")
for i in self.atoms:
i: pt.Atom
i.revise()
i.persist("electron")
self.load_finished_widget()
return
else:
logger.debug(f"建立新队列 {self.procession.phase}")
self.load_puzzle()
else: # 若不通过
self.procession.append()
self.update_display()
def action_quick_pass(self):
self.rating = 5
self.atom.minimize(5)
self.atom.registry["electron"].activate()
self.atom.lock(1)
def action_play_voice(self): def action_play_voice(self):
self.run_worker(self.play_voice, exclusive=True, thread=True) self.run_worker(self.play_voice, exclusive=True, thread=True)
@@ -153,23 +115,41 @@ class MemScreen(Screen):
from heurams.services.audio_service import play_by_path from heurams.services.audio_service import play_by_path
from heurams.services.hasher import get_md5 from heurams.services.hasher import get_md5
path = Path(config_var.get()["paths"]["cache_dir"]) path = Path(config_var.get()["paths"]['data']) / 'cache' / 'voice'
path = ( path = (
path path
/ f"{get_md5(self.atom.registry['nucleon'].metadata["formation"]["tts_text"])}.wav" / f"{get_md5(self.atom.registry['nucleon']["tts_text"])}.wav"
) )
if path.exists(): if path.exists():
play_by_path(path) play_by_path(path)
else: else:
from heurams.services.tts_service import convertor from heurams.services.tts_service import convertor
convertor( convertor(
self.atom.registry["nucleon"].metadata["formation"]["tts_text"], path self.atom.registry["nucleon"]["tts_text"], path
) )
play_by_path(path) play_by_path(path)
def action_toggle_dark(self): def watch_rating(self, old_rating, new_rating) -> None:
self.app.action_toggle_dark() self.update_state() # 刷新状态
if self.procession == None: # 已经完成记忆
def action_pop_screen(self): return
self.app.pop_screen() if new_rating == -1: # 安全值
return
forwards = 1 if new_rating >= 4 else 0 # 准许前进
self.rating = -1
logger.debug(f"试图前进: {"允许" if forwards else "禁止"}")
if forwards:
ret = self.procession.forward(1)
if ret == 0: # 若结束了此次队列
self.update_state()
if self.procession == 0: # 若所有队列都结束了
logger.debug(f"记忆进程结束")
self.mount_finished_widget()
return
else:
logger.debug(f"建立新队列 {self.procession.phase}")
self.update_state()
self.mount_puzzle()
else: # 若不通过
self.procession.append()
self.update_display()

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python3 """缓存工具界面"""
import pathlib import pathlib
from textual.app import ComposeResult from textual.app import ComposeResult
@@ -11,6 +12,7 @@ import heurams.kernel.particles as pt
import heurams.services.hasher as hasher import heurams.services.hasher as hasher
from heurams.context import * from heurams.context import *
cache_dir = pathlib.Path(config_var.get()["paths"]["data"]) / "cache" / 'voice'
class PrecachingScreen(Screen): class PrecachingScreen(Screen):
"""预缓存音频文件屏幕 """预缓存音频文件屏幕
@@ -37,12 +39,6 @@ class PrecachingScreen(Screen):
self.precache_worker = None self.precache_worker = None
self.cancel_flag = 0 self.cancel_flag = 0
self.desc = desc self.desc = desc
for i in nucleons:
i: pt.Nucleon
atom = pt.Atom()
atom.link("nucleon", i)
atom.do_eval()
# print("完成 EVAL")
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header(show_clock=True) yield Header(show_clock=True)
@@ -93,7 +89,6 @@ class PrecachingScreen(Screen):
"""预缓存单段文本的音频""" """预缓存单段文本的音频"""
from heurams.context import config_var, rootdir, workdir from heurams.context import config_var, rootdir, workdir
cache_dir = pathlib.Path(config_var.get()["paths"]["cache_dir"])
cache_dir.mkdir(parents=True, exist_ok=True) cache_dir.mkdir(parents=True, exist_ok=True)
cache_file = cache_dir / f"{hasher.get_md5(text)}.wav" cache_file = cache_dir / f"{hasher.get_md5(text)}.wav"
if not cache_file.exists(): if not cache_file.exists():
@@ -109,10 +104,8 @@ class PrecachingScreen(Screen):
def precache_by_nucleon(self, nucleon: pt.Nucleon): def precache_by_nucleon(self, nucleon: pt.Nucleon):
"""依据 Nucleon 缓存""" """依据 Nucleon 缓存"""
# print(nucleon.metadata['formation']['tts_text']) ret = self.precache_by_text(nucleon["tts_text"])
ret = self.precache_by_text(nucleon.metadata["formation"]["tts_text"])
return ret return ret
# print(f"TTS 缓存: {nucleon.metadata['formation']['tts_text']}")
def precache_by_list(self, nucleons: list): def precache_by_list(self, nucleons: list):
"""依据 Nucleons 列表缓存""" """依据 Nucleons 列表缓存"""
@@ -121,7 +114,7 @@ class PrecachingScreen(Screen):
worker = get_current_worker() worker = get_current_worker()
if worker and worker.is_cancelled: # 函数在worker中执行且已被取消 if worker and worker.is_cancelled: # 函数在worker中执行且已被取消
return False return False
text = nucleon.metadata["formation"]["tts_text"] text = nucleon["tts_text"]
# self.current_item = text[:30] + "..." if len(text) > 50 else text # self.current_item = text[:30] + "..." if len(text) > 50 else text
# print(text) # print(text)
self.processed += 1 self.processed += 1
@@ -151,38 +144,30 @@ class PrecachingScreen(Screen):
# print(f"返回 {ret}") # print(f"返回 {ret}")
return ret return ret
def precache_by_filepath(self, path: pathlib.Path):
"""预缓存单个文件的所有内容"""
lst = list()
for i in pt.load_nucleon(path):
lst.append(i[0])
return self.precache_by_list(lst)
def precache_all_files(self): def precache_all_files(self):
"""预缓存所有文件""" """预缓存所有文件"""
from heurams.context import config_var, rootdir, workdir from heurams.context import config_var, rootdir, workdir
from heurams.kernel.repolib import Repo
nucleon_path = pathlib.Path(config_var.get()["paths"]["nucleon_dir"]) repo_path = pathlib.Path(config_var.get()["paths"]["data"]) / "repo"
nucleon_files = [ repo_dirs = Repo.probe_vaild_repos_in_dir(repo_path)
f for f in nucleon_path.iterdir() if f.suffix == ".toml" repos = map(Repo.create_from_repodir, repo_dirs)
] # TODO: 解耦合
# 计算总项目数 # 计算总项目数
self.total = 0 self.total = 0
nu = list() nucleon_list = list()
for file in nucleon_files: for repo in repos:
try: try:
for i in pt.load_nucleon(file): for i in repo.ident_index:
nu.append(i[0]) nucleon_list.append(
pt.Nucleon.create_on_nucleonic_data(
repo.nucleonic_data_lict.get_itemic_unit(i)
)
)
except: except:
continue continue
self.total = len(nu) self.total = len(nucleon_list)
for i in nu: return self.precache_by_list(nucleon_list)
i: pt.Nucleon
atom = pt.Atom()
atom.link("nucleon", i)
atom.do_eval()
return self.precache_by_list(nu)
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:
event.stop() event.stop()
@@ -220,7 +205,7 @@ class PrecachingScreen(Screen):
from heurams.context import config_var, rootdir, workdir from heurams.context import config_var, rootdir, workdir
shutil.rmtree( shutil.rmtree(
f"{config_var.get()["paths"]["cache_dir"]}", ignore_errors=True cache_dir, ignore_errors=True
) )
self.update_status("已清空", "音频缓存已清空", 0) self.update_status("已清空", "音频缓存已清空", 0)
except Exception as e: except Exception as e:

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python3 """记忆准备界面"""
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import ScrollableContainer from textual.containers import ScrollableContainer
from textual.reactive import reactive from textual.reactive import reactive
@@ -11,6 +12,7 @@ import heurams.services.hasher as hasher
from heurams.context import * from heurams.context import *
from heurams.context import config_var from heurams.context import config_var
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from heurams.kernel.repolib import *
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -28,24 +30,19 @@ class PreparationScreen(Screen):
scheduled_num = reactive(config_var.get()["scheduled_num"]) scheduled_num = reactive(config_var.get()["scheduled_num"])
def __init__(self, nucleon_file: pathlib.Path, electron_file: pathlib.Path) -> None: def __init__(self, repo: Repo, repostat: dict) -> None:
super().__init__(name=None, id=None, classes=None) super().__init__(name=None, id=None, classes=None)
self.nucleon_file = nucleon_file self.repo = repo
self.electron_file = electron_file self.repostat = repostat
self.nucleons_with_orbital = pt.load_nucleon(self.nucleon_file)
self.electrons = pt.load_electron(self.electron_file)
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header(show_clock=True) yield Header(show_clock=True)
with ScrollableContainer(id="vice_container"): with ScrollableContainer(id="vice_container"):
yield Label(f"准备就绪: [b]{self.nucleon_file.stem}[/b]\n") yield Label(f"准备就绪: [b]{self.repostat['title']}[/b]\n")
yield Label( yield Label(
f"内容源文件: {config_var.get()['paths']['nucleon_dir']}/[b]{self.nucleon_file.name}[/b]" f"仓库路径: {config_var.get()['paths']['data']}/repo/[b]{self.repostat['dirname']}[/b]"
) )
yield Label( yield Label(f"\n单元数量: {len(self.repo)}\n")
f"元数据文件: {config_var.get()['paths']['electron_dir']}/[b]{self.electron_file.name}[/b]"
)
yield Label(f"\n单元数量: {len(self.nucleons_with_orbital)}\n")
yield Label(f"单次记忆数量: {self.scheduled_num}", id="schnum_label") yield Label(f"单次记忆数量: {self.scheduled_num}", id="schnum_label")
yield Button( yield Button(
@@ -75,10 +72,11 @@ class PreparationScreen(Screen):
def _get_full_content(self): def _get_full_content(self):
content = "" content = ""
for nucleon, orbital in self.nucleons_with_orbital: for i in self.repo.ident_index:
nucleon: pt.Nucleon n = pt.Nucleon.create_on_nucleonic_data(
# print(nucleon.payload) nucleonic_data=self.repo.nucleonic_data_lict.get_itemic_unit(i)
content += " - " + nucleon["content"] + " \n" )
content += f"- {n['content']} \n"
return content return content
def action_go_back(self): def action_go_back(self):
@@ -88,9 +86,15 @@ class PreparationScreen(Screen):
from ..screens.precache import PrecachingScreen from ..screens.precache import PrecachingScreen
lst = list() lst = list()
for i in self.nucleons_with_orbital: for i in self.repo.ident_index:
lst.append(i[0]) lst.append(
precache_screen = PrecachingScreen(lst) pt.Nucleon.create_on_nucleonic_data(
self.repo.nucleonic_data_lict.get_itemic_unit(i)
)
)
precache_screen = PrecachingScreen(
nucleons=lst, desc=self.repo.manifest["title"]
)
self.app.push_screen(precache_screen) self.app.push_screen(precache_screen)
def action_quit_app(self): def action_quit_app(self):
@@ -101,38 +105,32 @@ class PreparationScreen(Screen):
logger.debug("按下按钮") logger.debug("按下按钮")
if event.button.id == "start_memorizing_button": if event.button.id == "start_memorizing_button":
atoms = list() atoms = list()
for nucleon, orbital in self.nucleons_with_orbital: for i in self.repo.ident_index:
atom = pt.Atom(nucleon.ident) n = pt.Nucleon.create_on_nucleonic_data(
atom.link("nucleon", nucleon) nucleonic_data=self.repo.nucleonic_data_lict.get_itemic_unit(i)
try: )
atom.link("electron", self.electrons[nucleon.ident]) e = pt.Electron.create_on_electonic_data(
except KeyError: electronic_data=self.repo.electronic_data_lict.get_itemic_unit(i)
atom.link("electron", pt.Electron(nucleon.ident)) )
atom.link("orbital", orbital) a = pt.Atom(n, e, self.repo.orbitic_data)
atom.link("nucleon_fmt", "toml") atoms.append(a)
atom.link("electron_fmt", "json")
atom.link("orbital_fmt", "toml")
atom.link("nucleon_path", self.nucleon_file)
atom.link("electron_path", self.electron_file)
atom.link("orbital_path", None)
atoms.append(atom)
atoms_to_provide = list() atoms_to_provide = list()
left_new = self.scheduled_num left_new = self.scheduled_num
for i in atoms: for i in atoms:
i: pt.Atom i: pt.Atom
if i.registry["electron"].is_due(): if i.registry["electron"].is_activated():
atoms_to_provide.append(i) if i.registry["electron"].is_due():
atoms_to_provide.append(i)
else: else:
if i.registry["electron"].is_activated(): left_new -= 1
pass if left_new >= 0:
else: atoms_to_provide.append(i)
left_new -= 1 from .memoqueue import MemScreen
if left_new >= 0: import heurams.kernel.reactor as rt
atoms_to_provide.append(i) pheser = rt.Phaser(atoms_to_provide)
logger.debug(f"ATP: {atoms_to_provide}") memscreen = MemScreen(pheser)
from .memorizor import MemScreen
memscreen = MemScreen(atoms_to_provide)
self.app.push_screen(memscreen) self.app.push_screen(memscreen)
elif event.button.id == "precache_button": elif event.button.id == "precache_button":
self.action_precache() self.action_precache()

View File

@@ -0,0 +1 @@
""" "前进电台" 界面"""

View File

@@ -1,20 +1,20 @@
#!/usr/bin/env python3 """仓库创建向导界面"""
from pathlib import Path from pathlib import Path
import toml import toml
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import ScrollableContainer from textual.containers import ScrollableContainer
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import (Button, Footer, Header, Input, Label, Markdown, from textual.widgets import Button, Footer, Header, Input, Label, Markdown, Select
Select)
from heurams.context import config_var from heurams.context import config_var
from heurams.services.version import ver from heurams.services.version import ver
class NucleonCreatorScreen(Screen): class RepoCreatorScreen(Screen):
BINDINGS = [("q", "go_back", "返回")] BINDINGS = [("q", "go_back", "返回")]
SUB_TITLE = "单元集创建向导" SUB_TITLE = "仓库创建向导"
def __init__(self) -> None: def __init__(self) -> None:
super().__init__(name=None, id=None, classes=None) super().__init__(name=None, id=None, classes=None)

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python3 """同步工具界面"""
import pathlib import pathlib
import time import time

View File

@@ -1,37 +1,10 @@
"""Kernel 操作辅助函数库""" """Kernel 操作辅助函数库"""
import random
from typing import TypedDict
import heurams.interface.widgets as pzw import heurams.interface.widgets as pzw
import heurams.kernel.particles as pt import heurams.kernel.evaluators as pz
import heurams.kernel.puzzles as pz
staging = {} # 细粒度缓存区, 是 ident -> quality 的封装
def report_to_staging(atom: pt.Atom, quality):
staging[atom.ident] = min(quality, staging[atom.ident])
def clear():
staging = dict()
def deploy_to_electron():
for atom_ident, quality in staging.items():
if pt.atom_registry[atom_ident].registry["electron"].is_activated:
pt.atom_registry[atom_ident].registry["electron"].revisor(quality=quality)
else:
pt.atom_registry[atom_ident].registry["electron"].revisor(
quality=quality, is_new_activation=True
)
clear()
puzzle2widget = { puzzle2widget = {
pz.RecognitionPuzzle: pzw.Recognition, pz.RecognitionPuzzle: pzw.Recognition,
pz.ClozePuzzle: pzw.ClozePuzzle, pz.ClozePuzzle: pzw.ClozePuzzle,
pz.MCQPuzzle: pzw.MCQPuzzle, pz.MCQPuzzle: pzw.MCQPuzzle,
pz.BasePuzzle: pzw.BasePuzzleWidget, pz.BaseEvaluator: pzw.BasePuzzleWidget,
} }

View File

@@ -7,8 +7,8 @@ from textual.message import Message
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import Button, Label from textual.widgets import Button, Label
import heurams.kernel.evaluators as pz
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from .base_puzzle_widget import BasePuzzleWidget from .base_puzzle_widget import BasePuzzleWidget

View File

@@ -5,8 +5,8 @@ from textual.containers import Container, ScrollableContainer
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import Button, Label from textual.widgets import Button, Label
import heurams.kernel.evaluators as pz
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz
from heurams.services.hasher import hash from heurams.services.hasher import hash
from heurams.services.logger import get_logger from heurams.services.logger import get_logger

View File

@@ -54,8 +54,8 @@ class Recognition(BasePuzzleWidget):
autovoice = config_var.get()["interface"]["memorizor"]["autovoice"] autovoice = config_var.get()["interface"]["memorizor"]["autovoice"]
if autovoice: if autovoice:
self.screen.action_play_voice() # type: ignore self.screen.action_play_voice() # type: ignore
cfg: RecognitionConfig = self.atom.registry["orbital"]["puzzles"][self.alia] cfg: RecognitionConfig = self.atom.registry["nucleon"]["puzzles"][self.alia]
delim = self.atom.registry["nucleon"].metadata["formation"]["delimiter"] delim = self.atom.registry["nucleon"]["delimiter"]
replace_dict = { replace_dict = {
", ": ",", ", ": ",",
". ": ".", ". ": ".",
@@ -69,7 +69,7 @@ class Recognition(BasePuzzleWidget):
} }
nucleon = self.atom.registry["nucleon"] nucleon = self.atom.registry["nucleon"]
metadata = self.atom.registry["nucleon"].metadata metadata = self.atom.registry["nucleon"]
primary = cfg["primary"] primary = cfg["primary"]
with Center(): with Center():

View File

@@ -1,5 +1,6 @@
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from .base import BaseAlgorithm
from .sm2 import SM2Algorithm from .sm2 import SM2Algorithm
from .sm15m import SM15MAlgorithm from .sm15m import SM15MAlgorithm
@@ -7,12 +8,14 @@ logger = get_logger(__name__)
__all__ = [ __all__ = [
"SM2Algorithm", "SM2Algorithm",
"BaseAlgorithm",
"SM15MAlgorithm",
] ]
algorithms = { algorithms = {
"SM-2": SM2Algorithm, "SM-2": SM2Algorithm,
"SM-15M": SM15MAlgorithm, "SM-15M": SM15MAlgorithm,
# "SM-15M": SM15MAlgorithm, "Base": BaseAlgorithm,
} }
logger.debug("算法模块初始化完成, 注册的算法: %s", list(algorithms.keys())) logger.debug("算法模块初始化完成, 注册的算法: %s", list(algorithms.keys()))

View File

@@ -10,7 +10,6 @@ class BaseAlgorithm:
algo_name = "BaseAlgorithm" algo_name = "BaseAlgorithm"
class AlgodataDict(TypedDict): class AlgodataDict(TypedDict):
efactor: float
real_rept: int real_rept: int
rept: int rept: int
interval: int interval: int
@@ -52,7 +51,7 @@ class BaseAlgorithm:
return 1 return 1
@classmethod @classmethod
def rate(cls, algodata) -> str: def get_rating(cls, algodata) -> str:
"""获取评分信息""" """获取评分信息"""
logger.debug( logger.debug(
"BaseAlgorithm.rate 被调用, algodata keys: %s", "BaseAlgorithm.rate 被调用, algodata keys: %s",
@@ -68,3 +67,11 @@ class BaseAlgorithm:
list(algodata.keys()) if algodata else [], list(algodata.keys()) if algodata else [],
) )
return -1 return -1
@classmethod
def check_integrity(cls, algodata):
try:
cls.AlgodataDict(**algodata[cls.algo_name])
return 1
except:
return 0

View File

@@ -14,13 +14,22 @@ import pathlib
from typing import TypedDict from typing import TypedDict
from heurams.context import config_var from heurams.context import config_var
from heurams.kernel.algorithms.sm15m_calc import (MAX_AF, MIN_AF, NOTCH_AF, from heurams.kernel.algorithms.sm15m_calc import (
RANGE_AF, RANGE_REPETITION, MAX_AF,
SM, THRESHOLD_RECALL, Item) MIN_AF,
NOTCH_AF,
RANGE_AF,
RANGE_REPETITION,
SM,
THRESHOLD_RECALL,
Item,
)
# 全局状态文件路径 # 全局状态文件路径
_GLOBAL_STATE_FILE = os.path.expanduser( _GLOBAL_STATE_FILE = os.path.expanduser(
pathlib.Path(config_var.get()["paths"]["global_dir"]) / "sm15m_global_state.json" pathlib.Path(config_var.get()["paths"]["data"])
/ "global"
/ "sm15m_global_state.json"
) )

View File

@@ -116,7 +116,7 @@ class SM2Algorithm(BaseAlgorithm):
return result return result
@classmethod @classmethod
def rate(cls, algodata): def get_rating(cls, algodata):
efactor = algodata[cls.algo_name]["efactor"] efactor = algodata[cls.algo_name]["efactor"]
logger.debug("SM2.rate: efactor=%f", efactor) logger.debug("SM2.rate: efactor=%f", efactor)
return str(efactor) return str(efactor)

View File

@@ -1,20 +1,20 @@
""" """
Puzzle 模块 - 谜题生成系统 Evaluator 模块 - 生成评估模块
提供多种类型的谜题生成器, 支持从字符串字典等数据源导入题目 提供多种类型的辅助评估生成器, 支持从字符串字典等数据源导入题目
""" """
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
from .base import BasePuzzle from .base import BaseEvaluator
from .cloze import ClozePuzzle from .cloze import ClozePuzzle
from .mcq import MCQPuzzle from .mcq import MCQPuzzle
from .recognition import RecognitionPuzzle from .recognition import RecognitionPuzzle
__all__ = [ __all__ = [
"BasePuzzle", "BaseEvaluator",
"ClozePuzzle", "ClozePuzzle",
"MCQPuzzle", "MCQPuzzle",
"RecognitionPuzzle", "RecognitionPuzzle",
@@ -24,12 +24,12 @@ puzzles = {
"mcq": MCQPuzzle, "mcq": MCQPuzzle,
"cloze": ClozePuzzle, "cloze": ClozePuzzle,
"recognition": RecognitionPuzzle, "recognition": RecognitionPuzzle,
"base": BasePuzzle, "base": BaseEvaluator,
} }
@staticmethod @staticmethod
def create_by_dict(config_dict: dict) -> BasePuzzle: def create_by_dict(config_dict: dict) -> BaseEvaluator:
""" """
根据配置字典创建谜题 根据配置字典创建谜题

View File

@@ -4,7 +4,7 @@ from heurams.services.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
class BasePuzzle: class BaseEvaluator:
"""谜题基类""" """谜题基类"""
def refresh(self): def refresh(self):

View File

@@ -2,12 +2,12 @@ import random
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from .base import BasePuzzle from .base import BaseEvaluator
logger = get_logger(__name__) logger = get_logger(__name__)
class ClozePuzzle(BasePuzzle): class ClozePuzzle(BaseEvaluator):
"""填空题谜题生成器 """填空题谜题生成器
Args: Args:

View File

@@ -0,0 +1,12 @@
import random
from heurams.services.logger import get_logger
from .base import BaseEvaluator
logger = get_logger(__name__)
class GuessEvaluator(BaseEvaluator):
def __init__(self):
super().__init__()

View File

@@ -4,12 +4,12 @@ from typing import Dict, List, Optional, Union
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from .base import BasePuzzle from .base import BaseEvaluator
logger = get_logger(__name__) logger = get_logger(__name__)
class MCQPuzzle(BasePuzzle): class MCQPuzzle(BaseEvaluator):
"""选择题谜题生成器 """选择题谜题生成器
该类用于生成和管理选择题谜题, 支持多个题目同时生成, 该类用于生成和管理选择题谜题, 支持多个题目同时生成,

View File

@@ -3,12 +3,12 @@ import random
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from .base import BasePuzzle from .base import BaseEvaluator
logger = get_logger(__name__) logger = get_logger(__name__)
class RecognitionPuzzle(BasePuzzle): class RecognitionPuzzle(BaseEvaluator):
"""识别占位符""" """识别占位符"""
def __init__(self) -> None: def __init__(self) -> None:

View File

@@ -1,29 +1,4 @@
""" from .atom import Atom
Particle 模块 - 粒子对象系统
提供闪卡所需对象, 使用物理学粒子的领域驱动设计
"""
from heurams.services.logger import get_logger
logger = get_logger(__name__)
logger.debug("粒子模块已加载")
from .atom import Atom, atom_registry
from .electron import Electron from .electron import Electron
from .loader import load_electron, load_nucleon
from .nucleon import Nucleon from .nucleon import Nucleon
from .orbital import Orbital #from .orbital import Orbital
from .probe import probe_all, probe_by_filename
__all__ = [
"Electron",
"Nucleon",
"Orbital",
"Atom",
"probe_all",
"probe_by_filename",
"load_nucleon",
"load_electron",
"atom_registry",
]

View File

@@ -1,17 +1,10 @@
import json
import pathlib
import typing
from typing import TypedDict from typing import TypedDict
import bidict
import toml
from heurams.context import config_var
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from .electron import Electron from .electron import Electron
from .nucleon import Nucleon from .nucleon import Nucleon
from .orbital import Orbital
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -19,19 +12,13 @@ logger = get_logger(__name__)
class AtomRegister_runtime(TypedDict): class AtomRegister_runtime(TypedDict):
locked: bool # 只读锁定标识符 locked: bool # 只读锁定标识符
min_rate: int # 最低评分 min_rate: int # 最低评分
newact: bool # 新激活 new_activation: bool # 新激活
class AtomRegister(TypedDict): class AtomRegister(TypedDict):
nucleon: Nucleon nucleon: Nucleon
nucleon_path: pathlib.Path
nucleon_fmt: str
electron: Electron electron: Electron
electron_path: pathlib.Path orbital: dict
electron_fmt: str
orbital: Orbital
orbital_path: pathlib.Path
orbital_fmt: str
runtime: AtomRegister_runtime runtime: AtomRegister_runtime
@@ -44,88 +31,27 @@ class Atom:
以及关联路径 以及关联路径
""" """
def __init__(self, ident=""): default_runtime = {
logger.debug("创建 Atom 实例, ident: '%s'", ident) "locked": False,
self.ident = ident "min_rate": 0x3F3F3F3F,
atom_registry[ident] = self "new_activation": False,
logger.debug("Atom 已注册到全局注册表, 当前注册表大小: %d", len(atom_registry)) }
# self.is_evaled = False
def __init__(self, nucleon_obj=None, electron_obj=None, orbital_obj=None):
self.ident = nucleon_obj["ident"] # type: ignore
self.registry: AtomRegister = { # type: ignore self.registry: AtomRegister = { # type: ignore
"nucleon": None, "ident": nucleon_obj["ident"], # type: ignore
"nucleon_path": None, "nucleon": nucleon_obj,
"nucleon_fmt": "toml", "electron": electron_obj,
"electron": None, "orbital": orbital_obj,
"electron_path": None, "runtime": dict(),
"electron_fmt": "json",
"orbital": None,
"orbital_path": None, # 允许设置为 None, 此时使用 nucleon 文件内的推荐配置
"orbital_fmt": "toml",
"runtime": {"locked": False, "min_rate": 0x3F3F3F3F, "newact": False},
} }
logger.debug("Atom 初始化完成") self.init_runtime()
if self.registry["electron"].is_activated() == 0:
self.registry["runtime"]["new_activation"] = True
def link(self, key, value): def init_runtime(self):
logger.debug("Atom.link: key='%s', value type: %s", key, type(value).__name__) self.registry["runtime"] = AtomRegister_runtime(**self.default_runtime)
if key in self.registry.keys():
self.registry[key] = value
logger.debug("'%s' 已链接, 触发 do_eval", key)
if key == "electron":
if self.registry["electron"].is_activated() == 0:
self.registry["runtime"]["newact"] = True
else:
logger.error("尝试链接不受支持的键: '%s'", key)
raise ValueError("不受支持的原子元数据链接操作")
def do_eval(self):
"""
执行并以结果替换当前单元的所有 eval 语句
TODO: 带有限制的 eval, 异步/多线程执行避免堵塞
"""
logger.debug("EVAL 开始")
# eval 环境设置
def eval_with_env(s: str):
default = config_var.get()["puzzles"]
payload = self.registry["nucleon"].payload
metadata = self.registry["nucleon"].metadata
eval_value = eval(s)
if isinstance(eval_value, (int, float)):
ret = str(eval_value)
else:
ret = eval_value
logger.debug(
"eval 执行成功: '%s' -> '%s'",
s,
str(ret)[:50] + "..." if len(ret) > 50 else ret,
)
return ret
def traverse(data, modifier):
if isinstance(data, dict):
for key, value in data.items():
data[key] = traverse(value, modifier)
return data
elif isinstance(data, list):
for i, item in enumerate(data):
data[i] = traverse(item, modifier)
return data
elif isinstance(data, tuple):
return tuple(traverse(item, modifier) for item in data)
else:
if isinstance(data, str):
if data.startswith("eval:"):
logger.debug("发现 eval 表达式: '%s'", data[5:])
return modifier(data[5:])
return data
try:
traverse(self.registry["nucleon"].payload, eval_with_env)
traverse(self.registry["nucleon"].metadata, eval_with_env)
traverse(self.registry["orbital"], eval_with_env)
except Exception as e:
ret = f"此 eval 实例发生错误: {e}"
logger.warning(ret)
logger.debug("EVAL 完成")
def minimize(self, rating): def minimize(self, rating):
"""效果等同于 self.registry['runtime']['min_rate'] = min(rating, self.registry['runtime']['min_rate']) """效果等同于 self.registry['runtime']['min_rate'] = min(rating, self.registry['runtime']['min_rate'])
@@ -158,62 +84,21 @@ class Atom:
logger.debug(f"允许总评分: {self.registry['runtime']['min_rate']}") logger.debug(f"允许总评分: {self.registry['runtime']['min_rate']}")
self.registry["electron"].revisor( self.registry["electron"].revisor(
self.registry["runtime"]["min_rate"], self.registry["runtime"]["min_rate"],
is_new_activation=self.registry["runtime"]["newact"], is_new_activation=self.registry["runtime"]["new_activation"],
) )
else: else:
logger.debug("禁止总评分") logger.debug("禁止总评分")
def persist(self, key):
logger.debug("Atom.persist: key='%s'", key)
path: pathlib.Path | None = self.registry[key + "_path"]
if isinstance(path, pathlib.Path):
path = typing.cast(pathlib.Path, path)
logger.debug("持久化路径: %s, 格式: %s", path, self.registry[key + "_fmt"])
path.parent.mkdir(parents=True, exist_ok=True)
if self.registry[key + "_fmt"] == "toml":
with open(path, "r+") as f:
f.seek(0)
f.truncate()
toml.dump(self.registry[key], f)
logger.debug("TOML 数据已保存到: %s", path)
elif self.registry[key + "_fmt"] == "json":
with open(path, "r+") as f:
origin = json.load(f)
f.seek(0)
f.truncate()
origin[self.ident] = self.registry[key].algodata
json.dump(origin, f, indent=2, ensure_ascii=False)
logger.debug("JSON 数据已保存到: %s", path)
else:
logger.error("不受支持的持久化格式: %s", self.registry[key + "_fmt"])
raise KeyError("不受支持的持久化格式")
else:
logger.error("路径未初始化: %s_path", key)
raise TypeError("对未初始化的路径对象操作")
def __getitem__(self, key): def __getitem__(self, key):
logger.debug("Atom.__getitem__: key='%s'", key) return self.registry[key]
if key in self.registry:
value = self.registry[key]
logger.debug("返回 value type: %s", type(value).__name__)
return value
logger.error("不支持的键: '%s'", key)
raise KeyError(f"不支持的键: {key}")
def __setitem__(self, key, value): def __setitem__(self, key, value):
logger.debug( if key == "ident":
"Atom.__setitem__: key='%s', value type: %s", key, type(value).__name__ raise AttributeError("应为只读")
) self.registry[key] = value
if key in self.registry:
self.registry[key] = value
logger.debug("'%s' 已设置", key)
else:
logger.error("不支持的键: '%s'", key)
raise KeyError(f"不支持的键: {key}")
@staticmethod def __repr__(self):
def placeholder(): from pprint import pformat
return (Electron.placeholder(), Nucleon.placeholder(), {})
s = pformat(self.registry, indent=4)
atom_registry: bidict.bidict[str, Atom] = bidict.bidict() return s

View File

@@ -1,5 +1,8 @@
from copy import deepcopy
from typing import TypedDict
import heurams.kernel.algorithms as algolib
import heurams.services.timer as timer import heurams.services.timer as timer
from heurams.context import config_var
from heurams.kernel.algorithms import algorithms from heurams.kernel.algorithms import algorithms
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
@@ -7,94 +10,62 @@ logger = get_logger(__name__)
class Electron: class Electron:
"""电子: 记忆分析元数据及算法""" """电子: 单算法支持的记忆数据包装"""
def __init__(self, ident: str, algodata: dict = {}, algo_name: str = ""): def __init__(self, ident: str, algodata: dict = {}, algo_name: str = ""):
"""初始化电子对象 (记忆数据) """初始化电子对象 (记忆数据)
Args: Args:
ident: 算法的唯一标识符, 用于区分不同的算法实例, 使用 algodata[ident] 获取 ident: 算法的唯一标识符, 用于区分不同的算法实例, 使用 algodata[ident] 获取
algodata: 算法数据字典, 包含算法的各项参数和设置 algodata: 算法数据字典引用, 包含算法的各项参数和设置
algo: 使用的算法模块标识 algo_name: 使用的算法模块标识
""" """
if algo_name == "": if algo_name == "":
algo_name = config_var.get()["algorithm"]["default"] algo_name = "SM-2"
logger.debug(
"创建 Electron 实例, ident: '%s', algo_name: '%s', algodata: %s",
ident,
algo_name,
algodata,
)
self.algodata = algodata self.algodata = algodata
self.ident = ident self.ident = ident
self.algo = algorithms[algo_name] self.algo: algolib.BaseAlgorithm = algorithms[algo_name]
logger.debug("使用的算法类: %s", self.algo.__name__)
if self.algo.algo_name not in self.algodata.keys(): if not self.algo.check_integrity(self.algodata):
self.algodata[self.algo.algo_name] = {} self.algodata[self.algo.algo_name] = deepcopy(self.algo.defaults)
logger.debug("算法键 '%s' 不存在, 已创建空字典", self.algo)
if not self.algodata[self.algo.algo_name]:
logger.debug(
f"算法数据为空, 使用默认值初始化{self.algodata[self.algo.algo_name]}"
)
self._default_init(self.algo.defaults)
else:
logger.debug("算法数据已存在, 跳过默认初始化")
logger.debug(
"Electron 初始化完成, algodata keys: %s", list(self.algodata.keys())
)
def _default_init(self, defaults: dict): def __repr__(self):
"""默认初始化包装""" from pprint import pformat
logger.debug(
"Electron._default_init: 使用默认值, keys: %s", list(defaults.keys()) s = pformat(self.algodata, indent=4)
) return s
self.algodata[self.algo.algo_name] = defaults.copy()
def activate(self): def activate(self):
"""激活此电子""" """激活此电子"""
logger.debug("Electron.activate: 激活 ident='%s'", self.ident)
self.algodata[self.algo.algo_name]["is_activated"] = 1 self.algodata[self.algo.algo_name]["is_activated"] = 1
self.algodata[self.algo.algo_name]["last_modify"] = timer.get_timestamp() self.algodata[self.algo.algo_name]["last_modify"] = timer.get_timestamp()
logger.debug("电子已激活, is_activated=1")
def modify(self, var: str, value): def modify(self, key, value):
"""修改 algodata[algo] 中子字典数据""" """修改 algodata[algo] 中子字典数据"""
logger.debug("Electron.modify: var='%s', value=%s", var, value) if key in self.algodata[self.algo.algo_name]:
if var in self.algodata[self.algo.algo_name]: self.algodata[self.algo.algo_name][key] = value
self.algodata[self.algo.algo_name][var] = value
self.algodata[self.algo.algo_name]["last_modify"] = timer.get_timestamp() self.algodata[self.algo.algo_name]["last_modify"] = timer.get_timestamp()
logger.debug("变量 '%s' 已修改, 更新 last_modify", var)
else: else:
logger.warning("'%s' 非已知元数据字段", var) raise AttributeError("不存在的子键")
print(f"警告: '{var}' 非已知元数据字段")
def is_due(self): def is_due(self):
"""是否应该复习""" """是否应该复习"""
logger.debug("Electron.is_due: 检查 ident='%s'", self.ident)
result = self.algo.is_due(self.algodata) result = self.algo.is_due(self.algodata)
logger.debug("is_due 结果: %s", result)
return result and self.is_activated() return result and self.is_activated()
def is_activated(self): def is_activated(self):
result = self.algodata[self.algo.algo_name]["is_activated"] result = self.algodata[self.algo.algo_name]["is_activated"]
logger.debug("Electron.is_activated: ident='%s', 结果: %d", self.ident, result)
return result return result
def get_rate(self): def get_rating(self):
"评价"
try: try:
logger.debug("Electron.rate: ident='%s'", self.ident) result = self.algo.get_rating(self.algodata)
result = self.algo.rate(self.algodata)
logger.debug("rate 结果: %s", result)
return result return result
except: except:
return 0 return 0
def nextdate(self) -> int: def nextdate(self) -> int:
logger.debug("Electron.nextdate: ident='%s'", self.ident)
result = self.algo.nextdate(self.algodata) result = self.algo.nextdate(self.algodata)
logger.debug("nextdate 结果: %d", result)
return result return result
def revisor(self, quality: int = 5, is_new_activation: bool = False): def revisor(self, quality: int = 5, is_new_activation: bool = False):
@@ -104,32 +75,7 @@ class Electron:
quality (int): 记忆保留率量化参数 (0-5) quality (int): 记忆保留率量化参数 (0-5)
is_new_activation (bool): 是否为初次激活 is_new_activation (bool): 是否为初次激活
""" """
logger.debug(
"Electron.revisor: ident='%s', quality=%d, is_new_activation=%s",
self.ident,
quality,
is_new_activation,
)
self.algo.revisor(self.algodata, quality, is_new_activation) self.algo.revisor(self.algodata, quality, is_new_activation)
logger.debug(
"revisor 完成, 更新后的 algodata: %s", self.algodata.get(self.algo, {})
)
def __str__(self):
return (
f"记忆单元预览 \n"
f"标识符: '{self.ident}' \n"
f"算法: {self.algo} \n"
f"易度系数: {self.algodata[self.algo.algo_name]['efactor']:.2f} \n"
f"已经重复的次数: {self.algodata[self.algo.algo_name]['rept']} \n"
f"下次间隔: {self.algodata[self.algo.algo_name]['interval']}\n"
f"下次复习日期时间戳: {self.algodata[self.algo.algo_name]['next_date']}"
)
def __eq__(self, other):
if self.ident == other.ident:
return True
return False
def __hash__(self): def __hash__(self):
return hash(self.ident) return hash(self.ident)
@@ -153,6 +99,9 @@ class Electron:
return len(self.algodata[self.algo.algo_name]) return len(self.algodata[self.algo.algo_name])
@staticmethod @staticmethod
def placeholder(): def create_on_electonic_data(electronic_data: tuple, algo_name: str = ""):
"""生成一个电子占位符""" _data = electronic_data
return Electron("电子对象样例内容", {}) ident = _data[0]
algodata = _data[1]
ident = ident
return Electron(ident, algodata, algo_name)

View File

@@ -1,74 +0,0 @@
import json
import pathlib
from copy import deepcopy
import toml
import heurams.services.hasher as hasher
from heurams.services.logger import get_logger
from .electron import Electron
from .nucleon import Nucleon
logger = get_logger(__name__)
def load_nucleon(path: pathlib.Path, fmt="toml"):
logger.debug("load_nucleon: 加载文件 %s, 格式: %s", path, fmt)
with open(path, "r") as f:
dictdata = dict()
dictdata = toml.load(f) # type: ignore
logger.debug("TOML 解析成功, keys: %s", list(dictdata.keys()))
lst = list()
nested_data = dict()
# 修正 toml 解析器的不管嵌套行为
for key, value in dictdata.items():
if "__metadata__" in key: # 以免影响句号
if "." in key:
parts = key.split(".")
current = nested_data
for part in parts[:-1]:
if part not in current:
current[part] = {}
current = current[part]
current[parts[-1]] = value
logger.debug("处理元数据键: %s", key)
else:
nested_data[key] = value
logger.debug("嵌套数据处理完成, keys: %s", list(nested_data.keys()))
# print(nested_data)
for item, attr in nested_data.items():
if item == "__metadata__":
continue
logger.debug("处理项目: %s", item)
lst.append(
(
Nucleon(item, attr, deepcopy(nested_data["__metadata__"])),
deepcopy(nested_data["__metadata__"]["orbital"]),
)
)
logger.debug("load_nucleon 完成, 加载了 %d 个 Nucleon 对象", len(lst))
return lst
def load_electron(path: pathlib.Path, fmt="json") -> dict:
"""从文件路径加载电子对象
Args:
path (pathlib.Path): 路径
fmt (str): 文件格式(可选, 默认 json)
Returns:
dict: 键名是电子对象名称, 值是电子对象
"""
logger.debug("load_electron: 加载文件 %s, 格式: %s", path, fmt)
with open(path, "r") as f:
dictdata = dict()
dictdata = json.load(f) # type: ignore
logger.debug("JSON 解析成功, keys: %s", list(dictdata.keys()))
dic = dict()
for item, attr in dictdata.items():
logger.debug("处理电子项目: %s, %s", item, attr)
dic[item] = Electron(item, attr)
logger.debug("load_electron 完成, 加载了 %d 个 Electron 对象", len(dic))
return dic

View File

@@ -1,56 +1,62 @@
from copy import deepcopy
from logging import config
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from heurams.utils.evalizor import Evalizer
from heurams.context import config_var
logger = get_logger(__name__) logger = get_logger(__name__)
class Nucleon: class Nucleon:
"""原子核: 材料元数据""" """原子核: 带有运行时隔离的模板化只读材料元数据容器"""
def __init__(self, ident: str, payload: dict, metadata: dict = {}): def __init__(self, ident, payload, common):
"""初始化原子核 (记忆内容)
Args:
ident: 唯一标识符
payload: 记忆内容信息
metadata: 可选元数据信息
"""
logger.debug(
"创建 Nucleon 实例, ident: '%s', payload keys: %s, metadata keys: %s",
ident,
list(payload.keys()) if payload else [],
list(metadata.keys()) if metadata else [],
)
self.metadata = metadata
self.payload = payload
self.ident = ident self.ident = ident
logger.debug("Nucleon 初始化完成") env = {"payload": payload,
"default": config_var.get()['puzzles'],
"nucleon": (payload | common)}
self.evalizer = Evalizer(environment=env)
self.data: dict = self.evalizer(deepcopy((payload | common))) # type: ignore
def __getitem__(self, key): def __getitem__(self, key):
logger.debug("Nucleon.__getitem__: key='%s'", key) if isinstance(key, str):
if key == "ident": if key == "ident":
logger.debug("返回 ident: '%s'", self.ident) return self.ident
return self.ident return self.data[key]
if key in self.payload:
value = self.payload[key]
logger.debug(
"返回 payload['%s'], value type: %s", key, type(value).__name__
)
return value
else: else:
logger.error("'%s' 未在 payload 中找到", key) raise AttributeError
raise KeyError(f"Key '{key}' not found in payload.")
def __setitem__(self, key, value):
raise AttributeError("应为只读")
def __delitem__(self, key):
raise AttributeError("应为只读")
def __iter__(self): def __iter__(self):
yield from self.payload.keys() return iter(self.data)
def __contains__(self, key):
return key in (self.data)
def get(self, key, default=None):
if key in self:
return self[key]
return default
def __len__(self): def __len__(self):
return len(self.payload) return len(self.data)
def __hash__(self): def __repr__(self):
return hash(self.ident) from pprint import pformat
s = pformat(self.data, indent=4)
return s
@staticmethod @staticmethod
def placeholder(): def create_on_nucleonic_data(nucleonic_data: tuple):
"""生成一个占位原子核""" _data = nucleonic_data
logger.debug("创建 Nucleon 占位符") payload = _data[1][0]
return Nucleon("核子对象样例内容", {}) common = _data[1][1]
ident = _data[0] # TODO:实现eval
return Nucleon(ident, payload, common)

View File

@@ -1,30 +1,17 @@
from typing import TypedDict """轨道对象"""
from heurams.services.logger import get_logger # 似乎没有实现这个类的必要...
# 那不妨在这儿写点文档
logger = get_logger(__name__) """
logger.debug("Orbital 类型定义模块已加载") orbital, 即轨道, 是定义队列式复习阶段流程的数据结构, 其实就是个字典, 至于为何不用typeddict, 因为懒.
orbital_example = {
class OrbitalSchedule(TypedDict): "schedule": [列表 存储阶段(phases)名称]
quick_review: list "phases":{
recognition: list 阶段名称 = [["谜题(puzzle 现称 evaluator 评估器)名称", "概率系数 可大于1(整数部分为重复次数) 注意使用字符串包裹(toml 规范)"], ...],
final_review: list ...
}
}
class Orbital(TypedDict): 至于谜题定义 放在 nucleon['puzzles'], 这样设计是为了兼容多种不同谜题实现的记忆单元, 尽管如此, 你也可见其谜题调度方式必须是相同的.
schedule: OrbitalSchedule
puzzles: dict
"""一份示例
["__metadata__.orbital.puzzles"] # 谜题定义
"Recognition" = { __origin__ = "recognition", __hint__ = "", primary = "eval:nucleon['content']", secondery = ["eval:nucleon['keyword_note']", "eval:nucleon['note']"], top_dim = ["eval:nucleon['translation']"] }
"SelectMeaning" = { __origin__ = "mcq", __hint__ = "eval:nucleon['content']", jammer = "eval:nucleon['keyword_note']", max_riddles_num = "eval:default['mcq']['max_riddles_num']", prefix = "选择正确项: " }
"FillBlank" = { __origin__ = "cloze", __hint__ = "", text = "eval:nucleon['content']", delimiter = "eval:metadata['formation']['delimiter']", min_denominator = "eval:default['cloze']['min_denominator']"}
["__metadata__.orbital.schedule"] # 内置的推荐学习方案
quick_review = [["FillBlank", "1.0"], ["SelectMeaning", "0.5"], ["recognition", "1.0"]]
recognition = [["recognition", "1.0"]]
final_review = [["FillBlank", "0.7"], ["SelectMeaning", "0.7"], ["recognition", "1.0"]]
""" """

View File

@@ -1,62 +0,0 @@
import pathlib
from heurams.context import config_var
from heurams.services.logger import get_logger
logger = get_logger(__name__)
def probe_by_filename(filename):
"""探测指定文件 (无扩展名) 的所有信息"""
logger.debug("probe_by_filename: 探测文件 '%s'", filename)
paths: dict = config_var.get().get("paths")
logger.debug("配置路径: %s", paths)
formats = ["toml", "json"]
result = {}
for item, attr in paths.items():
for i in formats:
attr: pathlib.Path = pathlib.Path(attr) / filename + "." + i
if attr.exists():
logger.debug("找到文件: %s", attr)
result[item.replace("_dir", "")] = str(attr)
else:
logger.debug("文件不存在: %s", attr)
logger.debug("probe_by_filename 结果: %s", result)
return result
def probe_all(is_stem=1):
"""依据目录探测所有信息
Args:
is_stem (boolean): 是否**删除**文件扩展名
Returns:
dict: 有三项, 每一项的键名都是文件组类型, 值都是文件组列表, 只包含文件名
"""
logger.debug("probe_all: 开始探测, is_stem=%d", is_stem)
paths: dict = config_var.get().get("paths")
logger.debug("配置路径: %s", paths)
result = {}
for item, attr in paths.items():
attr: pathlib.Path = pathlib.Path(attr)
result[item.replace("_dir", "")] = list()
logger.debug("扫描目录: %s", attr)
file_count = 0
for i in attr.iterdir():
if not i.is_dir():
file_count += 1
if is_stem:
result[item.replace("_dir", "")].append(str(i.stem))
else:
result[item.replace("_dir", "")].append(str(i.name))
logger.debug("目录 %s 中找到 %d 个文件", attr, file_count)
logger.debug("probe_all 完成, 结果 keys: %s", list(result.keys()))
return result
if __name__ == "__main__":
import os
print(os.getcwd())
print(probe_all())

View File

@@ -0,0 +1 @@
# Reactor - 记忆过程状态机模块

View File

@@ -1,45 +1,70 @@
import random import random
import heurams.kernel.evaluators as puz
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
import heurams.kernel.puzzles as puz
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from .states import PhaserState from .states import PhaserState
class Fission: class Fission:
"""裂变器: 单原子调度展开器""" """单原子调度展开器"""
def __init__(self, atom: pt.Atom, phase=PhaserState.RECOGNITION): def __init__(self, atom: pt.Atom, phase_state=PhaserState.RECOGNITION):
self.cursor = 0
self.logger = get_logger(__name__) self.logger = get_logger(__name__)
self.atom = atom self.atom = atom
# print(f"{phase.value}")
self.orbital_schedule = atom.registry["orbital"]["schedule"][phase.value] # type: ignore # NOTE: phase 为 PhaserState 枚举实例需要获取其value
self.orbital_puzzles = atom.registry["orbital"]["puzzles"] phase_value = (
# print(self.orbital_schedule) phase_state.value if isinstance(phase_state, PhaserState) else phase_state
)
self.orbital_schedule = atom.registry['orbital']["phases"][phase_value] # type: ignore
self.orbital_puzzles = atom.registry["nucleon"]["puzzles"]
self.puzzles = list() self.puzzles = list()
for item, possibility in self.orbital_schedule: # type: ignore for item, possibility in self.orbital_schedule: # type: ignore
print(f"ad:{item}") self.logger.debug(f"开始处理: {item}")
self.logger.debug(f"开始处理 orbital 项: {item}")
if not isinstance(possibility, float): if not isinstance(possibility, float):
possibility = float(possibility) possibility = float(possibility)
while possibility > 1: while possibility > 1:
self.puzzles.append( self.puzzles.append(
{ {
"puzzle": puz.puzzles[self.orbital_puzzles[item]["__origin__"]], "puzzle": puz.puzzles[self.orbital_puzzles[item]["__origin__"]],
"alia": item, "alia": item,
"finished": 0,
} }
) )
possibility -= 1 possibility -= 1
if random.random() <= possibility: if random.random() <= possibility:
self.puzzles.append( self.puzzles.append(
{ {
"puzzle": puz.puzzles[self.orbital_puzzles[item]["__origin__"]], "puzzle": puz.puzzles[self.orbital_puzzles[item]["__origin__"]],
"alia": item, "alia": item,
"finished": 0,
} }
) )
print(f"ok:{item}")
self.logger.debug(f"orbital 项处理完成: {item}") self.logger.debug(f"orbital 项处理完成: {item}")
def generate(self): def get_puzzles(self):
yield from self.puzzles return self.puzzles
def get_current_puzzle(self, forward = 0):
if forward:
if len(self.puzzles) <= self.cursor + 1:
return 0
self.cursor += 1
return self.puzzles[self.cursor]
else:
return self.puzzles[self.cursor]
def check_passed(self):
for i in self.puzzles:
if i["finished"] == 0:
return 0
return 1

View File

@@ -1,7 +1,6 @@
# 移相器类定义
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from transitions import Machine
from .procession import Procession from .procession import Procession
from .states import PhaserState, ProcessionState from .states import PhaserState, ProcessionState
@@ -9,43 +8,133 @@ from .states import PhaserState, ProcessionState
logger = get_logger(__name__) logger = get_logger(__name__)
class Phaser: class Phaser(Machine):
"""移相器: 全局调度阶段管理器""" """全局调度阶段管理器"""
def __init__(self, atoms: list[pt.Atom]) -> None: def __init__(self, atoms: list[pt.Atom]) -> None:
logger.debug("Phaser.__init__: 原子数量=%d", len(atoms)) logger.debug("Phaser.__init__: 原子数量=%d", len(atoms))
new_atoms = list() new_atoms = list()
old_atoms = list() old_atoms = list()
self.state = PhaserState.UNSURE
for i in atoms: for i in atoms:
if not i.registry["electron"].is_activated(): if not i.registry["electron"].is_activated():
new_atoms.append(i) new_atoms.append(i)
else: else:
old_atoms.append(i) old_atoms.append(i)
logger.debug("新原子数量=%d, 旧原子数量=%d", len(new_atoms), len(old_atoms)) logger.debug("新原子数量=%d, 旧原子数量=%d", len(new_atoms), len(old_atoms))
self.processions = list() self.processions = list()
# TODO: 改进为基于配置文件的可变复习阶段管理
if len(old_atoms): if len(old_atoms):
self.processions.append( self.processions.append(
Procession(old_atoms, PhaserState.QUICK_REVIEW, "初始复习") Procession(old_atoms, PhaserState.QUICK_REVIEW, "初始复习")
) )
logger.debug("创建初始复习 Procession") logger.debug("创建初始复习 Procession")
if len(new_atoms): if len(new_atoms):
self.processions.append( self.processions.append(
Procession(new_atoms, PhaserState.RECOGNITION, "新记忆") Procession(new_atoms, PhaserState.RECOGNITION, "新记忆")
) )
logger.debug("创建新记忆 Procession") logger.debug("创建新记忆 Procession")
self.processions.append(Procession(atoms, PhaserState.FINAL_REVIEW, "总体复习")) self.processions.append(Procession(atoms, PhaserState.FINAL_REVIEW, "总体复习"))
logger.debug("创建总体复习 Procession") logger.debug("创建总体复习 Procession")
logger.debug("Phaser 初始化完成, processions 数量=%d", len(self.processions)) logger.debug("Phaser 初始化完成, processions 数量=%d", len(self.processions))
# 设置transitions状态机
states = [
{"name": PhaserState.UNSURE.value, "on_enter": "on_unsure"},
{"name": PhaserState.QUICK_REVIEW.value, "on_enter": "on_quick_review"},
{"name": PhaserState.RECOGNITION.value, "on_enter": "on_recognition"},
{"name": PhaserState.FINAL_REVIEW.value, "on_enter": "on_final_review"},
{"name": PhaserState.FINISHED.value, "on_enter": "on_finished"},
]
transitions = [
{"trigger": "to_unsure", "source": "*", "dest": PhaserState.UNSURE.value},
{
"trigger": "to_quick_review",
"source": "*",
"dest": PhaserState.QUICK_REVIEW.value,
},
{
"trigger": "to_recognition",
"source": "*",
"dest": PhaserState.RECOGNITION.value,
},
{
"trigger": "to_final_review",
"source": "*",
"dest": PhaserState.FINAL_REVIEW.value,
},
{
"trigger": "to_finished",
"source": "*",
"dest": PhaserState.FINISHED.value,
},
]
Machine.__init__(
self,
states=states,
transitions=transitions,
initial=PhaserState.UNSURE.value,
)
self.to_unsure()
def on_unsure(self):
"""进入UNSURE状态时的回调"""
logger.debug("Phaser 进入 UNSURE 状态")
def on_quick_review(self):
"""进入QUICK_REVIEW状态时的回调"""
logger.debug("Phaser 进入 QUICK_REVIEW 状态")
def on_recognition(self):
"""进入RECOGNITION状态时的回调"""
logger.debug("Phaser 进入 RECOGNITION 状态")
def on_final_review(self):
"""进入FINAL_REVIEW状态时的回调"""
logger.debug("Phaser 进入 FINAL_REVIEW 状态")
def on_finished(self):
"""进入FINISHED状态时的回调"""
logger.debug("Phaser 进入 FINISHED 状态")
def current_procession(self): def current_procession(self):
logger.debug("Phaser.current_procession 被调用") logger.debug("Phaser.current_procession 被调用")
for i in self.processions: for i in self.processions:
i: Procession i: Procession
if not i.state == ProcessionState.FINISHED: if i.state != ProcessionState.FINISHED.value:
self.state = i.phase # 根据当前procession的phase更新Phaser状态
if i.phase == PhaserState.QUICK_REVIEW:
self.to_quick_review()
elif i.phase == PhaserState.RECOGNITION:
self.to_recognition()
elif i.phase == PhaserState.FINAL_REVIEW:
self.to_final_review()
logger.debug("找到未完成的 Procession: phase=%s", i.phase) logger.debug("找到未完成的 Procession: phase=%s", i.phase)
return i return i
self.state = PhaserState.FINISHED
# 所有Procession都已完成
self.to_finished()
logger.debug("所有 Procession 已完成, 状态设置为 FINISHED") logger.debug("所有 Procession 已完成, 状态设置为 FINISHED")
return 0 return None
def __repr__(self):
from heurams.services.textproc import truncate
from tabulate import tabulate as tabu
lst = [
{
"Type": "Phaser",
"State": self.state,
"Processions": list(map(lambda f: (f.name_), self.processions)),
"Current Procession": "None" if not self.current_procession() else self.current_procession().name_, # type: ignore
},
]
return str(tabu(lst, headers="keys")) + '\n'

View File

@@ -1,60 +1,104 @@
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from transitions import Machine
from tabulate import tabulate as tabu
from .fission import Fission
from .states import PhaserState, ProcessionState from .states import PhaserState, ProcessionState
logger = get_logger(__name__) logger = get_logger(__name__)
class Procession: class Procession(Machine):
"""队列: 标识单次记忆流程""" """队列: 标识单次记忆流程"""
def __init__(self, atoms: list, phase: PhaserState, name: str = ""): def __init__(self, atoms: list, phase_state: PhaserState, name_: str = ""):
logger.debug( logger.debug(
"Procession.__init__: 原子数量=%d, phase=%s, name='%s'", "Procession.__init__: 原子数量=%d, phase=%s, name='%s'",
len(atoms), len(atoms),
phase.value, phase_state.value,
name, name_,
) )
self.current_atom: pt.Atom | None
self.atoms = atoms self.atoms = atoms
self.queue = atoms.copy() self.queue = atoms.copy()
self.current_atom = atoms[0] self.current_atom = atoms[0] if atoms else None
self.cursor = 0 self.cursor = 0
self.name = name self.name_ = name_
self.phase = phase self.phase = phase_state
self.state: ProcessionState = ProcessionState.RUNNING
states = [
{"name": ProcessionState.RUNNING.value, "on_enter": "on_running"},
{"name": ProcessionState.FINISHED.value, "on_enter": "on_finished"},
]
transitions = [
{
"trigger": "finish",
"source": ProcessionState.RUNNING.value,
"dest": ProcessionState.FINISHED.value,
},
{
"trigger": "restart",
"source": ProcessionState.FINISHED.value,
"dest": ProcessionState.RUNNING.value,
},
]
Machine.__init__(
self,
states=states,
transitions=transitions,
initial=ProcessionState.RUNNING.value,
)
logger.debug("Procession 初始化完成, 队列长度=%d", len(self.queue)) logger.debug("Procession 初始化完成, 队列长度=%d", len(self.queue))
def on_running(self):
"""进入RUNNING状态时的回调"""
logger.debug("Procession 进入 RUNNING 状态")
def on_finished(self):
"""进入FINISHED状态时的回调"""
logger.debug("Procession 进入 FINISHED 状态")
def forward(self, step=1): def forward(self, step=1):
"""将记忆原子指针向前移动并依情况更新原子(返回 1)或完成队列(返回 0)
"""
logger.debug("Procession.forward: step=%d, 当前 cursor=%d", step, self.cursor) logger.debug("Procession.forward: step=%d, 当前 cursor=%d", step, self.cursor)
self.cursor += step self.cursor += step
if self.cursor == len(self.queue): if self.cursor >= len(self.queue):
self.state = ProcessionState.FINISHED if self.state != ProcessionState.FINISHED.value:
self.finish() # 触发状态转换
logger.debug("Procession 已完成") logger.debug("Procession 已完成")
else: else:
self.state = ProcessionState.RUNNING if self.state != ProcessionState.RUNNING.value:
try: self.restart() # 确保在RUNNING状态
logger.debug("cursor 更新为: %d", self.cursor)
self.current_atom = self.queue[self.cursor] self.current_atom = self.queue[self.cursor]
logger.debug("当前原子更新为: %s", self.current_atom.ident) logger.debug("cursor 更新为: %d", self.cursor)
logger.debug(
"当前原子更新为: %s",
self.current_atom.ident if self.current_atom else "None",
)
return 1 # 成功 return 1 # 成功
except IndexError as e: return 0
logger.debug("IndexError: %s", e)
self.state = ProcessionState.FINISHED
logger.debug("Procession 因索引错误而完成")
return 0
def append(self, atom=None): def append(self, atom=None):
if atom == None: """追加(回忆失败的)原子(默认为当前原子)到队列末端
"""
if atom is None:
atom = self.current_atom atom = self.current_atom
logger.debug("Procession.append: atom=%s", atom.ident if atom else "None") logger.debug("Procession.append: atom=%s", atom.ident if atom else "None")
if self.queue[len(self.queue) - 1] != atom or len(self) <= 1:
if not self.queue or self.queue[-1] != atom or len(self) <= 1:
self.queue.append(atom) self.queue.append(atom)
logger.debug("原子已追加到队列, 新队列长度=%d", len(self.queue)) logger.debug("原子已追加到队列, 新队列长度=%d", len(self.queue))
else: else:
logger.debug("原子未追加(重复或队列长度<=1)") logger.debug("原子未追加(重复或队列长度<=1)")
def __len__(self): def __len__(self):
if not self.queue:
return 0
length = len(self.queue) - self.cursor length = len(self.queue) - self.cursor
logger.debug("Procession.__len__: 剩余长度=%d", length) logger.debug("Procession.__len__: 剩余长度=%d", length)
return length return length
@@ -69,6 +113,23 @@ class Procession:
return total return total
def is_empty(self): def is_empty(self):
empty = len(self.queue) empty = len(self.queue) == 0
logger.debug("Procession.is_empty: %d", empty) logger.debug("Procession.is_empty: %s", empty)
return empty return empty
def get_fission(self):
return Fission(atom=self.current_atom, phase_state=self.phase) # type: ignore
def __repr__(self):
from heurams.services.textproc import truncate
dic = [
{
"Type": "Procession",
"Name": self.name_,
"State": self.state,
"Progress": f"{self.cursor + 1} / {len(self.queue)}",
"Queue": list(map(lambda f: truncate(f.ident), self.queue)),
"Current Atom": self.current_atom.ident, # type: ignore
}
]
return str(tabu(dic, headers="keys")) + '\n'

View File

@@ -14,8 +14,8 @@ class PhaserState(Enum):
class ProcessionState(Enum): class ProcessionState(Enum):
RUNNING = auto() RUNNING = "running"
FINISHED = auto() FINISHED = "finished"
logger.debug("状态枚举定义已加载") logger.debug("状态枚举定义已加载")

View File

@@ -0,0 +1 @@
from .repo import *

View File

@@ -0,0 +1,5 @@
from ...utils.lict import Lict
def merge(x: Lict, y: Lict):
return Lict(list(x.values()) + list(y.values()))

View File

@@ -0,0 +1,3 @@
class Navi:
def __init__(self, init) -> None:
pass

View File

@@ -0,0 +1,176 @@
import json
from functools import reduce
from pathlib import Path
from typing import TypedDict
import toml
import heurams.kernel.particles as pt
from ...utils.lict import Lict
class RepoManifest(TypedDict):
title: str
author: str
desc: str
class Repo:
file_mapping = {
"schedule": "schedule.toml",
"payload": "payload.toml",
"algodata": "algodata.json",
"manifest": "manifest.toml",
"typedef": "typedef.toml",
}
type_mapping = {
"schedule": "dict",
"payload": "lict",
"algodata": "lict",
"manifest": "dict",
"typedef": "dict",
}
default_save_list = ["algodata"]
def __init__(
self,
schedule: dict,
payload: Lict,
manifest: dict,
typedef: dict,
algodata: Lict,
source=None,
) -> None:
self.schedule: dict = schedule
self.manifest: RepoManifest = manifest # type: ignore
self.typedef: dict = typedef
self.payload: Lict = payload
self.algodata: Lict = algodata
self.source: Path | None = source # 若存在, 指向 repo 所在 dir
self.database = {
"schedule": self.schedule,
"payload": self.payload,
"manifest": self.manifest,
"typedef": self.typedef,
"algodata": self.algodata,
"source": self.source,
}
self.generate_particles_data()
def generate_particles_data(self):
self.nucleonic_data_lict = Lict(
initlist=list(map(self._nucleonic_proc, self.payload))
)
self.orbitic_data = self.schedule
self.ident_index = self.nucleonic_data_lict.keys()
for i in self.ident_index:
self.algodata.append_new((i, {}))
self.electronic_data_lict = self.algodata
def _nucleonic_proc(self, unit):
ident = unit[0]
common = self.typedef["common"]
return (ident, (unit[1], common))
@staticmethod
def _merge(value):
def inner(x):
return (x, value)
return inner
def __len__(self):
return len(self.payload)
def __repr__(self):
from pprint import pformat
s = pformat(self.database, indent=4)
return s
def persist_to_repodir(
self, save_list: list | None = None, source: Path | None = None
):
if save_list == None:
save_list = self.default_save_list
if self.source != None and source == None:
source = self.source
if source == None:
raise FileNotFoundError("不存在仓库到文件的映射")
source.mkdir(parents=True, exist_ok=False)
for keyname in save_list:
filename = self.file_mapping[keyname]
with open(source / filename, "w") as f:
try:
dict_data = self.database[keyname].dicted_data
except:
dict_data = dict(self.database[keyname])
if filename.endswith("toml"):
toml.dump(dict_data, f)
elif filename.endswith("json"):
json.dump(dict_data, f)
else:
raise ValueError(f"不支持的文件类型: {filename}")
def export_to_single_dict(self):
return self.database
@classmethod
def create_new_repo(cls, source=None):
default_database = {
"schedule": {},
"payload": Lict([]),
"algodata": Lict([]),
"manifest": {},
"typedef": {},
"source": source,
}
return Repo(**default_database)
@classmethod
def create_from_repodir(cls, source: Path):
database = {}
for keyname, filename in cls.file_mapping.items():
with open(source / filename, "r") as f:
loaded: dict
if filename.endswith("toml"):
loaded = toml.load(f)
elif filename.endswith("json"):
loaded = json.load(f)
else:
raise ValueError(f"不支持的文件类型: {filename}")
if cls.type_mapping[keyname] == "lict":
database[keyname] = Lict(list(loaded.items()))
elif cls.type_mapping[keyname] == "dict":
database[keyname] = loaded
else:
raise ValueError(f"不支持的数据容器: {cls.type_mapping[keyname]}")
database["source"] = source
return Repo(**database)
@classmethod
def create_from_single_dict(cls, dictdata, source: Path | None = None):
database = dictdata
database["source"] = source
return Repo(**database)
@classmethod
def check_repodir(cls, source: Path):
try:
cls.create_from_repodir(source)
return 1
except:
return 0
@classmethod
def probe_vaild_repos_in_dir(cls, folder: Path):
lst = list()
for i in folder.iterdir():
if i.is_dir():
if cls.check_repodir(i):
lst.append(i)
return lst

View File

@@ -0,0 +1,4 @@
def truncate(text):
if len(text) <= 3:
return text
return text[:3] + ">"

View File

@@ -3,8 +3,9 @@ from heurams.services.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
ver = "0.4.3" ver = "0.5.0"
stage = "prototype" stage = "prototype"
codename = "fledge" # 雏鸟, 0.4.x 版本 codename = "fulcrom"
codename_cn = "支点"
logger.info("HeurAMS 版本: %s (%s), 阶段: %s", ver, codename, stage) logger.info("HeurAMS 版本: %s (%s), 阶段: %s", ver, codename, stage)

View File

@@ -1,2 +0,0 @@
# Utils - 实用工具
脚本与部分分离式工具函数

View File

@@ -0,0 +1,33 @@
class Evalizer:
"""几乎无副作用的模板系统
接受环境信息并创建一个模板解析工具, 工具传入参数支持list, dict及其嵌套
副作用问题: 仅存在于 eval 函数
"""
# TODO: 弃用风险极高的 eval
# TODO: 异步/多线程执行避免堵塞
def __init__(self, environment: dict) -> None:
self.env = environment
def __call__(self, anyobj):
return self.travel(anyobj)
def travel(self, anyobj):
if isinstance(anyobj, list):
return list(map(self.travel, anyobj))
elif isinstance(anyobj, dict):
return dict(map(self.travel, anyobj.items()))
elif isinstance(anyobj, tuple):
return tuple(map(self.travel, anyobj))
elif isinstance(anyobj, str):
if anyobj.startswith("eval:"):
return self.eval_with_env(anyobj[5:])
else:
return anyobj
else:
return anyobj
def eval_with_env(self, s: str):
ret = eval(s, globals(), self.env)
return ret

149
src/heurams/utils/lict.py Normal file
View File

@@ -0,0 +1,149 @@
from collections import UserList
from typing import Any, Iterator
class Lict(UserList): # TODO: 优化同步(惰性同步), 当前性能为 O(n)
""" "列典" 对象
同时兼容字典和列表大多数 API, 两边数据同步的容器
列表数据是 dictobj.items() 的格式
支持根据字典或列表初始化
限制要求:
- 键名一定唯一, 且仅能为字符串
- 值一定是引用对象
- 不使用并发
- 不在乎列表顺序语义(严格按键名字符序排列)和列表索引查找, 因此外部的 sort, index 等功能不可用
- append 的元组中, 表示键名的元素不能重复, 否则会导致覆盖行为
只有在 Python 3.7+ 中, forced_order 行为才能被取消.
"""
def __init__(
self,
initlist: list | None = None,
initdict: dict | None = None,
forced_order=False,
):
self.dicted_data = {}
if initdict != None:
initlist = list(initdict.items())
super().__init__(initlist=initlist)
self.forced_order = forced_order
self._sync_based_on_list()
if self.forced_order:
self.data.sort()
def _sync_based_on_dict(self):
self.data = list(self.dicted_data.items())
if self.forced_order:
self.data.sort()
def _sync_based_on_list(self):
self.dicted_data = {}
for i in self.data:
self.dicted_data[i[0]] = i[1]
def __iter__(self) -> Iterator:
return self.data.__iter__()
def __getitem__(self, i):
if isinstance(i, str):
return self.dicted_data[i]
else:
return super().__getitem__(i)
def get_itemic_unit(self, ident):
return (ident, self.dicted_data[ident])
def __setitem__(self, i, item):
if isinstance(i, str):
self.dicted_data[i] = item
self._sync_based_on_dict()
else:
if item != (item[0], item[1]):
raise NotImplementedError
super().__setitem__(i, item)
self._sync_based_on_list()
def __delitem__(self, i):
if isinstance(i, str):
del self.dicted_data[i]
self._sync_based_on_dict()
else:
super().__delitem__(i)
self._sync_based_on_list()
def __contains__(self, item):
return item in self.data or item in self.keys() or item in self.values()
def append(self, item: Any) -> None:
if item != (item[0], item[1]):
raise NotImplementedError
super().append(item)
self._sync_based_on_list()
if self.forced_order:
self.data.sort()
def append_new(self, item: Any):
if item != (item[0], item[1]):
raise NotImplementedError
if item[0] not in self:
super().append(item)
self._sync_based_on_list()
if self.forced_order:
self.data.sort()
def insert(self, i: int, item: Any) -> None:
if item != (item[0], item[1]): # 确保 item 是遵从限制的元组
raise NotImplementedError
super().insert(i, item)
self._sync_based_on_list()
if self.forced_order:
self.data.sort()
def pop(self, i: int = -1) -> Any:
res = super().pop(i)
self._sync_based_on_list()
return res
def remove(self, item: Any) -> None:
if isinstance(item, str):
item = (item, self.dicted_data[item])
if item != (item[0], item[1]):
raise NotImplementedError
super().remove(item)
self._sync_based_on_list()
if self.forced_order:
self.data.sort()
def clear(self) -> None:
super().clear()
self._sync_based_on_list()
def index(self):
raise NotImplementedError
def extend(self):
raise NotImplementedError
def sort(self):
raise NotImplementedError
def reverse(self):
raise NotImplementedError
def keys(self):
return self.dicted_data.keys()
def values(self):
return self.dicted_data.values()
def items(self):
return self.data
def keys_equal_with(self, other):
return self.key_equality(self, other)
@classmethod
def key_equality(cls, a, b):
return a.keys() == b.keys()

241
src/heurams/utils/refvar.py Normal file
View File

@@ -0,0 +1,241 @@
class RefVar:
def __init__(self, initvalue) -> None:
self.data = initvalue
def __repr__(self) -> str:
return f"RefVar({repr(self.data)})"
def __str__(self) -> str:
return str(self.data)
def __add__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data + other.data)
return RefVar(self.data + other)
def __radd__(self, other):
return RefVar(other + self.data)
def __sub__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data - other.data)
return RefVar(self.data - other)
def __rsub__(self, other):
return RefVar(other - self.data)
def __mul__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data * other.data)
return RefVar(self.data * other)
def __rmul__(self, other):
return RefVar(other * self.data)
def __truediv__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data / other.data)
return RefVar(self.data / other)
def __rtruediv__(self, other):
return RefVar(other / self.data)
def __floordiv__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data // other.data)
return RefVar(self.data // other)
def __rfloordiv__(self, other):
return RefVar(other // self.data)
def __mod__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data % other.data)
return RefVar(self.data % other)
def __rmod__(self, other):
return RefVar(other % self.data)
def __pow__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data**other.data)
return RefVar(self.data**other)
def __rpow__(self, other):
return RefVar(other**self.data)
def __neg__(self):
return RefVar(-self.data)
def __pos__(self):
return RefVar(+self.data)
def __abs__(self):
return RefVar(abs(self.data))
def __eq__(self, other):
if isinstance(other, RefVar):
return self.data == other.data
return self.data == other
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
if isinstance(other, RefVar):
return self.data < other.data
return self.data < other
def __le__(self, other):
if isinstance(other, RefVar):
return self.data <= other.data
return self.data <= other
def __gt__(self, other):
if isinstance(other, RefVar):
return self.data > other.data
return self.data > other
def __ge__(self, other):
if isinstance(other, RefVar):
return self.data >= other.data
return self.data >= other
# 位运算
def __and__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data & other.data)
return RefVar(self.data & other)
def __rand__(self, other):
return RefVar(other & self.data)
def __or__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data | other.data)
return RefVar(self.data | other)
def __ror__(self, other):
return RefVar(other | self.data)
def __xor__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data ^ other.data)
return RefVar(self.data ^ other)
def __rxor__(self, other):
return RefVar(other ^ self.data)
def __lshift__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data << other.data)
return RefVar(self.data << other)
def __rlshift__(self, other):
return RefVar(other << self.data)
def __rshift__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data >> other.data)
return RefVar(self.data >> other)
def __rrshift__(self, other):
return RefVar(other >> self.data)
def __invert__(self):
return RefVar(~self.data)
# 类型转换
def __int__(self):
return int(self.data)
def __float__(self):
return float(self.data)
def __bool__(self):
return bool(self.data)
def __complex__(self):
return complex(self.data)
def __bytes__(self):
return bytes(self.data)
def __hash__(self):
return hash(self.data)
# 容器操作(如果底层数据支持)
def __len__(self):
return len(self.data)
def __getitem__(self, key):
return self.data[key]
def __setitem__(self, key, value):
self.data[key] = value
def __delitem__(self, key):
del self.data[key]
def __contains__(self, item):
return item in self.data
def __iter__(self):
return iter(self.data)
def __iadd__(self, other):
if isinstance(other, RefVar):
self.data += other.data
else:
self.data += other
return self
def __isub__(self, other):
if isinstance(other, RefVar):
self.data -= other.data
else:
self.data -= other
return self
def __imul__(self, other):
if isinstance(other, RefVar):
self.data *= other.data
else:
self.data *= other
return self
def __itruediv__(self, other):
if isinstance(other, RefVar):
self.data /= other.data
else:
self.data /= other
return self
def __ifloordiv__(self, other):
if isinstance(other, RefVar):
self.data //= other.data
else:
self.data //= other
return self
def __imod__(self, other):
if isinstance(other, RefVar):
self.data %= other.data
else:
self.data %= other
return self
def __ipow__(self, other):
if isinstance(other, RefVar):
self.data **= other.data
else:
self.data **= other
return self
def __call__(self, *args, **kwargs):
if callable(self.data):
return self.data(*args, **kwargs)
raise TypeError(f"'{type(self.data).__name__}' object is not callable")
def __getattr__(self, name):
return getattr(self.data, name)

View File

@@ -10,8 +10,12 @@ from unittest.mock import MagicMock, Mock, patch
from heurams.context import ConfigContext from heurams.context import ConfigContext
from heurams.services.config import ConfigFile from heurams.services.config import ConfigFile
from heurams.services.sync_service import (ConflictStrategy, SyncConfig, from heurams.services.sync_service import (
SyncMode, SyncService) ConflictStrategy,
SyncConfig,
SyncMode,
SyncService,
)
class TestSyncServiceUnit(unittest.TestCase): class TestSyncServiceUnit(unittest.TestCase):
@@ -202,8 +206,7 @@ class TestSyncServiceUnit(unittest.TestCase):
mock_config.data = config_data mock_config.data = config_data
mock_config_var.get.return_value = mock_config mock_config_var.get.return_value = mock_config
from heurams.services.sync_service import \ from heurams.services.sync_service import create_sync_service_from_config
create_sync_service_from_config
service = create_sync_service_from_config() service = create_sync_service_from_config()

View File

@@ -174,7 +174,7 @@ class TestSM2Algorithm(unittest.TestCase):
def test_rate(self): def test_rate(self):
"""测试 rate 方法""" """测试 rate 方法"""
algodata = {SM2Algorithm.algo_name: {"efactor": 2.7}} algodata = {SM2Algorithm.algo_name: {"efactor": 2.7}}
self.assertEqual(SM2Algorithm.rate(algodata), "2.7") self.assertEqual(SM2Algorithm.get_rating(algodata), "2.7")
def test_nextdate(self): def test_nextdate(self):
"""测试 nextdate 方法""" """测试 nextdate 方法"""

View File

@@ -98,7 +98,7 @@ class TestElectron(unittest.TestCase):
electron = Electron("test_electron") electron = Electron("test_electron")
with patch.object(electron.algo, "rate") as mock_rate: with patch.object(electron.algo, "rate") as mock_rate:
mock_rate.return_value = "good" mock_rate.return_value = "good"
result = electron.get_rate() result = electron.get_rating()
mock_rate.assert_called_once_with(electron.algodata) mock_rate.assert_called_once_with(electron.algodata)
self.assertEqual(result, "good") self.assertEqual(result, "good")

View File

@@ -1,7 +1,7 @@
import unittest import unittest
from unittest.mock import Mock from unittest.mock import Mock
from heurams.kernel.puzzles.base import BasePuzzle from heurams.kernel.evaluators.base import BasePuzzle
class TestBasePuzzle(unittest.TestCase): class TestBasePuzzle(unittest.TestCase):

View File

@@ -1,7 +1,7 @@
import unittest import unittest
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from heurams.kernel.puzzles.cloze import ClozePuzzle from heurams.kernel.evaluators.cloze import ClozePuzzle
class TestClozePuzzle(unittest.TestCase): class TestClozePuzzle(unittest.TestCase):

View File

@@ -1,7 +1,7 @@
import unittest import unittest
from unittest.mock import MagicMock, call, patch from unittest.mock import MagicMock, call, patch
from heurams.kernel.puzzles.mcq import MCQPuzzle from heurams.kernel.evaluators.mcq import MCQPuzzle
class TestMCQPuzzle(unittest.TestCase): class TestMCQPuzzle(unittest.TestCase):

View File

@@ -3,7 +3,7 @@ from unittest.mock import MagicMock, Mock, patch
from heurams.kernel.particles.atom import Atom from heurams.kernel.particles.atom import Atom
from heurams.kernel.particles.electron import Electron from heurams.kernel.particles.electron import Electron
from heurams.kernel.reactor.phaser import Phaser from heurams.kernel.reactor.procession import Phaser
from heurams.kernel.reactor.states import PhaserState, ProcessionState from heurams.kernel.reactor.states import PhaserState, ProcessionState