288 lines
9.7 KiB
HTML
288 lines
9.7 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="en">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>Thermal Diffusion Solver | Cathy Mcmasters</title>
|
|||
|
|
<style>
|
|||
|
|
:root {
|
|||
|
|
--bg: #0f0f0f;
|
|||
|
|
--surface: #1a1a1a;
|
|||
|
|
--primary: #d4af37; /* Hampton Gold */
|
|||
|
|
--text: #e0e0e0;
|
|||
|
|
--muted: #808080;
|
|||
|
|
--border: #333333;
|
|||
|
|
}
|
|||
|
|
body {
|
|||
|
|
font-family: 'Georgia', serif;
|
|||
|
|
background: var(--bg);
|
|||
|
|
color: var(--text);
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 2rem;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
header {
|
|||
|
|
border-bottom: 1px solid var(--primary);
|
|||
|
|
padding-bottom: 1rem;
|
|||
|
|
margin-bottom: 2rem;
|
|||
|
|
}
|
|||
|
|
h1 {
|
|||
|
|
font-size: 2rem;
|
|||
|
|
color: var(--primary);
|
|||
|
|
letter-spacing: 0.05em;
|
|||
|
|
margin: 0;
|
|||
|
|
}
|
|||
|
|
p.subtitle {
|
|||
|
|
color: var(--muted);
|
|||
|
|
font-style: italic;
|
|||
|
|
margin-top: 0.5rem;
|
|||
|
|
}
|
|||
|
|
.grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
gap: 2rem;
|
|||
|
|
max-width: 1200px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
}
|
|||
|
|
.panel {
|
|||
|
|
background: var(--surface);
|
|||
|
|
border: 1px solid var(--border);
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
label {
|
|||
|
|
display: block;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
color: var(--primary);
|
|||
|
|
font-weight: bold;
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
}
|
|||
|
|
input[type="number"] {
|
|||
|
|
width: 100%;
|
|||
|
|
padding: 0.5rem;
|
|||
|
|
background: #000;
|
|||
|
|
border: 1px solid var(--border);
|
|||
|
|
color: var(--text);
|
|||
|
|
font-family: monospace;
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
button {
|
|||
|
|
background: var(--primary);
|
|||
|
|
color: #000;
|
|||
|
|
border: none;
|
|||
|
|
padding: 0.75rem 1.5rem;
|
|||
|
|
font-weight: bold;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.2s;
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
button:hover {
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.2);
|
|||
|
|
}
|
|||
|
|
.result {
|
|||
|
|
margin-top: 1rem;
|
|||
|
|
padding: 1rem;
|
|||
|
|
background: #000;
|
|||
|
|
border-left: 3px solid var(--primary);
|
|||
|
|
font-family: monospace;
|
|||
|
|
white-space: pre-wrap;
|
|||
|
|
}
|
|||
|
|
canvas {
|
|||
|
|
background: #000;
|
|||
|
|
border: 1px solid var(--border);
|
|||
|
|
width: 100%;
|
|||
|
|
height: 300px;
|
|||
|
|
margin-top: 1rem;
|
|||
|
|
}
|
|||
|
|
.citation {
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
color: var(--muted);
|
|||
|
|
margin-top: 1rem;
|
|||
|
|
border-top: 1px dashed var(--border);
|
|||
|
|
padding-top: 0.5rem;
|
|||
|
|
}
|
|||
|
|
.strata-img {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 200px;
|
|||
|
|
object-fit: cover;
|
|||
|
|
margin-top: 1rem;
|
|||
|
|
border: 1px solid var(--primary);
|
|||
|
|
opacity: 0.8;
|
|||
|
|
}
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
.grid { grid-template-columns: 1fr; }
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
<script defer src="https://analytics.4ort.xyz/script.js" data-website-id="d3ed927c-888a-4a6c-ae5f-0b1c613ddf5b"></script>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<header>
|
|||
|
|
<h1>Thermal Diffusion Solver</h1>
|
|||
|
|
<p class="subtitle">Calculating safe burial depth for root cellars in Hampton clay-loam (Q16019)</p>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<div class="grid">
|
|||
|
|
<section class="panel">
|
|||
|
|
<h3>Input Parameters</h3>
|
|||
|
|
<form id="solver-form">
|
|||
|
|
<label for="deltaT">Surface Temperature Swing (ΔT)</label>
|
|||
|
|
<input type="number" id="deltaT" step="0.1" value="28.0" title="Annual difference between summer peak and winter trough">
|
|||
|
|
|
|||
|
|
<label for="conductivity">Thermal Conductivity (k)</label>
|
|||
|
|
<input type="number" id="conductivity" step="0.01" value="1.40" title="Watts per meter-Kelvin">
|
|||
|
|
|
|||
|
|
<label for="density">Soil Density (ρ)</label>
|
|||
|
|
<input type="number" id="density" step="10" value="1600" title="Kilograms per cubic meter">
|
|||
|
|
|
|||
|
|
<label for="specificHeat">Specific Heat Capacity (c)</label>
|
|||
|
|
<input type="number" id="specificHeat" step="100" value="2000" title="Joules per kilogram-Kelvin">
|
|||
|
|
|
|||
|
|
<label for="threshold">Safety Threshold (°C)</label>
|
|||
|
|
<input type="number" id="threshold" step="0.1" value="2.0" title="Maximum allowable temp fluctuation at storage depth">
|
|||
|
|
|
|||
|
|
<button type="submit">Compute Burial Profile</button>
|
|||
|
|
</form>
|
|||
|
|
|
|||
|
|
<div class="citation">
|
|||
|
|
Constants derived from:<br>
|
|||
|
|
• Soil thermal properties: ASTM C518<br>
|
|||
|
|
• Hampton climate normals: NOAA NCEI (1991-2020)<br>
|
|||
|
|
• Diffusion equation: Fourier (1822)
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<img class="strata-img" src="https://images.pexels.com/photos/11924683/pexels-photo-11924683.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940" alt="Vertical cross-section of soil strata with exposed root systems, illustrating the interface between biological and geological layers">
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="panel">
|
|||
|
|
<h3>Computation Results</h3>
|
|||
|
|
<div id="output" class="result">Awaiting computation...</div>
|
|||
|
|
<canvas id="gradientChart"></canvas>
|
|||
|
|
</section>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
/**
|
|||
|
|
* Solves the 1D Heat Equation for periodic boundary conditions.
|
|||
|
|
* Amplitude decay follows: A(z) = A_0 * exp(-z * sqrt(πω / k))
|
|||
|
|
* where ω = angular frequency, k = thermal diffusivity (α)
|
|||
|
|
*/
|
|||
|
|
document.getElementById('solver-form').addEventListener('submit', function(e) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
|
|||
|
|
const deltaT = parseFloat(document.getElementById('deltaT').value);
|
|||
|
|
const k_cond = parseFloat(document.getElementById('conductivity').value); // W/(m·K)
|
|||
|
|
const rho = parseFloat(document.getElementById('density').value); // kg/m³
|
|||
|
|
const c_spec = parseFloat(document.getElementById('specificHeat').value); // J/(kg·K)
|
|||
|
|
const threshold = parseFloat(document.getElementById('threshold').value); // °C
|
|||
|
|
|
|||
|
|
// Calculate thermal diffusivity (alpha)
|
|||
|
|
const alpha = k_cond / (rho * c_spec); // m²/s
|
|||
|
|
|
|||
|
|
// Angular frequency for annual cycle (seconds/year ≈ 31,557,600)
|
|||
|
|
const omega = (2 * Math.PI) / 31557600;
|
|||
|
|
|
|||
|
|
// Decay constant
|
|||
|
|
const decayConst = Math.sqrt(Math.PI * omega / alpha);
|
|||
|
|
|
|||
|
|
// Solve for depth where amplitude drops to threshold
|
|||
|
|
// threshold = (deltaT/2) * exp(-depth * decayConst)
|
|||
|
|
const initialAmplitude = deltaT / 2;
|
|||
|
|
const targetRatio = threshold / initialAmplitude;
|
|||
|
|
|
|||
|
|
let requiredDepth = 0;
|
|||
|
|
if (targetRatio > 0 && targetRatio < 1) {
|
|||
|
|
requiredDepth = -Math.log(targetRatio) / decayConst;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Generate profile points
|
|||
|
|
const points = [];
|
|||
|
|
for (let z = 0; z <= requiredDepth * 1.5; z += 0.1) {
|
|||
|
|
const ampAtZ = initialAmplitude * Math.exp(-z * decayConst);
|
|||
|
|
points.push({ depth: z, amplitude: ampAtZ });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Render results
|
|||
|
|
const outputDiv = document.getElementById('output');
|
|||
|
|
outputDiv.innerHTML = `
|
|||
|
|
<span style="color:#d4af37">MINIMUM BURIAL DEPTH:</span> ${requiredDepth.toFixed(2)} meters (${(requiredDepth * 3.28084).toFixed(2)} ft)
|
|||
|
|
|
|||
|
|
<span style="color:#d4af37">THERMAL DIFFUSIVITY (α):</span> ${(alpha * 1e6).toFixed(2)} mm²/s
|
|||
|
|
|
|||
|
|
<span style="color:#d4af37">DECAY CONSTANT:</span> ${decayConst.toFixed(4)} m⁻¹
|
|||
|
|
|
|||
|
|
<span style="color:#d4af37">SAFETY FACTOR:</span> ${((initialAmplitude / threshold)).toFixed(2)}x
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// Draw Chart
|
|||
|
|
drawGradient(points, requiredDepth, threshold);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function drawGradient(data, criticalDepth, threshold) {
|
|||
|
|
const canvas = document.getElementById('gradientChart');
|
|||
|
|
const ctx = canvas.getContext('2d');
|
|||
|
|
const w = canvas.width = canvas.offsetWidth;
|
|||
|
|
const h = canvas.height = 300;
|
|||
|
|
|
|||
|
|
ctx.fillStyle = '#000';
|
|||
|
|
ctx.fillRect(0, 0, w, h);
|
|||
|
|
|
|||
|
|
// Axes
|
|||
|
|
ctx.strokeStyle = '#333';
|
|||
|
|
ctx.beginPath();
|
|||
|
|
ctx.moveTo(40, 20); ctx.lineTo(40, h-40); // Y axis
|
|||
|
|
ctx.lineTo(w-20, h-40); // X axis
|
|||
|
|
ctx.stroke();
|
|||
|
|
|
|||
|
|
// Labels
|
|||
|
|
ctx.fillStyle = '#808080';
|
|||
|
|
ctx.font = '10px monospace';
|
|||
|
|
ctx.fillText('DEPTH (m)', 10, h-20);
|
|||
|
|
ctx.fillText('AMPLITUDE (°C)', 10, 20);
|
|||
|
|
|
|||
|
|
// Plot curve
|
|||
|
|
ctx.strokeStyle = '#d4af37';
|
|||
|
|
ctx.lineWidth = 2;
|
|||
|
|
ctx.beginPath();
|
|||
|
|
|
|||
|
|
const xMax = data[data.length-1].depth;
|
|||
|
|
const yMax = data[0].amplitude;
|
|||
|
|
|
|||
|
|
data.forEach(p => {
|
|||
|
|
const x = 40 + (p.depth / xMax) * (w - 60);
|
|||
|
|
const y = h - 40 - (p.amplitude / yMax) * (h - 80);
|
|||
|
|
if (p.depth === 0) ctx.moveTo(x, y);
|
|||
|
|
else ctx.lineTo(x, y);
|
|||
|
|
});
|
|||
|
|
ctx.stroke();
|
|||
|
|
|
|||
|
|
// Critical line
|
|||
|
|
ctx.strokeStyle = '#ff4444';
|
|||
|
|
ctx.setLineDash([5, 5]);
|
|||
|
|
ctx.beginPath();
|
|||
|
|
const critY = h - 40 - (threshold / yMax) * (h - 80);
|
|||
|
|
ctx.moveTo(40, critY); ctx.lineTo(w-20, critY);
|
|||
|
|
ctx.stroke();
|
|||
|
|
ctx.setLineDash([]);
|
|||
|
|
|
|||
|
|
// Intersection marker
|
|||
|
|
const intersectIndex = data.findIndex(p => p.amplitude <= threshold);
|
|||
|
|
if (intersectIndex !== -1) {
|
|||
|
|
const p = data[intersectIndex];
|
|||
|
|
const ix = 40 + (p.depth / xMax) * (w - 60);
|
|||
|
|
const iy = h - 40 - (p.amplitude / yMax) * (h - 80);
|
|||
|
|
|
|||
|
|
ctx.fillStyle = '#d4af37';
|
|||
|
|
ctx.beginPath();
|
|||
|
|
ctx.arc(ix, iy, 4, 0, Math.PI*2);
|
|||
|
|
ctx.fill();
|
|||
|
|
|
|||
|
|
ctx.fillStyle = '#fff';
|
|||
|
|
ctx.fillText(`${p.depth.toFixed(1)}m`, ix + 10, iy + 15);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|