進階(選修) — 用程式碼重現一遍
給熟悉程式的讀者:用純 JavaScript 一步步跟著模擬器的運算。
關於這個進階頁面:這裡是選修模組,針對熟悉程式的讀者,用純 JavaScript 把前一個進階頁模擬器的運算一步一步走完。對於「讀懂石腦油」本身並不需要這一頁。沒興趣的話,直接跳到模組 4(用案例來判斷)就好。
頁面用到的詞
先看的 4 個階段
- 輸入驗證 — 先確認數值是數字、是否落在範圍內。
- 組成歸一化 — 把 P / N / A 的原始權重重新縮放,讓總和是 1。
- 切取映射 — 把「輕 ↔ 重」對應到平均碳數與沸點範圍。
- 指標計算 — 回傳揮發性、密度、重整適性等概念分數。
模擬器使用的核心程式碼
實際在跑的核心是下面這段 JavaScript。沒有使用外部函式庫;違反前提的輸入會直接以 RangeError 停住。
(function (global) {
"use strict";
function assertFinite(name, value) {
if (!Number.isFinite(value)) {
throw new RangeError(name + " must be finite");
}
}
function assertRange(name, value, min, max) {
assertFinite(name, value);
if (value < min || value > max) {
throw new RangeError(name + " must be between " + min + " and " + max);
}
}
function normalizeComposition(raw) {
const paraffins = Number(raw.paraffins);
const naphthenes = Number(raw.naphthenes);
const aromatics = Number(raw.aromatics);
assertFinite("paraffins", paraffins);
assertFinite("naphthenes", naphthenes);
assertFinite("aromatics", aromatics);
if (paraffins < 0 || naphthenes < 0 || aromatics < 0) {
throw new RangeError("composition weights must be non-negative");
}
const total = paraffins + naphthenes + aromatics;
if (total <= 0) {
throw new RangeError("composition weights must sum to a positive number");
}
return {
paraffins: paraffins / total,
naphthenes: naphthenes / total,
aromatics: aromatics / total
};
}
function estimateCutProfile(cut) {
assertRange("cut", cut, 0, 1);
return {
averageCarbonNumber: 5.2 + 5.0 * cut,
boilingStartC: 30 + 55 * cut,
boilingEndC: 95 + 105 * cut
};
}
function estimateIndicators(comp, cut) {
assertRange("cut", cut, 0, 1);
assertRange("paraffins", comp.paraffins, 0, 1);
assertRange("naphthenes", comp.naphthenes, 0, 1);
assertRange("aromatics", comp.aromatics, 0, 1);
const p = comp.paraffins;
const n = comp.naphthenes;
const a = comp.aromatics;
const profile = estimateCutProfile(cut);
return {
averageCarbonNumber: profile.averageCarbonNumber,
boilingStartC: profile.boilingStartC,
boilingEndC: profile.boilingEndC,
volatility: Math.round(100 * (0.75 * (1 - cut) + 0.15 * p + 0.10 * (1 - a))),
density: Math.round(100 * (0.15 + 0.35 * cut + 0.20 * n + 0.30 * a)),
octaneTendency: Math.round(100 * (0.10 + 0.25 * cut + 0.20 * n + 0.35 * a)),
steamCrackingSuitability: Math.round(100 * (0.35 + 0.30 * p + 0.20 * (1 - cut) - 0.15 * a)),
reformingSuitability: Math.round(100 * (0.15 + 0.35 * cut + 0.30 * n + 0.20 * a))
};
}
global.NC101Core = {
assertFinite: assertFinite,
assertRange: assertRange,
normalizeComposition: normalizeComposition,
estimateCutProfile: estimateCutProfile,
estimateIndicators: estimateIndicators
};
})(window);
指標公式的係數說明(D01 補充)
estimateIndicators 內各係數(0.75、0.15、0.10 等)並非依實測資料而定,而是為了讓正文所述的方向感能在滑桿上明顯呈現的「教材用參數」。以 volatility 為例:
0.75 × (1 - cut)— 揮發性最主要由「越輕越高」主導,所以切取貢獻最大。0.15 × p— 烷烴比例越高越易揮發的傾向,依模組 2a 的方向以中等權重反映。0.10 × (1 - a)— 芳香烴增多會壓低相對揮發性的傾向,以小權重作為輔助項。
係數總和大致控制在 0〜1 範圍內,由人手調整。它不是工廠製程模型 — 改動係數,行為就會改變。動手調整可以體會「哪條軸主導哪個指標」。
劇本判斷與指標值的關係(D02 補充)
describeScenario 在挑選劇本文字時,先用 cut 和 comp 的原始值分支,然後才看 indicators.volatility >= 60。原則上以 indicators 也能重現相同判斷,但劇本文字的目的是把「組成與切取的直覺狀態」化為文字,所以從輸入端先判斷會更易讀。閾值(0.35、0.45、0.6、60 等)皆為教材示意值,不代表真實製程的標準。
關於「合計 100」的預設(D03 補充)
PRESETS 中 P / N / A 為了易讀,已手動調整為合計 100。但程式碼端不假設這項條件。normalizeComposition 一律會除以總和,所以只要是正數,無論合計多少,結果都相同。新增預設時不必硬要保持合計 100,但為了與 UI 顯示一致,照 100 對齊比較保險。
程式碼與意義對照
| 程式碼 | 意義 |
|---|---|
normalizeComposition | 把 P / N / A 的原始權重縮放成比例 |
estimateCutProfile | 把「輕 ↔ 重」對應到平均碳數與沸點範圍 |
volatility | 輕與蒸發難易的概念分數 |
steamCrackingSuitability | 與「切小塊」故事線的適配程度 |
reformingSuitability | 與「重 × N 偏多」那邊的適配程度 |
這些數字不是嚴謹預測,而是為了把正文裡講的方向感「視覺化」出來。
這個模組的一句話:先把比例歸一化,再依序把切取與組成映射成分數。模擬器的運算本體就是這兩步。
理解檢核 — 每段程式碼的意思
檢查你對每行程式碼意義的理解的 4 題。
Q1. normalizeComposition({ paraffins: 2, naphthenes: 1, aromatics: 1 }).paraffins 的值是多少?
總和為 4,烷烴比例 = 2 / 4 = 0.5。
Q2. estimateCutProfile(0.8).averageCarbonNumber 的值是多少?
實作是 5.2 + 5.0 × cut,所以 5.2 + 4.0 = 9.2。
Q3. 哪種輸入最容易把 reformingSuitability 推高?
較重的切取 × 較厚的環烷烴比例,就是設計來把 reformingSuitability 推高的組合。
Q4. 遇到負值權重之類的非法輸入時,這個實作會丟出哪個例外?
實作裡違反前提的值會以 RangeError 直接停住。
進階模組回顧
- 歸一化 PNA 與映射切取,都能裝進幾個短的 JavaScript 函式。
- 一律先驗證輸入,前提不符就用例外直接停住。
- 模擬器的數字不是嚴謹預測,而是把方向感視覺化的計算。