Q 6-1 — Return value of normalizeComposition
What is normalizeComposition({ paraffins: 2, naphthenes: 1, aromatics: 1 }).paraffins?
- 0.25
- 0.5
- 2
Show answer and reasoning
Answer: B
The total is 4, so the paraffin share is 2 / 4 = 0.5.
Chapter 6
Walk through the simulator from the previous chapter one function at a time in plain JavaScript. Not rigorous chemical-engineering calculations — read it as "a minimal implementation that makes the direction visible."
Entry point: high-school chemistry equations and mole ratios
Rejecting out-of-range or invalid values up front.
The exception this implementation throws when it receives a value that violates the preconditions.
Rescaling raw weights so they sum to 1 and represent proportions.
A simplified model that highlights direction rather than predicting measured values.
The core that actually runs is the JavaScript shown below. No external libraries; inputs that violate the preconditions stop with a 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))
};
}
function describeScenario(comp, cut, indicators) {
if (cut < 0.35 && comp.paraffins >= 0.45) {
return "Light with a heavy share of paraffins. Highly volatile, so the steam-cracking storyline is easy to read.";
}
if (cut >= 0.6 && comp.naphthenes >= 0.35) {
return "Heavier end with a thick naphthene share. The link to catalytic reforming is easy to see here.";
}
if (comp.aromatics >= 0.35) {
return "Aromatics are running a bit rich. Density and octane tendency drift upward, but keep exposure and spec considerations in mind as well.";
}
if (indicators.volatility >= 60) {
return "Leaning toward the light side. Reading vapor pressure and flash behaviour first makes everything else easier.";
}
return "A middle-of-the-road cut and composition. The downstream direction can swing either way depending on pretreatment and spec conditions.";
}
const PRESETS = {
lightParaffinic: {
label: "Light paraffinic",
cut: 25,
paraffins: 60,
naphthenes: 25,
aromatics: 15
},
balanced: {
label: "Balanced",
cut: 55,
paraffins: 45,
naphthenes: 35,
aromatics: 20
},
reforming: {
label: "Reforming-oriented",
cut: 72,
paraffins: 20,
naphthenes: 50,
aromatics: 30
},
aromaticRich: {
label: "Aromatic-rich",
cut: 65,
paraffins: 15,
naphthenes: 20,
aromatics: 65
}
};
global.NC101Core = {
assertFinite: assertFinite,
assertRange: assertRange,
normalizeComposition: normalizeComposition,
estimateCutProfile: estimateCutProfile,
estimateIndicators: estimateIndicators,
describeScenario: describeScenario,
PRESETS: PRESETS
};
})(window);
The numerical coefficients inside estimateIndicators (0.75, 0.15, 0.10, etc.) are not derived from measured data — they are teaching parameters chosen so the directions described in the body text become visible on the sliders. For example, in the volatility formula:
0.75 × (1 - cut) — volatility is dominated by "the lighter the cut, the higher the volatility," so the cut term gets the largest weight.0.15 × p — the paraffin share has a moderate, secondary contribution, mirroring the Chapter 2 direction "more paraffins → more volatile."0.10 × (1 - a) — a small auxiliary weight reflects "more aromatics tends to reduce relative volatility."Coefficients are tuned by hand so the totals roughly fall in 0–1. They are not a process model — change them, and the behaviour changes. Tweaking them is a good way to feel which axis dominates which indicator.
describeScenario picks scenario text by branching first on the raw cut and comp values, then on indicators.volatility >= 60. The same selection could in principle be reproduced via indicators, but the scenario text is meant to verbalise the intuitive state of the inputs, so branching on the inputs reads more naturally. The thresholds (0.35, 0.45, 0.6, 60, …) are illustrative teaching values, not real-process boundaries.
P / N / A inside PRESETS are hand-tuned to sum to 100 for readability, but the code does not assume that. normalizeComposition always divides by the total, so any positive set of weights gives the same outcome. When you add new presets, you do not have to keep the sum at 100 — but matching the UI display is clearer, so 100 is a safe convention.
| Code | Meaning |
|---|---|
| normalizeComposition | Rescales raw P / N / A weights into proportions. |
| estimateCutProfile | Maps the light-to-heavy slider into an average carbon number and a boiling range. |
| volatility | Concept score for lightness and how readily it evaporates. |
| steamCrackingSuitability | Fit with the "break into smaller pieces" storyline. |
| reformingSuitability | Fit with the heavy, naphthene-rich side. |
The numbers are not a "rigorous prediction" — they exist to make the directions described in the body text visible.
Four questions on what each line of code does.
What is normalizeComposition({ paraffins: 2, naphthenes: 1, aromatics: 1 }).paraffins?
Answer: B
The total is 4, so the paraffin share is 2 / 4 = 0.5.
What is estimateCutProfile(0.8).averageCarbonNumber?
Answer: C
The implementation is 5.2 + 5.0 × cut, so 5.2 + 4.0 = 9.2.
Which input tends to push the reformingSuitability from estimateIndicators upward?
Answer: A
In this concept model, a heavier cut and a thicker naphthene share are designed to pull reformingSuitability up.
When the implementation receives an invalid input such as a negative weight, which exception does it throw?
Answer: B
The implementation stops with a RangeError whenever a precondition is violated.