dsh-math-calculator 2.3.2 → 2.3.4
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 +192 -0
- package/dsh.plugin.json +1 -1
- package/lib/help.js +547 -184
- 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/help.d.ts +15 -1
- 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 +561 -186
- 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/lib/index.js
CHANGED
|
@@ -38,6 +38,7 @@ import { buildReminderMessage, recordPrompt, shouldPromptNow, } from './detector
|
|
|
38
38
|
import { CalcError, bignum, currentDisplayPrecision, fast, normalizeSource, normalizeChineseUnits, rational, safeNumberForJson, setActiveDisplayPrecision, trimNumber, } from './ops/shared.js';
|
|
39
39
|
import { PRECISION_LOSS_THRESHOLD, consumePrecisionLoss, isDisplayPrecisionExplicit, setDisplayPrecisionExplicit, } from './precision-report.js';
|
|
40
40
|
import { opOde } from './ops/ode-op.js';
|
|
41
|
+
import { opRecurrence } from './ops/recurrence-op.js';
|
|
41
42
|
import { opWorld } from './ops/world-op.js';
|
|
42
43
|
import { opConstants } from './ops/constants-op.js';
|
|
43
44
|
import { opWorkspace } from './ops/workspace-op.js';
|
|
@@ -121,6 +122,7 @@ const OPERATIONS = [
|
|
|
121
122
|
'formula',
|
|
122
123
|
'formulas',
|
|
123
124
|
'ode',
|
|
125
|
+
'recurrence',
|
|
124
126
|
'world',
|
|
125
127
|
'constants',
|
|
126
128
|
'workspace',
|
|
@@ -2329,6 +2331,25 @@ function formaliveUnitOf(formula, name) {
|
|
|
2329
2331
|
} });
|
|
2330
2332
|
}
|
|
2331
2333
|
function opFormula(args, engine, precision) {
|
|
2334
|
+
/**
|
|
2335
|
+
* ## 批量调公式:`formulas: [...]`(结构性不足文档第四节)
|
|
2336
|
+
*
|
|
2337
|
+
* 实测反馈:「需要对 22 条公式做批量调用,但 `formula` 不支持 expressions 数组,
|
|
2338
|
+
* 每次只能手动拆成单条 evaluate,等于放弃了公式库的全部价值。」
|
|
2339
|
+
*
|
|
2340
|
+
* 这不是「再开一个 operation」,而是**同一条公式的 N 次调用**——
|
|
2341
|
+
* 所以做成 `formula` 的数组入参,语义与单个调用完全一致(含 unitFrom/unitTo/terse)。
|
|
2342
|
+
*
|
|
2343
|
+
* 元素两种写法:
|
|
2344
|
+
* · 字符串 "kleiber"
|
|
2345
|
+
* · 对象 {formula:"kleiber", overrides:{m:100}, label:"鼠"}
|
|
2346
|
+
*
|
|
2347
|
+
* ★ 一条失败不影响其它条 —— 与 `evaluate` 的 expressions 同一条纪律:
|
|
2348
|
+
* 批量是「多条独立查询」,不是「一个事务」。汇总里会如实报 failed 条数。
|
|
2349
|
+
*/
|
|
2350
|
+
if (Array.isArray(args.formulas) && args.formulas.length > 0) {
|
|
2351
|
+
return opFormulaBatch(args, engine, precision);
|
|
2352
|
+
}
|
|
2332
2353
|
const query = (args.formula ?? args.expression ?? '').trim();
|
|
2333
2354
|
if (query === '') {
|
|
2334
2355
|
throw new CalcError('invalid_argument', '需要 formula 参数', '传公式 id 或名称,例如 "escape_velocity"、"逃逸速度"。用 operation="formulas" 检索。');
|
|
@@ -2845,10 +2866,12 @@ const KNOWN_ARGUMENT_KEYS = new Set([
|
|
|
2845
2866
|
'matrix', 'vector', 'precision', 'displayPrecision',
|
|
2846
2867
|
// 数据与统计
|
|
2847
2868
|
'data', 'dataY', 'distribution', 'params', 'x', 'degree',
|
|
2848
|
-
//
|
|
2849
|
-
'formula', 'overrides', 'query', 'customExpression',
|
|
2850
|
-
// 微分方程 /
|
|
2851
|
-
'initial', 'from', 'to', 'steps', 'samples',
|
|
2869
|
+
// 公式(formulas 是批量,一次算多条)
|
|
2870
|
+
'formula', 'formulas', 'overrides', 'query', 'customExpression',
|
|
2871
|
+
// 微分方程 / 物理(at 是 ode 的指定采样时点)
|
|
2872
|
+
'initial', 'from', 'to', 'steps', 'samples', 'at',
|
|
2873
|
+
// 离散递推
|
|
2874
|
+
'recurrence', 'periods',
|
|
2852
2875
|
// 星球与推演
|
|
2853
2876
|
'world', 'terrain', 'constants', 'derivation', 'layer',
|
|
2854
2877
|
// 工作区
|
|
@@ -3097,7 +3120,7 @@ export function runMathTool(rawArguments) {
|
|
|
3097
3120
|
const CACHEABLE = [
|
|
3098
3121
|
'auto', 'evaluate', 'solve', 'system', 'derivative', 'integral',
|
|
3099
3122
|
'simplify', 'expand', 'roots', 'matrix', 'eigen', 'stats', 'dist',
|
|
3100
|
-
'regression', 'ode', 'formula', 'units',
|
|
3123
|
+
'regression', 'ode', 'recurrence', 'formula', 'units',
|
|
3101
3124
|
];
|
|
3102
3125
|
/**
|
|
3103
3126
|
* 状态变更类操作:先清空缓存,再走正常流程。
|
|
@@ -3274,15 +3297,51 @@ export function runMathTool(rawArguments) {
|
|
|
3274
3297
|
case 'regression':
|
|
3275
3298
|
outcome = opRegression(args, input, precision);
|
|
3276
3299
|
break;
|
|
3277
|
-
case 'formula':
|
|
3300
|
+
case 'formula': {
|
|
3278
3301
|
outcome = opFormula(args, engine, precision);
|
|
3302
|
+
/**
|
|
3303
|
+
* 批量公式的成败要在这里汇总成 `ok/partial/failed/succeeded`。
|
|
3304
|
+
*
|
|
3305
|
+
* 与 `evaluate` 的批量走同一套语义(见上方 3590 行那段注释):
|
|
3306
|
+
* · `ok:false` 告诉自动化流程「本批不是全好」
|
|
3307
|
+
* · `partial:true` 告诉它「但有成功的条目可用」
|
|
3308
|
+
* · 顶层 `error` 仍是 `undefined`(这是批量失败,不是整体错误)
|
|
3309
|
+
*
|
|
3310
|
+
* 踩过:这些字段直接在 `opFormulaBatch` 的返回值上展开,
|
|
3311
|
+
* 会被 `Outcome` 类型断言吞掉 —— 正文写着「成功 0,失败 3」,
|
|
3312
|
+
* 而结构化字段全是 `undefined`。所以在这里显式赋给 `MathResult`。
|
|
3313
|
+
*/
|
|
3314
|
+
if (Array.isArray(args.formulas)) {
|
|
3315
|
+
const batch = (outcome.value ?? {});
|
|
3316
|
+
const failedCount = typeof batch.failed === 'number' ? batch.failed : 0;
|
|
3317
|
+
const succeededCount = typeof batch.succeeded === 'number' ? batch.succeeded : 0;
|
|
3318
|
+
const batched = {
|
|
3319
|
+
ok: failedCount === 0,
|
|
3320
|
+
operation: 'formula',
|
|
3321
|
+
input: 'formulas: ' + args.formulas.length + ' 条',
|
|
3322
|
+
result: outcome.result,
|
|
3323
|
+
precision,
|
|
3324
|
+
};
|
|
3325
|
+
if (outcome.value !== undefined)
|
|
3326
|
+
batched.value = outcome.value;
|
|
3327
|
+
if (failedCount > 0) {
|
|
3328
|
+
batched.partial = succeededCount > 0;
|
|
3329
|
+
batched.failed = failedCount;
|
|
3330
|
+
batched.succeeded = succeededCount;
|
|
3331
|
+
}
|
|
3332
|
+
outcome = batched;
|
|
3333
|
+
}
|
|
3279
3334
|
break;
|
|
3335
|
+
}
|
|
3280
3336
|
case 'formulas':
|
|
3281
3337
|
outcome = opFormulas(args);
|
|
3282
3338
|
break;
|
|
3283
3339
|
case 'ode':
|
|
3284
3340
|
outcome = opOde(args, engine, precision);
|
|
3285
3341
|
break;
|
|
3342
|
+
case 'recurrence':
|
|
3343
|
+
outcome = opRecurrence(args, precision);
|
|
3344
|
+
break;
|
|
3286
3345
|
case 'world':
|
|
3287
3346
|
outcome = opWorld(args, precision);
|
|
3288
3347
|
break;
|
|
@@ -3324,8 +3383,19 @@ export function runMathTool(rawArguments) {
|
|
|
3324
3383
|
outcome = opEvaluate(engine, input, scope, precision);
|
|
3325
3384
|
break;
|
|
3326
3385
|
}
|
|
3386
|
+
/**
|
|
3387
|
+
* ## 这里重新构造 `MathResult`,所以 `outcome` 上的成败字段必须**显式搬过来**
|
|
3388
|
+
*
|
|
3389
|
+
* 踩过两次,都是同一类:
|
|
3390
|
+
* ① 批量公式把 `ok/partial/failed/succeeded` 挂在 `outcome` 上 → 被这里覆盖成 `ok:true`
|
|
3391
|
+
* ② 早先 `evaluate` 批量也有同样问题,但它有独立的组装分支绕过去了
|
|
3392
|
+
*
|
|
3393
|
+
* 教训:`Outcome` 只是中间载体,真正的返回对象在这一处生成。
|
|
3394
|
+
* 想加任何顶层字段,都得在这里接一次。
|
|
3395
|
+
*/
|
|
3396
|
+
const batchOk = outcome.ok;
|
|
3327
3397
|
const result = {
|
|
3328
|
-
ok: true,
|
|
3398
|
+
ok: batchOk === undefined ? true : batchOk,
|
|
3329
3399
|
operation,
|
|
3330
3400
|
input,
|
|
3331
3401
|
result: outcome.result,
|
|
@@ -3333,6 +3403,11 @@ export function runMathTool(rawArguments) {
|
|
|
3333
3403
|
};
|
|
3334
3404
|
if (outcome.value !== undefined)
|
|
3335
3405
|
result.value = outcome.value;
|
|
3406
|
+
for (const key of ['partial', 'failed', 'succeeded']) {
|
|
3407
|
+
const value = outcome[key];
|
|
3408
|
+
if (value !== undefined)
|
|
3409
|
+
result[key] = value;
|
|
3410
|
+
}
|
|
3336
3411
|
const variables = scopeSummary(args.variables);
|
|
3337
3412
|
if (variables !== undefined)
|
|
3338
3413
|
result.variables = variables;
|
|
@@ -3803,8 +3878,7 @@ function renderResult(value) {
|
|
|
3803
3878
|
const PARAMETER_SPEC = {
|
|
3804
3879
|
expression: {
|
|
3805
3880
|
type: 'string',
|
|
3806
|
-
description: '
|
|
3807
|
-
'例:10^20 + 5、factorial(50)、x^3 * sin(x)、[[1,2],[3,4]] * [[5],[6]]、[1,2,3,4]。operation=help 时可空。',
|
|
3881
|
+
description: '表达式、方程或数据。多方程逗号分隔。例:10^20+5、x^3*sin(x)、[[1,2],[3,4]]*[[5],[6]]。',
|
|
3808
3882
|
},
|
|
3809
3883
|
expressions: {
|
|
3810
3884
|
type: 'array',
|
|
@@ -3848,8 +3922,7 @@ const PARAMETER_SPEC = {
|
|
|
3848
3922
|
enum: OPERATIONS,
|
|
3849
3923
|
// 不逐个解释每个 operation 名称:enum 里已经列全,而 `help` 有完整说明。
|
|
3850
3924
|
// 这里只给「怎么选」的判断线索,那才是 AI 真正需要的。
|
|
3851
|
-
description: '默认 auto
|
|
3852
|
-
'改动设定后用 world/derivation;不确定就调 help 看全表。',
|
|
3925
|
+
description: '默认 auto。简易算式 evaluate;数据 stats;改设定 world/derivation;不确定调 help。',
|
|
3853
3926
|
},
|
|
3854
3927
|
variable: {
|
|
3855
3928
|
type: 'string',
|
|
@@ -3862,9 +3935,7 @@ const PARAMETER_SPEC = {
|
|
|
3862
3935
|
},
|
|
3863
3936
|
unitFrom: {
|
|
3864
3937
|
type: 'string',
|
|
3865
|
-
description: '
|
|
3866
|
-
'英制(英里/英尺/磅/加仑)、天文(AU/光年/秒差距/地球半径/太阳半径)。' +
|
|
3867
|
-
'只给单位不给参数则列出全部可用单位。',
|
|
3938
|
+
description: '源单位(units)。支持中文古制(里/丈/斤/亩/时辰/刻)、英制(英里/磅/加仑)、天文(AU/光年)。只给单位不给数值则列全表。',
|
|
3868
3939
|
},
|
|
3869
3940
|
unitTo: {
|
|
3870
3941
|
type: 'string',
|
|
@@ -3878,13 +3949,11 @@ const PARAMETER_SPEC = {
|
|
|
3878
3949
|
balance: {
|
|
3879
3950
|
type: 'object',
|
|
3880
3951
|
additionalProperties: true,
|
|
3881
|
-
description: '收支盘点(
|
|
3882
|
-
'只判社会收支;单项生理/工程上限用 feasibility。',
|
|
3952
|
+
description: '收支盘点(balance):每项 {demand, supply, unit?},给最短板。单项上限用 feasibility。',
|
|
3883
3953
|
},
|
|
3884
3954
|
bodyName: {
|
|
3885
3955
|
type: 'string',
|
|
3886
|
-
description: '
|
|
3887
|
-
'已收录太阳系全 8 大行星 + 月球类卫星 + 代表性系外行星 + 矮行星 + 各亮度级恒星。',
|
|
3956
|
+
description: '查天体(bodies):id(mars)或中文名(火星)。含行星、卫星、系外行星、矮行星、各级恒星。',
|
|
3888
3957
|
},
|
|
3889
3958
|
bodyKind: {
|
|
3890
3959
|
type: 'string',
|
|
@@ -3892,8 +3961,7 @@ const PARAMETER_SPEC = {
|
|
|
3892
3961
|
},
|
|
3893
3962
|
bodyConstant: {
|
|
3894
3963
|
type: 'string',
|
|
3895
|
-
description: 'bodies:改常数后看哪些天体数据该重算。例
|
|
3896
|
-
'输出分「必须重算」与「绝不该动」两块(行星质量/半径/轨道与 α 无关)。',
|
|
3964
|
+
description: 'bodies:改常数后看哪些天体数据该重算。例 alpha_fs + bodyFactor=2。分「必须重算」与「绝不该动」两块。',
|
|
3897
3965
|
},
|
|
3898
3966
|
bodyFactor: {
|
|
3899
3967
|
type: 'number',
|
|
@@ -3922,8 +3990,7 @@ const PARAMETER_SPEC = {
|
|
|
3922
3990
|
},
|
|
3923
3991
|
displayPrecision: {
|
|
3924
3992
|
type: 'integer',
|
|
3925
|
-
description: '**显示精度:小数点后几位**(0-64,默认 8
|
|
3926
|
-
'可用 workspace set displayPrecision=N 固化(那里的 0 = 清除)。',
|
|
3993
|
+
description: '**显示精度:小数点后几位**(0-64,默认 8)。只截小数、整数完整,0=只留整数。可 workspace set 固化(0=清除)。',
|
|
3927
3994
|
},
|
|
3928
3995
|
data: {
|
|
3929
3996
|
type: 'array',
|
|
@@ -3951,27 +4018,29 @@ const PARAMETER_SPEC = {
|
|
|
3951
4018
|
},
|
|
3952
4019
|
formula: {
|
|
3953
4020
|
type: 'string',
|
|
3954
|
-
description: 'operation="formula"
|
|
3955
|
-
'用 operation="formulas" 检索可用公式。',
|
|
4021
|
+
description: 'operation="formula":公式 id 或名称,如 "escape_velocity"、"逃逸速度"。检索用 operation="formulas";多条用 formulas 数组。',
|
|
3956
4022
|
},
|
|
4023
|
+
formulas: { type: 'array', items: { type: 'json', description: '"kleiber" 或 {formula, overrides?, label?}' }, description: '批量调多条公式;一条失败不影响其它条。' },
|
|
3957
4024
|
overrides: {
|
|
3958
4025
|
type: 'object',
|
|
3959
4026
|
additionalProperties: true,
|
|
3960
|
-
description: '
|
|
4027
|
+
description: '覆盖公式任意参数(架空改造的关键)。例:重力 1.5 倍 {"g":14.71}、光速 {"c":1e8}。',
|
|
3961
4028
|
},
|
|
3962
4029
|
customExpression: {
|
|
3963
4030
|
type: 'string',
|
|
3964
|
-
description: 'structure
|
|
4031
|
+
description: 'structure 级改造:临时替换公式表达式,沿用参数名。例 customExpression="m*g*1.3" 即重力 +30%。',
|
|
3965
4032
|
},
|
|
3966
4033
|
query: {
|
|
3967
4034
|
type: 'string',
|
|
3968
4035
|
description: 'operation="formulas" 的检索关键词,例如 "重力"、"熵"、"人口";传 "*" 或留空列出全部分类。',
|
|
3969
4036
|
},
|
|
4037
|
+
recurrence: { type: 'string', description: 'operation="recurrence":下一期 = f(本期),如 "P*2"、"P*min(1.2,1000/P)"。配 initial 与 periods 出逐期表。' },
|
|
4038
|
+
periods: { type: 'integer', description: 'recurrence 的迭代期数(默认 12,上限 5000)。' },
|
|
4039
|
+
at: { type: 'array', items: { type: 'number' }, description: 'ode 要精确输出的时点,如 [5,10,20](写年表用)。' },
|
|
3970
4040
|
initial: {
|
|
3971
4041
|
type: 'object',
|
|
3972
4042
|
additionalProperties: true,
|
|
3973
|
-
description: '
|
|
3974
|
-
'二阶方程降阶后可能还需要辅助变量(如 y_d 表示 dy/dt),缺了会提示补哪个。',
|
|
4043
|
+
description: 'ode/recurrence 初值,如 {"S":999,"I":1}。二阶降阶需辅助变量 y_d=dy/dt,缺了会提示。',
|
|
3975
4044
|
},
|
|
3976
4045
|
from: {
|
|
3977
4046
|
type: 'number',
|
|
@@ -3994,9 +4063,7 @@ const PARAMETER_SPEC = {
|
|
|
3994
4063
|
additionalProperties: true,
|
|
3995
4064
|
// 字段清单不写在 schema 里:schema 每轮都要付费,而这些字段只在
|
|
3996
4065
|
// 真用 world 时才需要。全文见 help(operation="help", query="world")。
|
|
3997
|
-
description: '星球参数(
|
|
3998
|
-
'常用键:planetMass/planetRadius、starMass/starTemp、rotationHours、obliquityDeg。' +
|
|
3999
|
-
'完整字段表见 help。',
|
|
4066
|
+
description: '星球参数(world),可选,默认地球。常用键 planetMass、starMass、rotationHours、obliquityDeg。完整见 help。',
|
|
4000
4067
|
},
|
|
4001
4068
|
planetRadius: {
|
|
4002
4069
|
type: 'number',
|
|
@@ -4018,15 +4085,13 @@ const PARAMETER_SPEC = {
|
|
|
4018
4085
|
type: 'object',
|
|
4019
4086
|
additionalProperties: true,
|
|
4020
4087
|
// 同上:地形字段有九项,逐项列在 schema 里太贵。
|
|
4021
|
-
description: '局部地形(
|
|
4022
|
-
'常用键:kind、elevation、latitudeDeg、oceanCurrent。完整键表见 help。',
|
|
4088
|
+
description: '局部地形(world 附带)。常用键 kind、elevation、latitudeDeg、oceanCurrent。完整见 help。',
|
|
4023
4089
|
},
|
|
4024
4090
|
constants: {
|
|
4025
4091
|
type: 'object',
|
|
4026
4092
|
additionalProperties: true,
|
|
4027
4093
|
// 旋钮清单移进 help:只有要改常数时才需要,不该每轮付费
|
|
4028
|
-
description: '要改的物理常数(
|
|
4029
|
-
'例:{c:1.5e8}。可用旋钮清单见 help 的 constants 节。',
|
|
4094
|
+
description: '要改的物理常数(constants):只改基础旋钮,派生量自动重算。例 {c:1.5e8}。旋钮清单见 help。',
|
|
4030
4095
|
},
|
|
4031
4096
|
helpTopic: {
|
|
4032
4097
|
type: 'string',
|
|
@@ -4100,6 +4165,96 @@ function checkPrecisionTruncation(requested, effective, text) {
|
|
|
4100
4165
|
' 位可能已失真。需要精确值时请用 precision:64(默认),' +
|
|
4101
4166
|
'只想让输出短些请用 displayPrecision。';
|
|
4102
4167
|
}
|
|
4168
|
+
/**
|
|
4169
|
+
* 批量调公式(`formulas: [...]`)。
|
|
4170
|
+
*
|
|
4171
|
+
* ## 为什么复用 opFormula 而不是重写一套
|
|
4172
|
+
*
|
|
4173
|
+
* 单个调用涉及:严格解析、overrides 覆盖、单位换算、terse 精简、
|
|
4174
|
+
* 适用域提示、未生效 key 上报 —— 这些逻辑**必须与单条完全一致**。
|
|
4175
|
+
* 重写一套必然漂移,而「单条能算、批量算得不一样」正是这个项目
|
|
4176
|
+
* 反复修过的那类缺陷(1107 报告的批量非有限值、单条/批量输出不一致)。
|
|
4177
|
+
*
|
|
4178
|
+
* 所以这里只做三件事:逐条调用、收集结果、汇总成败。
|
|
4179
|
+
*
|
|
4180
|
+
* ## 失败语义
|
|
4181
|
+
*
|
|
4182
|
+
* 一条失败 → 该条在输出里标 `失败[type]`,继续算下一条,
|
|
4183
|
+
* 最后由 `ok`/`failed`/`succeeded` 如实汇总。**不因为一条错就中断整批**。
|
|
4184
|
+
*
|
|
4185
|
+
* @param args - 含 `formulas` 数组的入参
|
|
4186
|
+
* @param engine - mathjs 实例
|
|
4187
|
+
* @param precision - 计算精度
|
|
4188
|
+
* @returns 批量结果
|
|
4189
|
+
*/
|
|
4190
|
+
function opFormulaBatch(args, engine, precision) {
|
|
4191
|
+
const list = args.formulas;
|
|
4192
|
+
const lines = [];
|
|
4193
|
+
const rows = [];
|
|
4194
|
+
let failed = 0;
|
|
4195
|
+
lines.push('═══ 批量公式(' + list.length + ' 条)═══');
|
|
4196
|
+
lines.push('');
|
|
4197
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
4198
|
+
const item = list[index];
|
|
4199
|
+
const isObject = item !== null && typeof item === 'object';
|
|
4200
|
+
const id = String(isObject ? (item.formula ?? item.id ?? '') : item).trim();
|
|
4201
|
+
const label = isObject && item.label !== undefined ? String(item.label) : '';
|
|
4202
|
+
/** 逐条构造入参:继承顶层公共参数(terse/unitFrom/unitTo/precision),再叠加元素自己的。 */
|
|
4203
|
+
const oneArgs = {
|
|
4204
|
+
...args,
|
|
4205
|
+
formulas: undefined,
|
|
4206
|
+
formula: id,
|
|
4207
|
+
expression: undefined,
|
|
4208
|
+
overrides: isObject && item.overrides ? item.overrides : (isObject ? undefined : args.overrides),
|
|
4209
|
+
};
|
|
4210
|
+
oneArgs.formulas = undefined;
|
|
4211
|
+
if (isObject && item.terse !== undefined)
|
|
4212
|
+
oneArgs.terse = item.terse;
|
|
4213
|
+
const prefix = label === '' ? '' : label + '(' + id + ')';
|
|
4214
|
+
try {
|
|
4215
|
+
const outcome = opFormula(oneArgs, engine, precision);
|
|
4216
|
+
const text = String(outcome.result ?? '').trim();
|
|
4217
|
+
/**
|
|
4218
|
+
* 抓「结果:」那一行 —— 公式输出的第一行是标题(名称 + 分类),
|
|
4219
|
+
* 数值在「结果:」行上。只取首行会得到 `克莱伯定律(代谢率) [physics/scaling]`,
|
|
4220
|
+
* 数值整个丢了,而批量恰恰是为了看一串数。
|
|
4221
|
+
*/
|
|
4222
|
+
const valueLine = text.split(/\r?\n/).find(function (line) { return line.trim().startsWith('结果'); });
|
|
4223
|
+
const first = valueLine ?? (text.split(/\r?\n/).find(function (line) { return line.trim() !== ''; }) ?? '');
|
|
4224
|
+
/** terse 输出本身已是 `kleiber = 31622…`,别再加一层 `id = ` 变成 `kleiber = kleiber = …`。 */
|
|
4225
|
+
const alreadyNamed = String(first).trim().startsWith(id + ' =');
|
|
4226
|
+
const shown = String(first).replace(/^结果\s*[::]\s*/, '').trim();
|
|
4227
|
+
const body = alreadyNamed ? shown : id + ' = ' + shown;
|
|
4228
|
+
lines.push(' ' + (prefix === '' ? body : prefix + ' = ' + shown));
|
|
4229
|
+
rows.push({ formula: id, label: label === '' ? null : label, ok: true, result: outcome.value ?? null });
|
|
4230
|
+
}
|
|
4231
|
+
catch (error) {
|
|
4232
|
+
failed += 1;
|
|
4233
|
+
const type = error?.type ?? 'unknown';
|
|
4234
|
+
const message = String(error?.message ?? error).slice(0, 120);
|
|
4235
|
+
lines.push(' ' + (prefix === '' ? id : prefix) + ' → 失败[' + type + '] ' + message);
|
|
4236
|
+
rows.push({ formula: id, label: label === '' ? null : label, ok: false, error: type });
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
const succeeded = list.length - failed;
|
|
4240
|
+
lines.push('');
|
|
4241
|
+
lines.push(failed === 0
|
|
4242
|
+
? '全部 ' + succeeded + ' 条都有结果(只表示公式存在且参数可算,不含结论正确性判断)'
|
|
4243
|
+
: '成功 ' + succeeded + ',失败 ' + failed + '(失败条已逐条标注原因)');
|
|
4244
|
+
/**
|
|
4245
|
+
* 汇总字段必须**真的出现在返回值里**:`ok` / `failed` / `succeeded` / `partial`。
|
|
4246
|
+
*
|
|
4247
|
+
* 踩过:`{...cond ? {...} : {...}}` 这种写法在 Outcome 类型下会被类型断言吞掉,
|
|
4248
|
+
* 实测返回 `ok=true` 却 `failed=undefined` —— 而正文里明明写着「成功 0,失败 3」。
|
|
4249
|
+
* 正文与结构化字段不一致,比只给正文更糟(调用方读哪个?)。
|
|
4250
|
+
* 所以改成显式赋值。
|
|
4251
|
+
*/
|
|
4252
|
+
/** 汇总字段放进 `value`,由调用方(`runMathTool`)提升到顶层 `MathResult`。 */
|
|
4253
|
+
return {
|
|
4254
|
+
result: lines.join('\n'),
|
|
4255
|
+
value: toJsonValue({ count: list.length, succeeded, failed, rows }),
|
|
4256
|
+
};
|
|
4257
|
+
}
|
|
4103
4258
|
/**
|
|
4104
4259
|
* 数学计算工具(`math_execute`)。
|
|
4105
4260
|
*
|
package/lib/ode.js
CHANGED
|
@@ -240,6 +240,9 @@ export function solveOde(engine, system, options) {
|
|
|
240
240
|
const sampleLimit = Math.max(2, options.sampleLimit ?? 24);
|
|
241
241
|
const sampleEvery = Math.max(1, Math.floor(steps / sampleLimit));
|
|
242
242
|
const samples = [];
|
|
243
|
+
/** 待消费的指定时点(已在调用方排好序)与已记录结果。 */
|
|
244
|
+
const atQueue = (options.atPoints ?? []).filter((point) => Number.isFinite(point) && point >= from && point <= to);
|
|
245
|
+
const atRecorded = [];
|
|
243
246
|
const warnings = [];
|
|
244
247
|
const record = (time, values) => {
|
|
245
248
|
const map = {};
|
|
@@ -298,7 +301,37 @@ export function solveOde(engine, system, options) {
|
|
|
298
301
|
if ((stepIndex + 1) % sampleEvery === 0 || stepIndex === steps - 1) {
|
|
299
302
|
record(t, state);
|
|
300
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* 指定时点:走到该点附近就记一次。
|
|
306
|
+
*
|
|
307
|
+
* 判据是「已跨过」而不是「恰好等于」—— RK4 的步长一般除不尽,
|
|
308
|
+
* 想要 t=5 时通常永远走不到 5.000000。跨过时取**跨过点的值**,
|
|
309
|
+
* 与主解同源,比插值准。
|
|
310
|
+
*/
|
|
311
|
+
if (atQueue.length > 0 && t >= atQueue[0] && from <= to) {
|
|
312
|
+
if (atRecorded.length === 0 || atRecorded[atRecorded.length - 1].t !== atQueue[0]) {
|
|
313
|
+
const values = {};
|
|
314
|
+
for (let index = 0; index < dimension; index += 1)
|
|
315
|
+
values[variables[index]] = state[index];
|
|
316
|
+
atRecorded.push({ t: atQueue.shift(), values });
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
atQueue.shift();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* 没被循环消费到的时点(落在终点之后或步长太粗)用终点状态补上,
|
|
325
|
+
* 不能静默丢弃 —— 请求了 `at:[20]` 却什么都不返回,比报错更糟。
|
|
326
|
+
*/
|
|
327
|
+
for (const point of atQueue) {
|
|
328
|
+
const values = {};
|
|
329
|
+
for (let index = 0; index < dimension; index += 1)
|
|
330
|
+
values[variables[index]] = state[index];
|
|
331
|
+
atRecorded.push({ t: point, values });
|
|
301
332
|
}
|
|
333
|
+
atQueue.length = 0;
|
|
334
|
+
atRecorded.sort((a, b) => a.t - b.t);
|
|
302
335
|
const final = {};
|
|
303
336
|
for (let index = 0; index < dimension; index += 1)
|
|
304
337
|
final[variables[index]] = state[index];
|
|
@@ -313,6 +346,7 @@ export function solveOde(engine, system, options) {
|
|
|
313
346
|
samples,
|
|
314
347
|
final,
|
|
315
348
|
extremes,
|
|
349
|
+
atValues: atRecorded,
|
|
316
350
|
steps,
|
|
317
351
|
stepSize,
|
|
318
352
|
warnings,
|
package/lib/ops/ode-op.js
CHANGED
|
@@ -33,13 +33,33 @@ export function opOde(args, engine, precision) {
|
|
|
33
33
|
const to = typeof args.to === 'number' ? args.to : 10;
|
|
34
34
|
const steps = typeof args.steps === 'number' ? Math.min(1_000_000, Math.max(1, Math.round(args.steps))) : 1000;
|
|
35
35
|
const sampleLimit = typeof args.samples === 'number' ? Math.max(2, Math.min(200, Math.round(args.samples))) : 24;
|
|
36
|
+
/**
|
|
37
|
+
* ## 指定采样时点(`at: [5, 10, 20]`)
|
|
38
|
+
*
|
|
39
|
+
* 实测反馈(能力评估文档 3.3 节):
|
|
40
|
+
*
|
|
41
|
+
* > 我要「第 5 年、第 10 年、第 20 年」的值,`ode` 给的是均匀采样:
|
|
42
|
+
* > `t = 1, 2.9, 4.8, 6.7, 8.6, 10.5, 12.4, …` —— **没有一年是我要的**。
|
|
43
|
+
*
|
|
44
|
+
* 而「写年表」恰恰是这类推演最常见的需求:写作时写的是
|
|
45
|
+
* 「第 5 年 1,756 人」这种具体整数,插值出来的 1,750.3 用不上。
|
|
46
|
+
*
|
|
47
|
+
* 实现方式:**在给定时点补算**,不是在采样表里插值。
|
|
48
|
+
* 用同一套 RK4 从区间起点走到该时点,结果与主解一致、无插值误差。
|
|
49
|
+
*/
|
|
50
|
+
const atPoints = Array.isArray(args.at)
|
|
51
|
+
? args.at
|
|
52
|
+
.map((value) => Number(value))
|
|
53
|
+
.filter((value) => Number.isFinite(value))
|
|
54
|
+
.sort((a, b) => a - b)
|
|
55
|
+
: [];
|
|
36
56
|
let solution;
|
|
37
57
|
try {
|
|
38
58
|
// 关键:数值积分必须用 Number 引擎,不能用 BigNumber 引擎。
|
|
39
59
|
// RK4 的中间值天然带 15 位以上小数(如 1.0050251567199238),
|
|
40
60
|
// BigNumber 会抛 "Cannot implicitly convert a number with >15 significant digits"。
|
|
41
61
|
// 数值 ODE 本身就只精度到 ~1e-12,用 BigNumber 毫无意义且必然失败。
|
|
42
|
-
solution = solveOde(fast(), system, { initial, from, to, steps, sampleLimit });
|
|
62
|
+
solution = solveOde(fast(), system, { initial, from, to, steps, sampleLimit, atPoints });
|
|
43
63
|
}
|
|
44
64
|
catch (error) {
|
|
45
65
|
if (error instanceof OdeError) {
|
|
@@ -72,6 +92,27 @@ export function opOde(args, engine, precision) {
|
|
|
72
92
|
];
|
|
73
93
|
lines.push(cells.join(' '));
|
|
74
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* 指定时点的值单独一段 —— 这是写年表时真正要抄的数。
|
|
97
|
+
*
|
|
98
|
+
* 放在终点值之前:读者的目的常常是「拿第 5/10/20 年」,
|
|
99
|
+
* 而不是「拿终点」。
|
|
100
|
+
*/
|
|
101
|
+
const atValues = solution.atValues ?? [];
|
|
102
|
+
if (atValues.length > 0) {
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push('指定时点:');
|
|
105
|
+
const atHeader = [system.independent.padEnd(12), ...shownVariables.map((name) => name.padStart(14))];
|
|
106
|
+
lines.push(atHeader.join(' '));
|
|
107
|
+
lines.push('-'.repeat(atHeader.join(' ').length));
|
|
108
|
+
for (const point of atValues) {
|
|
109
|
+
const cells = [
|
|
110
|
+
trimNumber(point.t, 6).padEnd(12),
|
|
111
|
+
...shownVariables.map((name) => trimNumber(point.values[name] ?? Number.NaN, precision).padStart(14)),
|
|
112
|
+
];
|
|
113
|
+
lines.push(cells.join(' '));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
75
116
|
lines.push('');
|
|
76
117
|
lines.push('终点值:');
|
|
77
118
|
for (const name of shownVariables) {
|