Appearance
结果契约:solve 与 simulate 返回什么
一次性调用的公开面只有两个 JSON 信封:esml.solve/v1 与 esml.simulate/v1。 本文说明每个字段的含义、单位约定、成功与失败语义。客户端(Python / Julia) 只是这两个信封的薄封装;直接调命令行与走客户端拿到的是同一份契约。
机器可校验的 JSON Schema: esml-result-v1.json。
契约遵循只增不改:字段只会增加,不会改名或删除;破坏性变更才升 apiVersion。同一份源文件 + 同一 engineVersion + 同一输入,结果可复现。
稳态:esml solve --json
bash
esml solve library/domains/thermal/tclab.comp.esml -L library --jsonjson
{
"apiVersion": "esml.solve/v1",
"system": "Demo",
"unknowns": 2,
"values": [
{ "path": "tclab.T1", "value": 339.0071428571428, "unit": "K" },
{ "path": "tclab.T2", "value": 303.29285714285714, "unit": "K" }
],
"meta": {
"engineVersion": "1.9.0",
"docHash": "612e8b852339681c6a6b62266240d1f13367dc44945c3c8a32058cfc1bdf13f3",
"durationMs": 0.423,
"converged": true,
"iterations": 2,
"residual": 1.734723475976807e-16,
"residualTolerance": 3.400071428571428e-08,
"unitCoverage": { "known": 2, "total": 2, "complete": true }
}
}| 字段 | 类型 | 含义 |
|---|---|---|
apiVersion | string | 固定 esml.solve/v1;客户端据此拒绝未知信封 |
system | string | 求解的 system 名 |
unknowns | int | 展平后原始变量数(含状态与代数变量) |
values[] | array | 按展平顺序稳定输出,每项一个变量 |
values[].path | string | 实例路径,例如 tclab.T1、b1.net.Vm |
values[].value | float | 数值;声明了单位时已换算到一致 SI 单位,未声明时保持模型原值 |
values[].unit | string | null | SI 量纲表达式(如 K、kg/m^3、kg*m/s^2);无量纲为 "1";模型未声明单位为 null |
meta.engineVersion | string | 产生该结果的引擎版本 |
meta.docHash | string | 源文件逐字节的 SHA-256(64 位十六进制),可作缓存/归档键 |
meta.durationMs | float | 引擎侧计算耗时(毫秒,含重构结果;不含进程启动与读文件) |
meta.converged | bool | 成功信封中恒为 true;未收敛不会返回信封,而是非零退出 |
meta.iterations | int | null | 产出该结果的 Newton 迭代次数(homotopy 各步累加;失败的鲁棒性尝试不计);--component 路径为 null |
meta.residual | float | null | Newton 接受解时的残差 ∞-范数 |
meta.residualTolerance | float | null | 该残差通过的门限(TOL·(1+‖u‖∞));residual ≤ residualTolerance;DDM / component 路径不适用时为 null |
meta.unitCoverage | object | known 个未知量有单位、total 个未知量;complete 为二者相等(simulate 按输出列计) |
单位语义
- 声明了单位的变量,返回值换算到一致 SI 单位:
K、Pa、kg/s、W…… 不是作者写的显示单位(°C、bar、kW)。需要显示单位时在客户端换算。 - 未声明单位的变量保持模型原值,
unit为null;它可能不是 SI(例如 标幺值)。不要把它当作 SI 数值直接做物理量纲运算。 unit是量纲表达式,不是单位名字。声明为无量纲的量(包括标幺值、 摩尔分数)显示为"1";未声明的仍为null。unit: null表示模型没有声明单位,引擎不猜。此时数值仍是模型原值。 给变量或类型补上unit=后,同一份结果就会带单位。- IEEE 300 潮流算例的
PowerPort已声明Vm/P/Q为标幺(unit="1")、Va为弧度(量纲同为"1"),因此 4,604 个值全部带单位、unitCoverage.complete为true;数值本身不变。 unitCoverage让调用方一眼判断结果能否直接进入带单位的后处理;不要用unit: null的数值去猜物理量。
时域:esml simulate --json
bash
esml simulate library/domains/thermal/tclab.comp.esml \
--tspan 0,10 --steps 2 -L library --jsonjson
{
"apiVersion": "esml.simulate/v1",
"system": "Demo",
"steps": 2,
"points": 3,
"columns": ["tclab.T1", "tclab.T2"],
"units": { "tclab.T1": "K", "tclab.T2": "K" },
"t": [0.0, 5.0, 10.0],
"rows": [
[296.15, 296.15],
[297.3636208318605, 296.15589136326145],
[298.55347675938515, 296.16555562766]
],
"meta": {
"engineVersion": "1.9.0",
"docHash": "612e8b852339681c6a6b62266240d1f13367dc44945c3c8a32058cfc1bdf13f3",
"durationMs": 0.59,
"unitCoverage": { "known": 2, "total": 2, "complete": true }
}
}| 字段 | 类型 | 含义 |
|---|---|---|
apiVersion | string | 固定 esml.simulate/v1 |
steps | int | 积分步数(区间数);points = steps + 1 |
points | int | 输出点数,等于 t 与 rows 的长度 |
columns | string[] | 每列对应的变量路径,顺序与 rows[] 一致;默认是状态变量,代数变量用 --observe 指定 |
units | object | 列名 → SI 量纲表达式;只列出有单位的列 |
t | float[] | 时间网格,单位秒 |
rows[][] | float | 与 columns 等长的数值行 |
meta | object | 版本、docHash、耗时、单位覆盖率;时间积分要么完成要么报错,因此没有 converged |
--observe 指定的变量也会进入 columns,并在 units 中给出单位;观测常量 参数时同样返回该参数声明的单位。找不到的路径列输出 null,并在 stderr 给出 warn:。
失败语义
契约只有成功信封。以下情况一律非零退出,stderr 给可操作的错误信息, stdout 不输出半个信封:
- 解析 / 结构 / 单位检查失败;
- 求解未收敛、奇异 Jacobian、非有限残差;
- 学术版超出未知量上限,或调用商业版能力(DTIL
session); - 文件不存在、库找不到、物性后端缺失。
因此客户端不需要判断“结果是否可信”:拿到信封就是成功结果,拿不到就是 带原因的失败。Python 客户端抛 EsmlError(超时抛 EsmlTimeout);Julia 客户端抛 EsmlError。
失败也是契约:esml.error/v1
带 --json 的命令失败时,stdout 仍为空;stderr 先给人类可读的 error: 行, 最后一行是机器可读信封:
json
{
"apiVersion": "esml.error/v1",
"code": "io.read",
"message": "read …: No such file or directory",
"hint": "Check the path … and its permissions.",
"docHash": null,
"engineVersion": "1.9.0"
}code | 含义 | 典型处理 |
|---|---|---|
license.size | 学术版超过 16,384 展平未知量 | 缩小模型或联系商业授权 |
license.dtil | 学术版调用了商业版 DTIL 会话 | 改用一次性调用或商业版 |
io.read | 文件不存在/不可读/权限不足 | 检查路径与权限 |
solve.failed | 求解未收敛、奇异、非有限残差 | 读 message 的阶段与原因 |
model.parse | 解析/语法错误 | 跑 esml check |
model.structure | 结构分析失败 | 读 struct[...] 诊断 |
usage | 子命令/参数用法错误 | 修命令行 |
engine.error | 其他引擎错误(兜底) | 读 message/hint |
message 是权威细节,code 只用于分类;不要解析 message 做分支。 未知 code 按 engine.error 处理。docHash 在源文件已读入后可用,否则为 null。Python 客户端抛 EsmlError 并带 .code / .hint / .doc_hash; Julia 客户端同名字段。会话协议的错误信封(esml.session/v1、op:"error") 同样带 code / hint;会话启动阶段的拒绝(学术版 DTIL、文件不可读)也先输出 一行这样的信封,再非零退出。
常驻会话(商业版)的对齐
esml.session/v1 的 solve 响应与一次性契约对齐:
- 每个值带
unit,并新增units(路径 → SI 量纲); meta含engineVersion、docHash(源文件 SHA-256)、durationMs、converged、iterations、residual、residualTolerance、unitCoverage;simulate/predict的每个 series 带unit,并新增units与meta(无converged);open/info带sourceHash与engineVersion。
顶层数值 docHash 仍是会话快照/恢复的协议令牌;meta.docHash 是源文件的 SHA-256。Python Session.solve() 返回 SolveResult.units / .meta, simulate / predict 返回 Trajectory.units / .meta;Julia 同名。
版本与可复现性
engineVersion是产生结果的引擎版本;升级引擎后请重新基线化数值。docHash是源文件的 SHA-256。它不包含--system、库文件、参数覆盖或引擎 版本:完整复现键请组合(docHash, engineVersion, system, 输入覆盖)。- 同一构建、同一输入的结果是确定的;跨平台可能有末位浮点差异。
客户端字段
Python 的 SolveOutput / SimulateOutput 与 Julia 的 SolveOutput / SimulateOutput 已暴露这些字段:
| 客户端字段 | 对应信封 |
|---|---|
values | values[].value(按路径索引) |
units | values[].unit(solve)或 units(simulate) |
meta | meta |
raw | 完整信封,保留未来新增字段 |
示例:
python
from esml import solve
out = solve("tclab.comp.esml", libraries=["library"])
print(out.values["tclab.T1"], out.units["tclab.T1"]) # 339.007... K
print(out.meta["converged"], out.meta["unitCoverage"])julia
using ESML
out = solve("tclab.comp.esml"; libraries = ["library"])
println(out.values["tclab.T1"], " ", out.units["tclab.T1"])
println(out.meta["converged"], " ", out.meta["unitCoverage"])已知边界
- 常驻会话(DTIL:
session/predict/snapshot)是商业版能力,协议是 行式esml.session/v1;结果字段已与一次性契约对齐,见上一节。 solve --ddm(域分解)仍在esml.solve/v1下,额外带ddm块;values[]的unit与meta已补齐,residualTolerance不适用于 DDM (其收敛判据由分区协调器给出)。durationMs是引擎侧耗时,不含 Python/Julia 进程启动与 JSON 解析;端到端 性能请以客户端实测为准。iterations只统计产出结果的收敛 Newton(homotopy 各步累加);失败后 换一条鲁棒性路径的尝试不计入,因此它不是“总共试了多少次”的计数器。converged在成功信封中恒为true;若未来支持“返回未收敛迭代点”,那会 改变成功信封的含义,将作为新语义引入并升apiVersion。