1.6.0
Fix and enhance the concrete calculation Minify the about.css file
This commit is contained in:
@@ -53,10 +53,11 @@
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
/* "↳ Pangbourne, Berkshire" */
|
||||
/* "↳ Location, Somewhere" */
|
||||
.utci-current-loc {
|
||||
font-family: Fraunces, serif;
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
margin-top: 12px;
|
||||
color: #1e1208;
|
||||
@@ -70,7 +71,7 @@
|
||||
color: #b09870;
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
margin-left: 18px;
|
||||
margin-left: 15px;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
|
||||
+36
-16
@@ -12,7 +12,7 @@
|
||||
|
||||
import {
|
||||
vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox,
|
||||
calcConcreteTemp, calcVehicleInteriorTemp,
|
||||
calcConcreteTempPass, calcVehicleInteriorTemp,
|
||||
calcIndoorTempPass, calcManagedIndoorTempPass,
|
||||
} from './physics.js';
|
||||
import { windCompass8, uvSplit, cloudCategory, precipPenalty } from './utils.js';
|
||||
@@ -55,20 +55,23 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
|
||||
const soilT0 = h.soil_temperature_0cm ? h.soil_temperature_0cm[i] : null;
|
||||
const soilT6 = h.soil_temperature_6cm ? h.soil_temperature_6cm[i] : null;
|
||||
const soilM = h.soil_moisture_0_to_1cm ? h.soil_moisture_0_to_1cm[i] : null;
|
||||
// iso is a local wall-clock string e.g. "2026-05-13T14:00" (no Z).
|
||||
// For display we slice the string directly - no Date object needed.
|
||||
// For solarElevationDeg (which uses .getUTC* internally) we need the
|
||||
// true UTC instant: treat the local time as UTC then subtract the offset.
|
||||
// e.g. Brisbane UTC+10: local 14:00 - parse as UTC 14:00 - subtract 10h - UTC 04:00 -
|
||||
// iso is a local wall-clock string e.g. "2026-05-13T14:00" (no Z).
|
||||
// For solarElevationDeg (which uses .getUTC* internally) we need the
|
||||
// true UTC instant: treat the local time as UTC then subtract the offset.
|
||||
const dt = new Date(Date.parse(iso + 'Z') - utcOffsetMs);
|
||||
const elev = solarElevationDeg(location.lat, location.lon, dt);
|
||||
// Use direct_radiation (beam sunlight) + a fraction of diffuse for concrete.
|
||||
// direct_radiation is zero on fully overcast days - far more accurate than
|
||||
// shortwave_radiation which can be unreliably high even at 100% cloud cover.
|
||||
// Diffuse (scattered light through cloud) contributes ~20% as much heat to
|
||||
// a surface as direct beam, so we weight it accordingly.
|
||||
const effectiveRad = dir + dif * 0.2;
|
||||
const concreteT = calcConcreteTemp(Ta, effectiveRad, va);
|
||||
// iso is a local wall-clock string e.g. "2026-05-13T14:00" (no Z).
|
||||
// For display we slice the string directly - no Date object needed.
|
||||
// For solarElevationDeg (which uses .getUTC* internally) we need the
|
||||
// true UTC instant: treat the local time as UTC then subtract the offset.
|
||||
// e.g. Brisbane UTC+10: local 14:00 - parse as UTC 14:00 - subtract 10h - UTC 04:00 -
|
||||
const dt = new Date(Date.parse(iso + 'Z') - utcOffsetMs);
|
||||
const elev = solarElevationDeg(location.lat, location.lon, dt);
|
||||
// concreteT is now stamped in the two-pass section below (thermal lag).
|
||||
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent);
|
||||
const eh = vaporPressureHpa(Ta, RH);
|
||||
const Tmrt = calcTmrt(Ta, dir, dif, glob, elev);
|
||||
@@ -132,20 +135,37 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
|
||||
cc, ccLow, ccMid, ccHigh, cloudCat,
|
||||
uv, uvA, uvB,
|
||||
precip, snow,
|
||||
soilT0, soilT6, soilM, concreteT, vehicleT,
|
||||
soilT0, soilT6, soilM, vehicleT,
|
||||
effectiveRad,
|
||||
elev, Tmrt, utci, utciAdj, eh, compass,
|
||||
visKm, aqi,
|
||||
grassPollen, birchPollen, alderPollen, mugwortPollen, olivePollen, ragweedPollen,
|
||||
};
|
||||
}) : [];
|
||||
|
||||
// Two-pass indoor temperature: needs the full hourly arrays so thermal
|
||||
// lag can look back at previous hours. Run after hourlyRows is built,
|
||||
// then stamp each row with its indoorT value.
|
||||
// Two-pass calculations: concrete thermal lag + indoor temperature.
|
||||
// Both need the full hourly arrays so they can look back at previous
|
||||
// hours. Run after hourlyRows is built, then stamp each row.
|
||||
if (hourlyRows.length > 0) {
|
||||
const TaArr = hourlyRows.map(r => r.Ta);
|
||||
const globArr = hourlyRows.map(r => r.glob);
|
||||
const elevArr = hourlyRows.map(r => r.elev);
|
||||
const TaArr = hourlyRows.map(r => r.Ta);
|
||||
const globArr = hourlyRows.map(r => r.glob);
|
||||
const elevArr = hourlyRows.map(r => r.elev);
|
||||
const radArr = hourlyRows.map(r => r.effectiveRad);
|
||||
const vaArr = hourlyRows.map(r => r.va);
|
||||
const uvArr = hourlyRows.map(r => r.uv);
|
||||
const cloudCatArr = hourlyRows.map(r => r.cloudCat);
|
||||
const soilMArr = hourlyRows.map(r => r.soilM);
|
||||
const precipArr = hourlyRows.map(r => r.precip);
|
||||
const snowArr = hourlyRows.map(r => r.snow);
|
||||
|
||||
// Concrete surface temperature with thermal lag (1.5 h time constant).
|
||||
// A slab baking in the sun retains heat when cloud rolls in, and takes
|
||||
// a couple of hours of sunshine to fully heat up from a cold start.
|
||||
const concreteTemps = calcConcreteTempPass(
|
||||
TaArr, radArr, vaArr, elevArr, uvArr, cloudCatArr, soilMArr, precipArr, snowArr
|
||||
);
|
||||
hourlyRows.forEach((r, i) => { r.concreteT = concreteTemps[i]; });
|
||||
|
||||
const indoorTemps = calcIndoorTempPass(TaArr, globArr, elevArr, buildingType);
|
||||
const managedTemps = calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType);
|
||||
hourlyRows.forEach((r, i) => { r.indoorT = indoorTemps[i]; r.managedT = managedTemps[i]; });
|
||||
|
||||
+181
-16
@@ -6,7 +6,9 @@
|
||||
//
|
||||
// Exports (in order of appearance):
|
||||
// SIGMA, EPSILON_P, A_K, ALBEDO_GRASS, ALBEDO_CONCRETE (radiation constants)
|
||||
// calcConcreteTemp(Ta, globalRad, windSpeed) urban surface temp
|
||||
// LAG_HOURS_CONCRETE slab thermal time constant
|
||||
// calcConcreteTemp(Ta, globalRad, windSpeed, ...) urban surface temp (instantaneous target)
|
||||
// calcConcreteTempPass(arrays...) thermal-lag pass over full hourly arrays
|
||||
// calcIndoorTempPass(TaArr, globArr, elevArr, buildingType) passive indoor temp
|
||||
// calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType) managed indoor temp
|
||||
// calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType, ventilated)
|
||||
@@ -42,26 +44,189 @@ export const ALBEDO_CONCRETE = 0.30;
|
||||
// CONCRETE SURFACE TEMPERATURE (Urban profile)
|
||||
// -------------------------------------------------------------------
|
||||
// Estimates the surface temperature of exposed concrete using a
|
||||
// simplified energy-balance approach:
|
||||
// - Absorbed solar = globalRad - (1 - albedo)
|
||||
// - No latent heat (no evaporation - concrete is dry)
|
||||
// - Convective loss to air proportional to wind speed
|
||||
// - Result is clamped to a physically plausible range
|
||||
// multi-factor energy-balance approach. Six physical effects are
|
||||
// modelled beyond the basic solar-gain / convective-loss pair:
|
||||
//
|
||||
// This is what matters for contact heat stress in cities - the UTCI
|
||||
// standard uses grass, which runs ~5-15 -C cooler than urban concrete
|
||||
// on a sunny day because grass sweats (transpires).
|
||||
// 1. SUN ANGLE CORRECTION
|
||||
// At low solar elevation the sun hits the surface obliquely,
|
||||
// spreading energy over a larger area. A sin(elev) factor
|
||||
// reduces absorbed solar proportionally. Clamped at a 5-deg
|
||||
// minimum so the result stays finite near the horizon.
|
||||
//
|
||||
// 2. CLOUD TYPE TRANSMITTANCE
|
||||
// Cloud cover category drives a transmittance multiplier.
|
||||
// Low cloud (stratus) is far more opaque than high cirrus:
|
||||
// clear 1.00 - full beam reaches the surface
|
||||
// wispy 0.92 - cirrus barely attenuates
|
||||
// scattered 0.72 - broken cumulus, significant blocking
|
||||
// overcast 0.28 - thick stratus, mostly diffuse remains
|
||||
// The raw effectiveRad already dims with cloud cover from the
|
||||
// API, but this adds the qualitative distinction between cloud
|
||||
// types that the single radiation number does not capture.
|
||||
//
|
||||
// 3. UV CLARITY FACTOR
|
||||
// UV index is a proxy for atmospheric clarity beyond cloud cover -
|
||||
// aerosols, haze, and humidity all reduce it. A UV of 8+ indicates
|
||||
// a very clean, dry atmosphere with maximum direct-beam intensity.
|
||||
// Normalised to a 0.85-1.00 range so it modulates rather than
|
||||
// dominates. No UV data defaults to neutral (1.0).
|
||||
//
|
||||
// 4. EVAPORATIVE COOLING FROM SOIL MOISTURE
|
||||
// Wet concrete loses heat via evaporation. Soil moisture at 0-1cm
|
||||
// is used as a proxy for surface wetness (0 = bone dry, 1 = fully
|
||||
// saturated). A saturated surface loses up to ~8-C relative to
|
||||
// the dry case - consistent with published wet-pavement studies.
|
||||
//
|
||||
// 5. RAIN-WET SURFACE
|
||||
// Active precipitation forces surface wetness regardless of soil
|
||||
// moisture data. Above 0.5 mm/h the surface is considered fully
|
||||
// wet and the maximum evaporative penalty applies.
|
||||
//
|
||||
// 6. SNOW COVER
|
||||
// Snow on concrete insulates the slab from solar gain AND strongly
|
||||
// reflects incoming radiation (albedo ~0.80 for fresh snow vs 0.30
|
||||
// for bare concrete). When snowfall is active or lying snow is
|
||||
// implied (snow > 0), absorbed radiation is cut by 85% and a small
|
||||
// insulating offset is applied instead.
|
||||
//
|
||||
// Colour thresholds (same as before - surface contact risk):
|
||||
// < Ta - should not occur (clamped)
|
||||
// Ta - 45 -C - warm but bearable contact
|
||||
// 45 - 60 -C - pain threshold for bare skin contact
|
||||
// > 60 -C - burns on contact (relevant for paws / bare feet)
|
||||
// -------------------------------------------------------------------
|
||||
export function calcConcreteTemp(Ta, globalRad, windSpeed) {
|
||||
export function calcConcreteTemp(Ta, globalRad, windSpeed, solElev, uv, cloudCat, soilM, precip, snow) {
|
||||
if (globalRad == null || Ta == null) return null;
|
||||
const absorbed = globalRad * (1 - ALBEDO_CONCRETE); // W/m-
|
||||
// Convective heat transfer coefficient: ~10 W/m-K still air, rises with wind
|
||||
// (10 reflects realistic natural convection; 5 was too low and ran too hot)
|
||||
|
||||
// -- 6. Snow short-circuit --------------------------------------------
|
||||
// Snow-covered concrete behaves like a white reflective insulator.
|
||||
// Absorbed solar collapses; slab temp stays close to air temp.
|
||||
if (snow != null && snow > 0) {
|
||||
const snowAbsorbed = globalRad * (1 - 0.80); // fresh snow albedo ~0.80
|
||||
const hcSnow = 10 + 4.5 * Math.sqrt(Math.max(windSpeed || 0, 0));
|
||||
const Ts = Ta + snowAbsorbed / hcSnow;
|
||||
return Math.max(Ta - 1, Math.min(Ts, 40)); // snow-covered slab rarely exceeds 40-C
|
||||
}
|
||||
|
||||
// -- 1. Sun angle correction ------------------------------------------
|
||||
// Low-angle sun spreads energy across a larger surface area.
|
||||
// sin(elev) = 1.0 at 90-deg (overhead), ~0.17 at 10-deg (grazing).
|
||||
// Default to sin(45-deg) ~0.71 when elevation is unknown.
|
||||
const elevDeg = (solElev != null) ? Math.max(5, solElev) : 45;
|
||||
const angleCorrection = Math.sin(elevDeg * Math.PI / 180);
|
||||
|
||||
// -- 2. Cloud type transmittance --------------------------------------
|
||||
// Modulates beam quality beyond what raw radiation already captures.
|
||||
const cloudTransmit = cloudCat === 'clear' ? 1.00
|
||||
: cloudCat === 'wispy' ? 0.92
|
||||
: cloudCat === 'scattered' ? 0.72
|
||||
: /* overcast */ 0.28;
|
||||
|
||||
// -- 3. UV clarity factor --------------------------------------------
|
||||
// UV index as atmospheric-clarity proxy. Scaled to 0.85-1.00 range.
|
||||
// uv=0 (night or heavy cloud) - neutral 1.0 (no adjustment needed,
|
||||
// radiation already near-zero). uv=8+ - max clarity bonus of 1.0.
|
||||
const uvClarity = (uv && uv > 0)
|
||||
? 0.85 + 0.15 * Math.min(1, uv / 8)
|
||||
: 1.0;
|
||||
|
||||
// -- Absorbed solar with all modifiers --------------------------------
|
||||
const absorbed = globalRad * (1 - ALBEDO_CONCRETE)
|
||||
* angleCorrection
|
||||
* cloudTransmit
|
||||
* uvClarity;
|
||||
|
||||
// -- Convective loss --------------------------------------------------
|
||||
const hc = 10 + 4.5 * Math.sqrt(Math.max(windSpeed || 0, 0));
|
||||
// Surface temp: Ta + solar gain / convective loss
|
||||
|
||||
// -- Dry surface temperature ------------------------------------------
|
||||
const Ts = Ta + absorbed / hc;
|
||||
// Clamp: can't be cooler than air, cap at 85 -C (melting asphalt territory)
|
||||
return Math.max(Ta, Math.min(Ts, 85));
|
||||
|
||||
// -- 4 + 5. Evaporative cooling ---------------------------------------
|
||||
// Rain-wet surface overrides soil moisture - surface is fully saturated.
|
||||
const surfaceWet = (precip != null && precip >= 0.5)
|
||||
? 1.0
|
||||
: Math.max(0, Math.min(1, soilM ?? 0));
|
||||
// Max evaporative delta ~8-C at full saturation (published wet-pavement data).
|
||||
const evapCooling = surfaceWet * 8;
|
||||
|
||||
const TsCooled = Ts - evapCooling;
|
||||
|
||||
// -- Final clamp: no cooler than air, no hotter than 85-C -------------
|
||||
return Math.max(Ta, Math.min(TsCooled, 85));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// CONCRETE THERMAL LAG TIME CONSTANT
|
||||
// -------------------------------------------------------------------
|
||||
// A standard urban pavement slab (~100 mm thick, exposed top surface,
|
||||
// air below via sub-base) has moderate thermal mass. Real-world
|
||||
// measurement studies put the e-folding time constant at 1.5-2 h for
|
||||
// this geometry - we use 1.5 h as representative of the thin end of
|
||||
// typical footway construction (block paving, tarmac-over-hardcore).
|
||||
//
|
||||
// If this ever needs to vary by surface type, pull it into a
|
||||
// SURFACE_TYPES preset object mirroring BUILDING_TYPES in utils.js.
|
||||
// For now a single named constant keeps the intent obvious.
|
||||
// -------------------------------------------------------------------
|
||||
export const LAG_HOURS_CONCRETE = 1.5;
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// CONCRETE SURFACE TEMPERATURE - THERMAL LAG PASS
|
||||
// -------------------------------------------------------------------
|
||||
// Wraps calcConcreteTemp in the same exponential-blending pattern used
|
||||
// by calcIndoorTempPass. Instead of snapping to the instantaneous
|
||||
// target each hour, the slab temperature blends toward it at a rate
|
||||
// controlled by LAG_HOURS_CONCRETE.
|
||||
//
|
||||
// Effect in practice:
|
||||
// - A slab baking at 55-C when cloud rolls in will still read ~48-C
|
||||
// an hour later and ~44-C two hours later - not instantly 22-C.
|
||||
// - A cold slab at dawn takes 2-3 hours of strong sun to fully heat.
|
||||
// - Post-rain cool-down persists into the next hour even if it stops.
|
||||
//
|
||||
// Call AFTER building hourlyRows (same pattern as calcIndoorTempPass).
|
||||
// Returns a Float64-like plain Array of concrete temps, one per hour.
|
||||
//
|
||||
// Arguments are parallel arrays (one value per forecast hour):
|
||||
// TaArr - air temperature (-C)
|
||||
// radArr - effectiveRad (dir + dif*0.2) (W/m-)
|
||||
// vaArr - wind speed (m/s)
|
||||
// elevArr - solar elevation (degrees)
|
||||
// uvArr - UV index
|
||||
// cloudCatArr - cloud category string
|
||||
// soilMArr - soil moisture 0-1cm (0-1 fraction)
|
||||
// precipArr - precipitation (mm/h)
|
||||
// snowArr - snowfall (cm/h)
|
||||
// -------------------------------------------------------------------
|
||||
export function calcConcreteTempPass(TaArr, radArr, vaArr, elevArr, uvArr, cloudCatArr, soilMArr, precipArr, snowArr) {
|
||||
const n = TaArr.length;
|
||||
const result = new Array(n);
|
||||
const alpha = 1 - Math.exp(-1 / LAG_HOURS_CONCRETE);
|
||||
|
||||
// Seed from the first hours instantaneous value so we start somewhere
|
||||
// physically reasonable rather than zero.
|
||||
let Tc = calcConcreteTemp(
|
||||
TaArr[0], radArr[0], vaArr[0],
|
||||
elevArr[0], uvArr[0], cloudCatArr[0],
|
||||
soilMArr[0], precipArr[0], snowArr[0]
|
||||
) ?? TaArr[0];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const target = calcConcreteTemp(
|
||||
TaArr[i], radArr[i], vaArr[i],
|
||||
elevArr[i], uvArr[i], cloudCatArr[i],
|
||||
soilMArr[i], precipArr[i], snowArr[i]
|
||||
) ?? TaArr[i];
|
||||
|
||||
// Blend slab temp toward this hours target.
|
||||
Tc = Tc + alpha * (target - Tc);
|
||||
|
||||
// Slab cannot be cooler than air (no active cooling mechanism).
|
||||
result[i] = Math.max(TaArr[i], Tc);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user