Terms to know before this page

Input validation

Rejecting out-of-range or invalid values up front.

RangeError

The exception this implementation throws when it receives a value that violates the preconditions.

Normalisation

Rescaling raw weights so they sum to 1 and represent proportions.

Concept model

A simplified model that highlights direction rather than predicting measured values.

Four stages to look at first

  1. Input validation — check that values are numbers and that they fall in range.
  2. Composition normalisation — rescale raw P / N / A weights so they sum to 1.
  3. Cut mapping — translate the light-to-heavy slider into an average carbon number and a boiling range.
  4. Indicator calculation — return concept scores for volatility, density, reforming suitability, and so on.

The core code behind the simulator

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);

Where the indicator coefficients come from (D01 note)

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.

How scenarios relate to the indicator values (D02 note)

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.

Why the presets sum to 100 (D03 note)

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.

What each piece of code means

CodeMeaning
normalizeCompositionRescales raw P / N / A weights into proportions.
estimateCutProfileMaps the light-to-heavy slider into an average carbon number and a boiling range.
volatilityConcept score for lightness and how readily it evaporates.
steamCrackingSuitabilityFit with the "break into smaller pieces" storyline.
reformingSuitabilityFit 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.

Self-check

Four questions on what each line of code does.

Q 6-1 — Return value of normalizeComposition

What is normalizeComposition({ paraffins: 2, naphthenes: 1, aromatics: 1 }).paraffins?

  1. 0.25
  2. 0.5
  3. 2
Show answer and reasoning

Answer: B

The total is 4, so the paraffin share is 2 / 4 = 0.5.

Q 6-2 — Value returned by estimateCutProfile

What is estimateCutProfile(0.8).averageCarbonNumber?

  1. 7.2
  2. 8.2
  3. 9.2
Show answer and reasoning

Answer: C

The implementation is 5.2 + 5.0 × cut, so 5.2 + 4.0 = 9.2.

Q 6-3 — Inputs that raise reformingSuitability

Which input tends to push the reformingSuitability from estimateIndicators upward?

  1. Increase naphthenes.
  2. Make the cut lighter.
  3. Set vapour pressure to zero.
Show answer and reasoning

Answer: A

In this concept model, a heavier cut and a thicker naphthene share are designed to pull reformingSuitability up.

Q 6-4 — The exception raised on bad input

When the implementation receives an invalid input such as a negative weight, which exception does it throw?

  1. TypeError
  2. RangeError
  3. SyntaxError
Show answer and reasoning

Answer: B

The implementation stops with a RangeError whenever a precondition is violated.

Chapter 6 summary

  • Normalising PNA and mapping the cut fit into a few short JavaScript functions.
  • Validate inputs first and stop on precondition failures with an exception.
  • The simulator's numbers are not rigorous predictions — they visualise direction.