Created
February 22, 2026 04:54
-
-
Save jbdamask/f2f577864aecba720179c3884848dd1f to your computer and use it in GitHub Desktop.
NowIGetIt: jay-eigen.pdf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Consistently Oriented Eigenvectors — Interactive Explainer</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap'); | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| :root { | |
| --bg: #0a0a0f; | |
| --bg2: #111118; | |
| --bg3: #1a1a24; | |
| --accent: #6366f1; | |
| --accent2: #818cf8; | |
| --accent3: #a5b4fc; | |
| --green: #34d399; | |
| --red: #f87171; | |
| --orange: #fb923c; | |
| --yellow: #fbbf24; | |
| --cyan: #22d3ee; | |
| --pink: #f472b6; | |
| --text: #e2e8f0; | |
| --text2: #94a3b8; | |
| --text3: #64748b; | |
| --border: #1e293b; | |
| } | |
| html { scroll-behavior: smooth; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: 'Inter', sans-serif; | |
| line-height: 1.7; | |
| overflow-x: hidden; | |
| } | |
| /* Hero */ | |
| .hero { | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| text-align: center; | |
| position: relative; | |
| padding: 2rem; | |
| overflow: hidden; | |
| } | |
| .hero-bg { | |
| position: absolute; | |
| inset: 0; | |
| z-index: 0; | |
| } | |
| .hero-bg canvas { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .hero-content { | |
| position: relative; | |
| z-index: 1; | |
| max-width: 800px; | |
| } | |
| .hero h1 { | |
| font-size: clamp(2.2rem, 5vw, 4rem); | |
| font-weight: 900; | |
| background: linear-gradient(135deg, var(--accent3), var(--cyan), var(--green)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| line-height: 1.15; | |
| margin-bottom: 1.5rem; | |
| } | |
| .hero p { | |
| font-size: 1.15rem; | |
| color: var(--text2); | |
| max-width: 600px; | |
| margin: 0 auto 2rem; | |
| } | |
| .hero .author { | |
| font-size: 0.9rem; | |
| color: var(--text3); | |
| margin-bottom: 2.5rem; | |
| } | |
| .scroll-hint { | |
| animation: bounce 2s ease infinite; | |
| color: var(--accent2); | |
| font-size: 1.5rem; | |
| } | |
| @keyframes bounce { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(10px); } | |
| } | |
| /* Sections */ | |
| section { | |
| max-width: 1100px; | |
| margin: 0 auto; | |
| padding: 5rem 2rem; | |
| } | |
| .section-label { | |
| font-size: 0.75rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.15em; | |
| color: var(--accent2); | |
| margin-bottom: 0.75rem; | |
| } | |
| h2 { | |
| font-size: clamp(1.8rem, 3.5vw, 2.5rem); | |
| font-weight: 800; | |
| margin-bottom: 1.5rem; | |
| line-height: 1.2; | |
| } | |
| h3 { | |
| font-size: 1.3rem; | |
| font-weight: 700; | |
| margin-bottom: 1rem; | |
| color: var(--accent3); | |
| } | |
| p { margin-bottom: 1rem; color: var(--text2); } | |
| .highlight { color: var(--accent3); font-weight: 600; } | |
| .green { color: var(--green); } | |
| .red { color: var(--red); } | |
| .orange { color: var(--orange); } | |
| .cyan { color: var(--cyan); } | |
| /* Cards */ | |
| .card-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
| gap: 1.5rem; | |
| margin: 2rem 0; | |
| } | |
| .card { | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| transition: transform 0.3s, border-color 0.3s; | |
| } | |
| .card:hover { | |
| transform: translateY(-4px); | |
| border-color: var(--accent); | |
| } | |
| .card .icon { | |
| font-size: 2rem; | |
| margin-bottom: 1rem; | |
| } | |
| .card h3 { color: var(--text); } | |
| /* Interactive demos */ | |
| .demo-container { | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| margin: 2rem 0; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .demo-container canvas { | |
| display: block; | |
| margin: 0 auto; | |
| cursor: grab; | |
| } | |
| .demo-container canvas:active { cursor: grabbing; } | |
| .demo-controls { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.75rem; | |
| margin-top: 1.5rem; | |
| justify-content: center; | |
| } | |
| .btn { | |
| padding: 0.6rem 1.4rem; | |
| border-radius: 10px; | |
| border: 1px solid var(--border); | |
| background: var(--bg3); | |
| color: var(--text); | |
| font-family: 'Inter', sans-serif; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .btn:hover { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: #fff; | |
| } | |
| .btn.active { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: #fff; | |
| } | |
| .slider-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .slider-group label { | |
| font-size: 0.85rem; | |
| color: var(--text2); | |
| font-weight: 500; | |
| white-space: nowrap; | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| height: 6px; | |
| background: var(--bg3); | |
| border-radius: 3px; | |
| outline: none; | |
| width: 150px; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| cursor: pointer; | |
| } | |
| /* Comparison */ | |
| .comparison { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 2rem; | |
| margin: 2rem 0; | |
| } | |
| @media (max-width: 700px) { | |
| .comparison { grid-template-columns: 1fr; } | |
| } | |
| .comparison-side { | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| text-align: center; | |
| } | |
| .comparison-side canvas { | |
| display: block; | |
| margin: 0 auto 1rem; | |
| } | |
| /* Timeline */ | |
| .timeline { | |
| position: relative; | |
| margin: 3rem 0; | |
| } | |
| .timeline::before { | |
| content: ''; | |
| position: absolute; | |
| left: 24px; | |
| top: 0; | |
| bottom: 0; | |
| width: 2px; | |
| background: var(--border); | |
| } | |
| .timeline-item { | |
| display: flex; | |
| gap: 2rem; | |
| margin-bottom: 2.5rem; | |
| position: relative; | |
| } | |
| .timeline-dot { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| flex-shrink: 0; | |
| margin-top: 0.5rem; | |
| position: relative; | |
| left: 19px; | |
| z-index: 1; | |
| } | |
| .timeline-content { | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| flex: 1; | |
| margin-left: 1rem; | |
| } | |
| .timeline-content h3 { | |
| color: var(--text); | |
| font-size: 1.1rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| /* Polar plot */ | |
| .polar-demo { | |
| text-align: center; | |
| } | |
| /* Key insight boxes */ | |
| .insight { | |
| background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(34,211,238,0.05)); | |
| border-left: 3px solid var(--accent); | |
| border-radius: 0 12px 12px 0; | |
| padding: 1.5rem 2rem; | |
| margin: 2rem 0; | |
| } | |
| .insight strong { color: var(--accent3); } | |
| /* Code */ | |
| code { | |
| font-family: 'JetBrains Mono', monospace; | |
| background: var(--bg3); | |
| padding: 0.15em 0.4em; | |
| border-radius: 4px; | |
| font-size: 0.85em; | |
| color: var(--cyan); | |
| } | |
| /* Footer */ | |
| footer { | |
| text-align: center; | |
| padding: 3rem 2rem; | |
| color: var(--text3); | |
| font-size: 0.85rem; | |
| border-top: 1px solid var(--border); | |
| } | |
| footer a { color: var(--accent2); text-decoration: none; } | |
| /* Fade in animation */ | |
| .fade-in { | |
| opacity: 0; | |
| transform: translateY(30px); | |
| transition: opacity 0.6s ease, transform 0.6s ease; | |
| } | |
| .fade-in.visible { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| /* Nav */ | |
| nav { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| z-index: 1000; | |
| background: rgba(10,10,15,0.85); | |
| backdrop-filter: blur(20px); | |
| border-bottom: 1px solid var(--border); | |
| padding: 0.75rem 2rem; | |
| display: flex; | |
| justify-content: center; | |
| gap: 2rem; | |
| flex-wrap: wrap; | |
| font-size: 0.82rem; | |
| font-weight: 500; | |
| } | |
| nav a { | |
| color: var(--text2); | |
| text-decoration: none; | |
| transition: color 0.2s; | |
| } | |
| nav a:hover { color: var(--accent3); } | |
| /* Tooltip for math */ | |
| .math { | |
| font-family: 'JetBrains Mono', monospace; | |
| color: var(--yellow); | |
| font-size: 0.92em; | |
| } | |
| .eq-block { | |
| background: var(--bg3); | |
| border-radius: 12px; | |
| padding: 1.5rem 2rem; | |
| margin: 1.5rem 0; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.9rem; | |
| color: var(--yellow); | |
| overflow-x: auto; | |
| text-align: center; | |
| line-height: 2; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav> | |
| <a href="#problem">The Problem</a> | |
| <a href="#eigenvectors">Eigenvectors 101</a> | |
| <a href="#original">Original Method</a> | |
| <a href="#new">New Method</a> | |
| <a href="#demo2d">2D Demo</a> | |
| <a href="#demo3d">3D Demo</a> | |
| <a href="#rmt">Random Matrix Theory</a> | |
| <a href="#stabilization">Stabilization</a> | |
| </nav> | |
| <!-- HERO --> | |
| <div class="hero"> | |
| <div class="hero-bg"><canvas id="heroBg"></canvas></div> | |
| <div class="hero-content"> | |
| <h1>Fixing the Compass of Data</h1> | |
| <p>How a new algorithm gives eigenvectors a consistent sense of direction — and why it matters for understanding complex systems.</p> | |
| <div class="author">Based on the paper by <strong>Jay Damask</strong> · arXiv:2402.08139 · 2024</div> | |
| <div class="scroll-hint">↓</div> | |
| </div> | |
| </div> | |
| <!-- THE PROBLEM --> | |
| <section id="problem" class="fade-in"> | |
| <div class="section-label">The Problem</div> | |
| <h2>Eigenvectors don't know which way to point</h2> | |
| <p>When you ask a computer to find the <span class="highlight">principal directions</span> in your data (the "eigenvectors"), the software returns vectors that are mathematically correct — but their <span class="red">signs are arbitrary</span>. Run the same analysis twice and a vector might flip from pointing "north" to pointing "south."</p> | |
| <p>This is like having a compass that randomly reverses polarity. You can still measure distances, but you can't reliably say <em>which way</em> things are moving over time.</p> | |
| <div class="card-grid"> | |
| <div class="card"> | |
| <div class="icon">🔀</div> | |
| <h3>Random Sign Flips</h3> | |
| <p>Software routines like <code>svd</code> and <code>eig</code> return eigenvectors with arbitrary signs. Consecutive runs on similar data can produce opposite-pointing vectors.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="icon">📊</div> | |
| <h3>Breaks Tracking Over Time</h3> | |
| <p>If you track how your system's principal directions evolve — like in financial markets or sensor arrays — sign flips create artificial jumps.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="icon">🧭</div> | |
| <h3>Limited Angular Range</h3> | |
| <p>Even with the original fix (arcsin method), angles were limited to a half-circle (π interval). Vectors near the boundary could appear to "wrap around."</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- EIGENVECTORS 101 --> | |
| <section id="eigenvectors" class="fade-in"> | |
| <div class="section-label">Background</div> | |
| <h2>Eigenvectors in 60 Seconds</h2> | |
| <p>Imagine you have a cloud of data points in multiple dimensions. <span class="highlight">Eigenvectors</span> are the natural "axes" of that cloud — they point in the directions where the data varies the most.</p> | |
| <div class="demo-container"> | |
| <canvas id="eigenCloud" width="700" height="400"></canvas> | |
| <div class="demo-controls"> | |
| <button class="btn" id="reshuffleCloud">🎲 New Data Cloud</button> | |
| <button class="btn" id="toggleEigen">Show Eigenvectors</button> | |
| </div> | |
| <p style="text-align:center;margin-top:1rem;font-size:0.85rem;color:var(--text3);">The colored arrows are the eigenvectors — the principal directions of variation in the data.</p> | |
| </div> | |
| <div class="insight"> | |
| <strong>Key point:</strong> Eigenvectors come in pairs of opposite directions (like a line, not an arrow). The computer picks one direction arbitrarily. This paper's algorithm makes that choice <em>consistent</em>. | |
| </div> | |
| </section> | |
| <!-- ORIGINAL METHOD --> | |
| <section id="original" class="fade-in"> | |
| <div class="section-label">Original Algorithm</div> | |
| <h2>The First Fix: arcsin Method</h2> | |
| <p>The original paper (2020) solved the sign problem by checking each eigenvector's direction: if it pointed "backward" relative to a reference axis, the algorithm <span class="highlight">reflected</span> (flipped) it. The rotation angles were computed using <code>arcsin</code>.</p> | |
| <div class="timeline"> | |
| <div class="timeline-item"> | |
| <div class="timeline-dot"></div> | |
| <div class="timeline-content"> | |
| <h3>Step 1: Sort Eigenvectors</h3> | |
| <p>Order them by eigenvalue magnitude (largest first). This ensures a consistent labeling.</p> | |
| </div> | |
| </div> | |
| <div class="timeline-item"> | |
| <div class="timeline-dot"></div> | |
| <div class="timeline-content"> | |
| <h3>Step 2: Check Hemisphere</h3> | |
| <p>For each eigenvector, check if it points in the "front" hemisphere relative to its reference axis. If not, <span class="red">flip its sign</span>.</p> | |
| </div> | |
| </div> | |
| <div class="timeline-item"> | |
| <div class="timeline-dot"></div> | |
| <div class="timeline-content"> | |
| <h3>Step 3: Rotate to Align</h3> | |
| <p>Use Givens rotations to swing the eigenvector onto its target axis. Record the rotation angles.</p> | |
| </div> | |
| </div> | |
| <div class="timeline-item"> | |
| <div class="timeline-dot"></div> | |
| <div class="timeline-content"> | |
| <h3>Step 4: Move to Next Subspace</h3> | |
| <p>Drop down one dimension and repeat for the next eigenvector, until done.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="insight"> | |
| <strong>Limitation:</strong> Because <code>arcsin</code> returns angles only in [−π/2, π/2], each eigenvector's direction is tracked over only a <span class="orange">half-circle</span>. Vectors near the edge can appear to jump from one side to the other — a "wrap-around" artifact. | |
| </div> | |
| </section> | |
| <!-- NEW METHOD --> | |
| <section id="new" class="fade-in"> | |
| <div class="section-label">The Breakthrough</div> | |
| <h2>The New Fix: Modified arctan2 Method</h2> | |
| <p>The key insight is that a reflection (sign flip) in a <em>reducible</em> subspace can be replaced by a <span class="green">rotation through a major angle</span> (more than 90°). This means:</p> | |
| <div class="card-grid"> | |
| <div class="card"> | |
| <div class="icon">🔄</div> | |
| <h3>Full-Circle Tracking</h3> | |
| <p>The primary rotation for each mode now spans a full <span class="math">2π</span> circle instead of just <span class="math">π</span>. No more wrap-around.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="icon">🪞</div> | |
| <h3>Minimal Reflections</h3> | |
| <p>Reflections are deferred to the very last subspace (which is "irreducible" — no rotation can fix it). At most <em>one</em> reflection is needed.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="icon">📐</div> | |
| <h3>Clean Statistics</h3> | |
| <p>With full-circle angles, you can compute proper directional statistics: means, variances, and detect outliers without artifacts.</p> | |
| </div> | |
| </div> | |
| <div class="eq-block"> | |
| θ<sub>1,2</sub> = arctan2(a₂, a₁) ← full 2π range<br> | |
| θ<sub>1,3</sub> = arctan2(a₃, |a₂ csc θ₂|) ← minor angle<br> | |
| θ<sub>1,4</sub> = arctan2(a₄, |a₃ csc θ₃|) ← minor angle | |
| </div> | |
| <p>Only the <span class="green">first rotation</span> upon entering each new subspace uses the full <span class="math">2π</span> range. Subsequent rotations within that subspace are naturally limited to <span class="math">π</span> — and that's perfectly fine.</p> | |
| </section> | |
| <!-- 2D INTERACTIVE DEMO --> | |
| <section id="demo2d" class="fade-in"> | |
| <div class="section-label">Interactive Demo</div> | |
| <h2>See It in Action: 2D Rotation</h2> | |
| <p>Drag the vector around. Watch how the two methods track the angle differently. The <span class="orange">arcsin method</span> is limited to a half-circle, while the <span class="green">arctan2 method</span> tracks the full rotation.</p> | |
| <div class="comparison"> | |
| <div class="comparison-side"> | |
| <h3 style="color:var(--orange);">arcsin Method</h3> | |
| <canvas id="arcsinDemo" width="300" height="300"></canvas> | |
| <p id="arcsinAngle" style="font-family:'JetBrains Mono';font-size:1.1rem;color:var(--orange);">θ = 0°</p> | |
| <p style="font-size:0.8rem;">Range: −90° to +90°</p> | |
| </div> | |
| <div class="comparison-side"> | |
| <h3 style="color:var(--green);">arctan2 Method</h3> | |
| <canvas id="arctan2Demo" width="300" height="300"></canvas> | |
| <p id="arctan2Angle" style="font-family:'JetBrains Mono';font-size:1.1rem;color:var(--green);">θ = 0°</p> | |
| <p style="font-size:0.8rem;">Range: −180° to +180°</p> | |
| </div> | |
| </div> | |
| <p style="text-align:center;font-size:0.85rem;color:var(--text3);">Click and drag inside either circle to rotate the vector. Notice the wrap-around in the arcsin version!</p> | |
| </section> | |
| <!-- 3D GIVENS ROTATION DEMO --> | |
| <section id="demo3d" class="fade-in"> | |
| <div class="section-label">Deeper Dive</div> | |
| <h2>Givens Rotations: Building Blocks</h2> | |
| <p>The algorithm uses <span class="highlight">Givens rotations</span> — simple rotations within a 2D plane embedded in higher-dimensional space. They annihilate one component at a time, like zeroing out entries in a matrix.</p> | |
| <div class="demo-container"> | |
| <canvas id="givensDemo" width="700" height="450"></canvas> | |
| <div class="demo-controls"> | |
| <button class="btn active" id="givensStep0">Original Vector</button> | |
| <button class="btn" id="givensStep1">Rotate in (x,y)</button> | |
| <button class="btn" id="givensStep2">Rotate in (x,z)</button> | |
| <button class="btn" id="givensStep3">Aligned!</button> | |
| <button class="btn" id="givensAuto">▶ Auto-Play</button> | |
| </div> | |
| <p style="text-align:center;margin-top:1rem;font-size:0.85rem;color:var(--text3);">A 3D vector is progressively rotated onto the x-axis using two Givens rotations.</p> | |
| </div> | |
| </section> | |
| <!-- POLAR PLOT / RMT --> | |
| <section id="rmt" class="fade-in"> | |
| <div class="section-label">Application</div> | |
| <h2>Signal vs. Noise: Random Matrix Theory</h2> | |
| <p>When the algorithm is applied to real financial market data (foreign exchange quotes & trades), something beautiful emerges:</p> | |
| <div class="card-grid"> | |
| <div class="card" style="border-color: var(--green);"> | |
| <div class="icon">🎯</div> | |
| <h3><span class="green">Informative Modes</span></h3> | |
| <p>Eigenvectors carrying real information point in <em>consistent directions</em> over time. On the polar plot, they cluster tightly.</p> | |
| </div> | |
| <div class="card" style="border-color: var(--red);"> | |
| <div class="icon">🎲</div> | |
| <h3><span class="red">Noise Modes</span></h3> | |
| <p>Eigenvectors corrupted by sample noise point in <em>random directions</em>. On the polar plot, they scatter uniformly around the circle.</p> | |
| </div> | |
| </div> | |
| <div class="demo-container polar-demo"> | |
| <canvas id="polarPlot" width="700" height="400"></canvas> | |
| <div class="demo-controls"> | |
| <div class="slider-group"> | |
| <label>Signal Strength:</label> | |
| <input type="range" id="signalStrength" min="0" max="100" value="70"> | |
| </div> | |
| <div class="slider-group"> | |
| <label>Noise Level:</label> | |
| <input type="range" id="noiseLevel" min="0" max="100" value="30"> | |
| </div> | |
| <button class="btn" id="regeneratePolar">🔄 Regenerate</button> | |
| </div> | |
| <p style="margin-top:1rem;font-size:0.85rem;color:var(--text3);text-align:center;"> | |
| <span class="green">Green dots</span> = informative mode (clustered). <span class="red">Red dots</span> = noise mode (scattered). Adjust the sliders to see how signal and noise separate. | |
| </p> | |
| </div> | |
| <div class="insight"> | |
| <strong>The punchline:</strong> This distinction between clustered (informative) and scattered (noisy) modes matches perfectly with what the <em>eigenvalue</em> analysis predicts via the Marčenko–Pastur distribution. The angular analysis provides <strong>independent confirmation</strong> of which modes carry real information. | |
| </div> | |
| </section> | |
| <!-- STABILIZATION --> | |
| <section id="stabilization" class="fade-in"> | |
| <div class="section-label">Practical Impact</div> | |
| <h2>Stabilizing the Compass Over Time</h2> | |
| <p>Once informative and noisy modes are identified, two stabilization strategies can be applied:</p> | |
| <div class="comparison"> | |
| <div class="comparison-side"> | |
| <h3 style="color:var(--cyan);">Dynamic Stabilization</h3> | |
| <canvas id="dynamicStab" width="300" height="250"></canvas> | |
| <p style="font-size:0.85rem;">For <span class="green">informative</span> modes: filter the eigenvectors over time to reduce wobble. Like smoothing a noisy compass reading.</p> | |
| </div> | |
| <div class="comparison-side"> | |
| <h3 style="color:var(--pink);">Static Stabilization</h3> | |
| <canvas id="staticStab" width="300" height="250"></canvas> | |
| <p style="font-size:0.85rem;">For <span class="red">noise</span> modes: fix their angles to zero (point them at identity). Trading variance for bias — they were random anyway!</p> | |
| </div> | |
| </div> | |
| <div class="demo-container"> | |
| <h3 style="text-align:center;margin-bottom:1rem;">Correlation Matrix Cleaning</h3> | |
| <canvas id="corrMatrix" width="700" height="240"></canvas> | |
| <div class="demo-controls"> | |
| <button class="btn active" id="corrRaw">Raw</button> | |
| <button class="btn" id="corrDynamic">+ Dynamic</button> | |
| <button class="btn" id="corrStatic">+ Static</button> | |
| </div> | |
| <p style="text-align:center;margin-top:1rem;font-size:0.85rem;color:var(--text3);">Watch the correlation matrix become cleaner and more stable as stabilization is applied.</p> | |
| </div> | |
| </section> | |
| <!-- SUMMARY --> | |
| <section class="fade-in"> | |
| <div class="section-label">Summary</div> | |
| <h2>What This All Means</h2> | |
| <div class="card-grid"> | |
| <div class="card"> | |
| <div class="icon">✅</div> | |
| <h3>Consistent Directions</h3> | |
| <p>Eigenvectors now have a reliable, consistent orientation that doesn't randomly flip between computations.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="icon">📏</div> | |
| <h3>Full Angular Range</h3> | |
| <p>Primary rotation angles span the full 360° circle, eliminating wrap-around artifacts and enabling proper directional statistics.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="icon">🔍</div> | |
| <h3>Signal Detection</h3> | |
| <p>The angular analysis independently confirms which modes carry real information vs. noise — cross-validating eigenvalue-based methods.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="icon">🧹</div> | |
| <h3>Matrix Cleaning</h3> | |
| <p>Dynamic and static stabilization of eigenvectors leads to cleaner, more stable correlation matrices for downstream use.</p> | |
| </div> | |
| </div> | |
| <div class="eq-block"> | |
| R<sup>T</sup> V S = I ← The central equation: orient V so it's a pure rotation away from identity. | |
| </div> | |
| <p style="text-align:center;color:var(--text3);">Available as the <code>thucyd</code> Python package on PyPI and Conda-Forge.</p> | |
| </section> | |
| <footer> | |
| <p>Interactive explainer based on <a href="https://arxiv.org/abs/2402.08139" target="_blank">"A Consistently Oriented Basis for Eigenanalysis: Improved Directional Statistics"</a> by Jay Damask (2024).</p> | |
| <p style="margin-top:0.5rem;">Built for educational purposes. Not affiliated with the author.</p> | |
| </footer> | |
| <script> | |
| // ============================================================ | |
| // UTILITY FUNCTIONS | |
| // ============================================================ | |
| function lerp(a, b, t) { return a + (b - a) * t; } | |
| function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); } | |
| function randRange(lo, hi) { return lo + Math.random() * (hi - lo); } | |
| function gaussRand() { | |
| let u = 0, v = 0; | |
| while (u === 0) u = Math.random(); | |
| while (v === 0) v = Math.random(); | |
| return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); | |
| } | |
| // ============================================================ | |
| // HERO BACKGROUND ANIMATION | |
| // ============================================================ | |
| (function() { | |
| const c = document.getElementById('heroBg'); | |
| const ctx = c.getContext('2d'); | |
| let W, H; | |
| const particles = []; | |
| function resize() { | |
| W = c.width = c.parentElement.clientWidth; | |
| H = c.height = c.parentElement.clientHeight; | |
| } | |
| resize(); | |
| window.addEventListener('resize', resize); | |
| for (let i = 0; i < 80; i++) { | |
| particles.push({ | |
| x: Math.random() * 2000, | |
| y: Math.random() * 1200, | |
| vx: (Math.random() - 0.5) * 0.4, | |
| vy: (Math.random() - 0.5) * 0.4, | |
| r: Math.random() * 2 + 1, | |
| hue: Math.random() * 60 + 220 | |
| }); | |
| } | |
| function drawHero() { | |
| ctx.clearRect(0, 0, W, H); | |
| for (const p of particles) { | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| if (p.x < 0) p.x += W; | |
| if (p.x > W) p.x -= W; | |
| if (p.y < 0) p.y += H; | |
| if (p.y > H) p.y -= H; | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); | |
| ctx.fillStyle = `hsla(${p.hue}, 80%, 70%, 0.5)`; | |
| ctx.fill(); | |
| } | |
| // connect nearby | |
| for (let i = 0; i < particles.length; i++) { | |
| for (let j = i + 1; j < particles.length; j++) { | |
| const dx = particles[i].x - particles[j].x; | |
| const dy = particles[i].y - particles[j].y; | |
| const d = Math.sqrt(dx * dx + dy * dy); | |
| if (d < 120) { | |
| ctx.beginPath(); | |
| ctx.moveTo(particles[i].x, particles[i].y); | |
| ctx.lineTo(particles[j].x, particles[j].y); | |
| ctx.strokeStyle = `rgba(99,102,241,${0.15 * (1 - d / 120)})`; | |
| ctx.lineWidth = 0.5; | |
| ctx.stroke(); | |
| } | |
| } | |
| } | |
| requestAnimationFrame(drawHero); | |
| } | |
| drawHero(); | |
| })(); | |
| // ============================================================ | |
| // SCROLL FADE-IN | |
| // ============================================================ | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(e => { | |
| if (e.isIntersecting) e.target.classList.add('visible'); | |
| }); | |
| }, { threshold: 0.1 }); | |
| document.querySelectorAll('.fade-in').forEach(el => observer.observe(el)); | |
| // ============================================================ | |
| // EIGEN CLOUD DEMO | |
| // ============================================================ | |
| (function() { | |
| const c = document.getElementById('eigenCloud'); | |
| const ctx = c.getContext('2d'); | |
| let showEigen = false; | |
| let points = []; | |
| let eigenAngle = 0.4; | |
| let eigenRatio = 0.3; | |
| function generate() { | |
| points = []; | |
| eigenAngle = randRange(-0.8, 0.8); | |
| eigenRatio = randRange(0.15, 0.45); | |
| const s1 = 120, s2 = s1 * eigenRatio; | |
| for (let i = 0; i < 200; i++) { | |
| const u = gaussRand(), v = gaussRand(); | |
| const x = u * s1 * Math.cos(eigenAngle) - v * s2 * Math.sin(eigenAngle); | |
| const y = u * s1 * Math.sin(eigenAngle) + v * s2 * Math.cos(eigenAngle); | |
| points.push({ x, y }); | |
| } | |
| } | |
| generate(); | |
| function draw() { | |
| ctx.clearRect(0, 0, c.width, c.height); | |
| const cx = c.width / 2, cy = c.height / 2; | |
| // grid | |
| ctx.strokeStyle = 'rgba(255,255,255,0.05)'; | |
| ctx.lineWidth = 1; | |
| for (let x = 0; x < c.width; x += 50) { | |
| ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, c.height); ctx.stroke(); | |
| } | |
| for (let y = 0; y < c.height; y += 50) { | |
| ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(c.width, y); ctx.stroke(); | |
| } | |
| // points | |
| for (const p of points) { | |
| ctx.beginPath(); | |
| ctx.arc(cx + p.x, cy + p.y, 2.5, 0, Math.PI * 2); | |
| ctx.fillStyle = 'rgba(148, 163, 184, 0.5)'; | |
| ctx.fill(); | |
| } | |
| if (showEigen) { | |
| const s1 = 120, s2 = s1 * eigenRatio; | |
| // eigenvector 1 | |
| const dx1 = Math.cos(eigenAngle) * 180; | |
| const dy1 = Math.sin(eigenAngle) * 180; | |
| drawArrow(ctx, cx, cy, cx + dx1, cy + dy1, '#6366f1', 3); | |
| drawArrow(ctx, cx, cy, cx - dx1, cy - dy1, '#6366f1', 3); | |
| // eigenvector 2 | |
| const dx2 = -Math.sin(eigenAngle) * 100; | |
| const dy2 = Math.cos(eigenAngle) * 100; | |
| drawArrow(ctx, cx, cy, cx + dx2, cy + dy2, '#22d3ee', 3); | |
| drawArrow(ctx, cx, cy, cx - dx2, cy - dy2, '#22d3ee', 3); | |
| ctx.fillStyle = '#6366f1'; | |
| ctx.font = '600 14px Inter'; | |
| ctx.fillText('v₁ (most variance)', cx + dx1 + 10, cy + dy1 - 10); | |
| ctx.fillStyle = '#22d3ee'; | |
| ctx.fillText('v₂ (least variance)', cx + dx2 + 10, cy + dy2 - 10); | |
| } | |
| } | |
| function drawArrow(ctx, x0, y0, x1, y1, color, width) { | |
| const angle = Math.atan2(y1 - y0, x1 - x0); | |
| const headLen = 12; | |
| ctx.beginPath(); | |
| ctx.moveTo(x0, y0); | |
| ctx.lineTo(x1, y1); | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = width; | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.moveTo(x1, y1); | |
| ctx.lineTo(x1 - headLen * Math.cos(angle - 0.4), y1 - headLen * Math.sin(angle - 0.4)); | |
| ctx.lineTo(x1 - headLen * Math.cos(angle + 0.4), y1 - headLen * Math.sin(angle + 0.4)); | |
| ctx.closePath(); | |
| ctx.fillStyle = color; | |
| ctx.fill(); | |
| } | |
| draw(); | |
| document.getElementById('reshuffleCloud').onclick = () => { generate(); draw(); }; | |
| document.getElementById('toggleEigen').onclick = function() { | |
| showEigen = !showEigen; | |
| this.classList.toggle('active', showEigen); | |
| this.textContent = showEigen ? 'Hide Eigenvectors' : 'Show Eigenvectors'; | |
| draw(); | |
| }; | |
| })(); | |
| // ============================================================ | |
| // 2D ARCSIN vs ARCTAN2 COMPARISON | |
| // ============================================================ | |
| (function() { | |
| const cSin = document.getElementById('arcsinDemo'); | |
| const cTan = document.getElementById('arctan2Demo'); | |
| const ctxSin = cSin.getContext('2d'); | |
| const ctxTan = cTan.getContext('2d'); | |
| let angle = 0.5; | |
| let dragging = false; | |
| function drawComparison(ctx, canvas, method) { | |
| const W = canvas.width, H = canvas.height; | |
| const cx = W / 2, cy = H / 2, R = W / 2 - 30; | |
| ctx.clearRect(0, 0, W, H); | |
| // background circle | |
| ctx.beginPath(); | |
| ctx.arc(cx, cy, R, 0, Math.PI * 2); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.1)'; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| // axes | |
| ctx.strokeStyle = 'rgba(255,255,255,0.07)'; | |
| ctx.beginPath(); ctx.moveTo(cx - R, cy); ctx.lineTo(cx + R, cy); ctx.stroke(); | |
| ctx.beginPath(); ctx.moveTo(cx, cy - R); ctx.lineTo(cx, cy + R); ctx.stroke(); | |
| // valid range shading | |
| if (method === 'arcsin') { | |
| ctx.beginPath(); | |
| ctx.arc(cx, cy, R, -Math.PI / 2, Math.PI / 2); | |
| ctx.lineTo(cx, cy); | |
| ctx.closePath(); | |
| ctx.fillStyle = 'rgba(251, 146, 60, 0.07)'; | |
| ctx.fill(); | |
| // shade the restricted zone | |
| ctx.beginPath(); | |
| ctx.arc(cx, cy, R, Math.PI / 2, Math.PI * 1.5); | |
| ctx.lineTo(cx, cy); | |
| ctx.closePath(); | |
| ctx.fillStyle = 'rgba(248, 113, 113, 0.05)'; | |
| ctx.fill(); | |
| } else { | |
| ctx.beginPath(); | |
| ctx.arc(cx, cy, R, 0, Math.PI * 2); | |
| ctx.fillStyle = 'rgba(52, 211, 153, 0.05)'; | |
| ctx.fill(); | |
| } | |
| // vector | |
| const vx = Math.cos(angle); | |
| const vy = Math.sin(angle); | |
| let displayAngle; | |
| if (method === 'arcsin') { | |
| displayAngle = Math.asin(vy); | |
| } else { | |
| displayAngle = Math.atan2(vy, vx); | |
| } | |
| // Draw the vector the method "sees" | |
| let seenVx, seenVy; | |
| if (method === 'arcsin') { | |
| seenVx = Math.cos(displayAngle); | |
| seenVy = Math.sin(displayAngle); | |
| } else { | |
| seenVx = vx; | |
| seenVy = vy; | |
| } | |
| // actual vector (faded) | |
| ctx.beginPath(); | |
| ctx.moveTo(cx, cy); | |
| ctx.lineTo(cx + vx * R * 0.85, cy - vy * R * 0.85); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.15)'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| // seen vector | |
| const color = method === 'arcsin' ? '#fb923c' : '#34d399'; | |
| ctx.beginPath(); | |
| ctx.moveTo(cx, cy); | |
| const ex = cx + seenVx * R * 0.85; | |
| const ey = cy - seenVy * R * 0.85; | |
| ctx.lineTo(ex, ey); | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = 3; | |
| ctx.stroke(); | |
| // arrow head | |
| const ha = Math.atan2(-(seenVy), seenVx); | |
| const hl = 12; | |
| ctx.beginPath(); | |
| ctx.moveTo(ex, ey); | |
| ctx.lineTo(ex - hl * Math.cos(ha - 0.4), ey - hl * Math.sin(ha - 0.4)); | |
| ctx.lineTo(ex - hl * Math.cos(ha + 0.4), ey - hl * Math.sin(ha + 0.4)); | |
| ctx.closePath(); | |
| ctx.fillStyle = color; | |
| ctx.fill(); | |
| // dot at tip | |
| ctx.beginPath(); | |
| ctx.arc(ex, ey, 5, 0, Math.PI * 2); | |
| ctx.fillStyle = color; | |
| ctx.fill(); | |
| // arc showing angle | |
| ctx.beginPath(); | |
| if (displayAngle >= 0) { | |
| ctx.arc(cx, cy, 35, 0, -displayAngle, true); | |
| } else { | |
| ctx.arc(cx, cy, 35, 0, -displayAngle, false); | |
| } | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| // labels | |
| ctx.fillStyle = 'rgba(255,255,255,0.3)'; | |
| ctx.font = '11px Inter'; | |
| ctx.fillText('π₁', cx + R + 5, cy + 4); | |
| ctx.fillText('π₂', cx - 5, cy - R - 8); | |
| // update text | |
| const deg = (displayAngle * 180 / Math.PI).toFixed(1); | |
| const el = method === 'arcsin' ? document.getElementById('arcsinAngle') : document.getElementById('arctan2Angle'); | |
| el.textContent = `θ = ${deg}°`; | |
| } | |
| function draw() { | |
| drawComparison(ctxSin, cSin, 'arcsin'); | |
| drawComparison(ctxTan, cTan, 'arctan2'); | |
| } | |
| draw(); | |
| function handleMouse(e, canvas) { | |
| const rect = canvas.getBoundingClientRect(); | |
| const scaleX = canvas.width / rect.width; | |
| const scaleY = canvas.height / rect.height; | |
| const mx = (e.clientX - rect.left) * scaleX - canvas.width / 2; | |
| const my = -((e.clientY - rect.top) * scaleY - canvas.height / 2); | |
| angle = Math.atan2(my, mx); | |
| draw(); | |
| } | |
| [cSin, cTan].forEach(canvas => { | |
| canvas.addEventListener('mousedown', (e) => { dragging = true; handleMouse(e, canvas); }); | |
| canvas.addEventListener('mousemove', (e) => { if (dragging) handleMouse(e, canvas); }); | |
| canvas.addEventListener('touchstart', (e) => { e.preventDefault(); dragging = true; handleMouse(e.touches[0], canvas); }); | |
| canvas.addEventListener('touchmove', (e) => { e.preventDefault(); if (dragging) handleMouse(e.touches[0], canvas); }); | |
| }); | |
| document.addEventListener('mouseup', () => dragging = false); | |
| document.addEventListener('touchend', () => dragging = false); | |
| })(); | |
| // ============================================================ | |
| // GIVENS ROTATION 3D DEMO | |
| // ============================================================ | |
| (function() { | |
| const c = document.getElementById('givensDemo'); | |
| const ctx = c.getContext('2d'); | |
| const W = c.width, H = c.height; | |
| let step = 0; | |
| let animT = 0; | |
| let autoPlay = false; | |
| let autoTimer = null; | |
| // Original vector | |
| const orig = { x: 0.5, y: 0.7, z: 0.5 }; | |
| const len = Math.sqrt(orig.x * orig.x + orig.y * orig.y + orig.z * orig.z); | |
| orig.x /= len; orig.y /= len; orig.z /= len; | |
| function getVec(step, t) { | |
| const cx = orig.x, cy = orig.y, cz = orig.z; | |
| // Step 1: rotate in (x,y) to eliminate y | |
| const angle1 = Math.atan2(cy, cx); | |
| // Step 2: rotate in (x,z) to eliminate z | |
| const r1 = Math.sqrt(cx * cx + cy * cy); | |
| const angle2 = Math.atan2(cz, r1); | |
| if (step === 0) return { x: cx, y: cy, z: cz }; | |
| // After step 1 | |
| const a1 = step >= 1 ? angle1 * Math.min(t, 1) : 0; | |
| let vx = cx * Math.cos(a1) + cy * Math.sin(a1); | |
| let vy = -cx * Math.sin(a1) + cy * Math.cos(a1); | |
| let vz = cz; | |
| if (step === 1) return { x: vx, y: vy, z: vz }; | |
| // After step 2 | |
| const a2 = step >= 2 ? angle2 * Math.min(t, 1) : 0; | |
| const vx2 = vx * Math.cos(a2) + vz * Math.sin(a2); | |
| const vy2 = vy; | |
| const vz2 = -vx * Math.sin(a2) + vz * Math.cos(a2); | |
| if (step === 2) return { x: vx2, y: vy2, z: vz2 }; | |
| return { x: vx2, y: vy2, z: vz2 }; | |
| } | |
| function project(x, y, z) { | |
| const scale = 150; | |
| const angleY = 0.5; | |
| const angleX = 0.3; | |
| // rotate for view | |
| const c1 = Math.cos(angleY), s1 = Math.sin(angleY); | |
| const rx = x * c1 + z * s1; | |
| const rz = -x * s1 + z * c1; | |
| const c2 = Math.cos(angleX), s2 = Math.sin(angleX); | |
| const ry = y * c2 - rz * s2; | |
| const rz2 = y * s2 + rz * c2; | |
| return { | |
| px: W / 2 + rx * scale, | |
| py: H / 2 - ry * scale, | |
| depth: rz2 | |
| }; | |
| } | |
| function drawArrow3D(x0, y0, z0, x1, y1, z1, color, width) { | |
| const p0 = project(x0, y0, z0); | |
| const p1 = project(x1, y1, z1); | |
| ctx.beginPath(); | |
| ctx.moveTo(p0.px, p0.py); | |
| ctx.lineTo(p1.px, p1.py); | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = width; | |
| ctx.stroke(); | |
| // arrowhead | |
| const a = Math.atan2(p1.py - p0.py, p1.px - p0.px); | |
| const hl = 10; | |
| ctx.beginPath(); | |
| ctx.moveTo(p1.px, p1.py); | |
| ctx.lineTo(p1.px - hl * Math.cos(a - 0.4), p1.py - hl * Math.sin(a - 0.4)); | |
| ctx.lineTo(p1.px - hl * Math.cos(a + 0.4), p1.py - hl * Math.sin(a + 0.4)); | |
| ctx.closePath(); | |
| ctx.fillStyle = color; | |
| ctx.fill(); | |
| } | |
| function draw() { | |
| ctx.clearRect(0, 0, W, H); | |
| // Draw axes | |
| drawArrow3D(0,0,0, 1.4,0,0, 'rgba(255,255,255,0.15)', 1); | |
| drawArrow3D(0,0,0, 0,1.4,0, 'rgba(255,255,255,0.15)', 1); | |
| drawArrow3D(0,0,0, 0,0,1.4, 'rgba(255,255,255,0.15)', 1); | |
| // axis labels | |
| const lx = project(1.5, 0, 0); | |
| const ly = project(0, 1.5, 0); | |
| const lz = project(0, 0, 1.5); | |
| ctx.fillStyle = 'rgba(255,255,255,0.3)'; | |
| ctx.font = '13px Inter'; | |
| ctx.fillText('x (π₁)', lx.px, lx.py); | |
| ctx.fillText('y (π₂)', ly.px, ly.py); | |
| ctx.fillText('z (π₃)', lz.px, lz.py); | |
| // ghost of original | |
| if (step > 0) { | |
| drawArrow3D(0, 0, 0, orig.x, orig.y, orig.z, 'rgba(255,255,255,0.1)', 1); | |
| } | |
| // current vector | |
| const v = getVec(step, 1); | |
| const colors = ['#6366f1', '#34d399', '#22d3ee', '#fbbf24']; | |
| drawArrow3D(0, 0, 0, v.x, v.y, v.z, colors[step], 3); | |
| // dot | |
| const p = project(v.x, v.y, v.z); | |
| ctx.beginPath(); | |
| ctx.arc(p.px, p.py, 6, 0, Math.PI * 2); | |
| ctx.fillStyle = colors[step]; | |
| ctx.fill(); | |
| // info | |
| ctx.fillStyle = '#fff'; | |
| ctx.font = '600 14px Inter'; | |
| const labels = [ | |
| `Original: (${v.x.toFixed(2)}, ${v.y.toFixed(2)}, ${v.z.toFixed(2)})`, | |
| `After R(x,y): y → 0 | (${v.x.toFixed(2)}, ${v.y.toFixed(3)}, ${v.z.toFixed(2)})`, | |
| `After R(x,z): z → 0 | (${v.x.toFixed(2)}, ${v.y.toFixed(3)}, ${v.z.toFixed(3)})`, | |
| `Aligned to x-axis! (${v.x.toFixed(2)}, ${v.y.toFixed(3)}, ${v.z.toFixed(3)})` | |
| ]; | |
| ctx.fillText(labels[step], 20, 30); | |
| // components bars | |
| const barX = W - 160, barY = 30, barW = 120, barH = 14; | |
| const comps = [ | |
| { label: 'x', val: Math.abs(v.x), color: '#f87171' }, | |
| { label: 'y', val: Math.abs(v.y), color: '#34d399' }, | |
| { label: 'z', val: Math.abs(v.z), color: '#60a5fa' }, | |
| ]; | |
| comps.forEach((comp, i) => { | |
| const y = barY + i * 24; | |
| ctx.fillStyle = 'rgba(255,255,255,0.2)'; | |
| ctx.fillRect(barX, y, barW, barH); | |
| ctx.fillStyle = comp.color; | |
| ctx.fillRect(barX, y, barW * comp.val, barH); | |
| ctx.fillStyle = '#fff'; | |
| ctx.font = '11px JetBrains Mono'; | |
| ctx.fillText(comp.label, barX - 15, y + 11); | |
| }); | |
| } | |
| draw(); | |
| function setStep(s) { | |
| step = s; | |
| draw(); | |
| ['givensStep0','givensStep1','givensStep2','givensStep3'].forEach((id, i) => { | |
| document.getElementById(id).classList.toggle('active', i === s); | |
| }); | |
| } | |
| document.getElementById('givensStep0').onclick = () => setStep(0); | |
| document.getElementById('givensStep1').onclick = () => setStep(1); | |
| document.getElementById('givensStep2').onclick = () => setStep(2); | |
| document.getElementById('givensStep3').onclick = () => setStep(3); | |
| document.getElementById('givensAuto').onclick = function() { | |
| autoPlay = !autoPlay; | |
| this.textContent = autoPlay ? '⏸ Pause' : '▶ Auto-Play'; | |
| if (autoPlay) { | |
| let s = 0; | |
| setStep(s); | |
| autoTimer = setInterval(() => { | |
| s = (s + 1) % 4; | |
| setStep(s); | |
| }, 1200); | |
| } else { | |
| clearInterval(autoTimer); | |
| } | |
| }; | |
| })(); | |
| // ============================================================ | |
| // POLAR PLOT DEMO | |
| // ============================================================ | |
| (function() { | |
| const c = document.getElementById('polarPlot'); | |
| const ctx = c.getContext('2d'); | |
| const W = c.width, H = c.height; | |
| let signalData = []; | |
| let noiseData = []; | |
| function generate() { | |
| const strength = document.getElementById('signalStrength').value / 100; | |
| const noise = document.getElementById('noiseLevel').value / 100; | |
| signalData = []; | |
| noiseData = []; | |
| const baseAngle = 0.7; | |
| for (let i = 0; i < 23; i++) { | |
| const a = baseAngle + gaussRand() * (1 - strength) * 0.8; | |
| const r = 0.5 + strength * 0.4 + gaussRand() * 0.08; | |
| signalData.push({ angle: a, r: clamp(r, 0.1, 1) }); | |
| } | |
| for (let i = 0; i < 23; i++) { | |
| const a = Math.random() * Math.PI * 2; | |
| const r = 0.15 + Math.random() * 0.35; | |
| noiseData.push({ angle: a, r: r }); | |
| } | |
| } | |
| generate(); | |
| function draw() { | |
| ctx.clearRect(0, 0, W, H); | |
| // Two polar plots side by side | |
| const plots = [ | |
| { cx: W * 0.28, cy: H / 2, R: Math.min(W * 0.22, H * 0.4), data: signalData, color: '#34d399', label: 'Mode 1 (Signal)' }, | |
| { cx: W * 0.72, cy: H / 2, R: Math.min(W * 0.22, H * 0.4), data: noiseData, color: '#f87171', label: 'Mode 4 (Noise)' }, | |
| ]; | |
| plots.forEach(plot => { | |
| const { cx, cy, R, data, color, label } = plot; | |
| // concentric circles | |
| for (let i = 1; i <= 4; i++) { | |
| ctx.beginPath(); | |
| ctx.arc(cx, cy, R * i / 4, 0, Math.PI * 2); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.06)'; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| } | |
| // radial lines | |
| for (let a = 0; a < Math.PI * 2; a += Math.PI / 6) { | |
| ctx.beginPath(); | |
| ctx.moveTo(cx, cy); | |
| ctx.lineTo(cx + Math.cos(a) * R, cy - Math.sin(a) * R); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.04)'; | |
| ctx.stroke(); | |
| } | |
| // points | |
| data.forEach(d => { | |
| const px = cx + Math.cos(d.angle) * d.r * R; | |
| const py = cy - Math.sin(d.angle) * d.r * R; | |
| ctx.beginPath(); | |
| ctx.arc(px, py, 5, 0, Math.PI * 2); | |
| ctx.fillStyle = color; | |
| ctx.globalAlpha = 0.7; | |
| ctx.fill(); | |
| ctx.globalAlpha = 1; | |
| }); | |
| // label | |
| ctx.fillStyle = color; | |
| ctx.font = '600 14px Inter'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(label, cx, cy + R + 30); | |
| ctx.textAlign = 'left'; | |
| // degree labels | |
| ctx.fillStyle = 'rgba(255,255,255,0.2)'; | |
| ctx.font = '10px Inter'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('0°', cx + R + 12, cy + 4); | |
| ctx.fillText('90°', cx, cy - R - 6); | |
| ctx.fillText('180°', cx - R - 16, cy + 4); | |
| ctx.fillText('270°', cx, cy + R + 14); | |
| ctx.textAlign = 'left'; | |
| }); | |
| } | |
| draw(); | |
| document.getElementById('signalStrength').oninput = () => { generate(); draw(); }; | |
| document.getElementById('noiseLevel').oninput = () => { generate(); draw(); }; | |
| document.getElementById('regeneratePolar').onclick = () => { generate(); draw(); }; | |
| })(); | |
| // ============================================================ | |
| // DYNAMIC STABILIZATION DEMO | |
| // ============================================================ | |
| (function() { | |
| const c = document.getElementById('dynamicStab'); | |
| const ctx = c.getContext('2d'); | |
| const W = c.width, H = c.height; | |
| const N = 20; | |
| const baseAngle = 0.6; | |
| const raw = []; | |
| const filtered = []; | |
| for (let i = 0; i < N; i++) { | |
| raw.push(baseAngle + gaussRand() * 0.3); | |
| } | |
| // simple 5-point filter | |
| for (let i = 0; i < N; i++) { | |
| let sum = 0, wt = 0; | |
| for (let j = Math.max(0, i - 2); j <= i; j++) { | |
| const w = 1; | |
| sum += raw[j] * w; | |
| wt += w; | |
| } | |
| filtered.push(sum / wt); | |
| } | |
| function draw() { | |
| ctx.clearRect(0, 0, W, H); | |
| const padL = 40, padR = 20, padT = 20, padB = 40; | |
| const gw = W - padL - padR; | |
| const gh = H - padT - padB; | |
| // y range | |
| const allVals = [...raw, ...filtered]; | |
| const ymin = Math.min(...allVals) - 0.2; | |
| const ymax = Math.max(...allVals) + 0.2; | |
| function toX(i) { return padL + (i / (N - 1)) * gw; } | |
| function toY(v) { return padT + (1 - (v - ymin) / (ymax - ymin)) * gh; } | |
| // grid | |
| ctx.strokeStyle = 'rgba(255,255,255,0.05)'; | |
| for (let i = 0; i < N; i++) { | |
| const x = toX(i); | |
| ctx.beginPath(); ctx.moveTo(x, padT); ctx.lineTo(x, padT + gh); ctx.stroke(); | |
| } | |
| // raw | |
| ctx.beginPath(); | |
| raw.forEach((v, i) => { | |
| if (i === 0) ctx.moveTo(toX(i), toY(v)); | |
| else ctx.lineTo(toX(i), toY(v)); | |
| }); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.2)'; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| raw.forEach((v, i) => { | |
| ctx.beginPath(); | |
| ctx.arc(toX(i), toY(v), 3, 0, Math.PI * 2); | |
| ctx.fillStyle = 'rgba(255,255,255,0.3)'; | |
| ctx.fill(); | |
| }); | |
| // filtered | |
| ctx.beginPath(); | |
| filtered.forEach((v, i) => { | |
| if (i === 0) ctx.moveTo(toX(i), toY(v)); | |
| else ctx.lineTo(toX(i), toY(v)); | |
| }); | |
| ctx.strokeStyle = '#22d3ee'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| filtered.forEach((v, i) => { | |
| ctx.beginPath(); | |
| ctx.arc(toX(i), toY(v), 4, 0, Math.PI * 2); | |
| ctx.fillStyle = '#22d3ee'; | |
| ctx.fill(); | |
| }); | |
| // labels | |
| ctx.fillStyle = 'rgba(255,255,255,0.3)'; | |
| ctx.font = '10px Inter'; | |
| ctx.fillText('Raw', padL, padT - 5); | |
| ctx.fillStyle = '#22d3ee'; | |
| ctx.fillText('Filtered', padL + 40, padT - 5); | |
| ctx.fillStyle = 'var(--text3)'; | |
| ctx.font = '10px Inter'; | |
| ctx.fillText('time →', W / 2 - 15, H - 5); | |
| ctx.save(); | |
| ctx.translate(10, H / 2); | |
| ctx.rotate(-Math.PI / 2); | |
| ctx.fillStyle = 'rgba(255,255,255,0.2)'; | |
| ctx.fillText('θ (angle)', 0, 0); | |
| ctx.restore(); | |
| } | |
| draw(); | |
| })(); | |
| // ============================================================ | |
| // STATIC STABILIZATION DEMO | |
| // ============================================================ | |
| (function() { | |
| const c = document.getElementById('staticStab'); | |
| const ctx = c.getContext('2d'); | |
| const W = c.width, H = c.height; | |
| const N = 20; | |
| const raw = []; | |
| for (let i = 0; i < N; i++) { | |
| raw.push(Math.random() * Math.PI * 2 - Math.PI); | |
| } | |
| function draw() { | |
| ctx.clearRect(0, 0, W, H); | |
| const padL = 40, padR = 20, padT = 20, padB = 40; | |
| const gw = W - padL - padR; | |
| const gh = H - padT - padB; | |
| const ymin = -Math.PI - 0.3; | |
| const ymax = Math.PI + 0.3; | |
| function toX(i) { return padL + (i / (N - 1)) * gw; } | |
| function toY(v) { return padT + (1 - (v - ymin) / (ymax - ymin)) * gh; } | |
| // grid | |
| ctx.strokeStyle = 'rgba(255,255,255,0.05)'; | |
| for (let i = 0; i < N; i++) { | |
| const x = toX(i); | |
| ctx.beginPath(); ctx.moveTo(x, padT); ctx.lineTo(x, padT + gh); ctx.stroke(); | |
| } | |
| // zero line | |
| ctx.beginPath(); | |
| ctx.moveTo(padL, toY(0)); | |
| ctx.lineTo(padL + gw, toY(0)); | |
| ctx.strokeStyle = '#f472b6'; | |
| ctx.lineWidth = 2; | |
| ctx.setLineDash([5, 5]); | |
| ctx.stroke(); | |
| ctx.setLineDash([]); | |
| // raw (scattered) | |
| ctx.beginPath(); | |
| raw.forEach((v, i) => { | |
| if (i === 0) ctx.moveTo(toX(i), toY(v)); | |
| else ctx.lineTo(toX(i), toY(v)); | |
| }); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.15)'; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| raw.forEach((v, i) => { | |
| ctx.beginPath(); | |
| ctx.arc(toX(i), toY(v), 3, 0, Math.PI * 2); | |
| ctx.fillStyle = 'rgba(248,113,113,0.5)'; | |
| ctx.fill(); | |
| }); | |
| // labels | |
| ctx.fillStyle = 'rgba(248,113,113,0.5)'; | |
| ctx.font = '10px Inter'; | |
| ctx.fillText('Random (noise)', padL, padT - 5); | |
| ctx.fillStyle = '#f472b6'; | |
| ctx.fillText('Fixed to θ=0', padL + 100, padT - 5); | |
| ctx.fillStyle = 'rgba(255,255,255,0.2)'; | |
| ctx.font = '10px Inter'; | |
| ctx.fillText('time →', W / 2 - 15, H - 5); | |
| ctx.save(); | |
| ctx.translate(10, H / 2); | |
| ctx.rotate(-Math.PI / 2); | |
| ctx.fillText('θ (angle)', 0, 0); | |
| ctx.restore(); | |
| } | |
| draw(); | |
| })(); | |
| // ============================================================ | |
| // CORRELATION MATRIX DEMO | |
| // ============================================================ | |
| (function() { | |
| const c = document.getElementById('corrMatrix'); | |
| const ctx = c.getContext('2d'); | |
| const W = c.width, H = c.height; | |
| const N = 7; | |
| // generate 3 correlation matrices | |
| function genCorrMatrix(noiseLevel) { | |
| const mat = []; | |
| for (let i = 0; i < N; i++) { | |
| mat.push([]); | |
| for (let j = 0; j < N; j++) { | |
| if (i === j) mat[i].push(1); | |
| else if (j > i) { | |
| let base = 0.3 + 0.4 * Math.exp(-Math.abs(i - j) * 0.5); | |
| base += (Math.random() - 0.5) * noiseLevel; | |
| base = clamp(base, -0.3, 0.95); | |
| mat[i].push(base); | |
| } else { | |
| mat[i].push(0); | |
| } | |
| } | |
| } | |
| // symmetrize | |
| for (let i = 0; i < N; i++) | |
| for (let j = 0; j < i; j++) | |
| mat[i][j] = mat[j][i]; | |
| return mat; | |
| } | |
| const rawMat = genCorrMatrix(0.4); | |
| const dynMat = genCorrMatrix(0.2); | |
| const statMat = genCorrMatrix(0.08); | |
| let currentMat = rawMat; | |
| function drawMatrix(mat, ox, title) { | |
| const cellSize = Math.min(28, (H - 60) / N); | |
| const totalW = cellSize * N; | |
| const startX = ox; | |
| const startY = 40; | |
| ctx.fillStyle = '#fff'; | |
| ctx.font = '600 12px Inter'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(title, startX + totalW / 2, 25); | |
| for (let i = 0; i < N; i++) { | |
| for (let j = 0; j < N; j++) { | |
| const v = mat[i][j]; | |
| const x = startX + j * cellSize; | |
| const y = startY + i * cellSize; | |
| // color mapping | |
| let r, g, b; | |
| if (v >= 0) { | |
| r = Math.round(30 + 69 * v); | |
| g = Math.round(30 + 181 * v); | |
| b = Math.round(50 + 200 * v); | |
| } else { | |
| r = Math.round(30 - 218 * v); | |
| g = Math.round(30 + 80 * v); | |
| b = Math.round(50 + 80 * v); | |
| } | |
| ctx.fillStyle = `rgb(${r},${g},${b})`; | |
| ctx.fillRect(x + 1, y + 1, cellSize - 2, cellSize - 2); | |
| } | |
| } | |
| ctx.textAlign = 'left'; | |
| } | |
| function draw() { | |
| ctx.clearRect(0, 0, W, H); | |
| const spacing = (W - 3 * N * 28) / 4; | |
| drawMatrix(rawMat, spacing, 'Raw'); | |
| drawMatrix(dynMat, spacing * 2 + N * 28, '+ Dynamic Filter'); | |
| drawMatrix(statMat, spacing * 3 + N * 28 * 2, '+ Static Stabilize'); | |
| } | |
| draw(); | |
| // Highlight the active one | |
| function setMode(mode) { | |
| ['corrRaw', 'corrDynamic', 'corrStatic'].forEach(id => { | |
| document.getElementById(id).classList.remove('active'); | |
| }); | |
| document.getElementById('corr' + mode).classList.add('active'); | |
| draw(); | |
| // add highlight | |
| const spacing = (W - 3 * N * 28) / 4; | |
| const cellSize = 28; | |
| let ox; | |
| if (mode === 'Raw') ox = spacing; | |
| else if (mode === 'Dynamic') ox = spacing * 2 + N * 28; | |
| else ox = spacing * 3 + N * 28 * 2; | |
| ctx.strokeStyle = '#6366f1'; | |
| ctx.lineWidth = 3; | |
| ctx.strokeRect(ox - 2, 38, N * cellSize + 4, N * cellSize + 4); | |
| } | |
| document.getElementById('corrRaw').onclick = () => setMode('Raw'); | |
| document.getElementById('corrDynamic').onclick = () => setMode('Dynamic'); | |
| document.getElementById('corrStatic').onclick = () => setMode('Static'); | |
| setMode('Raw'); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment