vikram-physics/winter-load.html
2026-07-18 19:42:30 +00:00

496 lines
19 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Winter Load Simulator | Anthony Figueroa</title>
<link rel="stylesheet" href="/styles.css">
<style>
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--accent-cyan: #00d4ff;
--accent-red: #ff4757;
--text-primary: #f0f0f5;
--text-muted: #8b8b99;
--border-glow: rgba(0, 212, 255, 0.3);
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
margin: 0;
padding: 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
header {
text-align: center;
margin-bottom: 60px;
position: relative;
}
h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
background: linear-gradient(135deg, var(--accent-cyan), #ffffff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 20px;
letter-spacing: -0.02em;
}
.subtitle {
font-size: 1.25rem;
color: var(--text-muted);
max-width: 600px;
margin: 0 auto;
}
.hero-image {
width: 100%;
height: 400px;
object-fit: cover;
border-radius: 12px;
margin: 40px 0;
filter: brightness(0.8) contrast(1.1);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 40px;
margin-bottom: 60px;
}
.panel {
background: var(--bg-secondary);
border: 1px solid var(--border-glow);
border-radius: 16px;
padding: 32px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.panel h2 {
font-size: 1.5rem;
color: var(--accent-cyan);
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 2px solid var(--border-glow);
}
.input-group {
margin-bottom: 20px;
}
label {
display: block;
font-size: 0.9rem;
color: var(--text-muted);
margin-bottom: 8px;
font-weight: 500;
}
input[type="number"], select {
width: 100%;
padding: 12px 16px;
background: rgba(255, 255, 5, 0.05);
border: 1px solid var(--border-glow);
border-radius: 8px;
color: var(--text-primary);
font-size: 1rem;
transition: all 0.2s ease;
}
input[type="number"]:focus, select:focus {
outline: none;
border-color: var(--accent-cyan);
box-shadow: 0 0 20px rgba(0, 212, 255, 0.2);
}
button.simulate-btn {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, var(--accent-cyan), #0099cc);
border: none;
border-radius: 8px;
color: #000;
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 16px;
}
button.simulate-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 212, 255, 0.4);
}
.results-panel {
min-height: 400px;
}
.result-card {
background: rgba(0, 212, 255, 0.05);
border-left: 4px solid var(--accent-cyan);
padding: 20px;
margin-bottom: 16px;
border-radius: 0 8px 8px 0;
}
.result-label {
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 4px;
}
.result-value {
font-size: 2rem;
font-weight: 700;
color: var(--accent-cyan);
}
.result-unit {
font-size: 1rem;
color: var(--text-muted);
}
.warning {
border-left-color: var(--accent-red);
}
.warning .result-value {
color: var(--accent-red);
}
canvas {
width: 100%;
height: 200px;
margin-top: 20px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.2);
}
.formula-box {
background: rgba(255, 71, 87, 0.05);
border: 1px dashed var(--accent-red);
border-radius: 8px;
padding: 20px;
margin: 24px 0;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.9rem;
overflow-x: auto;
}
.citation {
font-size: 0.8rem;
color: var(--text-muted);
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border-glow);
}
.citation a {
color: var(--accent-cyan);
text-decoration: none;
}
footer {
text-align: center;
padding: 40px;
color: var(--text-muted);
font-size: 0.9rem;
}
.nav-link {
display: inline-block;
margin: 0 12px;
color: var(--accent-cyan);
text-decoration: none;
}
.loading {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
</style>
<script defer src="https://analytics.4ort.xyz/script.js" data-website-id="d3ed927c-888a-4a6c-ae5f-0b1c613ddf5b"></script>
</head>
<body>
<div class="container">
<header>
<h1>Winter Load Simulator</h1>
<p class="subtitle">Model the stochastic chaos of a Minnesota winter. Input your roof geometry, watch the Monte Carlo engine simulate 10,000 storm scenarios, and see if your structure survives.</p>
<img src="https://images.pexels.com/photos/4156673/pexels-photo-4156673.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"
alt="Snow-covered Minneapolis streets during a winter storm"
class="hero-image">
</header>
<div class="grid">
<div class="panel">
<h2>⚙️ Simulation Parameters</h2>
<div class="input-group">
<label for="ground-snow">Ground Snow Load (psf)</label>
<input type="number" id="ground-snow" value="25" min="0" max="200" step="1">
<small style="color: var(--text-muted)">Minneapolis avg: 25 psf (ASCE 7-16 Table 7.1-1)</small>
</div>
<div class="input-group">
<label for="roof-slope">Roof Slope (degrees)</label>
<input type="number" id="roof-slope" value="30" min="0" max="90" step="1">
<small style="color: var(--text-muted)">Flat = 0°, Steep = 90°</small>
</div>
<div class="input-group">
<label for="exposure-factor">Exposure Factor (Ce)</label>
<select id="exposure-factor">
<option value="0.8">Exposed (Open Terrain)</option>
<option value="1.0" selected>Partially Sheltered</option>
<option value="1.2">Sheltered (Urban Core)</option>
</select>
</div>
<div class="input-group">
<label for="thermal-factor">Thermal Factor ( Ct )</label>
<select id="thermal-factor">
<option value="1.1">Cold Roof (Minimal Heat Loss)</option>
<option value="1.0" selected>Heated Roof</option>
</select>
</div>
<div class="input-group">
<label for="slope-factor">Slope Factor (Cs)</label>
<select id="slope-factor" disabled>
<option value="1.0">Auto-Calculated</option>
</select>
<small style="color: var(--text-muted)">Computed from θ per ASCE 7 Eq. 7-6</small>
</div>
<button class="simulate-btn" onclick="runSimulation()">▶ Run Monte Carlo Simulation</button>
<div class="formula-box">
<strong>Core Formula (ASCE 7-16 §7.3.1):</strong><br><br>
p_s = 0.7 × C_e × C_t × C_s × I_s × p_g<br><br>
<span style="color: var(--accent-cyan)">Where:</span><br>
p_s = Flat-roof snow load (psf)<br>
C_e = Exposure factor<br>
C_t = Thermal factor<br>
C_s = Slope factor<br>
I_s = Importance factor (1.0 residential, 1.2 critical)<br>
p_g = Ground snow load<br><br>
<span style="font-style: italic; color: var(--text-muted)">Monte Carlo variance: ±15% on p_g (storm intensity distribution)</span>
</div>
</div>
<div class="panel results-panel">
<h2>🎯 Simulation Results</h2>
<div id="results-container">
<p style="color: var(--text-muted); text-align: center; padding: 60px 0;">
Configure parameters above and run simulation.<br>
Engine will compute 10,000 stochastic iterations.
</p>
</div>
<canvas id="distribution-chart"></canvas>
<div class="citation">
<strong>Grounded in:</strong>
<a href="https://4ort.xyz/entity/structural-load" target="_blank">Wikidata Q7964839 (Structural Load)</a>
<a href="https://4ort.xyz/entity/minneapolis" target="_blank">Minneapolis Climate Data (Q36091)</a><br>
<span style="font-size: 0.75rem">Formula: ASCE/SEI 7-16 Minimum Design Loads for Buildings</span>
</div>
</div>
</div>
<div class="panel" style="margin-bottom: 60px;">
<h2>📊 How This Works</h2>
<p>This isn't a deterministic calculator—it's a <strong>probabilistic engine</strong>. Each simulation runs 10,000 iterations where the ground snow load varies according to a normal distribution (μ = your input, σ = 15%). Why? Because storms aren't averages. They're outliers.</p>
<p style="margin-top: 20px;">The slope factor C_s is computed per ASCE 7 Equation 7-6:</p>
<div class="formula-box">
C_s = 1.0 30°)/70° for 30° ≤ θ ≤ 50°<br>
C_s = 1.0 for θ &lt; 30°<br>
C_s = 0.0 for θ ≥ 50°
</div>
<p style="margin-top: 20px;"><strong>Failure Probability:</strong> The percentage of iterations where p_s exceeds your structural capacity. If you're designing a porch roof rated for 40 psf and 8,234/10,000 simulations exceed that—you've got a problem.</p>
</div>
<footer>
<a href="/" class="nav-link">← Back to Home</a>
<a href="/data/winter-load-params.json" class="nav-link">Download Parameters (JSON)</a>
<p style="margin-top: 24px;">Built in Minneapolis, MN • Anthony Figueroa • 2026</p>
</footer>
</div>
<script>
// Monte Carlo Simulation Engine
function runSimulation() {
const pg_base = parseFloat(document.getElementById('ground-snow').value);
const theta = parseFloat(document.getElementById('roof-slope').value);
const Ce = parseFloat(document.getElementById('exposure-factor').value);
const Ct = parseFloat(document.getElementById('thermal-factor').value);
const Is = 1.0; // Residential default
// Calculate slope factor per ASCE 7 Eq. 7-6
let Cs;
if (theta < 30) {
Cs = 1.0;
} else if (theta >= 30 && theta <= 50) {
Cs = 1.0 - (theta - 30) / 70;
} else {
Cs = 0.0;
}
document.getElementById('slope-factor').value = Cs.toFixed(3);
// Run Monte Carlo: 10,000 iterations
const iterations = 10000;
const sigma = pg_base * 0.15; // 15% variance
const results = [];
for (let i = 0; i < iterations; i++) {
// Box-Muller transform for normal distribution
const u1 = Math.random();
const u2 = Math.random();
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
const pg_variant = pg_base + z * sigma;
// Apply ASCE 7 formula
const ps = 0.7 * Ce * Ct * Cs * Is * Math.max(0, pg_variant);
results.push(ps);
}
// Statistics
const sorted = [...results].sort((a, b) => a - b);
const mean = results.reduce((a, b) => a + b, 0) / iterations;
const median = sorted[Math.floor(iterations / 2)];
const p95 = sorted[Math.floor(iterations * 0.95)];
const p99 = sorted[Math.floor(iterations * 0.99)];
const max = sorted[sorted.length - 1];
// Generate results HTML
const warningClass = p95 > mean * 1.3 ? 'warning' : '';
const resultsHTML = `
<div class="result-card">
<div class="result-label">Mean Snow Load (p̄_s)</div>
<span class="result-value">${mean.toFixed(2)}</span>
<span class="result-unit">psf</span>
</div>
<div class="result-card ${warningClass}">
<div class="result-label">95th Percentile (Design Extreme)</div>
<span class="result-value">${p95.toFixed(2)}</span>
<span class="result-unit">psf</span>
</div>
<div class="result-card ${warningClass}">
<div class="result-label">99th Percentile (Outlier Event)</div>
<span class="result-value">${p99.toFixed(2)}</span>
<span class="result-unit">psf</span>
</div>
<div class="result-card">
<div class="result-label">Absolute Maximum (10k trials)</div>
<span class="result-value">${max.toFixed(2)}</span>
<span class="result-unit">psf</span>
</div>
<div class="result-card">
<div class="result-label">Effective Slope Factor (C_s)</div>
<span class="result-value">${Cs.toFixed(3)}</span>
</div>
`;
document.getElementById('results-container').innerHTML = resultsHTML;
// Draw distribution histogram
drawHistogram(sorted);
}
function drawHistogram(data) {
const canvas = document.getElementById('distribution-chart');
const ctx = canvas.getContext('2d');
// Resize canvas for retina displays
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
const width = rect.width;
const height = rect.height;
// Clear
ctx.fillStyle = '#0a0a0f';
ctx.fillRect(0, 0, width, height);
// Histogram bins
const bins = 50;
const binWidth = (data[data.length - 1] - data[0]) / bins;
const counts = new Array(bins).fill(0);
for (const val of data) {
const binIdx = Math.min(Math.floor((val - data[0]) / binWidth), bins - 1);
counts[binIdx]++;
}
const maxCount = Math.max(...counts);
const barWidth = width / bins;
const maxHeight = height - 40;
// Draw bars
ctx.fillStyle = 'rgba(0, 212, 255, 0.6)';
for (let i = 0; i < bins; i++) {
const barHeight = (counts[i] / maxCount) * maxHeight;
const x = i * barWidth;
const y = height - barHeight - 30;
ctx.fillRect(x + 1, y, barWidth - 2, barHeight);
}
// Mean line
const mean = data.reduce((a, b) => a + b, 0) / data.length;
const meanX = ((mean - data[0]) / (data[data.length - 1] - data[0])) * width;
ctx.strokeStyle = '#ff4757';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(meanX, 0);
ctx.lineTo(meanX, height - 30);
ctx.stroke();
ctx.setLineDash([]);
// Labels
ctx.fillStyle = '#f0f0f5';
ctx.font = '12px Inter';
ctx.fillText(`Mean: ${mean.toFixed(2)} psf`, meanX + 10, 20);
ctx.fillText(`Range: ${data[0].toFixed(1)} ${data[data.length - 1].toFixed(1)} psf`, 10, height - 10);
}
// Auto-update slope factor on input change
document.getElementById('roof-slope').addEventListener('input', function() {
const theta = parseFloat(this.value);
let Cs;
if (theta < 30) Cs = 1.0;
else if (theta >= 30 && theta <= 50) Cs = 1.0 - (theta - 30) / 70;
else Cs = 0.0;
document.getElementById('slope-factor').value = Cs.toFixed(3);
});
</script>
</body>
</html>