Land hailBlockFor(size, porosity) — the honest fabric-hail rule for B
B asked for the hail ruling before coding fabric choice; this is it as code. The physics: porosity is about AIR (blows through → less wind load) and WATER (drains → no ponding), NOT ice. A knitted shade cloth's gaps are ~1-3 mm; a damaging hailstone is 6-45 mm, so it can't pass a mesh an order of magnitude finer than itself — porous and membrane block the big stones identically. The ONE true difference is the finest pea hail, which IS small enough to rattle through an open weave. So: membrane stops everything; porous stops everything except the smallest stones. Verified across the storm sizes: shade cloth (0.3) fully blocks the wild-night 1.3/1.4 stones and leaks 26% of storm_03's 0.7 pea hail; an open 0.5 weave leaks 90% of pea hail but still catches the big ice. That makes the fabric choice cost you exactly on the mild-hail nights and stay honest on the ice nights — a real tradeoff without a physics lie. The assert also proves the integration end to end so nobody wires it blind: a membrane-covered bed takes ~0 hail on storm_03, a porous-covered one takes 0.62 — the choice is genuinely non-trivial. Formula for whoever wires the garden drain: coveredHail = hailShadowOver(bed) * hailBlockFor(hailSize, sail.porosity). Pure helper (no THREE), exported through weather.js alongside stormStats/ forecastFor; NOT on the wind contract, so no router change. Selftest 263/0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
09e5083515
commit
0d05cb1936
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
createWindField, validateStorm, gustEnvelope, GUST, RAIN_TIME_COMPRESSION,
|
createWindField, validateStorm, gustEnvelope, GUST, RAIN_TIME_COMPRESSION,
|
||||||
stormStats, forecastFor,
|
stormStats, forecastFor, hailBlockFor,
|
||||||
} from '../weather.core.js';
|
} from '../weather.core.js';
|
||||||
|
|
||||||
const DT = 1 / 60;
|
const DT = 1 / 60;
|
||||||
@ -664,6 +664,64 @@ export function weatherCases(storms) {
|
|||||||
assert(forecastFor(storms.storm_03_southerly, 0).hail.chance === 'possible', 'storm_03 hails mildly');
|
assert(forecastFor(storms.storm_03_southerly, 0).hail.chance === 'possible', 'storm_03 hails mildly');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- 13. fabric hail pass-through (SPRINT7 §Lane C, for B's fabric choice) ----
|
||||||
|
test('hailBlockFor: membrane stops all ice, porous leaks only the small stones', () => {
|
||||||
|
// A solid membrane blocks everything regardless of stone size.
|
||||||
|
for (const size of [0.4, 0.7, 1.0, 1.3, 1.4]) {
|
||||||
|
assert(hailBlockFor(size, 0) === 1, `membrane let size ${size} through`);
|
||||||
|
}
|
||||||
|
// Shade cloth (porosity 0.3) fully blocks the wild-night stones (1.3+) but
|
||||||
|
// leaks the finest pea hail — the honest, size-gated difference.
|
||||||
|
assert(hailBlockFor(1.3, 0.3) > 0.99, 'shade cloth failed to stop a 1.3 storm stone');
|
||||||
|
assert(hailBlockFor(1.4, 0.3) > 0.99, 'shade cloth failed to stop a 1.4 ice-night stone');
|
||||||
|
const pea = hailBlockFor(0.7, 0.3);
|
||||||
|
assert(pea > 0.4 && pea < 0.9, `shade cloth blocks ${pea.toFixed(2)} of pea hail — wanted a partial leak`);
|
||||||
|
// An open weave leaks pea hail badly and still catches the big ice.
|
||||||
|
assert(hailBlockFor(0.7, 0.5) < 0.3, 'an open weave should let most pea hail through');
|
||||||
|
assert(hailBlockFor(1.3, 0.5) > 0.8, 'even an open weave should mostly stop a 1.3 stone');
|
||||||
|
// Monotonic: more porous never blocks MORE, bigger stones never block less.
|
||||||
|
for (const size of [0.6, 1.0, 1.4]) {
|
||||||
|
assert(hailBlockFor(size, 0.5) <= hailBlockFor(size, 0.3) + 1e-9, 'more porous blocked more hail');
|
||||||
|
}
|
||||||
|
for (const por of [0.3, 0.5]) {
|
||||||
|
assert(hailBlockFor(0.5, por) <= hailBlockFor(1.2, por) + 1e-9, 'a bigger stone passed more easily');
|
||||||
|
}
|
||||||
|
metrics['fabric.shadecloth.blocksPeaHail'] = +pea.toFixed(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the fabric choice is real: porous costs garden on pea-hail nights, not ice nights', () => {
|
||||||
|
// The integration formula B/A will wire (documented for them, proven here):
|
||||||
|
// coveredHail = hailShadowOver(bed) * hailBlockFor(hailSize, sail.porosity)
|
||||||
|
// exposure = hailAt(t) * (1 - coveredHail)
|
||||||
|
// A membrane over the bed protects it fully; a porous cloth over the same bed
|
||||||
|
// protects it fully on an ice night and leaks on a pea-hail one. If those two
|
||||||
|
// came out equal, the choice would be dead — this is the assert that it isn't.
|
||||||
|
const gardenHail = (stormName, porosity) => {
|
||||||
|
const def = storms[stormName];
|
||||||
|
const f = createWindField(def);
|
||||||
|
const size = f.hailSize;
|
||||||
|
let dmg = 0;
|
||||||
|
const COVER = 1; // bed fully under the cloth
|
||||||
|
for (let t = 0; t <= f.duration; t += DT) {
|
||||||
|
const covered = COVER * hailBlockFor(size, porosity);
|
||||||
|
dmg += f.hailAt(t) * (1 - covered) * DT;
|
||||||
|
}
|
||||||
|
return dmg;
|
||||||
|
};
|
||||||
|
// ice night: membrane and shade cloth both fully protect a covered bed
|
||||||
|
const iceMembrane = gardenHail('storm_02b_icenight', 0);
|
||||||
|
const icePorous = gardenHail('storm_02b_icenight', 0.3);
|
||||||
|
assert(Math.abs(iceMembrane - icePorous) < 0.2 && iceMembrane < 0.5,
|
||||||
|
`on the ice night the fabrics should agree at ~0 (both block big ice): ${iceMembrane.toFixed(2)} vs ${icePorous.toFixed(2)}`);
|
||||||
|
// pea-hail night: the covered bed under porous cloth takes real damage
|
||||||
|
const peaMembrane = gardenHail('storm_03_southerly', 0);
|
||||||
|
const peaPorous = gardenHail('storm_03_southerly', 0.3);
|
||||||
|
metrics['fabric.storm03.gardenHail.membrane'] = +peaMembrane.toFixed(2);
|
||||||
|
metrics['fabric.storm03.gardenHail.porous'] = +peaPorous.toFixed(2);
|
||||||
|
assert(peaMembrane < 0.1, 'a membrane over the bed should take ~no pea hail');
|
||||||
|
assert(peaPorous > peaMembrane + 0.3, `porous should leak real pea hail: ${peaPorous.toFixed(2)} vs ${peaMembrane.toFixed(2)}`);
|
||||||
|
});
|
||||||
|
|
||||||
return { cases, metrics };
|
return { cases, metrics };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -188,6 +188,34 @@ export function hailBurstEnvelope(dt, ramp, hold, fade, peak) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What fraction of hail a cloth of this porosity STOPS (0..1). Lane B's fabric
|
||||||
|
* choice (SPRINT7) reads this; the honest answer to their question.
|
||||||
|
*
|
||||||
|
* The ruling first: porosity is about AIR and WATER, not ice. A knitted shade
|
||||||
|
* cloth's gaps are ~1-3 mm; a damaging hailstone is 6-45 mm, so it can't pass a
|
||||||
|
* mesh an order of magnitude finer than itself — porous and membrane block the
|
||||||
|
* big stones identically. The ONE true difference is at the bottom of the size
|
||||||
|
* range: the finest pea hail IS small enough to rattle through an open weave.
|
||||||
|
* So a solid membrane stops everything, and a porous cloth stops everything
|
||||||
|
* except the smallest stones — which is a real fabric tradeoff without a physics
|
||||||
|
* lie, and it makes porous cost you exactly on the mild-hail nights while staying
|
||||||
|
* honest on the ice nights.
|
||||||
|
*
|
||||||
|
* `size` is in hail.size units (1.0 ≈ a 1.5 cm stone; storms run 0.7 pea → 1.4).
|
||||||
|
* `porosity` matches SailRig's (0 = membrane, ~0.3 = knitted shade cloth).
|
||||||
|
*/
|
||||||
|
export function hailBlockFor(size, porosity = 0) {
|
||||||
|
if (!(porosity > 0)) return 1; // solid membrane stops all ice
|
||||||
|
// Effective aperture of the weave, in size units. A 70%-shade knit (porosity
|
||||||
|
// ~0.3) reads ~0.6 — it leaks only the finest hail; a very open 0.5 weave
|
||||||
|
// reads ~1.0 and lets small stones through too.
|
||||||
|
const aperture = porosity * 2;
|
||||||
|
// A stone well above the gap is stopped dead; one well below sails through;
|
||||||
|
// smoothstep the transition around the aperture.
|
||||||
|
return smoothstep(aperture * 0.5, aperture * 1.5, size);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- the field ----------
|
// ---------- the field ----------
|
||||||
/**
|
/**
|
||||||
* @param {object} def parsed storm JSON (see data/storms/*.json)
|
* @param {object} def parsed storm JSON (see data/storms/*.json)
|
||||||
|
|||||||
@ -10,9 +10,12 @@
|
|||||||
// determinism rule can't be broken by accident.
|
// determinism rule can't be broken by accident.
|
||||||
|
|
||||||
import * as THREE from '../vendor/three.module.js';
|
import * as THREE from '../vendor/three.module.js';
|
||||||
import { createWindField, validateStorm, GUST, RAIN_TIME_COMPRESSION } from './weather.core.js';
|
import {
|
||||||
|
createWindField, validateStorm, GUST, RAIN_TIME_COMPRESSION,
|
||||||
|
hailBlockFor, stormStats, forecastFor,
|
||||||
|
} from './weather.core.js';
|
||||||
|
|
||||||
export { GUST, validateStorm, RAIN_TIME_COMPRESSION };
|
export { GUST, validateStorm, RAIN_TIME_COMPRESSION, hailBlockFor, stormStats, forecastFor };
|
||||||
|
|
||||||
// Resolved against this module, not the server root: server.py serves the repo
|
// Resolved against this module, not the server root: server.py serves the repo
|
||||||
// root (so the 2D prototype stays reachable), but the demo bench serves web/.
|
// root (so the 2D prototype stays reachable), but the demo bench serves web/.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user