dsh-math-calculator 2.3.2 → 2.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +121 -0
- package/dsh.plugin.json +1 -1
- package/lib/help.js +121 -33
- package/lib/index.js +190 -35
- package/lib/ode.js +34 -0
- package/lib/ops/ode-op.js +42 -1
- package/lib/ops/recurrence-op.js +222 -0
- package/lib/ops/shared.js +19 -0
- package/lib/types/ode.d.ts +17 -0
- package/lib/types/ops/recurrence-op.d.ts +44 -0
- package/lib/types/ops/shared.d.ts +22 -1
- package/lib/units-data.js +29 -6
- package/package.json +1 -1
- package/src/help.ts +122 -33
- package/src/index.ts +190 -35
- package/src/ode.ts +46 -0
- package/src/ops/ode-op.ts +43 -1
- package/src/ops/recurrence-op.ts +215 -0
- package/src/ops/shared.ts +33 -0
- package/src/units-data.ts +29 -6
package/src/help.ts
CHANGED
|
@@ -269,30 +269,67 @@ const STATS_BODY = (): string => `【统计与概率】
|
|
|
269
269
|
A/B 结果差异是不是巧合 → t 分布`
|
|
270
270
|
|
|
271
271
|
/** 【微分方程】ODE 数值解。 */
|
|
272
|
-
const ODE_BODY = (): string =>
|
|
272
|
+
const ODE_BODY = (): string => `【动力学】连续用 ode,离散用 recurrence
|
|
273
273
|
|
|
274
|
+
★★ 先判断:你的模型是「连续变化」还是「按期结算」?
|
|
275
|
+
连续(每个人随时都在增长)→ ode
|
|
276
|
+
离散(年度结算、批次解锁、世代更替)→ recurrence
|
|
277
|
+
两者答案不同,写年表必须用离散值(见下面第 3 段)。
|
|
278
|
+
|
|
279
|
+
══ 1. ode:连续时间积分(RK4)══
|
|
274
280
|
一阶方程组(SIR / 捕食模型)与二阶方程(自动降阶)都能解。
|
|
275
281
|
|
|
276
282
|
参数:
|
|
277
|
-
expression
|
|
278
|
-
例:"S'=-k1*S*I; I'=k1*S*I-k2*I; R'=k2*I" ← SIR
|
|
279
|
-
"x'=y; y'=-k*x" ←
|
|
280
|
-
initial
|
|
283
|
+
expression 状态方程,分号分隔。等号左边是导数变量
|
|
284
|
+
例:"S'=-k1*S*I; I'=k1*S*I-k2*I; R'=k2*I" ← SIR
|
|
285
|
+
"x'=y; y'=-k*x" ← 简谐
|
|
286
|
+
initial 初值 {S:999, I:1, R:0}
|
|
281
287
|
from/to 积分区间(默认 0 → 10)
|
|
282
|
-
steps RK4 步数(默认 1000
|
|
283
|
-
samples
|
|
288
|
+
steps RK4 步数(默认 1000,刚性方程加大)
|
|
289
|
+
samples 返回采样点数(默认 24)
|
|
290
|
+
at ★ 要精确输出的时点,如 [5,10,20]
|
|
291
|
+
|
|
292
|
+
返回:轨迹、终点值、极值、指定时点的值、**发散警告**
|
|
293
|
+
|
|
294
|
+
★ at 是什么用的:默认采样是均匀的(t = 1, 2.9, 4.8, …),
|
|
295
|
+
写年表时「第 5 年、第 10 年」一个都对不上。传 at:[5,10,20] 就精确给那几个。
|
|
296
|
+
|
|
297
|
+
★ 二阶方程降阶:y'' 写成 y'=y_d; y_d'=…(y_d = dy/dt)。
|
|
298
|
+
缺辅助变量会提示补哪个,不会静默给错。
|
|
299
|
+
|
|
300
|
+
★ 数值解不是解析解:steps 太少会失真,刚性方程易发散 ——
|
|
301
|
+
看到发散警告就加大 steps,或换解析思路。
|
|
302
|
+
|
|
303
|
+
══ 2. recurrence:离散递推(出逐年表)══
|
|
304
|
+
{operation:"recurrence", recurrence:"P*2", initial:{P:1}, periods:20}
|
|
305
|
+
→ 逐期表 + 终值 + 与连续模型的差额
|
|
306
|
+
|
|
307
|
+
参数:
|
|
308
|
+
recurrence 下一期 = f(本期)。单状态最常见:
|
|
309
|
+
"P*2" ← 每期翻倍
|
|
310
|
+
"P*min(1.2, 1000/P)" ← 带资源上限的年度增长
|
|
311
|
+
initial 初值,可多个状态(逗号/分号分隔的表达式各自求一次)
|
|
312
|
+
periods 迭代期数(默认 12,上限 5000)
|
|
284
313
|
|
|
285
|
-
|
|
314
|
+
★ 支持 min()/max() 约束 —— 「增速 20% 但不超过 1000」这种写法直接可用,
|
|
315
|
+
结果会在第 38 期左右平滑收敛到 1000,不会越过。
|
|
286
316
|
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
══ 3. 为什么非要区分(实测数字)══
|
|
318
|
+
同一件事「从 1 起每年 ×2」:
|
|
319
|
+
离散(recurrence) 2^19 = 524,288
|
|
320
|
+
连续(ode, 0.693P) e^(0.693×19) = 522,824
|
|
321
|
+
差 0.28%
|
|
289
322
|
|
|
290
|
-
|
|
291
|
-
|
|
323
|
+
0.28% 看着可忽略,但第 33 年换成本「亿人」时差 0.5 亿 ——
|
|
324
|
+
**而写作时写的是「第 33 年,四十二亿人」这种具体整数。**
|
|
325
|
+
连续模型的 4.24e9 会让你在「42 亿还是 43 亿」上反复纠结。
|
|
292
326
|
|
|
293
|
-
★
|
|
294
|
-
|
|
295
|
-
|
|
327
|
+
★ 所以 recurrence 会在结果末尾直接给出「连续近似 vs 离散」的差额,
|
|
328
|
+
让你一眼看出「用 ode 顶替会差多少」。
|
|
329
|
+
|
|
330
|
+
★ 写故事时的常见用法:
|
|
331
|
+
传染病扩散(SIR)、捕食者-猎物振荡(Lotka-Volterra)、
|
|
332
|
+
碳循环、人口动态、冷却曲线、**逐年人口/资源年表**`
|
|
296
333
|
|
|
297
334
|
/** 【公式库】442 条公式的调用与检索。 */
|
|
298
335
|
const FORMULA_BODY = (): string => `【公式库】共 ${FORMULAS.length} 条自然科学公式
|
|
@@ -832,7 +869,7 @@ export const HELP_TOPICS: HelpTopic[] = [
|
|
|
832
869
|
tier: 'core',
|
|
833
870
|
/** 实践文档信号量 250,断层第一:算术/带单位/批量/label 全落在这一节。 */
|
|
834
871
|
weight: 250,
|
|
835
|
-
title: '
|
|
872
|
+
title: '求值 evaluate',
|
|
836
873
|
summary: '算一条式子。物理量带单位写,量纲错当场拦下;批量一次多条。',
|
|
837
874
|
aliases: ['算式', '计算', '求值', 'mathjs', 'expression'],
|
|
838
875
|
body: EVALUATE_BODY,
|
|
@@ -846,7 +883,7 @@ export const HELP_TOPICS: HelpTopic[] = [
|
|
|
846
883
|
* 前者要写方程、要看解的形式(唯一解/无穷多解/无解)。
|
|
847
884
|
*/
|
|
848
885
|
weight: 59,
|
|
849
|
-
title: '解方程与微积分(solve
|
|
886
|
+
title: '解方程与微积分(solve)',
|
|
850
887
|
summary: '解方程与方程组、符号求导积分。退化方程组会说「无穷多解」而非静默给单解。',
|
|
851
888
|
aliases: ['方程', '求解', 'solve', '积分', '求导', '微积分', 'algebra'],
|
|
852
889
|
body: ALGEBRA_BODY,
|
|
@@ -856,7 +893,7 @@ export const HELP_TOPICS: HelpTopic[] = [
|
|
|
856
893
|
tier: 'core',
|
|
857
894
|
/** 信号量 173:中制单位(里/丈/斤/亩)与 to 换算在各文档频繁出现。 */
|
|
858
895
|
weight: 173,
|
|
859
|
-
title: '
|
|
896
|
+
title: '单位换算 units',
|
|
860
897
|
summary: '中外古今单位换算、自定义单位,以及输出为什么是大数配吨/公里。',
|
|
861
898
|
aliases: ['单位', '换算', '里', '丈', '斤', '亩', 'unit', 'conversion'],
|
|
862
899
|
body: UNITS_BODY,
|
|
@@ -877,7 +914,7 @@ export const HELP_TOPICS: HelpTopic[] = [
|
|
|
877
914
|
/** 信号量 26。 */
|
|
878
915
|
weight: 26,
|
|
879
916
|
title: '世界观推演(world / 地形 / 气候)',
|
|
880
|
-
summary: '
|
|
917
|
+
summary: '星球参数 → 体感画像:重力昼夜气候、天空、恒星寿命、卫星、生命闸门。★ 字段表在这。',
|
|
881
918
|
aliases: ['星球', '世界', '重力', '气候', '恒星', '地形', 'world', 'planet'],
|
|
882
919
|
body: WORLD_BODY,
|
|
883
920
|
},
|
|
@@ -894,7 +931,7 @@ export const HELP_TOPICS: HelpTopic[] = [
|
|
|
894
931
|
/** 新增节,文档中尚无信号;按用户明确要求保留 core。 */
|
|
895
932
|
weight: 0,
|
|
896
933
|
title: '工程 / 生物 / 材料 / 财政 / 宏观(57 条领域公式)',
|
|
897
|
-
summary: '
|
|
934
|
+
summary: '梁柱应力、热机煤耗、泵功率;扩散供氧、异速生长;胡克与断裂韧性、熔点与结合能;财政赤字与债务螺旋。★ 每条都写了 scaling 律。',
|
|
898
935
|
aliases: ['工程', '生物', '材料', '经济', '宏观', '财政', '税收', '债务', '建筑', '结构', '热机', '流体', '生态', '畜牧', '通胀', '作物'],
|
|
899
936
|
body: DOMAINS_BODY,
|
|
900
937
|
},
|
|
@@ -922,13 +959,30 @@ export const HELP_TOPICS: HelpTopic[] = [
|
|
|
922
959
|
},
|
|
923
960
|
{
|
|
924
961
|
key: 'ode',
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
962
|
+
/**
|
|
963
|
+
* ★ 从 extra 提到 core(2026-11-08)。
|
|
964
|
+
*
|
|
965
|
+
* 实测反馈里最重的一条:该会话「写了 300 行 Python 做逐年迭代、参数扫描、
|
|
966
|
+
* 双状态耦合」,而 ode 就在那里 —— 只是排在「低频」栏的最后一行。
|
|
967
|
+
*
|
|
968
|
+
* 所以「低频」这个分档本身是错的:它表达的是「用得少」,
|
|
969
|
+
* 但用得少不等于「该在想建模时绕过它」。
|
|
970
|
+
*/
|
|
971
|
+
tier: 'core',
|
|
972
|
+
/**
|
|
973
|
+
* 权重从 4 提到 60。
|
|
974
|
+
*
|
|
975
|
+
* 原来的 4 是「实践文档里出现 4 次」的信号量 —— 但这只反映**已经发生的使用**,
|
|
976
|
+
* 不反映**该用而没用的机会数**。实测那份会话里手写了 6 个脚本做逐年迭代,
|
|
977
|
+
* 那些都该是一次 ode/recurrence 调用。所以按「错过的机会」重估。
|
|
978
|
+
*/
|
|
979
|
+
weight: 60,
|
|
980
|
+
short: '微分方程与递推',
|
|
981
|
+
title: '动力学:微分方程与离散递推(ode / recurrence)',
|
|
982
|
+
summary:
|
|
983
|
+
'连续用 ode(RK4,支持 min() 约束/多状态);离散用 recurrence 出逐年表。' +
|
|
984
|
+
'★ 写年表用离散值;ode 传 at:[5,10,20] 精确取那几个年份。',
|
|
985
|
+
aliases: ['微分方程', 'ode', '递推', 'recurrence', '逐年', '年表', '增长模型', '传染病', '捕食模型', 'rk4'],
|
|
932
986
|
body: ODE_BODY,
|
|
933
987
|
},
|
|
934
988
|
{
|
|
@@ -955,12 +1009,13 @@ export const HELP_TOPICS: HelpTopic[] = [
|
|
|
955
1009
|
},
|
|
956
1010
|
{
|
|
957
1011
|
key: 'derivation',
|
|
958
|
-
|
|
1012
|
+
/** 同上,从 extra 提到 core:参数扫描与结论翻转是建模的一部分。 */
|
|
1013
|
+
tier: 'core',
|
|
959
1014
|
/** 信号量 21。 */
|
|
960
1015
|
weight: 21,
|
|
961
1016
|
short: '推演栈 29 子命令',
|
|
962
1017
|
title: '推演栈(derivation 子命令)',
|
|
963
|
-
summary: '
|
|
1018
|
+
summary: '敏感度/结论翻转/逆推校准/相图/算力预算等 29 子命令。★ 全部子命令在这。',
|
|
964
1019
|
aliases: ['推演', 'derivation', '混沌', '白皮书', '相图', '敏感度', '算力'],
|
|
965
1020
|
body: DERIVATION_BODY,
|
|
966
1021
|
},
|
|
@@ -977,12 +1032,13 @@ export const HELP_TOPICS: HelpTopic[] = [
|
|
|
977
1032
|
},
|
|
978
1033
|
{
|
|
979
1034
|
key: 'recipe',
|
|
980
|
-
|
|
1035
|
+
/** 同上:配方是「怎么把上面几个串起来」,属于建模段。 */
|
|
1036
|
+
tier: 'core',
|
|
981
1037
|
/** 信号量 17。 */
|
|
982
1038
|
weight: 17,
|
|
983
1039
|
short: '锚点/账本/错误码',
|
|
984
1040
|
title: '常见配方(锚点/账本闭合/多步链/错误码)',
|
|
985
|
-
summary: '
|
|
1041
|
+
summary: '锚点比对、账本闭合、多步链、三条推演配方(文明/巨兽/材料)、错误码。',
|
|
986
1042
|
aliases: ['锚点', 'sanity', '账本', '闭合', '多步', '错误', '配方'],
|
|
987
1043
|
body: RECIPE_BODY,
|
|
988
1044
|
},
|
|
@@ -1057,12 +1113,45 @@ function renderIndex(): string {
|
|
|
1057
1113
|
const byWeight = (a: HelpTopic, b: HelpTopic) => (b.weight ?? 0) - (a.weight ?? 0)
|
|
1058
1114
|
const core = HELP_TOPICS.filter((item) => item.tier === 'core').sort(byWeight)
|
|
1059
1115
|
const extra = HELP_TOPICS.filter((item) => item.tier === 'extra').sort(byWeight)
|
|
1116
|
+
/**
|
|
1117
|
+
* ## 目录分三段:算术 / 建模与推演 / 其余
|
|
1118
|
+
*
|
|
1119
|
+
* 实测反馈(能力评估文档第 4 节)是本轮最重的一条:
|
|
1120
|
+
*
|
|
1121
|
+
* > 「一个能做动力学 + 参数扫描 + 499 条领域公式的工具,被排在了『低频』栏里。」
|
|
1122
|
+
* > 「我写了 300 行 Python 做逐年迭代、参数扫描、双状态耦合 ——
|
|
1123
|
+
* > 而 ode、sensitivity、formula 就在那里,只是排在了『低频』栏的最后一行。」
|
|
1124
|
+
*
|
|
1125
|
+
* 原始证据也很硬:该会话调用 math_execute 18 次、手写 Python 6 个脚本约 300 行,
|
|
1126
|
+
* 而脚本做的事(逐年迭代、参数扫描、状态耦合)工具全都有对应的 operation。
|
|
1127
|
+
*
|
|
1128
|
+
* ## 所以改动只有一处,但很关键
|
|
1129
|
+
*
|
|
1130
|
+
* 不是「再加一句说明」,而是**把建模类从『低频』栏里提出来单独成段**。
|
|
1131
|
+
* 因为「低频」这个词本身就在劝退 —— 它表达的是「用得少」,
|
|
1132
|
+
* 而 `ode` 用得少不代表不该在写增长模型时想起它。
|
|
1133
|
+
*
|
|
1134
|
+
* 并且在这一段的开头直接给一句**触发词**:「正在手写循环/逐年表/参数扫描?先看这几节」。
|
|
1135
|
+
* 文档的建议原话也是这句:「能力不缺,缺的是一句『你正在手写循环?先看这里』」。
|
|
1136
|
+
*/
|
|
1137
|
+
const MODELING_KEYS = ['ode', 'formula', 'derivation', 'recipe', 'world', 'domains']
|
|
1138
|
+
const isModeling = (topic: HelpTopic) => MODELING_KEYS.includes(topic.key)
|
|
1139
|
+
const arithmetic = core.filter((topic) => !isModeling(topic))
|
|
1140
|
+
const modeling = core.filter(isModeling)
|
|
1060
1141
|
const lines: string[] = [
|
|
1061
1142
|
'══════ math_execute 能力目录(' + HELP_TOPICS.length + ' 节)══════',
|
|
1062
1143
|
'helpTopic:"键名" 取该节全文;也可用中文词(矩阵/星球/单位…)。',
|
|
1063
1144
|
'',
|
|
1145
|
+
' ── 一、算术 ──',
|
|
1064
1146
|
]
|
|
1065
|
-
for (const topic of
|
|
1147
|
+
for (const topic of arithmetic) {
|
|
1148
|
+
lines.push(' ' + topic.key.padEnd(10) + topic.title)
|
|
1149
|
+
lines.push(' ' + topic.summary)
|
|
1150
|
+
}
|
|
1151
|
+
lines.push('')
|
|
1152
|
+
lines.push(' ── 二、建模与推演 ──')
|
|
1153
|
+
lines.push(' ★ 正在手写循环 / 逐年表 / 参数扫描?先看这几节,别用脚本重写')
|
|
1154
|
+
for (const topic of modeling) {
|
|
1066
1155
|
lines.push(' ' + topic.key.padEnd(10) + topic.title)
|
|
1067
1156
|
lines.push(' ' + topic.summary)
|
|
1068
1157
|
}
|
|
@@ -1072,7 +1161,7 @@ function renderIndex(): string {
|
|
|
1072
1161
|
* 而它们本来只是「顺带知道一下」的存在。
|
|
1073
1162
|
*/
|
|
1074
1163
|
lines.push('')
|
|
1075
|
-
lines.push('
|
|
1164
|
+
lines.push(' ── 三、其余能力(同样可查,正文里有完整说明)──')
|
|
1076
1165
|
const folded = extra.map(function (item) {
|
|
1077
1166
|
const label = item.short ?? item.title.slice(0, 8)
|
|
1078
1167
|
return item.key + '=' + label
|
package/src/index.ts
CHANGED
|
@@ -208,6 +208,7 @@ import {
|
|
|
208
208
|
setDisplayPrecisionExplicit,
|
|
209
209
|
} from './precision-report.js'
|
|
210
210
|
import { opOde } from './ops/ode-op.js'
|
|
211
|
+
import { opRecurrence } from './ops/recurrence-op.js'
|
|
211
212
|
import { opWorld } from './ops/world-op.js'
|
|
212
213
|
import { opConstants } from './ops/constants-op.js'
|
|
213
214
|
import { opWorkspace } from './ops/workspace-op.js'
|
|
@@ -349,6 +350,7 @@ const OPERATIONS: readonly MathOperation[] = [
|
|
|
349
350
|
'formula',
|
|
350
351
|
'formulas',
|
|
351
352
|
'ode',
|
|
353
|
+
'recurrence',
|
|
352
354
|
'world',
|
|
353
355
|
'constants',
|
|
354
356
|
'workspace',
|
|
@@ -2677,6 +2679,25 @@ function formaliveUnitOf(formula: FormulaDef, name: string): string {
|
|
|
2677
2679
|
return unitFromDescription(spec.desc, (name) => { try { return findUnit(name) !== undefined } catch { return false } })
|
|
2678
2680
|
}
|
|
2679
2681
|
function opFormula(args: MathArgs, engine: any, precision: number): Outcome {
|
|
2682
|
+
/**
|
|
2683
|
+
* ## 批量调公式:`formulas: [...]`(结构性不足文档第四节)
|
|
2684
|
+
*
|
|
2685
|
+
* 实测反馈:「需要对 22 条公式做批量调用,但 `formula` 不支持 expressions 数组,
|
|
2686
|
+
* 每次只能手动拆成单条 evaluate,等于放弃了公式库的全部价值。」
|
|
2687
|
+
*
|
|
2688
|
+
* 这不是「再开一个 operation」,而是**同一条公式的 N 次调用**——
|
|
2689
|
+
* 所以做成 `formula` 的数组入参,语义与单个调用完全一致(含 unitFrom/unitTo/terse)。
|
|
2690
|
+
*
|
|
2691
|
+
* 元素两种写法:
|
|
2692
|
+
* · 字符串 "kleiber"
|
|
2693
|
+
* · 对象 {formula:"kleiber", overrides:{m:100}, label:"鼠"}
|
|
2694
|
+
*
|
|
2695
|
+
* ★ 一条失败不影响其它条 —— 与 `evaluate` 的 expressions 同一条纪律:
|
|
2696
|
+
* 批量是「多条独立查询」,不是「一个事务」。汇总里会如实报 failed 条数。
|
|
2697
|
+
*/
|
|
2698
|
+
if (Array.isArray((args as any).formulas) && (args as any).formulas.length > 0) {
|
|
2699
|
+
return opFormulaBatch(args, engine, precision)
|
|
2700
|
+
}
|
|
2680
2701
|
const query = (args.formula ?? args.expression ?? '').trim()
|
|
2681
2702
|
if (query === '') {
|
|
2682
2703
|
throw new CalcError('invalid_argument', '需要 formula 参数', '传公式 id 或名称,例如 "escape_velocity"、"逃逸速度"。用 operation="formulas" 检索。')
|
|
@@ -3211,10 +3232,12 @@ const KNOWN_ARGUMENT_KEYS: ReadonlySet<string> = new Set<string>([
|
|
|
3211
3232
|
'matrix', 'vector', 'precision', 'displayPrecision',
|
|
3212
3233
|
// 数据与统计
|
|
3213
3234
|
'data', 'dataY', 'distribution', 'params', 'x', 'degree',
|
|
3214
|
-
//
|
|
3215
|
-
'formula', 'overrides', 'query', 'customExpression',
|
|
3216
|
-
// 微分方程 /
|
|
3217
|
-
'initial', 'from', 'to', 'steps', 'samples',
|
|
3235
|
+
// 公式(formulas 是批量,一次算多条)
|
|
3236
|
+
'formula', 'formulas', 'overrides', 'query', 'customExpression',
|
|
3237
|
+
// 微分方程 / 物理(at 是 ode 的指定采样时点)
|
|
3238
|
+
'initial', 'from', 'to', 'steps', 'samples', 'at',
|
|
3239
|
+
// 离散递推
|
|
3240
|
+
'recurrence', 'periods',
|
|
3218
3241
|
// 星球与推演
|
|
3219
3242
|
'world', 'terrain', 'constants', 'derivation', 'layer',
|
|
3220
3243
|
// 工作区
|
|
@@ -3478,7 +3501,7 @@ export function runMathTool(rawArguments: unknown): MathResult {
|
|
|
3478
3501
|
const CACHEABLE: MathOperation[] = [
|
|
3479
3502
|
'auto', 'evaluate', 'solve', 'system', 'derivative', 'integral',
|
|
3480
3503
|
'simplify', 'expand', 'roots', 'matrix', 'eigen', 'stats', 'dist',
|
|
3481
|
-
'regression', 'ode', 'formula', 'units',
|
|
3504
|
+
'regression', 'ode', 'recurrence', 'formula', 'units',
|
|
3482
3505
|
]
|
|
3483
3506
|
/**
|
|
3484
3507
|
* 状态变更类操作:先清空缓存,再走正常流程。
|
|
@@ -3661,15 +3684,50 @@ export function runMathTool(rawArguments: unknown): MathResult {
|
|
|
3661
3684
|
case 'regression':
|
|
3662
3685
|
outcome = opRegression(args, input, precision)
|
|
3663
3686
|
break
|
|
3664
|
-
case 'formula':
|
|
3687
|
+
case 'formula': {
|
|
3665
3688
|
outcome = opFormula(args, engine, precision)
|
|
3689
|
+
/**
|
|
3690
|
+
* 批量公式的成败要在这里汇总成 `ok/partial/failed/succeeded`。
|
|
3691
|
+
*
|
|
3692
|
+
* 与 `evaluate` 的批量走同一套语义(见上方 3590 行那段注释):
|
|
3693
|
+
* · `ok:false` 告诉自动化流程「本批不是全好」
|
|
3694
|
+
* · `partial:true` 告诉它「但有成功的条目可用」
|
|
3695
|
+
* · 顶层 `error` 仍是 `undefined`(这是批量失败,不是整体错误)
|
|
3696
|
+
*
|
|
3697
|
+
* 踩过:这些字段直接在 `opFormulaBatch` 的返回值上展开,
|
|
3698
|
+
* 会被 `Outcome` 类型断言吞掉 —— 正文写着「成功 0,失败 3」,
|
|
3699
|
+
* 而结构化字段全是 `undefined`。所以在这里显式赋给 `MathResult`。
|
|
3700
|
+
*/
|
|
3701
|
+
if (Array.isArray((args as any).formulas)) {
|
|
3702
|
+
const batch = (outcome.value ?? {}) as Record<string, unknown>
|
|
3703
|
+
const failedCount = typeof batch.failed === 'number' ? batch.failed : 0
|
|
3704
|
+
const succeededCount = typeof batch.succeeded === 'number' ? batch.succeeded : 0
|
|
3705
|
+
const batched: MathResult = {
|
|
3706
|
+
ok: failedCount === 0,
|
|
3707
|
+
operation: 'formula',
|
|
3708
|
+
input: 'formulas: ' + (args as any).formulas.length + ' 条',
|
|
3709
|
+
result: outcome.result,
|
|
3710
|
+
precision,
|
|
3711
|
+
}
|
|
3712
|
+
if (outcome.value !== undefined) batched.value = outcome.value
|
|
3713
|
+
if (failedCount > 0) {
|
|
3714
|
+
batched.partial = succeededCount > 0
|
|
3715
|
+
batched.failed = failedCount
|
|
3716
|
+
batched.succeeded = succeededCount
|
|
3717
|
+
}
|
|
3718
|
+
outcome = batched
|
|
3719
|
+
}
|
|
3666
3720
|
break
|
|
3721
|
+
}
|
|
3667
3722
|
case 'formulas':
|
|
3668
3723
|
outcome = opFormulas(args)
|
|
3669
3724
|
break
|
|
3670
3725
|
case 'ode':
|
|
3671
3726
|
outcome = opOde(args, engine, precision)
|
|
3672
3727
|
break
|
|
3728
|
+
case 'recurrence':
|
|
3729
|
+
outcome = opRecurrence(args, precision)
|
|
3730
|
+
break
|
|
3673
3731
|
case 'world':
|
|
3674
3732
|
outcome = opWorld(args, precision)
|
|
3675
3733
|
break
|
|
@@ -3712,14 +3770,29 @@ export function runMathTool(rawArguments: unknown): MathResult {
|
|
|
3712
3770
|
break
|
|
3713
3771
|
}
|
|
3714
3772
|
|
|
3773
|
+
/**
|
|
3774
|
+
* ## 这里重新构造 `MathResult`,所以 `outcome` 上的成败字段必须**显式搬过来**
|
|
3775
|
+
*
|
|
3776
|
+
* 踩过两次,都是同一类:
|
|
3777
|
+
* ① 批量公式把 `ok/partial/failed/succeeded` 挂在 `outcome` 上 → 被这里覆盖成 `ok:true`
|
|
3778
|
+
* ② 早先 `evaluate` 批量也有同样问题,但它有独立的组装分支绕过去了
|
|
3779
|
+
*
|
|
3780
|
+
* 教训:`Outcome` 只是中间载体,真正的返回对象在这一处生成。
|
|
3781
|
+
* 想加任何顶层字段,都得在这里接一次。
|
|
3782
|
+
*/
|
|
3783
|
+
const batchOk = (outcome as any).ok as boolean | undefined
|
|
3715
3784
|
const result: MathResult = {
|
|
3716
|
-
ok: true,
|
|
3785
|
+
ok: batchOk === undefined ? true : batchOk,
|
|
3717
3786
|
operation,
|
|
3718
3787
|
input,
|
|
3719
3788
|
result: outcome.result,
|
|
3720
3789
|
precision,
|
|
3721
3790
|
}
|
|
3722
3791
|
if (outcome.value !== undefined) result.value = outcome.value
|
|
3792
|
+
for (const key of ['partial', 'failed', 'succeeded'] as const) {
|
|
3793
|
+
const value = (outcome as any)[key]
|
|
3794
|
+
if (value !== undefined) (result as any)[key] = value
|
|
3795
|
+
}
|
|
3723
3796
|
const variables = scopeSummary(args.variables)
|
|
3724
3797
|
if (variables !== undefined) result.variables = variables
|
|
3725
3798
|
|
|
@@ -4238,8 +4311,7 @@ const PARAMETER_SPEC = {
|
|
|
4238
4311
|
expression: {
|
|
4239
4312
|
type: 'string',
|
|
4240
4313
|
description:
|
|
4241
|
-
'
|
|
4242
|
-
'例:10^20 + 5、factorial(50)、x^3 * sin(x)、[[1,2],[3,4]] * [[5],[6]]、[1,2,3,4]。operation=help 时可空。',
|
|
4314
|
+
'表达式、方程或数据。多方程逗号分隔。例:10^20+5、x^3*sin(x)、[[1,2],[3,4]]*[[5],[6]]。',
|
|
4243
4315
|
},
|
|
4244
4316
|
expressions: {
|
|
4245
4317
|
type: 'array',
|
|
@@ -4285,8 +4357,7 @@ const PARAMETER_SPEC = {
|
|
|
4285
4357
|
// 不逐个解释每个 operation 名称:enum 里已经列全,而 `help` 有完整说明。
|
|
4286
4358
|
// 这里只给「怎么选」的判断线索,那才是 AI 真正需要的。
|
|
4287
4359
|
description:
|
|
4288
|
-
'默认 auto
|
|
4289
|
-
'改动设定后用 world/derivation;不确定就调 help 看全表。',
|
|
4360
|
+
'默认 auto。简易算式 evaluate;数据 stats;改设定 world/derivation;不确定调 help。',
|
|
4290
4361
|
},
|
|
4291
4362
|
variable: {
|
|
4292
4363
|
type: 'string',
|
|
@@ -4301,9 +4372,7 @@ const PARAMETER_SPEC = {
|
|
|
4301
4372
|
unitFrom: {
|
|
4302
4373
|
type: 'string',
|
|
4303
4374
|
description:
|
|
4304
|
-
'
|
|
4305
|
-
'英制(英里/英尺/磅/加仑)、天文(AU/光年/秒差距/地球半径/太阳半径)。' +
|
|
4306
|
-
'只给单位不给参数则列出全部可用单位。',
|
|
4375
|
+
'源单位(units)。支持中文古制(里/丈/斤/亩/时辰/刻)、英制(英里/磅/加仑)、天文(AU/光年)。只给单位不给数值则列全表。',
|
|
4307
4376
|
},
|
|
4308
4377
|
unitTo: {
|
|
4309
4378
|
type: 'string',
|
|
@@ -4319,14 +4388,12 @@ const PARAMETER_SPEC = {
|
|
|
4319
4388
|
type: 'object',
|
|
4320
4389
|
additionalProperties: true,
|
|
4321
4390
|
description:
|
|
4322
|
-
'收支盘点(
|
|
4323
|
-
'只判社会收支;单项生理/工程上限用 feasibility。',
|
|
4391
|
+
'收支盘点(balance):每项 {demand, supply, unit?},给最短板。单项上限用 feasibility。',
|
|
4324
4392
|
},
|
|
4325
4393
|
bodyName: {
|
|
4326
4394
|
type: 'string',
|
|
4327
4395
|
description:
|
|
4328
|
-
'
|
|
4329
|
-
'已收录太阳系全 8 大行星 + 月球类卫星 + 代表性系外行星 + 矮行星 + 各亮度级恒星。',
|
|
4396
|
+
'查天体(bodies):id(mars)或中文名(火星)。含行星、卫星、系外行星、矮行星、各级恒星。',
|
|
4330
4397
|
},
|
|
4331
4398
|
bodyKind: {
|
|
4332
4399
|
type: 'string',
|
|
@@ -4336,8 +4403,7 @@ const PARAMETER_SPEC = {
|
|
|
4336
4403
|
bodyConstant: {
|
|
4337
4404
|
type: 'string',
|
|
4338
4405
|
description:
|
|
4339
|
-
'bodies:改常数后看哪些天体数据该重算。例
|
|
4340
|
-
'输出分「必须重算」与「绝不该动」两块(行星质量/半径/轨道与 α 无关)。',
|
|
4406
|
+
'bodies:改常数后看哪些天体数据该重算。例 alpha_fs + bodyFactor=2。分「必须重算」与「绝不该动」两块。',
|
|
4341
4407
|
},
|
|
4342
4408
|
bodyFactor: {
|
|
4343
4409
|
type: 'number',
|
|
@@ -4368,8 +4434,7 @@ const PARAMETER_SPEC = {
|
|
|
4368
4434
|
displayPrecision: {
|
|
4369
4435
|
type: 'integer',
|
|
4370
4436
|
description:
|
|
4371
|
-
'**显示精度:小数点后几位**(0-64,默认 8
|
|
4372
|
-
'可用 workspace set displayPrecision=N 固化(那里的 0 = 清除)。',
|
|
4437
|
+
'**显示精度:小数点后几位**(0-64,默认 8)。只截小数、整数完整,0=只留整数。可 workspace set 固化(0=清除)。',
|
|
4373
4438
|
},
|
|
4374
4439
|
data: {
|
|
4375
4440
|
type: 'array',
|
|
@@ -4398,30 +4463,32 @@ const PARAMETER_SPEC = {
|
|
|
4398
4463
|
formula: {
|
|
4399
4464
|
type: 'string',
|
|
4400
4465
|
description:
|
|
4401
|
-
'operation="formula"
|
|
4402
|
-
'用 operation="formulas" 检索可用公式。',
|
|
4466
|
+
'operation="formula":公式 id 或名称,如 "escape_velocity"、"逃逸速度"。检索用 operation="formulas";多条用 formulas 数组。',
|
|
4403
4467
|
},
|
|
4468
|
+
formulas: { type: 'array', items: { type: 'json', description: '"kleiber" 或 {formula, overrides?, label?}' }, description: '批量调多条公式;一条失败不影响其它条。' },
|
|
4404
4469
|
overrides: {
|
|
4405
4470
|
type: 'object',
|
|
4406
4471
|
additionalProperties: true,
|
|
4407
4472
|
description:
|
|
4408
|
-
'
|
|
4473
|
+
'覆盖公式任意参数(架空改造的关键)。例:重力 1.5 倍 {"g":14.71}、光速 {"c":1e8}。',
|
|
4409
4474
|
},
|
|
4410
4475
|
customExpression: {
|
|
4411
4476
|
type: 'string',
|
|
4412
4477
|
description:
|
|
4413
|
-
'structure
|
|
4478
|
+
'structure 级改造:临时替换公式表达式,沿用参数名。例 customExpression="m*g*1.3" 即重力 +30%。',
|
|
4414
4479
|
},
|
|
4415
4480
|
query: {
|
|
4416
4481
|
type: 'string',
|
|
4417
4482
|
description: 'operation="formulas" 的检索关键词,例如 "重力"、"熵"、"人口";传 "*" 或留空列出全部分类。',
|
|
4418
4483
|
},
|
|
4484
|
+
recurrence: { type: 'string', description: 'operation="recurrence":下一期 = f(本期),如 "P*2"、"P*min(1.2,1000/P)"。配 initial 与 periods 出逐期表。' },
|
|
4485
|
+
periods: { type: 'integer', description: 'recurrence 的迭代期数(默认 12,上限 5000)。' },
|
|
4486
|
+
at: { type: 'array', items: { type: 'number' }, description: 'ode 要精确输出的时点,如 [5,10,20](写年表用)。' },
|
|
4419
4487
|
initial: {
|
|
4420
4488
|
type: 'object',
|
|
4421
4489
|
additionalProperties: true,
|
|
4422
4490
|
description:
|
|
4423
|
-
'
|
|
4424
|
-
'二阶方程降阶后可能还需要辅助变量(如 y_d 表示 dy/dt),缺了会提示补哪个。',
|
|
4491
|
+
'ode/recurrence 初值,如 {"S":999,"I":1}。二阶降阶需辅助变量 y_d=dy/dt,缺了会提示。',
|
|
4425
4492
|
},
|
|
4426
4493
|
from: {
|
|
4427
4494
|
type: 'number',
|
|
@@ -4445,9 +4512,7 @@ const PARAMETER_SPEC = {
|
|
|
4445
4512
|
// 字段清单不写在 schema 里:schema 每轮都要付费,而这些字段只在
|
|
4446
4513
|
// 真用 world 时才需要。全文见 help(operation="help", query="world")。
|
|
4447
4514
|
description:
|
|
4448
|
-
'星球参数(
|
|
4449
|
-
'常用键:planetMass/planetRadius、starMass/starTemp、rotationHours、obliquityDeg。' +
|
|
4450
|
-
'完整字段表见 help。',
|
|
4515
|
+
'星球参数(world),可选,默认地球。常用键 planetMass、starMass、rotationHours、obliquityDeg。完整见 help。',
|
|
4451
4516
|
},
|
|
4452
4517
|
planetRadius: {
|
|
4453
4518
|
type: 'number',
|
|
@@ -4470,16 +4535,14 @@ const PARAMETER_SPEC = {
|
|
|
4470
4535
|
additionalProperties: true,
|
|
4471
4536
|
// 同上:地形字段有九项,逐项列在 schema 里太贵。
|
|
4472
4537
|
description:
|
|
4473
|
-
'局部地形(
|
|
4474
|
-
'常用键:kind、elevation、latitudeDeg、oceanCurrent。完整键表见 help。',
|
|
4538
|
+
'局部地形(world 附带)。常用键 kind、elevation、latitudeDeg、oceanCurrent。完整见 help。',
|
|
4475
4539
|
},
|
|
4476
4540
|
constants: {
|
|
4477
4541
|
type: 'object',
|
|
4478
4542
|
additionalProperties: true,
|
|
4479
4543
|
// 旋钮清单移进 help:只有要改常数时才需要,不该每轮付费
|
|
4480
4544
|
description:
|
|
4481
|
-
'要改的物理常数(
|
|
4482
|
-
'例:{c:1.5e8}。可用旋钮清单见 help 的 constants 节。',
|
|
4545
|
+
'要改的物理常数(constants):只改基础旋钮,派生量自动重算。例 {c:1.5e8}。旋钮清单见 help。',
|
|
4483
4546
|
},
|
|
4484
4547
|
helpTopic: {
|
|
4485
4548
|
type: 'string',
|
|
@@ -4558,6 +4621,98 @@ function checkPrecisionTruncation(
|
|
|
4558
4621
|
' 位可能已失真。需要精确值时请用 precision:64(默认),' +
|
|
4559
4622
|
'只想让输出短些请用 displayPrecision。'
|
|
4560
4623
|
}
|
|
4624
|
+
|
|
4625
|
+
/**
|
|
4626
|
+
* 批量调公式(`formulas: [...]`)。
|
|
4627
|
+
*
|
|
4628
|
+
* ## 为什么复用 opFormula 而不是重写一套
|
|
4629
|
+
*
|
|
4630
|
+
* 单个调用涉及:严格解析、overrides 覆盖、单位换算、terse 精简、
|
|
4631
|
+
* 适用域提示、未生效 key 上报 —— 这些逻辑**必须与单条完全一致**。
|
|
4632
|
+
* 重写一套必然漂移,而「单条能算、批量算得不一样」正是这个项目
|
|
4633
|
+
* 反复修过的那类缺陷(1107 报告的批量非有限值、单条/批量输出不一致)。
|
|
4634
|
+
*
|
|
4635
|
+
* 所以这里只做三件事:逐条调用、收集结果、汇总成败。
|
|
4636
|
+
*
|
|
4637
|
+
* ## 失败语义
|
|
4638
|
+
*
|
|
4639
|
+
* 一条失败 → 该条在输出里标 `失败[type]`,继续算下一条,
|
|
4640
|
+
* 最后由 `ok`/`failed`/`succeeded` 如实汇总。**不因为一条错就中断整批**。
|
|
4641
|
+
*
|
|
4642
|
+
* @param args - 含 `formulas` 数组的入参
|
|
4643
|
+
* @param engine - mathjs 实例
|
|
4644
|
+
* @param precision - 计算精度
|
|
4645
|
+
* @returns 批量结果
|
|
4646
|
+
*/
|
|
4647
|
+
function opFormulaBatch(args: MathArgs, engine: any, precision: number): Outcome {
|
|
4648
|
+
const list = (args as any).formulas as any[]
|
|
4649
|
+
const lines: string[] = []
|
|
4650
|
+
const rows: Record<string, unknown>[] = []
|
|
4651
|
+
let failed = 0
|
|
4652
|
+
lines.push('═══ 批量公式(' + list.length + ' 条)═══')
|
|
4653
|
+
lines.push('')
|
|
4654
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
4655
|
+
const item = list[index]
|
|
4656
|
+
const isObject = item !== null && typeof item === 'object'
|
|
4657
|
+
const id = String(isObject ? (item.formula ?? item.id ?? '') : item).trim()
|
|
4658
|
+
const label = isObject && item.label !== undefined ? String(item.label) : ''
|
|
4659
|
+
/** 逐条构造入参:继承顶层公共参数(terse/unitFrom/unitTo/precision),再叠加元素自己的。 */
|
|
4660
|
+
const oneArgs: MathArgs = {
|
|
4661
|
+
...args,
|
|
4662
|
+
formulas: undefined,
|
|
4663
|
+
formula: id,
|
|
4664
|
+
expression: undefined,
|
|
4665
|
+
overrides: isObject && item.overrides ? item.overrides : (isObject ? undefined : args.overrides),
|
|
4666
|
+
} as MathArgs
|
|
4667
|
+
(oneArgs as any).formulas = undefined
|
|
4668
|
+
if (isObject && item.terse !== undefined) (oneArgs as any).terse = item.terse
|
|
4669
|
+
const prefix = label === '' ? '' : label + '(' + id + ')'
|
|
4670
|
+
try {
|
|
4671
|
+
const outcome = opFormula(oneArgs, engine, precision)
|
|
4672
|
+
const text = String(outcome.result ?? '').trim()
|
|
4673
|
+
/**
|
|
4674
|
+
* 抓「结果:」那一行 —— 公式输出的第一行是标题(名称 + 分类),
|
|
4675
|
+
* 数值在「结果:」行上。只取首行会得到 `克莱伯定律(代谢率) [physics/scaling]`,
|
|
4676
|
+
* 数值整个丢了,而批量恰恰是为了看一串数。
|
|
4677
|
+
*/
|
|
4678
|
+
const valueLine = text.split(/\r?\n/).find(function (line) { return line.trim().startsWith('结果') })
|
|
4679
|
+
const first = valueLine ?? (text.split(/\r?\n/).find(function (line) { return line.trim() !== '' }) ?? '')
|
|
4680
|
+
/** terse 输出本身已是 `kleiber = 31622…`,别再加一层 `id = ` 变成 `kleiber = kleiber = …`。 */
|
|
4681
|
+
const alreadyNamed = String(first).trim().startsWith(id + ' =')
|
|
4682
|
+
const shown = String(first).replace(/^结果\s*[::]\s*/, '').trim()
|
|
4683
|
+
const body = alreadyNamed ? shown : id + ' = ' + shown
|
|
4684
|
+
lines.push(' ' + (prefix === '' ? body : prefix + ' = ' + shown))
|
|
4685
|
+
rows.push({ formula: id, label: label === '' ? null : label, ok: true, result: outcome.value ?? null })
|
|
4686
|
+
} catch (error) {
|
|
4687
|
+
failed += 1
|
|
4688
|
+
const type = (error as any)?.type ?? 'unknown'
|
|
4689
|
+
const message = String((error as any)?.message ?? error).slice(0, 120)
|
|
4690
|
+
lines.push(' ' + (prefix === '' ? id : prefix) + ' → 失败[' + type + '] ' + message)
|
|
4691
|
+
rows.push({ formula: id, label: label === '' ? null : label, ok: false, error: type })
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
const succeeded = list.length - failed
|
|
4695
|
+
lines.push('')
|
|
4696
|
+
lines.push(
|
|
4697
|
+
failed === 0
|
|
4698
|
+
? '全部 ' + succeeded + ' 条都有结果(只表示公式存在且参数可算,不含结论正确性判断)'
|
|
4699
|
+
: '成功 ' + succeeded + ',失败 ' + failed + '(失败条已逐条标注原因)',
|
|
4700
|
+
)
|
|
4701
|
+
/**
|
|
4702
|
+
* 汇总字段必须**真的出现在返回值里**:`ok` / `failed` / `succeeded` / `partial`。
|
|
4703
|
+
*
|
|
4704
|
+
* 踩过:`{...cond ? {...} : {...}}` 这种写法在 Outcome 类型下会被类型断言吞掉,
|
|
4705
|
+
* 实测返回 `ok=true` 却 `failed=undefined` —— 而正文里明明写着「成功 0,失败 3」。
|
|
4706
|
+
* 正文与结构化字段不一致,比只给正文更糟(调用方读哪个?)。
|
|
4707
|
+
* 所以改成显式赋值。
|
|
4708
|
+
*/
|
|
4709
|
+
/** 汇总字段放进 `value`,由调用方(`runMathTool`)提升到顶层 `MathResult`。 */
|
|
4710
|
+
return {
|
|
4711
|
+
result: lines.join('\n'),
|
|
4712
|
+
value: toJsonValue({ count: list.length, succeeded, failed, rows }),
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4561
4716
|
/**
|
|
4562
4717
|
* 数学计算工具(`math_execute`)。
|
|
4563
4718
|
*
|