☰
Penrose Edgeworth 合成器 UI 解析:Substance 程序变异合成引擎与版本演进全指南
2026/9/27 21:48:15 网站建设 项目流程
  • 开发工具
  • 数据可视化

【免费下载链接】penrose

Create beautiful diagrams just by typing notation in plain text.

项目地址:https://gitcode.com/gh_mirrors/pe/penrose
点击查看免费下载

本文以仓库中 packages/edgeworth/CHANGELOG.md 为骨架,结合 packages/edgeworth/README.md 与@penrose/edgeworth包的源码实现(合成器、变异算子、搜索算法与 UI 组件),系统讲解 Penrose 项目中的“问题作者(problem author)工具”——Edgeworth 的定位、核心工作流、底层变异合成原理,以及从 v2.0.0 到 v3.3.0 的功能演进脉络。读完本文,你将掌握 Edgeworth 的配置结构(SynthesizerSetting)、三类变异操作与八种更新子类型的实现细节、基于观测等价(observational equivalence)的去重与路径搜索策略,并能在仓库源码中快速定位对应实现。

一、Edgeworth 是什么:面向问题作者的合成器 UI

Edgeworth(包名@penrose/edgeworth,早期版本名为@penrose/synthesizer-ui,见 CHANGELOG v2.1.1 中的“Version bump only for package @penrose/synthesizer-ui”)是 Penrose 仓库中的一个 Web 应用包。它本身不负责绘制图形,而是为 Penrose 的“问题作者”提供一个交互界面,用于:

  • 基于一个文件三元组(domain / substance / style)运行合成器,批量生成多个经过变异的 Substance 程序;
  • 允许作者编辑默认的 substance、domain 和 style 程序;
  • 提供可调设置:生成多少个程序、每个程序做多少次变异、可以对哪些类型的语句做变异;
  • 每个变异程序会展示其CIEE(编译并优化后的能量信息)、变异操作记录以及变异后的 substance 程序;
  • 通过勾选将喜欢的图 staged(暂存),点击Export将选中的图以 SVG 形式打包下载。

从 README.md 看,官方将其定位为“first pass”——首版覆盖了核心功能,并预留了扩展空间。该包当前依赖@penrose/core(编译器与优化器)、@penrose/components(Simple渲染组件、Listing编辑器等)、@penrose/examples(几何、分子、图论等领域示例),详见 packages/edgeworth/package.json。

运行与构建

在packages/edgeworth目录下可执行(脚本定义见 package.json):

命令作用
yarn start(或yarn dev/yarn watch)以 Vite 启动开发服务器,README 说明浏览器访问http://localhost:4000(实际端口以当前 Vite 配置与终端输出为准)
yarn build使用cross-env NODE_OPTIONS='--max-old-space-size=8192' vite build构建生产版本到dist目录
yarn preview本地预览生产构建产物
yarn typecheck运行tsc类型检查
yarn test运行vitest run --no-threads执行单元测试
yarn coverage运行测试并生成覆盖率报告

构建/测试目标通过 Nx 配置了依赖顺序(dependsOn: ["^build", "^build-decls"]),即先构建依赖包再构建本包。

应用入口 src/App.tsx 使用react-router-dom的createHashRouter定义了/(主界面)、/chemistry、/geometry、/graphs、/problems五个路由,对应 src/problems 下的领域问题集合。

二、核心工作流:从三文件到批量变异程序

Edgeworth 的完整工作流可以概括为:

  1. 选择领域与预设:在左侧设置抽屉中选择 Domain(Molecules / Geometry / Directed Graphs / Undirected Graphs),见 src/examples.ts 中的domains与presets。
  2. 编辑输入场景:通过Listing(substance 语言编辑器)修改模板 substance 程序。
  3. 生成变异:点击 “Generate Variations” 或 “More Variations”,前端调用generateProgs。
  4. 浏览与筛选:右侧 Grid 逐格渲染变异后的图,展示每个程序对应的 substance 与变异操作记录。
  5. 标注与导出:勾选 checkbox staged 候选图,用开关标记正确/错误,最后 Export 打包为 zip。

生成流程的源码实现

生成入口在 src/components/Content.tsx 的generateProgs:

  1. 用compileDomain(dsl)编译 domain 得到domEnv;
  2. 若提供了模板 substance,用compileSubstance(sub, domEnv)编译得到subEnv;
  3. 构造new Synthesizer(domEnv, subEnv, setting, subEnv === undefined ? undefined : [subEnv, domEnv], seed);
  4. 调用synth.generateSubstances(numPrograms)生成多个程序;
  5. 将模板程序作为第 0 号(header 显示 “Original diagram”),变异结果依次为 “Mutated diagram #1/#2/…”(见 src/components/Content.tsx 对Grid的header回调)。

Settings 侧栏(src/components/Settings.tsx)默认numPrograms: 9、seed: "test0"。“More Variations” 按钮通过generateVariation()生成随机种子(格式为颜色名 + 动物名 + 3~5 位数字,例如BlueFox123),然后以新种子重新生成,从而在同一领域配置下获得不同的随机变异结果——这是seedrandom种子化随机数在 UI 层的直接体现。

三、变异合成引擎:Synthesizer 的配置与执行原理

合成器核心在 src/synthesis/Synthesizer.ts。它接收一个已编译的 Domain 环境、一个可选的模板 Substance 环境、一个SynthesizerSetting配置和一个随机种子,通过seedrandom(seed)派生确定性随机数生成器(choice、weightedChoice、random),因此同一 seed 下生成的变异序列可复现。

3.1 配置结构 SynthesizerSetting

SynthesizerSetting定义于 Synthesizer.ts#L97-L115,是理解整个引擎的钥匙:

export interface SynthesizerSetting { mutationCount: [number, number]; // 每个程序变异次数的区间 [min, max](含端点) argOption: ArgOption; // "existing" | "generated" | "mixed" argReuse: ArgReuse; // "distinct" | "repeated" weights: { type: number; predicate: number; constructor: number }; opWeights: { [t in MutationType]: number }; // add / delete / edit add: DeclTypes; // 允许 add 的语句类型匹配("*" 或名称数组) delete: DeclTypes; edit: DeclTypes; }

各字段含义:

  • mutationCount:generateSubstance中通过this.random(...this.setting.mutationCount)取整随机数,决定当前程序执行的变异步数(Synthesizer.ts#L485)。
  • argOption:生成新语句参数时的取值策略。generateArg中(Synthesizer.ts#L1039-L1111):
    • "existing":从ctx.declaredIDs中按类型(含子类型subTypesOf)挑选已存在的标识符;找不到时才降级为"generated";
    • "generated":按类型声明generateDecl生成新标识符(如类型Set生成s0、s1…,前缀取类型首字母小写,见generateID);
    • "mixed":对每个参数随机在"existing"与"generated"之间选择。
  • argReuse:"distinct"时同一组参数中不允许重复使用同一 ID(通过usedIDs排除);"repeated"则允许重复。
  • weights:选择新增/编辑哪种 Domain 声明(type / predicate / constructor)的权重。
  • opWeights:mutateProgram中先用weightedChoice按权重选出add/delete/edit三种变异大类之一(Synthesizer.ts#L533-L541)。
  • add/delete/edit:每种操作可匹配的语句类型白名单,值可为"*"(通配全部)或具体名称数组。filterContext会据此裁剪环境中的类型、函数、谓词、构造器声明(Synthesizer.ts#L165-L233),并可选地按模板程序中实际出现的类型进一步过滤。

src/examples.ts 中给出了三套有代表性的预设参数,可直接对照:

// 分子(Lewis 结构):只做 edit,不增删 const lewisParams: SynthesizerSetting = { mutationCount: [1, 4], argOption: "existing", argReuse: "distinct", weights: { type: 0.15, predicate: 0.5, constructor: 0.35 }, opWeights: { add: 0, delete: 0, edit: 1 }, add: { type: "*", function: "*", constructor: "*", predicate: "*" }, delete: { type: "*", function: "*", constructor: "*", predicate: "*" }, edit: { type: "*", function: "*", constructor: "*", predicate: "*" }, }; // 几何:add=0, delete=0.2, edit=0.8(以编辑为主、偶发删除) const geometryParams = { ...lewisParams, opWeights: { add: 0, delete: 0.2, edit: 0.8 } }; // 图论:add=0.5, delete=0.4, edit=0.1(增删为主) const graphParams = { ...lewisParams, opWeights: { add: 0.5, delete: 0.4, edit: 0.1 } };

3.2 三类变异操作与八种更新子类型

变异操作类型定义在 src/synthesis/Mutation.ts:

  • Mutation = Add | Delete | Update;MutationType = "add" | "delete" | "edit";
  • Update是八种具体更新操作的联合:SwapStmtArgs、SwapExprArgs、SwapInStmtArgs、SwapInExprArgs、ReplaceStmtName、ReplaceExprName、ChangeStmtType、ChangeExprType。

每种变异都实现了统一的MutationBase接口:{ tag, additionalMutations?, mutate(op, prog, ctx) },mutate返回WithContext<SubProg<A>>(新程序 + 新的合成上下文),这是“语句级操作 + 上下文同步”的关键抽象。例如Add在追加Decl语句时会调用addID把新标识符登记进ctx.declaredIDs,Delete在删除Decl时会调用removeID同步移除(Mutation.ts#L238-L262),保证后续变异的候选 ID 集合始终与当前程序一致。

八种更新的语义(对应showMutation的字符串输出,Mutation.ts#L143-L171):

变异语义
SwapStmtArgs交换谓词语句中两个类型匹配的参数位置(如Subset(A, B)→Subset(B, A))
SwapExprArgs交换 Bind 表达式中两个类型匹配的参数
SwapInStmtArgs/SwapInExprArgs将语句/表达式中的某个 ID 换成同类型的其他已声明 ID
ReplaceStmtName/ReplaceExprName将谓词/表达式名替换为签名匹配的其他声明(matchSignatures)
ChangeStmtType/ChangeExprType将语句/表达式整体替换为参数兼容的其他声明,并携带additionalMutations(删除旧语句、新增依赖语句),必要时触发cascadingDelete级联清理引用

cascadingDelete是删除语义的关键:当删除一个被其他语句引用的Bind/Decl时,会递归删除所有引用它的语句(Synthesizer.test.ts 的cascading delete测试验证了这一点:删除Set A会连带删除Set C := Subset(B, A)与Set E := Intersection(D, C),仅剩Set B与Set D)。

3.3 单步变异的选择与执行

Synthesizer.mutateProgram(Synthesizer.ts#L518-L575)是单步变异的主流程:

  1. 分别用filterContext得到 add / delete / edit 各自可用的上下文;
  2. 调用enumerateAdd、enumerateDelete、enumerateUpdate枚举出三类候选变异组(MutationGroup[]);
  3. 用weightedChoice(按opWeights)选出本次变异的大类;
  4. 对选中的变异组执行executeMutations,把结果写入currentProg并追加到currentMutations记录;
  5. 若某类候选为空则递归重试,直到找到可行变异。

enumerateAdd(Synthesizer.ts#L740-L807)从nonEmptyDecls中随机选一种声明类型(Type / Predicate / Function / Constructor),再按类型生成一组Add变异——例如生成一个函数调用时,除了Bind语句本身,还会包含为参数和返回值生成的Decl语句,作为一个不可分割的变异组返回。enumerateDelete(Synthesizer.ts#L813-L841)则对选中声明执行级联删除并收集Delete组。

3.4 结果收尾:去重与语句规整

generateSubstance(Synthesizer.ts#L484-L513)在完成mutationCount步变异后,对最终程序执行:

const prog = sortStmts(dedupStmts(this.currentProg)); // 排序 + 去重,避免编译错误 return { prog, ops: this.currentMutations, src: prettySubstance(prog) };

generateSubstances(Synthesizer.ts#L457-L482)更进一步:通过dedupSynthesizedSubstances保证批量结果互不重复,若去重后不足numProgs个,则继续生成补齐——这正是 CHANGELOG v3.0.0 中 “deduplication of mutated Substance programs inedgeworth(#1481)” 落地为代码的直接证据。

四、变异路径搜索:从 diff 到可执行变异序列

除随机合成外,Edgeworth 还实现了反向搜索能力:给定源程序与目标程序,找出“如何用一组变异把源变成目标”。这部分集中在 src/synthesis/Search.ts,对应 CHANGELOG v2.0.0 的 “enumerative search of Substance mutations (#638)”。

4.1 AST 细粒度 diff

  • diffSubProgs(Search.ts#L93-L106):用recursive-diff计算两棵 Substance AST 的精确差异,并过滤掉所有与元属性(metaProps)相关的噪声 diff;
  • diffSubStmts(Search.ts#L115-L124):先sortStmts归一化语句顺序再求 diff,使结果与语句书写顺序无关;
  • subProgDiffs(Search.ts#L217-L244):基于公共语句集合 +similarNodes相似度映射,将差异分类为DiffSet { add, delete, update },其中UpdateDiff携带source → result语句对。

Search.test.ts 的 “Compute AST diff based on tree sets” 测试展示了典型输出:把D := Union(A, B)改成E := Union(A, B)会得到一条Updatediff(变量名变化),而Equal(E, E)的移除被识别为Delete。

4.2 单路径与全路径搜索

  • findMutationPaths(Search.ts#L442-L489):把 add/delete 打包为固定变异,对每个 update 语句枚举候选变异,仅保留执行后与目标语句完全一致的变异,再用cartesianProduct组合出所有合法变异组;
  • enumerateAllPaths(Search.ts#L491-L526):不做匹配过滤,枚举全部可能的组合;
  • enumerateMutationPaths(Search.ts#L544-L610):按maxDepth做广度优先搜索,逐层把每个候选程序的所有可行变异执行一遍,并用观测等价(observational equivalence)剪枝——_.uniqBy(candidates, (c) => prettySubstance(c.prog))保证“产生相同输出程序的候选路径只保留一条”,从而显著压缩搜索空间。这正是 CHANGELOG 中“enumerative search + observational equivalence”的工程实现。

对应测试见 Search.test.ts:例如 “recognizing swap mutation” 验证Subset(A, B)→Subset(B, A)能被识别为SwapStmtArgs; “recognizing multiple mutations on multiple stmts” 验证Subset(A,B)+C := Intersection(A, B)到Equal(B, A)+Intersecting(A, B)的最短路径恰好包含SwapStmtArgs、ChangeExprType、ReplaceStmtName三个变异。

五、UI 组件与导出管线

5.1 网格展示 Grid

src/components/Grid.tsx 实现自定义网格:每个Gridbox通过@penrose/components的Simple组件渲染一张可交互的 Penrose 图,并提供:

  • 右上角Resample按钮:更换随机variation(布局随机种子)重新优化同一程序;
  • Checkbox:勾选后进入 staged 列表;
  • 正确/错误开关(CustomSwitch):staged 后用绿/红开关标记答案;
  • 点击图可切换查看元信息(Substance 程序文本 + 变异操作记录)。

网格通过onStateUpdate回调收集每张图的PenroseState,当所有格子都完成优化(isOptimized全为 true)时触发onComplete。CHANGELOG v4.0.0-alpha 系列中的 “simplified sharedGridand customGridin Edgeworth (#1729)” 即指对该组件体系的简化(共享网格在@penrose/components,自定义网格在本包)。

5.2 导出管线:一个 zip 打包全部产物

Content.exportDiagrams(Content.tsx#L233-L261)用jszip+file-saver打包 staged 图,生成diagrams.zip,内含:

  • anwser.json:staged 图的索引与正确/错误标记;
  • domain.domain、style.style:当前使用的领域与样式源码;
  • 每个图三个文件:${idx}.svg(由toSVG(state, resolver, "diagram")生成)、${idx}.substance(prettySubstance(prog)格式化)、mutations_${idx}.txt(showMutations(ops)的人类可读变异记录)。

这意味着导出的 zip完整保留了问题作者出题所需的一切证据:图形、程序文本与变异操作轨迹,可直接用于生成多项选择题。

5.3 多选题模式

Content.problem(Content.tsx#L263-L332)把 staged 的“正确/错误”图组合成MultipleChoiceProblem(来自@penrose/components),打乱选项后以全屏遮罩展示。顶部 “Show Problem” 按钮控制显隐,prompt来自各预设(见 examples.ts 中如"In which of the following diagrams are points $B$, $D$, $E$ collinear?"的题目文案)。这一整套“生成→筛选→标注→组卷”流程,正是 Edgeworth 作为问题作者工具的核心价值。

六、版本演进解读:CHANGELOG 对照源码

CHANGELOG.md 记录了从 v2.0.0(2023-01)到 v3.3.0(2025-09)的完整演进。按时间线梳理如下:

v2.x:从浏览器原型到成熟 UI(2023-01 ~ 2023-03)

  • v2.0.0:新增 synthesizer 浏览器(#640)、Substance 变异的枚举式搜索(#638,即 Search.ts 的前身)、synthesizer-ui的预设加载(#1133,对应今天的 examples.tspresets)、在Simple组件中显示错误(#953)、docusaurus 站点(#771)、统一 browser-ui 与 editor(#1000)。
  • v2.2.0:改进 registry schema 与加载(#1212)、支持更长文件扩展名(#1280)。
  • v2.3.0:editor 中网格展示多图实例(#1287,对应 Grid.tsx)、修复 renderer 非确定性(#1316,呼应variation随机种子机制);向 synthesizer-ui 补充 Lewis 结构示例(#1334)、图论示例(#1336)、扩展预设(#1149)。

v3.0.0:工程化整顿(2023-07-14)

这是一次破坏性版本,四个 BREAKING CHANGE 全部是仓库级重构,Edgeworth 随之适配:

  • 清理core导出与 synthesizer 模块(#1367)——从本包源码可见大量@penrose/core/dist/...的深层导入;
  • 合并automator与roger(#1387);
  • 更可读的core语言 API(#1527);
  • 每个 trio 独立 JSON 文件(#1393)——即packages/examples/src下大量*.trio.json的组织方式;
  • 新增Edgeworth 中变异 Substance 程序的去重(#1481),即dedupSynthesizedSubstances;
  • 内部:TypeScript 升至 5.0(#1395)、抽出公共tsconfig.json(#1392)、从 jest 切换到 vitest(#1406)——当前 package.json 中"test": "vitest run --no-threads"即此变更的延续。

v3.1.0 / v3.2.0:能力补全(2023-07 ~ 2023-08)

  • v3.1.0:在 edgeworth 中加入 LLM 程序生成 UI(#1556)。值得说明:该项记录在 CHANGELOG 中,属官方版本事实;当前仓库源码中并未保留独立的 LLM 入口组件(ContentState.prompt目前服务于多选题题干)。
  • v3.2.0:Substance indexed sets(#1572),是 core 语言层新增能力,为后续合成更丰富的程序提供类型系统支持。

4.0.0-alpha 系列与 v3.3.0:2024-2025 的平行重构(2024-05 ~ 2025-09)

CHANGELOG 在 v3.2.0 与 v3.3.0 之间插入了v4.0.0-alpha.0到v4.0.0-alpha.5六个预发布版本(2024-05-07 至 2024-10-28),随后在 2025-09-23 发布v3.3.0,将 alpha 系列成果收敛回主线。核心变更包括:

  • BREAKING CHANGE:分离 Substance environment 并移除未用功能(#1677)。对应源码中SubstanceEnv与DomainEnv的严格分离——initSubstanceEnv()、subEnv.objIds、subEnv.objs、subEnv.ast等字段在 Synthesizer.ts 中全面使用,SynthesisContext同时携带subEnv与domEnv两个环境。
  • 新功能:CodeMirror 迁移(#1798,editor 包从旧版 CodeMirror 迁移)、未知变量的初始值(#1638,core 引擎可为未被约束赋值的变量提供初始值)、Substance 字面量值(#1682,Substance 程序可直接书写字面量)。
  • Bug 修复:edgeworth 的 UI 问题(#1706)。
  • Polish:简化共享Grid与 Edgeworth 自定义Grid(#1729)。
  • 内部:将IsSubset、NotIntersecting改名简化(#1724)、简化 edgeworth 实现(#1708)、多轮版本号 bump。

值得注意的是 v3.3.0 同时把 “4.0.0-alpha.4 (#1865)” 列为新功能,说明 alpha.4 的产物被整体合入主线——这是该仓库“预发布分支 + 主线收敛”版本策略的直接体现。当前仓库packages/edgeworth/package.json版本号正是3.3.0,与 CHANGELOG 顶部一致,二者互相印证。

七、测试体系:如何验证合成器的正确性

Edgeworth 的合成逻辑由两套 vitest 测试守护:

  • src/synthesis/Synthesizer.test.ts:验证cascadingDelete的级联删除行为——删除Set A后,依赖它的Set C := Subset(B, A)与Set E := Intersection(D, C)一并被删,最终只剩Set B、Set D;同时演示了initContext、executeMutations、removeStmtCtx的典型用法。
  • src/synthesis/Search.test.ts:覆盖 AST diff(含语句顺序无关性)、变异识别(swap / replace / change-type)、带噪声的路径搜索(add + delete + update 混合路径)、以及enumerateMutationPaths的观测等价剪枝(两步路径唯一性)。

测试中使用seedrandom固定随机源、prettySubstance(sortStmts(...))作为语义等价性判据,与生产代码的判定方式完全一致——测试即规范。

八、结语与深入方向

通过 CHANGELOG 与源码的对照可以清晰看到:Edgeworth 不是简单的“图生成器”,而是一套以随机化、可复现的 Substance 程序变异为核心、以观测等价剪枝为优化手段、以导出/组卷为出口的问题作者工具链。它的工程要点可归纳为:

  1. 可复现性:一切随机决策由seedrandom(seed)驱动;
  2. 上下文一致性:变异执行与SynthesisContext(已声明 ID、生成名、环境)同步更新;
  3. 结果质量:sortStmts+dedupStmts+dedupSynthesizedSubstances三重规整,保证产物可编译、不重复;
  4. 可解释性:每次变异都留有人类可读的showMutations记录并随图导出。

如需继续深入,建议按以下路径阅读仓库源码:

  • 合成主流程:packages/edgeworth/src/synthesis/Synthesizer.ts
  • 变异算子全集:packages/edgeworth/src/synthesis/Mutation.ts
  • 路径搜索与 diff:packages/edgeworth/src/synthesis/Search.ts
  • UI 工作流:packages/edgeworth/src/components/Content.tsx、packages/edgeworth/src/components/Settings.tsx、packages/edgeworth/src/components/Grid.tsx
  • 领域预设:packages/edgeworth/src/examples.ts
  • 底层编译器/优化器:packages/core/src(compileDomain、compileSubstance、toSVG等 API 均来自@penrose/core)
  • 开发工具
  • 数据可视化

【免费下载链接】penrose

Create beautiful diagrams just by typing notation in plain text.

项目地址:https://gitcode.com/gh_mirrors/pe/penrose
点击查看免费下载

相关推荐

上一篇:Minecraft 1.21终极指南:如何轻松安装MASA模组全家桶中文汉化包
下一篇:终极开源字体解决方案:Barlow 54款现代无衬线字体的深度应用指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询