Update desktop profile hover button
Recalibrated vehicle ventilation calcs
This commit is contained in:
fraxle
2026-07-26 16:03:53 +01:00
parent 84d035d903
commit dbadb1d410
10 changed files with 271 additions and 81 deletions
+121 -45
View File
@@ -11,7 +11,8 @@
// 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, speedMph)
// calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType, ventilated, speedMph, windMps) equilibrium cabin temp
// calcVehicleInteriorTempPass(TaArr, globArr, elevArr, vaArr, vehicleType, ventilated, speedMph) lagged cabin temp
// calcShadeAirTemp(TaEnv, effRadEnv, va, elev, shade) per-env shade air temp
// vaporPressureHpa(Ta, RH) Magnus formula - hPa
// solarElevationDeg(lat, lon, dateUTC) NOAA simplified solar position (-)
@@ -375,92 +376,115 @@ export function calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType
// fleet; dark paint ~0.10, silver/white ~0.40).
// Panel surface temp - conductive gain into cabin air.
//
// 2. SIDE-WINDOW GLAZING GAIN (sun-angle dependent)
// 2. VERTICAL GLAZING GAIN (sun-angle dependent)
// When solar elevation is between ~10- and ~60-, the sun's rays
// cut through the side glass at an angle that allows significant
// transmission into the cabin (rather than hitting the roof or
// reflecting off at a shallow angle). This warms the cabin air
// cut through the side glass and windscreen at an angle that allows
// significant transmission into the cabin. This warms the cabin air
// but the occupant is modelled as NOT sitting in the beam -
// so it adds to ambient cabin temp, not direct radiant load.
// Above 60- the sun mostly hits the roof; below 10- it reflects.
// Outside that window a diffuse floor still applies: scattered sky
// light enters the glass at any sun angle, so the tent function is
// clamped rather than switched off (an unclamped tent produced a
// discontinuity where a LOWER sun gave MORE glazing gain).
//
// Wind is ignored unless ventilation is enabled. No evaporative cooling.
// 3. HORIZONTAL GLAZING GAIN (rooflights / panoramic roofs)
// Rooflights collect most strongly when the sun is high - the exact
// condition under which the vertical-glass term is tailing off. Scales
// with sin(elevation) and uses a lower transmission (0.40) because
// rooflights are typically smoked/tinted acrylic rather than clear glass.
//
// Ambient wind scrubs the bodywork whether or not the windows are open, so
// it feeds the shell convection coefficient in every case; road speed does
// the same job and the larger of the two wins. No evaporative cooling.
// Cars warm quickly; motorhomes and caravans are treated as insulated living
// spaces with 25-35 mm sandwich panels, so panel heat gain is much smaller
// and the interior response is slower than a car cabin. Because they are
// occupied living spaces, they also retain warmth from previous hours, people,
// appliances, and background heating; without that, cool sunny days are
// under-estimated badly.
// spaces with 25-35 mm sandwich panels, so panel heat gain is much smaller.
// Their interiors are SLOWER, not COOLER - the insulation that keeps heat out
// also keeps it in, so a closed-up van ends up about as hot as a closed-up car
// once it has had a few hours to get there. Because they are occupied living
// spaces, they also retain a little warmth from previous hours, people,
// appliances, and background heating.
//
// Colour thresholds in the table:
// < 35 -C - warm but tolerable for short periods
// 35-45 -C - dangerous for children/pets (hyperthermia risk)
// > 45 -C - potentially fatal within minutes
// -------------------------------------------------------------------
export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'car', ventilated = false, speedMph = 0) {
export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'car', ventilated = false, speedMph = 0, windMps = 0) {
if (globalRad == null || Ta == null) return null;
// Look up vehicle preset; fall back to a standard car if key unknown.
const preset = VEHICLE_TYPES[vehicleType] || VEHICLE_TYPES.car;
// Road speed in m/s. speedMph = 0 is "Static" (parked) and reproduces the
// original stationary model exactly; higher speeds scrub the shell with
// forced airflow and (windows down) flush the cabin toward ambient.
// Road speed in m/s. speedMph = 0 is "Static" (parked); higher speeds scrub
// the shell with forced airflow and (windows down) flush the cabin toward
// ambient.
const vMs = Math.max(0, speedMph) * 0.447; // mph -> m/s
// -- 1. Panel conduction ------------------------------------------
const panelAbsorbed = globalRad * (1 - preset.albedo); // W/m- absorbed by bodywork
// Panel surface temp: absorbed solar / convective loss to outside air.
// Parked, a light breeze over the panels gives hOut ~10 W/m-K. Once moving,
// forced convection rises with road speed (same sqrt form as the surface
// model), so the bodywork runs progressively closer to ambient.
const hOut = 10 + 5.5 * Math.sqrt(vMs);
// Convection over the shell is driven by whichever airflow is stronger -
// the ambient wind or the car's own road speed. A parked vehicle in a
// 15 mph wind has bodywork far closer to ambient than one in still air,
// so ambient wind must not be ignored just because the windows are shut.
// The 1.5 m/s floor is not fudge: hOut is steep near calm, and a reported
// wind of 0 m/s does not mean still air at the bodywork - thermal plumes off
// hot panels and ordinary gustiness keep it moving. Without the floor a dead
// calm hour sent a sealed car to ~67 -C. It only bites below ~3 mph, so it
// leaves the calibration anchors untouched.
const vShell = Math.max(1.5, vMs, Math.max(0, windMps || 0));
const hOut = 10 + 5.5 * Math.sqrt(vShell);
const panelSurfaceTemp = Ta + panelAbsorbed / hOut;
// Conductive gain into cabin. Cars are thin metal + trim; motorhomes and
// caravans use insulated sandwich panels, so their bodyU is much lower.
const hCabin = preset.bodyU ?? 4;
const conductionGain = hCabin * (panelSurfaceTemp - Ta); // W/m-
// -- 2. Side-window glazing gain (angle-dependent) -----------------
// -- 2. Vertical glazing gain (angle-dependent) --------------------
// Glazing transmission for auto glass ~0.70; scaled by vehicle glazing area.
// Tent function peaks at 35- elevation, where the sun cuts squarely through
// side glass and windscreen, and tapers either side. It is clamped at 0.30
// (= the 0.15 diffuse floor once the 0.5 not-in-beam factor is applied) so
// scattered sky light always gets in. Without that clamp the gain fell to
// zero at 10- and 60- and then jumped back up outside the range.
const tau = 0.70 * preset.glazingArea;
let glazingGain = 0;
if (solElev != null && solElev > 10 && solElev < 60) {
// Scale factor: peaks around 30-40- elevation (sun cuts squarely
// through side glass), tapers off toward 10- (shallow/reflected)
// and 60- (sun increasingly hitting roof not side glass).
// Use a simple tent function peaking at 35-.
const peak = 35;
const halfWidth = 25; // degrees either side
const factor = Math.max(0, 1 - Math.abs(solElev - peak) / halfWidth);
// Diffuse radiation also enters through glass regardless of angle
glazingGain = tau * globalRad * factor * 0.5; // occupant not in beam - 50% ambient
} else {
// Outside the side-window zone: diffuse only (scattered sky light)
glazingGain = tau * (globalRad * 0.15); // ~15% diffuse fraction
}
const tent = (solElev != null && solElev > 0)
? Math.max(0, 1 - Math.abs(solElev - 35) / 25)
: 0;
const glazingGain = tau * globalRad * Math.max(0.30, tent) * 0.5; // occupant not in beam
// -- 3. Horizontal glazing gain (rooflights, panoramic roof) -------
// Rooflights collect in proportion to sin(elevation), so they peak at midday
// just as the vertical-glass term is falling away. Smoked acrylic transmits
// roughly 0.40 rather than the 0.70 of clear auto glass.
const roofGain = (solElev != null && solElev > 0)
? 0.40 * (preset.roofGlazing ?? 0) * globalRad * Math.sin(solElev * Math.PI / 180)
: 0;
// -- Combine into cabin air temperature ---------------------------
// Total heat input per m- of cabin surface
const totalGain = conductionGain + glazingGain;
const totalGain = conductionGain + glazingGain + roofGain;
// Cabin heat rejection: an effective blend of leakage, internal air volume,
// and surfaces exchanging heat with the outside. With windows open it is
// roughly 5x higher when parked - air moves freely through the cabin,
// flushing heat out and capping interior temperature much closer to ambient.
// and surfaces exchanging heat with the outside. Opening the windows when
// parked multiplies it by the preset's ventMult - a car with every window
// down flushes far harder per unit volume than a van with two windows and a
// rooflight open, so that multiplier is per-vehicle, not a shared constant.
// On the move the through-draught multiplies this further (a 70 mph open
// window flushes the cabin almost to ambient). Sealed but moving rejects a
// little faster too, because the cooler shell pulls cabin heat out.
const speedFactor = 1 + vMs / 12; // grows with road speed (windows-open draught)
const lossMult = ventilated ? 5 * speedFactor : 1 + vMs / 40;
const lossMult = ventilated ? (preset.ventMult ?? 4) * speedFactor : 1 + vMs / 40;
const effectiveHLoss = preset.hCabinLoss * lossMult;
const thermalMass = preset.thermalMass ?? 1;
const solarRise = (totalGain / effectiveHLoss) * thermalMass;
const solarRise = totalGain / effectiveHLoss;
// Motorhomes/caravans behave more like small insulated rooms than parked
// cars. This term captures retained living-space warmth: strongest on cool
// days, tapering away as outdoor air warms, and reduced when ventilated.
// It used to be much larger (8 - 0.25*Ta) because it was silently standing
// in for solar gain the model was throwing away; now that the glazing terms
// are right it only has to cover occupancy and residual warmth.
const retainedWarmth = preset.retainedWarmth
? Math.max(0, 8 - 0.25 * Ta) * (ventilated ? 0.35 : 1)
? Math.max(0, 5 - 0.20 * Ta) * (ventilated ? 0.35 : 1)
: 0;
const internalGain = (preset.internalGain ?? 0) * (ventilated ? 0.35 : 1);
@@ -470,6 +494,57 @@ export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'c
return Math.max(Ta, Math.min(Ti, 90));
}
// -------------------------------------------------------------------
// VEHICLE INTERIOR TEMPERATURE - TWO-PASS (thermal lag)
// -------------------------------------------------------------------
// calcVehicleInteriorTemp above returns the EQUILIBRIUM cabin temperature
// for one hour's conditions - where the interior would settle if those
// conditions held. Real cabins take time to get there, so this pass relaxes
// toward that target with a per-vehicle time constant, exactly as
// calcConcreteTempPass and calcIndoorTempPass do.
//
// This replaces an earlier 'thermalMass' multiplier that scaled the
// equilibrium rise down (motorhomes ran at 0.35x). That conflated two
// different things: thermal mass delays how fast you reach equilibrium, it
// does not lower the equilibrium itself. A van parked in the sun since
// breakfast is close to equilibrium by mid-afternoon, so the multiplier
// under-predicted every long parked spell - badly enough that a ventilated
// motorhome came out barely a degree above ambient at peak sun.
//
// Cars use a short constant (~0.5 h - a car is hot within the half hour);
// motorhomes and caravans a longer one (~1.5-1.7 h) reflecting their larger
// air volume and heavier interior fit-out.
//
// vaArr is ambient wind in m/s (Open-Meteo wind_speed_10m with
// wind_speed_unit=ms) and may be null/omitted, in which case still air is
// assumed. speedMph is the selected road-speed class, not per-hour data.
// -------------------------------------------------------------------
export function calcVehicleInteriorTempPass(TaArr, globArr, elevArr, vaArr, vehicleType = 'car', ventilated = false, speedMph = 0) {
const preset = VEHICLE_TYPES[vehicleType] || VEHICLE_TYPES.car;
const n = TaArr.length;
const result = new Array(n);
const alpha = 1 - Math.exp(-1 / (preset.lagHours ?? 0.5));
// Seed at the first hour's air temp - a vehicle left overnight has
// equalised with the outside air.
let Ti = TaArr[0] ?? 15;
for (let i = 0; i < n; i++) {
const Ta = TaArr[i];
if (Ta == null) { result[i] = null; continue; }
const target = calcVehicleInteriorTemp(
Ta, globArr[i] ?? 0, elevArr[i], vehicleType, ventilated, speedMph,
vaArr ? (vaArr[i] ?? 0) : 0
);
if (target == null) { result[i] = null; continue; }
Ti = Ti + alpha * (target - Ti);
// Can't be cooler than outside air; physical cap at 90 -C.
result[i] = Math.max(Ta, Math.min(Ti, 90));
}
return result;
}
// -------------------------------------------------------------------
// SHADE AIR TEMPERATURE (per-environment microclimate)
// -------------------------------------------------------------------
@@ -576,7 +651,8 @@ export function calcFurSurfaceTempPass(TaArr, radArr, vaArr, elevArr, furAlbedo
// calcVehicleInteriorTemp now takes a speedMph argument (Static / 20 / 50 / 70
// mph in the UI). Forced convection over the shell scales with road speed, and
// windows-open through-flow scales further with speed, so a moving cabin runs
// cooler than the same parked car. speedMph = 0 reproduces the static model.
// cooler than the same parked car. speedMph = 0 is the parked case - which is
// no longer "no airflow", since ambient wind now drives shell convection too.
//
// -- CYCLIST AT SPEED (FUTURE) ------------------------------------------------
// A cyclist generates their own headwind, so the felt temperature (UTCI) is