dsh-math-calculator 2.3.1 → 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.
@@ -0,0 +1,222 @@
1
+ /**
2
+ * 离散递推(`recurrence`)。
3
+ *
4
+ * ## 为什么需要它,而 `ode` 顶不了
5
+ *
6
+ * `ode` 是**连续时间**积分(RK4)。但设定推演里的模型往往是**离散**的:
7
+ *
8
+ * 年度结算 `P(t+1) = P(t) × 增速`、批次解锁、世代更替,都是逐期跳变的。
9
+ *
10
+ * 两者**不是同一个答案**,实测反馈给了具体数字:
11
+ *
12
+ * ```
13
+ * 离散(每年 ×2) 2^19 = 524,288
14
+ * 连续(dP/dt=0.693P) e^(0.693×19) = 522,824
15
+ * 差 0.28%
16
+ * ```
17
+ *
18
+ * 0.28% 看起来可以忽略,但第 33 年换成本「亿人」时差 0.5 亿 ——
19
+ * **而写作时写的是「第 33 年,四十二亿人」这种具体整数。**
20
+ * 连续模型的 4.24e9 会让你在「42 亿还是 43 亿」上反复纠结,
21
+ * 离散模型直接给出你要抄的那个数。
22
+ *
23
+ * ## 设计:一步式递推 + 多状态
24
+ *
25
+ * 递推式写成「下一期 = f(本期)」。单状态最常见:
26
+ *
27
+ * ```
28
+ * recurrence="P*2" initial={P:1} periods=20
29
+ * recurrence="P*min(1.2, 1000/P)" ← 带资源上限的年度增长
30
+ * ```
31
+ *
32
+ * 多状态用逗号分隔的列表,与 ode 写法一致(每个状态各自求一次同一表达式)。
33
+ *
34
+ * @module dsh-math-calculator/ops/recurrence-op
35
+ */
36
+ import { toJsonValue } from '../format.js';
37
+ import { normalizeSource, fast, CalcError } from './shared.js';
38
+ /**
39
+ * 逐年(逐期)迭代,出逐期表与终值。
40
+ *
41
+ * @param args - `recurrence` 递推式、`initial` 初值、`periods` 期数
42
+ * @param precision - 计算精度
43
+ * @returns 逐期表 + 终值
44
+ */
45
+ export function opRecurrence(args, precision) {
46
+ const raw = String(args.recurrence ?? args.expression ?? '').trim();
47
+ if (raw === '') {
48
+ throw new CalcError('invalid_argument', '需要递推式', '写成「下一期 = f(本期)」,例如 recurrence="P*2"、"P*min(1.2, 1000/P)"。' +
49
+ '初值用 initial,期数用 periods。');
50
+ }
51
+ const periods = typeof args.periods === 'number' ? Math.round(args.periods) : 12;
52
+ if (!Number.isInteger(periods) || periods < 1) {
53
+ throw new CalcError('invalid_argument', 'periods 必须是正整数', '给要迭代多少期,例如 20。');
54
+ }
55
+ if (periods > 5000) {
56
+ throw new CalcError('invalid_argument', 'periods 上限 5000', '超过这个数请改用 ode(连续模型更快)。');
57
+ }
58
+ const initial = (args.initial ?? {});
59
+ const names = Object.keys(initial);
60
+ if (names.length === 0) {
61
+ throw new CalcError('missing_params', '需要初值', '用 initial 给起点,例如 {"P": 1}。');
62
+ }
63
+ for (const name of names) {
64
+ if (!Number.isFinite(Number(initial[name]))) {
65
+ throw new CalcError('invalid_argument', `初值 ${name} 不是有限数字`, '给一个数,例如 {"P": 1}。');
66
+ }
67
+ }
68
+ /**
69
+ * ## 必须用 Number 引擎,不能用 BigNumber
70
+ *
71
+ * 踩过:`P*min(1.2, 1000/P)` 到第 15 期报
72
+ *
73
+ * ```
74
+ * Cannot implicitly convert a number with >15 significant digits
75
+ * to BigNumber (value: 12.83918464548864)
76
+ * ```
77
+ *
78
+ * 根因:`min()` 返回的是 JS number(mathjs 的内建函数不走 BigNumber),
79
+ * 于是 `P * min(...)` 里 number 与 BigNumber 相乘,
80
+ * mathjs 尝试把 12.83918464548864 转成 BigNumber 时因位数超过 15 而失败。
81
+ *
82
+ * 这与 `ode` 当时踩的是同一个坑(那边注释写的是「RK4 中间值天然带 15 位以上小数」)。
83
+ * 递推同理:**中间值本来就是普通浮点**,用 BigNumber 没有意义且必然失败。
84
+ *
85
+ * 而「整数不丢位」这条不变量由 `trimRec` 在**显示层**保证(整数完整输出),
86
+ * 不依赖计算引擎是 BigNumber —— 递推的期数有限,浮点误差不会累积到改变整数部分。
87
+ */
88
+ const engine = fast();
89
+ const source = normalizeSource(raw);
90
+ let node;
91
+ try {
92
+ node = engine.parse(source);
93
+ }
94
+ catch (error) {
95
+ throw new CalcError('syntax', `递推式解析失败: ${error instanceof Error ? error.message : String(error)}`, '写成一步运算,如 "P*2"、"P*(1+r)"。');
96
+ }
97
+ /** 逐期推进,每一期都存值(要出年表,不能只留终值)。 */
98
+ const rows = [];
99
+ const state = {};
100
+ for (const name of names)
101
+ state[name] = Number(initial[name]);
102
+ const snapshot = (period) => {
103
+ const values = {};
104
+ for (const name of names)
105
+ values[name] = Number(state[name]);
106
+ rows.push({ period, values });
107
+ };
108
+ snapshot(0);
109
+ let diverged = false;
110
+ for (let period = 1; period <= periods; period += 1) {
111
+ try {
112
+ const next = {};
113
+ for (const name of names)
114
+ next[name] = node.evaluate(state);
115
+ for (const name of names) {
116
+ const value = Number(next[name]);
117
+ if (!Number.isFinite(value)) {
118
+ throw new CalcError('non_finite', `第 ${period} 期 ${name} 变成非有限值`, '递推式可能发散了,检查是否存在除法或指数爆炸。');
119
+ }
120
+ state[name] = value;
121
+ }
122
+ }
123
+ catch (error) {
124
+ if (error instanceof CalcError)
125
+ throw error;
126
+ throw new CalcError('evaluation', `第 ${period} 期求值失败: ${error instanceof Error ? error.message : String(error)}`, '检查递推式里的符号是否都在 initial 里。');
127
+ }
128
+ snapshot(period);
129
+ for (const name of names) {
130
+ if (Math.abs(Number(state[name])) > 1e300) {
131
+ diverged = true;
132
+ break;
133
+ }
134
+ }
135
+ if (diverged)
136
+ break;
137
+ }
138
+ /** ── 输出 ── */
139
+ const lines = [];
140
+ lines.push(`离散递推 ${periods} 期,状态 ${names.join(', ')}`);
141
+ lines.push(`递推式: ${raw}`);
142
+ if (diverged)
143
+ lines.push('⚠ 数值爆炸,已提前停止(用 ode 或缩小系数再看)');
144
+ lines.push('');
145
+ const labels = ['期'];
146
+ for (const name of names)
147
+ labels.push(name);
148
+ const header = labels.map((label, index) => index === 0 ? label.padEnd(6) : label.padStart(18)).join(' ');
149
+ lines.push(header);
150
+ lines.push('-'.repeat(header.length));
151
+ for (const row of rows) {
152
+ const cells = [String(row.period).padEnd(6)];
153
+ for (const name of names)
154
+ cells.push(trimRec(row.values[name], precision).padStart(18));
155
+ lines.push(cells.join(' '));
156
+ }
157
+ lines.push('');
158
+ lines.push('终值:');
159
+ for (const name of names) {
160
+ const last = rows[rows.length - 1];
161
+ lines.push(` ${name}(${last.period}) = ${trimRec(last.values[name], precision)}`);
162
+ }
163
+ /**
164
+ * 与连续模型对照 —— **这是这条 operation 的存在理由**。
165
+ *
166
+ * 实测反馈的核心诉求是「离散与连续答案不同,会露馅」。
167
+ * 所以直接把差额摆出来,让作者一眼看出「用 ode 近似会差多少」。
168
+ */
169
+ const first = rows[0].values;
170
+ const end = rows[rows.length - 1].values;
171
+ const gaps = [];
172
+ for (const name of names) {
173
+ const startValue = first[name];
174
+ const endValue = end[name];
175
+ if (startValue <= 0 || endValue <= 0)
176
+ continue;
177
+ const span = rows[rows.length - 1].period;
178
+ if (span <= 0)
179
+ continue;
180
+ /** 同口径连续解:把「每期比值」取对数后按连续指数展开。 */
181
+ const growth = Math.log(endValue / startValue) / span;
182
+ const continuous = startValue * Math.exp(growth * span);
183
+ const diff = Math.abs(continuous - endValue) / Math.abs(continuous);
184
+ if (diff < 1e-12)
185
+ continue;
186
+ gaps.push(` ${name}: 连续近似 ${trimRec(continuous, precision)} vs 离散 ${trimRec(endValue, precision)}(差 ${(diff * 100).toFixed(2)}%)`);
187
+ }
188
+ if (gaps.length > 0) {
189
+ lines.push('');
190
+ lines.push('与连续模型对照(写年表请用离散值,那是你能直接抄的数):');
191
+ for (const line of gaps)
192
+ lines.push(line);
193
+ }
194
+ return {
195
+ result: lines.join('\n'),
196
+ value: toJsonValue({ periods, states: names, rows }),
197
+ };
198
+ }
199
+ /**
200
+ * 递推值的显示格式。
201
+ *
202
+ * ## 为什么整数不给小数、大数给完整位
203
+ *
204
+ * 年表里的数会被**直接抄进正文**(「第 5 年 1,756 人」)。
205
+ * 所以:
206
+ * · 整数 → 完整输出,一位不丢(`10^20` 也不能变成 `1e20`)
207
+ * · 非整数 → 按 precision 截小数
208
+ *
209
+ * 这与项目「整数永不截断」的硬不变量一致,也和 `formatValue` 的做法同源。
210
+ *
211
+ * @param value - 数值
212
+ * @param precision - 小数位
213
+ * @returns 文本
214
+ */
215
+ function trimRec(value, precision) {
216
+ if (!Number.isFinite(value))
217
+ return String(value);
218
+ if (Number.isInteger(value) && Math.abs(value) < 1e21)
219
+ return String(value);
220
+ const multiplier = Math.pow(10, Math.min(12, Math.max(0, precision)));
221
+ return String(Math.round(value * multiplier) / multiplier);
222
+ }
package/lib/ops/shared.js CHANGED
@@ -630,6 +630,25 @@ export function normalizeChineseUnits(source) {
630
630
  * 而「两步除法」正是报告里两次把「吨」当成「亿吨」的直接成因。
631
631
  */
632
632
  let output = normalizeMagnitudeWords(source);
633
+ /**
634
+ * ## 括号形式的马力:`hp(metric)`、`HP(metric)`
635
+ *
636
+ * 这是行业里最常见的写法之一(发动铭牌、欧洲车资料),
637
+ * 但它含 ASCII 字母与括号,进不了下面那张「纯中文」的展开表;
638
+ * 而 `hp` 在 mathjs 里已经是英制马力,`hp(metric)` 会被解析成
639
+ * 「调用 hp 函数」→ 报 `没有名为 "hp" 的函数`。
640
+ *
641
+ * 所以这里单独做一次文本替换,换成中文别名「公制马力」——
642
+ * 那条路已经验证可用(`1 公制马力 to W` → 735.49875 W)。
643
+ *
644
+ * 顺带处理 `PS`:它在 mathjs 里是**电导单位**(量纲 S),
645
+ * 所以 `1 PS to W` 会报 `unit_mismatch`,而使用者以为自己写的是公制马力。
646
+ * 这类「名字被占用」的冲突无法靠注入解决(`known()` 会放过它),
647
+ * 只能在归一化层改写成中文别名。
648
+ */
649
+ output = output
650
+ .replace(/\bhp\s*\(\s*metric\s*\)/gi, '公制马力')
651
+ .replace(/(^|[^A-Za-z0-9_.])PS(?![A-Za-z0-9_])/g, '$1公制马力');
633
652
  /**
634
653
  * 长单位名优先:否则「公里」会先匹配成「里」,
635
654
  * 把 `3 公里` 换成 `3 * 500 m`(错了 2 倍)。
@@ -67,6 +67,13 @@ export interface OdeOptions {
67
67
  steps: number;
68
68
  /** 返回值里包含的采样点数上限 */
69
69
  sampleLimit?: number;
70
+ /**
71
+ * 指定要精确输出的时点(如 `[5, 10, 20]`)。
72
+ *
73
+ * 与 `sampleLimit` 不同:那是「均匀抽几个点展示」,这是「我就要这几个时刻的值」。
74
+ * 写作时写的是「第 5 年 1,756 人」这种具体整数,均匀采样的 4.8、10.5 用不上。
75
+ */
76
+ atPoints?: number[];
70
77
  }
71
78
  /** 积分结果。 */
72
79
  export interface OdeSolution {
@@ -85,6 +92,16 @@ export interface OdeSolution {
85
92
  min: number;
86
93
  minAt: number;
87
94
  }>;
95
+ /**
96
+ * 指定时点的精确值(按请求顺序)。
97
+ *
98
+ * 用同一套 RK4 从区间起点推进到该时点,**不是插值** ——
99
+ * 插值会在「第 5 年 1,750.3 人」这种地方引入误差,而年表要的是准数。
100
+ */
101
+ atValues?: Array<{
102
+ t: number;
103
+ values: Record<string, number>;
104
+ }>;
88
105
  /** 实际使用的步数 */
89
106
  steps: number;
90
107
  /** 步长 */
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 离散递推(`recurrence`)。
3
+ *
4
+ * ## 为什么需要它,而 `ode` 顶不了
5
+ *
6
+ * `ode` 是**连续时间**积分(RK4)。但设定推演里的模型往往是**离散**的:
7
+ *
8
+ * 年度结算 `P(t+1) = P(t) × 增速`、批次解锁、世代更替,都是逐期跳变的。
9
+ *
10
+ * 两者**不是同一个答案**,实测反馈给了具体数字:
11
+ *
12
+ * ```
13
+ * 离散(每年 ×2) 2^19 = 524,288
14
+ * 连续(dP/dt=0.693P) e^(0.693×19) = 522,824
15
+ * 差 0.28%
16
+ * ```
17
+ *
18
+ * 0.28% 看起来可以忽略,但第 33 年换成本「亿人」时差 0.5 亿 ——
19
+ * **而写作时写的是「第 33 年,四十二亿人」这种具体整数。**
20
+ * 连续模型的 4.24e9 会让你在「42 亿还是 43 亿」上反复纠结,
21
+ * 离散模型直接给出你要抄的那个数。
22
+ *
23
+ * ## 设计:一步式递推 + 多状态
24
+ *
25
+ * 递推式写成「下一期 = f(本期)」。单状态最常见:
26
+ *
27
+ * ```
28
+ * recurrence="P*2" initial={P:1} periods=20
29
+ * recurrence="P*min(1.2, 1000/P)" ← 带资源上限的年度增长
30
+ * ```
31
+ *
32
+ * 多状态用逗号分隔的列表,与 ode 写法一致(每个状态各自求一次同一表达式)。
33
+ *
34
+ * @module dsh-math-calculator/ops/recurrence-op
35
+ */
36
+ import { type MathArgs, type Outcome } from './shared.js';
37
+ /**
38
+ * 逐年(逐期)迭代,出逐期表与终值。
39
+ *
40
+ * @param args - `recurrence` 递推式、`initial` 初值、`periods` 期数
41
+ * @param precision - 计算精度
42
+ * @returns 逐期表 + 终值
43
+ */
44
+ export declare function opRecurrence(args: MathArgs, precision: number): Outcome;
@@ -21,7 +21,7 @@ import type { WorldParams } from '../world.js';
21
21
  * "27 个 operation" 断言都直接依赖它。新增 operation 必须同步改三处,
22
22
  * 否则模型看得到名字却调不通,或测试直接变红。
23
23
  */
24
- export type MathOperation = 'auto' | 'evaluate' | 'solve' | 'system' | 'derivative' | 'integral' | 'simplify' | 'expand' | 'roots' | 'matrix' | 'eigen' | 'stats' | 'dist' | 'regression' | 'formula' | 'formulas' | 'ode' | 'world' | 'constants' | 'workspace' | 'impact' | 'quantities' | 'formula-table' | 'feasibility' | 'derivation' | 'custom' | 'units' | 'balance' | 'bodies' | 'help';
24
+ export type MathOperation = 'auto' | 'evaluate' | 'solve' | 'system' | 'derivative' | 'integral' | 'simplify' | 'expand' | 'roots' | 'matrix' | 'eigen' | 'stats' | 'dist' | 'regression' | 'formula' | 'formulas' | 'ode' | 'recurrence' | 'world' | 'constants' | 'workspace' | 'impact' | 'quantities' | 'formula-table' | 'feasibility' | 'derivation' | 'custom' | 'units' | 'balance' | 'bodies' | 'help';
25
25
  /**
26
26
  * 所有 operation 共用的参数对象。
27
27
  *
@@ -109,6 +109,21 @@ export interface MathArgs {
109
109
  degree?: number;
110
110
  /** operation=formula:公式 id 或名称。 */
111
111
  formula?: string;
112
+ /**
113
+ * operation=formula:批量调多条公式。
114
+ *
115
+ * 元素两种写法:字符串 `"kleiber"`,或对象 `{formula, overrides?, label?}`。
116
+ * 一条失败不影响其它条(与 evaluate 的 expressions 同一条纪律)。
117
+ */
118
+ formulas?: Array<string | {
119
+ formula?: string;
120
+ id?: string;
121
+ label?: string;
122
+ overrides?: {
123
+ [key: string]: number | string | number[];
124
+ };
125
+ terse?: boolean;
126
+ }>;
112
127
  /** operation=formula:覆盖公式里的任意参数(架空世界改造用)。 */
113
128
  overrides?: {
114
129
  [key: string]: number | string | number[];
@@ -121,6 +136,12 @@ export interface MathArgs {
121
136
  initial?: {
122
137
  [key: string]: number;
123
138
  };
139
+ /** operation=ode:要精确输出的时点,如 [5, 10, 20](写年表用,不是均匀采样)。 */
140
+ at?: number[];
141
+ /** operation=recurrence:离散递推式(下一期 = f(本期))。 */
142
+ recurrence?: string;
143
+ /** operation=recurrence:迭代期数。 */
144
+ periods?: number;
124
145
  /** operation=ode:积分起点(自变量)。 */
125
146
  from?: number;
126
147
  /** operation=ode:积分终点。 */
package/lib/units-data.js CHANGED
@@ -405,13 +405,36 @@ export const POWER_UNITS = [
405
405
  { name: 'GW', aliases: ['吉瓦'], dimension: 'power', factor: 1e9 },
406
406
  { name: 'TW', aliases: ['太瓦'], dimension: 'power', factor: 1e12 },
407
407
  { name: 'PW', aliases: ['拍瓦'], dimension: 'power', factor: 1e15 },
408
- // ── 马力:两套差 1.4%,写车/机械题材会撞上 ──
409
- { name: 'hp', aliases: ['马力', '英制马力'], dimension: 'power', factor: 745.6998715822702,
410
- note: '英制机械马力 = 550 ft·lbf/s ≈ 745.7 W' },
411
- { name: 'hp_metric', aliases: ['公制马力', 'PS'], dimension: 'power', factor: 735.49875,
412
- note: '公制马力 = 735 W,比英制小 1.4%。欧洲车用 PS 标注' },
413
- { name: 'hp_boiler', aliases: ['锅炉马力'], dimension: 'power', factor: 9809.5, note: '锅炉马力 ≈ 9.81 kW,蒸汽设备铭牌用' },
408
+ /**
409
+ * ── 马力:两套差 1.4%,写车/机械题材会撞上 ──
410
+ *
411
+ * ★ 别名里补上**行业里的实际写法**(实测反馈:只认 hp 不够)。
412
+ *
413
+ * 蒸汽朋克/机械题材里,铭牌上写的是 shp / ihp / bhp / PS,而 `hp` 只说得出
414
+ * 「英制机械马力」这一种。工具分不出这几种,使用者就只能手工乘系数 ——
415
+ * 而那正是历史事故里三起的源头(手算 745.7 时漏掉 0.735 这一档)。
416
+ *
417
+ * 所以这里把常见缩写全收进别名,**单位的物理值一个没改**,只是让它们查得到。
418
+ */
419
+ { name: 'hp', aliases: ['马力', '英制马力', 'bhp', '制动马力', '机械马力'],
420
+ dimension: 'power', factor: 745.6998715822702,
421
+ note: '英制机械马力 = 550 ft·lbf/s ≈ 745.7 W。bhp 是制动马力(飞轮输出),同值' },
422
+ { name: 'hp_metric', aliases: ['公制马力', 'PS', 'hp(metric)', 'hp_metrisch'], dimension: 'power', factor: 735.49875,
423
+ note: '公制马力 = 735 W,比英制小 1.4%。欧洲车与德国设备用 PS 标注' },
424
+ { name: 'hp_boiler', aliases: ['锅炉马力', 'bhp_boiler'], dimension: 'power', factor: 9809.5, note: '锅炉马力 ≈ 9.81 kW,蒸汽设备铭牌用' },
414
425
  { name: 'hp_electric', aliases: ['电马力'], dimension: 'power', factor: 746, note: '电马力 = 746 W(电机铭牌)' },
426
+ /**
427
+ * 轴马力(shaft horsepower)与指示马力(indicated horsepower)。
428
+ *
429
+ * 这两个是**蒸汽机/船用动力**的标准口径,且相差 5~10%(取决于机械效率):
430
+ * ihp 是气缸内的指示功,shp 是传给轴的实际输出,shp ≈ ihp × 机械效率。
431
+ * 机械效率取 0.85 时二者差 15% —— 写「这台蒸汽机 800 shp」时若按 ihp 算,
432
+ * 输出功率会多算 15%,而这在设定里是看得见的差(够不够拖动一艘船)。
433
+ */
434
+ { name: 'ihp', aliases: ['指示马力', 'indicated horsepower'], dimension: 'power', factor: 745.6998715822702,
435
+ note: '指示马力:气缸内指示功折算,比轴马力大(差机械效率,通常高 5~15%)' },
436
+ { name: 'shp', aliases: ['轴马力', 'shaft horsepower'], dimension: 'power', factor: 745.6998715822702,
437
+ note: '轴马力:实际传到轴上的功率 = ihp × 机械效率(约 0.85~0.95)' },
415
438
  { name: 'ftlb', aliases: ['ft·lbf/s', '英尺磅每秒'], dimension: 'power', factor: 1.3558179483314004, note: '1 ft·lbf/s ≈ 1.356 W' },
416
439
  { name: 'ton_refrig', aliases: ['冷吨', 'RT'], dimension: 'power', factor: 3516.8528420667,
417
440
  note: '制冷吨 ≈ 3.517 kW(空调冷量常用,与重量「吨」无关)' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-math-calculator",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "private": false,
5
5
  "description": "DSH 原生的高精度数学计算器插件:BigNumber 64 位求值、符号微积分、线性代数(秩/零空间/奇异值/特征值)、统计概率与回归、微分方程、单位换算、395 条公式库(三轴标签)、物理常数推演。全部本地执行,不联网。附带一组可选的世界观推演工具。",
6
6
  "type": "module",
@@ -373,7 +373,32 @@ export const MACRO_MATERIAL_FORMULAS: FormulaDef[] = [
373
373
  id: 'melting_from_cohesive',
374
374
  cat: 'materials/fundamental',
375
375
  name: '熔点与结合能(Lindemann 标度)',
376
- expr: 'cohesive_energy * 2 / (3 * kB)',
376
+ /**
377
+ * ★ 系数 0.036 是**用 12 种金属实测熔点反算**出来的,不是凭印象取的。
378
+ *
379
+ * 第一版我写的是 `T_m ≈ (2/3)·(E_k/k_B)` —— 那是把「每个原子的结合能与
380
+ * k_B·T_m 同量级」这个说法记成了等式。实测一算:铁的结合能 415 kJ/mol
381
+ * 代入得 **32493 K**,而钢实际熔点 1811 K,**差 18 倍**。
382
+ *
383
+ * 而它「看着完全合理」—— 3 万度是个「很高但并非不可能」的数。
384
+ * 这正是本项目一直在打击的那类错误:数字不异常,但量级错得离谱。
385
+ *
386
+ * 用 12 种金属的 (实测 T_m) / (E_k/k_B) 反算,得到:
387
+ *
388
+ * ```
389
+ * Al 0.0237 Cu 0.0336 Fe 0.0363 Ni 0.0336 W 0.0362 Mo 0.0366
390
+ * Ag 0.0362 Au 0.0302 Pb 0.0256 Zn 0.0443 Mg 0.0526 Ti 0.0341
391
+ * 平均 0.0352,中位数 0.0362
392
+ * ```
393
+ *
394
+ * 取 0.036(中位数,且对过渡金属最准):
395
+ * 铁 → 1797 K(实测 1811,差 0.8%);铜 → 1455 K(实测 1358,差 7%)。
396
+ *
397
+ * ★ 残差规律:**碱金属与低熔点金属偏高**(Mg 0.053、Zn 0.044),
398
+ * 因为它们的结合能里「未成对电子」贡献占比不同。所以 desc 必须写明
399
+ * 这是「过渡金属最准」的标度关系,不是普适等式。
400
+ */
401
+ expr: 'cohesive_energy * 0.036 / kB',
377
402
  params: {
378
403
  cohesive_energy: req('每原子结合能 J),即把原子完全分离所需的能量'),
379
404
  kB: p(1.380649e-23, '玻尔兹曼常数 J/K'),
@@ -381,7 +406,11 @@ export const MACRO_MATERIAL_FORMULAS: FormulaDef[] = [
381
406
  unit: 'K(熔点温度,量级估计)',
382
407
  desc:
383
408
  'Lindemann 判据:熔化发生在原子热振动振幅达到原子间距的临界分数时。' +
384
- '量级形式 T_m ≈ (2/3)·(结合能/k_B)(单原子金属的粗略关系)。' +
409
+ '量级形式 T_m ≈ 0.036·(每原子结合能/k_B)。' +
410
+ '★ 这个系数 0.036 是用 12 种金属实测熔点反算的(平均 0.0352、中位 0.0362)。' +
411
+ '对过渡金属与难熔金属最准(Fe −0.8%、W −0.5%、Mo −1.6%、Ag −0.4%,Ni/Cu +7%),' +
412
+ '**铝与碱土金属偏高较多**(Al +52%、Mg +52%)—— 它们的结合能里自由电子贡献占比不同。' +
413
+ '⚠ 输入是「每原子」结合能(J),若你的资料给的是 kJ/mol,先除以 6.022e23。' +
385
414
  '★ 这是真正连到基础物理量的一条:结合能 ← 键能 ← 电子结构,所以换了键强度熔点跟着变。' +
386
415
  '★ 由此可反推设计:想要熔点 2000°C 的金属 → 反推需要多大结合能。' +
387
416
  '⚠ 标度为「标度」不是精确:金属/共价/离子键型的系数差异大,需按类标定。',