HAL-QuIC
  • Home
  • Project Roadmap
  • Modeling
  • Explorer
  • Experiments
  • Slides
  • GageRowden.com
  1. Model Explorer
  • Experiments
  • Model Explorer
  • Modeling RT-QuIC Kinetics
  • Kinetic Modeling of the RT-QuIC Assay
  • Project Roadmap
  • Experiments
    • Positive Seed Dilution Series
    • Substrate Dilution Series

Model Explorer

An interactive tool for exploring the double sigmoidal RT-QuIC model. Drag the sliders to see how each parameter shapes the growth and decay phases of the reaction in real time.

Author

Gage Rowden

Published

October 1, 2026

tex`f(t)=\frac{${S1.toFixed(2)}}{1+e^{${a1.toFixed(2)}(${b1.toFixed(2)}-t)}}+\frac{${S2.toFixed(2)}}{1+e^{${a2.toFixed(2)}(${b2.toFixed(2)}-t)}}`
d3 = require("d3-random");
maxTime = 72;
nPoints = Math.floor(maxTime / 0.25);
S1Range = [0, 20];
S2Range = [-15, 5];
a1Range = [0.1, 10];
a2Range = [0, 10];
b1Range = [5, maxTime];
b2Range = [-20, maxTime];

function getRandom(min, max) {
  return Math.random() * (max - min + 1) + min;
}

function generateRawData() {
  const S1 = +getRandom(10, 20).toFixed(2);
  const S2 = +getRandom(-5, 0).toFixed(2);
  const a1 = +getRandom(0.15, 10).toFixed(2);
  const a2 = +getRandom(0, 0.7).toFixed(2);
  const b1 = +getRandom(10, 60).toFixed(2);
  const b2 = +getRandom(0.9 * b1, 1.1 * b1).toFixed(2);
  const points = Array.from({length: nPoints}, (_, i) => {
    const t = i * maxTime / (nPoints - 1);
    return (S1 / (1 + Math.exp(a1 * (b1 - t)))) +
           (S2 / (1 + Math.exp(a2 * (b2 - t)))) +
           d3.randomNormal(0, 0.1)();
  });
  return {params: {S1, S2, a1, a2, b1, b2}, points};
}

mutable rawData = generateRawData()
mutable showAnswer = false

Primary Parameters

viewof S1 = {
  const input = Inputs.range(S1Range, {value: 10, step: 0.01, label: "S₁", attributes: {class: "primary-slider"}});
  input.classList.add("primary-slider");
  return input;
}
viewof a1 = {
  const input = Inputs.range(a1Range, {value: 0.5, step: 0.01, label: "a₁", attributes: {class: "primary-slider"}});
  input.classList.add("primary-slider");
  return input;
}
viewof b1 = {
  const input = Inputs.range(b1Range, {value: 30, step: 0.01, label: "b₁", attributes: {class: "primary-slider"}});
  input.classList.add("primary-slider");
  return input;
}

S₁:

a₁:

b₁:

Secondary Parameters

viewof S2 = {
  const input = Inputs.range(S2Range, {value: 0, step: 0.01, label: "S₂", attributes: {class: "secondary-slider"}});
  input.classList.add("secondary-slider");
  return input;
}
viewof a2 = {
  const input = Inputs.range(a2Range, {value: 0, step: 0.01, label: "a₂", attributes: {class: "secondary-slider"}});
  input.classList.add("secondary-slider");
  return input;
}
viewof b2 = {
  const input = Inputs.range(b2Range, {value: 0, step: 0.01, label: "b₂", attributes: {class: "secondary-slider"}});
  input.classList.add("secondary-slider");
  return input;
}

S₂:

a₂:

b₂:

{
  const reset = Inputs.button("Reset Plot", {
    reduce: () => { mutable rawData = generateRawData(); mutable showAnswer = false; }
  });
  const reveal = Inputs.button("Reveal Answer", {
    reduce: () => (mutable showAnswer = !showAnswer)
  });
  return html`<div style="display:flex;gap:8px;">
    ${reset}
    ${reveal}
  </div>`;
}
showAnswer ? html`<div style="margin-top:12px;padding:10px;border:1px solid #282828;border-radius:4px;font-size:13px;">
  <div style="color:var(--primary-color);margin-bottom:6px;">
    S₁ = <strong>${rawData.params.S1}</strong> &emsp;
    a₁ = <strong>${rawData.params.a1}</strong> &emsp;
    b₁ = <strong>${rawData.params.b1}</strong>
  </div>
  <div style="color:var(--secondary-color);">
    S₂ = <strong>${rawData.params.S2}</strong> &emsp;
    a₂ = <strong>${rawData.params.a2}</strong> &emsp;
    b₂ = <strong>${rawData.params.b2}</strong>
  </div>
</div>` : html``

Attempt to fit the data

g₁(t) g₂(t) f(t) Raw data Residual
curve = Array.from({length: nPoints}, (_, i) => {
  const t = i * maxTime / (nPoints - 1);
  const g1 = S1 / (1 + Math.exp(a1 * (b1 - t)));
  const g2 = S2 / (1 + Math.exp(a2 * (b2 - t)));
  const f = g1 + g2;
  const raw = rawData.points[i];
  return {t, g1, g2, f, raw, resid: f - raw};
})

plotSize = Generators.observe(notify => {
  let ro;
  const attach = () => {
    const el = document.querySelector(".plot-area");
    if (!el) { requestAnimationFrame(attach); return; }
    const measure = () => notify({
      width: Math.max(200, el.clientWidth),
      height: Math.max(200, el.clientHeight)
    });
    measure();
    ro = new ResizeObserver(measure);
    ro.observe(el);
  };
  notify({width: 640, height: 500});
  attach();
  return () => ro?.disconnect();
})

{
  const rawMean = curve.reduce((s, d) => s + d.raw, 0) / curve.length;
  const ssTot = curve.reduce((s, d) => s + (d.raw - rawMean) ** 2, 0);
  const ssRes = curve.reduce((s, d) => s + d.resid ** 2, 0);
  const r2 = 1 - ssRes / ssTot;

  const plot = Plot.plot({
    width: plotSize.width,
    height: plotSize.height,
    style: {background: "#060606", color: "#adafae", overflow: "visible"},
    x: {label: "Time (h)"},
    y: {label: "Normalized RFU"},
    marks: [
      Plot.gridX({stroke: "#282828"}),
      Plot.gridY({stroke: "#282828"}),
      Plot.frame({stroke: "#282828"}),
      Plot.ruleY([0], {stroke: "#adafae", strokeOpacity: 0.3}),
      Plot.dot(curve, {x: "t", y: "raw", fill: "#ffffff", stroke: "#ffffff", strokeWidth: 0.5}),
      Plot.line(curve, {x: "t", y: "resid", stroke: "#92ff84", strokeWidth: 1}),
      Plot.line(curve, {x: "t", y: "g1", stroke: "#77b300", strokeWidth: 2.5, strokeDasharray: "6,3"}),
      Plot.line(curve, {x: "t", y: "g2", stroke: "#e83e8c", strokeWidth: 2.5, strokeDasharray: "6,3"}),
      Plot.line(curve, {x: "t", y: "f", stroke: "#2a9fd6", strokeWidth: 5}),
      Plot.ruleY([0], {stroke: "#ffffff", strokeWidth: 1.5, strokeDasharray: "4,4"}),
      Plot.text([{label: `R² = ${r2.toFixed(4)}`}], {
        text: "label",
        frameAnchor: "right",
        fill: r2 >= 0.9 ? "#77b300" : r2 >= 0.7 ? "#f80" : "#c00",
        fontSize: 20,
        fontWeight: "bold",
        dx: -20,
        dy: -50
      }),
    ]
  });

  return plot;
}