The slider below controls how close a 2×2 linear system gets to singular (i.e., how large its condition number grows). A tiny perturbation ε is added to the right-hand side, and you can see how much that error grows in the solution.
<!-- Condition number demo: a 2x2 linear system tuned to approach singular -->
<p class="hint">Drag the slider to make the matrix nearly singular. A tiny perturbation ε is added to the right-hand side — watch how the output error grows as the condition number rises.</p>
<div class="slider-row">
<label for="kSlider">Condition number κ: <strong id="kappaVal">1</strong></label>
<input type="range" id="kSlider" min="0" max="100" value="0" step="1">
</div>
<div class="panels">
<div class="panel">
<div class="panel-title">Linear system Ax = b</div>
<div id="sysDisplay" class="matrix-display"></div>
</div>
<div class="panel">
<div class="panel-title">Exact solution</div>
<div id="exactDisplay" class="vec-display"></div>
</div>
<div class="panel">
<div class="panel-title">Perturbed solution (b + ε)</div>
<div id="pertDisplay" class="vec-display"></div>
</div>
</div>
<div class="panels">
<div class="panel">
<div class="panel-title">Condition number κ</div>
<div id="kappaDisplay" class="number-big"></div>
</div>
<div class="panel">
<div class="panel-title">Input error (relative)</div>
<div id="inErrDisplay" class="number-big"></div>
</div>
<div class="panel">
<div class="panel-title">Output error (relative)</div>
<div id="outErrDisplay" class="number-big good"></div>
</div>
</div>
<div class="status" id="status"></div>
<button id="resetBtn" type="button" class="ghost">Reset</button>
/* Layout and color styles for the condition number demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.slider-row { display: flex; align-items: center; gap: .8rem; margin-bottom: .9rem; flex-wrap: wrap; }
.slider-row label { font-size: .9rem; white-space: nowrap; }
#kSlider { flex: 1; min-width: 120px; accent-color: #1d3557; }
.panels { display: flex; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
.panel { flex: 1; min-width: 100px; background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 8px; padding: .5rem .7rem; }
.panel-title { font-size: .72rem; font-weight: 700; color: #5a7088; text-transform: uppercase; letter-spacing: .04em; margin-bottom: .3rem; }
.matrix-display { font: 600 .78rem ui-monospace, monospace; color: #1d3557; line-height: 1.7; white-space: pre; }
.vec-display { font: 600 .78rem ui-monospace, monospace; color: #1d3557; line-height: 1.7; white-space: pre; }
.number-big { font: 700 1.1rem ui-monospace, monospace; color: #1d3557; padding-top: .1rem; }
.number-big.good { color: #0a7d33; }
.number-big.warn { color: #c97a00; }
.number-big.bad { color: #c92f3c; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.4em; margin: .4rem 0; }
.status.ok { color: #0a7d33; }
.status.warn { color: #c97a00; }
.status.bad { color: #c92f3c; }
button.ghost { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557; background: #fff; color: #1d3557; border-radius: 8px; cursor: pointer; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A(t) = [[1, 1-t], [1, 1+t]] with true solution x = [1, 0]^T
// A(t) = [[1, 1-t], [1, 1+t]] with true solution x = [1, 0]^T
// b is computed from A and the true solution so the exact answer is always x=[1,0]
// As t->0, A becomes [[1,1],[1,1]] which is singular (kappa->infinity).
// A perturbation EPS is added to b[0] to simulate a tiny measurement error
const EPS = 1e-7; // size of the input perturbation (similar to machine epsilon)
function buildSystem(t) {
// Build matrix A and right-hand side b for parameter t
const A = [[1, 1 - t], [1, 1 + t]];
const b = [A[0][0] * 1 + A[0][1] * 0, A[1][0] * 1 + A[1][1] * 0];
return { A, b };
}
function solve2x2(A, b) {
// Solve 2x2 linear system by Cramer's rule
const det = A[0][0] * A[1][1] - A[0][1] * A[1][0];
if (Math.abs(det) < 1e-15) return null;
const x0 = (b[0] * A[1][1] - b[1] * A[0][1]) / det;
const x1 = (A[0][0] * b[1] - A[1][0] * b[0]) / det;
return [x0, x1];
}
function condNumber(A) {
// Condition number via SVD formula for 2x2 matrices
// For 2x2: sigma_max and sigma_min via SVD shortcut
const a = A[0][0], b = A[0][1], c = A[1][0], d = A[1][1];
const s1 = a * a + b * b + c * c + d * d;
const s2 = Math.sqrt(Math.pow(a * a + b * b - c * c - d * d, 2) + 4 * Math.pow(a * c + b * d, 2));
const sigMax = Math.sqrt((s1 + s2) / 2);
const sigMin = Math.sqrt(Math.max(0, (s1 - s2) / 2));
if (sigMin < 1e-15) return Infinity;
return sigMax / sigMin;
}
function fmt2(n) {
// Format a number for display
if (!isFinite(n)) return '∞';
if (Math.abs(n) >= 1e6) return n.toExponential(2);
return n.toFixed(n < 0.01 && n > -0.01 ? 6 : 4);
}
function update() {
const slider = document.getElementById('kSlider');
// Map slider (0-100) to t in (0, 0.5] so kappa grows smoothly to very large values
const t = 0.5 * Math.pow(1 - slider.value / 100, 3);
const { A, b } = buildSystem(t);
const kappa = condNumber(A);
// Solve exact and perturbed systems
const bPerturb = [b[0] + EPS, b[1]];
const xExact = solve2x2(A, b);
const xPerturb = solve2x2(A, bPerturb);
// Compute relative input and output errors
const inErr = EPS / Math.sqrt(b[0] * b[0] + b[1] * b[1]);
let outErr = null;
if (xExact && xPerturb) {
const dx0 = xPerturb[0] - xExact[0], dx1 = xPerturb[1] - xExact[1];
const xNorm = Math.sqrt(xExact[0] * xExact[0] + xExact[1] * xExact[1]);
outErr = Math.sqrt(dx0 * dx0 + dx1 * dx1) / (xNorm || 1);
}
// Update condition number displays
document.getElementById('kappaVal').textContent = isFinite(kappa) ? Math.round(kappa).toLocaleString() : '∞';
document.getElementById('kappaDisplay').textContent = isFinite(kappa) ? Math.round(kappa).toLocaleString() : '∞';
// Update system display
document.getElementById('sysDisplay').textContent =
`[${fmt2(A[0][0])} ${fmt2(A[0][1])}] [x₀] [${fmt2(b[0])}]\n` +
`[${fmt2(A[1][0])} ${fmt2(A[1][1])}] [x₁] = [${fmt2(b[1])}]`;
// Update solution displays
if (xExact) {
document.getElementById('exactDisplay').textContent =
`x₀ = ${fmt2(xExact[0])}\nx₁ = ${fmt2(xExact[1])}`;
}
if (xPerturb) {
document.getElementById('pertDisplay').textContent =
`x₀ = ${fmt2(xPerturb[0])}\nx₁ = ${fmt2(xPerturb[1])}`;
}
// Update error displays with color coding
document.getElementById('inErrDisplay').textContent = inErr.toExponential(2);
const outEl = document.getElementById('outErrDisplay');
if (outErr !== null) {
outEl.textContent = outErr.toExponential(2);
if (outErr < 1e-4) { outEl.className = 'number-big good'; }
else if (outErr < 1e-1) { outEl.className = 'number-big warn'; }
else { outEl.className = 'number-big bad'; }
}
// Update status message
const statusEl = document.getElementById('status');
if (!isFinite(kappa) || kappa > 1e10) {
statusEl.textContent = '⚠ Nearly singular — solution meaningless!';
statusEl.className = 'status bad';
} else if (kappa > 1e5) {
statusEl.textContent = i18nFmt('⚠ Ill-conditioned (κ = %kappa%) — large error amplification.', { kappa: Math.round(kappa).toLocaleString() });
statusEl.className = 'status warn';
} else if (kappa > 100) {
statusEl.textContent = i18nFmt('△ Moderately conditioned (κ = %kappa%) — some amplification.', { kappa: Math.round(kappa).toLocaleString() });
statusEl.className = 'status warn';
} else {
statusEl.textContent = i18nFmt('✓ Well-conditioned (κ = %kappa%) — small error amplification.', { kappa: Math.round(kappa).toLocaleString() });
statusEl.className = 'status ok';
}
}
document.getElementById('kSlider').addEventListener('input', update);
document.getElementById('resetBtn').addEventListener('click', () => {
document.getElementById('kSlider').value = 0;
update();
});
update();
<!-- Condition number demo: a 2x2 linear system tuned to approach singular -->
<p class="hint">Drag the slider to make the matrix nearly singular. A tiny perturbation ε is added to the right-hand side — watch how the output error grows as the condition number rises.</p>
<div class="slider-row">
<label for="kSlider">Condition number κ: <strong id="kappaVal">1</strong></label>
<input type="range" id="kSlider" min="0" max="100" value="0" step="1">
</div>
<div class="panels">
<div class="panel">
<div class="panel-title">Linear system Ax = b</div>
<div id="sysDisplay" class="matrix-display"></div>
</div>
<div class="panel">
<div class="panel-title">Exact solution</div>
<div id="exactDisplay" class="vec-display"></div>
</div>
<div class="panel">
<div class="panel-title">Perturbed solution (b + ε)</div>
<div id="pertDisplay" class="vec-display"></div>
</div>
</div>
<div class="panels">
<div class="panel">
<div class="panel-title">Condition number κ</div>
<div id="kappaDisplay" class="number-big"></div>
</div>
<div class="panel">
<div class="panel-title">Input error (relative)</div>
<div id="inErrDisplay" class="number-big"></div>
</div>
<div class="panel">
<div class="panel-title">Output error (relative)</div>
<div id="outErrDisplay" class="number-big good"></div>
</div>
</div>
<div class="status" id="status"></div>
<button id="resetBtn" type="button" class="ghost">Reset</button>
/* Layout and color styles for the condition number demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.slider-row { display: flex; align-items: center; gap: .8rem; margin-bottom: .9rem; flex-wrap: wrap; }
.slider-row label { font-size: .9rem; white-space: nowrap; }
#kSlider { flex: 1; min-width: 120px; accent-color: #1d3557; }
.panels { display: flex; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
.panel { flex: 1; min-width: 100px; background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 8px; padding: .5rem .7rem; }
.panel-title { font-size: .72rem; font-weight: 700; color: #5a7088; text-transform: uppercase; letter-spacing: .04em; margin-bottom: .3rem; }
.matrix-display { font: 600 .78rem ui-monospace, monospace; color: #1d3557; line-height: 1.7; white-space: pre; }
.vec-display { font: 600 .78rem ui-monospace, monospace; color: #1d3557; line-height: 1.7; white-space: pre; }
.number-big { font: 700 1.1rem ui-monospace, monospace; color: #1d3557; padding-top: .1rem; }
.number-big.good { color: #0a7d33; }
.number-big.warn { color: #c97a00; }
.number-big.bad { color: #c92f3c; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.4em; margin: .4rem 0; }
.status.ok { color: #0a7d33; }
.status.warn { color: #c97a00; }
.status.bad { color: #c92f3c; }
button.ghost { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557; background: #fff; color: #1d3557; border-radius: 8px; cursor: pointer; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A(t) = [[1, 1-t], [1, 1+t]] with true solution x = [1, 0]^T
// A(t) = [[1, 1-t], [1, 1+t]] with true solution x = [1, 0]^T
// b is computed from A and the true solution so the exact answer is always x=[1,0]
// As t->0, A becomes [[1,1],[1,1]] which is singular (kappa->infinity).
// A perturbation EPS is added to b[0] to simulate a tiny measurement error
const EPS = 1e-7; // size of the input perturbation (similar to machine epsilon)
function buildSystem(t) {
// Build matrix A and right-hand side b for parameter t
const A = [[1, 1 - t], [1, 1 + t]];
const b = [A[0][0] * 1 + A[0][1] * 0, A[1][0] * 1 + A[1][1] * 0];
return { A, b };
}
function solve2x2(A, b) {
// Solve 2x2 linear system by Cramer's rule
const det = A[0][0] * A[1][1] - A[0][1] * A[1][0];
if (Math.abs(det) < 1e-15) return null;
const x0 = (b[0] * A[1][1] - b[1] * A[0][1]) / det;
const x1 = (A[0][0] * b[1] - A[1][0] * b[0]) / det;
return [x0, x1];
}
function condNumber(A) {
// Condition number via SVD formula for 2x2 matrices
// For 2x2: sigma_max and sigma_min via SVD shortcut
const a = A[0][0], b = A[0][1], c = A[1][0], d = A[1][1];
const s1 = a * a + b * b + c * c + d * d;
const s2 = Math.sqrt(Math.pow(a * a + b * b - c * c - d * d, 2) + 4 * Math.pow(a * c + b * d, 2));
const sigMax = Math.sqrt((s1 + s2) / 2);
const sigMin = Math.sqrt(Math.max(0, (s1 - s2) / 2));
if (sigMin < 1e-15) return Infinity;
return sigMax / sigMin;
}
function fmt2(n) {
// Format a number for display
if (!isFinite(n)) return '∞';
if (Math.abs(n) >= 1e6) return n.toExponential(2);
return n.toFixed(n < 0.01 && n > -0.01 ? 6 : 4);
}
function update() {
const slider = document.getElementById('kSlider');
// Map slider (0-100) to t in (0, 0.5] so kappa grows smoothly to very large values
const t = 0.5 * Math.pow(1 - slider.value / 100, 3);
const { A, b } = buildSystem(t);
const kappa = condNumber(A);
// Solve exact and perturbed systems
const bPerturb = [b[0] + EPS, b[1]];
const xExact = solve2x2(A, b);
const xPerturb = solve2x2(A, bPerturb);
// Compute relative input and output errors
const inErr = EPS / Math.sqrt(b[0] * b[0] + b[1] * b[1]);
let outErr = null;
if (xExact && xPerturb) {
const dx0 = xPerturb[0] - xExact[0], dx1 = xPerturb[1] - xExact[1];
const xNorm = Math.sqrt(xExact[0] * xExact[0] + xExact[1] * xExact[1]);
outErr = Math.sqrt(dx0 * dx0 + dx1 * dx1) / (xNorm || 1);
}
// Update condition number displays
document.getElementById('kappaVal').textContent = isFinite(kappa) ? Math.round(kappa).toLocaleString() : '∞';
document.getElementById('kappaDisplay').textContent = isFinite(kappa) ? Math.round(kappa).toLocaleString() : '∞';
// Update system display
document.getElementById('sysDisplay').textContent =
`[${fmt2(A[0][0])} ${fmt2(A[0][1])}] [x₀] [${fmt2(b[0])}]\n` +
`[${fmt2(A[1][0])} ${fmt2(A[1][1])}] [x₁] = [${fmt2(b[1])}]`;
// Update solution displays
if (xExact) {
document.getElementById('exactDisplay').textContent =
`x₀ = ${fmt2(xExact[0])}\nx₁ = ${fmt2(xExact[1])}`;
}
if (xPerturb) {
document.getElementById('pertDisplay').textContent =
`x₀ = ${fmt2(xPerturb[0])}\nx₁ = ${fmt2(xPerturb[1])}`;
}
// Update error displays with color coding
document.getElementById('inErrDisplay').textContent = inErr.toExponential(2);
const outEl = document.getElementById('outErrDisplay');
if (outErr !== null) {
outEl.textContent = outErr.toExponential(2);
if (outErr < 1e-4) { outEl.className = 'number-big good'; }
else if (outErr < 1e-1) { outEl.className = 'number-big warn'; }
else { outEl.className = 'number-big bad'; }
}
// Update status message
const statusEl = document.getElementById('status');
if (!isFinite(kappa) || kappa > 1e10) {
statusEl.textContent = '⚠ Nearly singular — solution meaningless!';
statusEl.className = 'status bad';
} else if (kappa > 1e5) {
statusEl.textContent = i18nFmt('⚠ Ill-conditioned (κ = %kappa%) — large error amplification.', { kappa: Math.round(kappa).toLocaleString() });
statusEl.className = 'status warn';
} else if (kappa > 100) {
statusEl.textContent = i18nFmt('△ Moderately conditioned (κ = %kappa%) — some amplification.', { kappa: Math.round(kappa).toLocaleString() });
statusEl.className = 'status warn';
} else {
statusEl.textContent = i18nFmt('✓ Well-conditioned (κ = %kappa%) — small error amplification.', { kappa: Math.round(kappa).toLocaleString() });
statusEl.className = 'status ok';
}
}
document.getElementById('kSlider').addEventListener('input', update);
document.getElementById('resetBtn').addEventListener('click', () => {
document.getElementById('kSlider').value = 0;
update();
});
update();
Notice what happens as the matrix approaches singular: a perturbation that is invisible in the input — smaller than a typical rounding error — produces an error in the solution that is many times larger. This ratio is exactly the condition number at work.
Comments
Loading comments...