Root cause: configUtils.initConfig was silently stripping the four unknown constraint keys (nominalFlowMin, flowMax, maxRainRef, minSampleIntervalSec) because they weren't declared in the schema. With those keys gone, validateFlowBounds saw NaN/NaN and routed every i_start into the invalid-bounds branch — _beginRun never fired, so sumPuls stayed 0. Fix is in generalFunctions/src/configs/monster.json (declared the four keys with sensible defaults). Plus a 4-line comment at the guard site documenting the schema dependency. 10/10 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
61 lines
2.2 KiB
JavaScript
61 lines
2.2 KiB
JavaScript
'use strict';
|
|
|
|
// Sampling-cabinet boundary + target math + rain-scaled flow prediction.
|
|
// All operations are pure given a domain handle — the domain owns the
|
|
// mutable fields (maxVolume, targetPuls, …) so legacy tests that read
|
|
// `monster.maxVolume` keep working.
|
|
|
|
const RAIN_STALE_MS = 2 * 60 * 60 * 1000;
|
|
|
|
function applyBoundsAndTargets(m) {
|
|
m.maxVolume = m.maxWeight - m.emptyWeightBucket;
|
|
m.minPuls = Math.round(m.minVolume / m.volume_pulse);
|
|
m.maxPuls = Math.round(m.maxVolume / m.volume_pulse);
|
|
m.absMaxPuls = Math.round(m.cap_volume / m.volume_pulse);
|
|
m.targetVolume = m.minVolume * Math.sqrt(m.maxVolume / m.minVolume);
|
|
m.targetPuls = Math.round(m.targetVolume / m.volume_pulse);
|
|
}
|
|
|
|
function validateFlowBounds(m) {
|
|
// nominalFlowMin / flowMax must be declared in monster.json's `constraints`
|
|
// section. If they aren't, configUtils strips them as unknown keys and the
|
|
// bounds collapse to undefined → NaN → invalid here, which silently blocks
|
|
// every sampling run. (Fixed 2026-05-11.)
|
|
const min = Number(m.nominalFlowMin);
|
|
const max = Number(m.flowMax);
|
|
const valid = Number.isFinite(min) && Number.isFinite(max) && min >= 0 && max > 0 && min < max;
|
|
m.invalidFlowBounds = !valid;
|
|
if (!valid) m.logger.warn(`Invalid flow bounds. nominalFlowMin=${m.nominalFlowMin}, flowMax=${m.flowMax}`);
|
|
return valid;
|
|
}
|
|
|
|
function getRainIndex(m) {
|
|
if (!m.lastRainUpdate) return 0;
|
|
if (Date.now() - m.lastRainUpdate > RAIN_STALE_MS) return 0;
|
|
return Number.isFinite(m.avgRain) ? m.avgRain : 0;
|
|
}
|
|
|
|
function getPredictedFlowRate(m) {
|
|
const min = Number(m.nominalFlowMin);
|
|
const max = Number(m.flowMax);
|
|
if (!Number.isFinite(min) || !Number.isFinite(max) || min < 0 || max <= 0 || min >= max) return 0;
|
|
const rainIndex = getRainIndex(m);
|
|
const scale = Math.max(0, Math.min(1, m.rainMaxRef > 0 ? rainIndex / m.rainMaxRef : 0));
|
|
return min + (max - min) * scale;
|
|
}
|
|
|
|
function getSampleCooldownMs(m) {
|
|
if (!m.lastSampleTime) return 0;
|
|
const remaining = (m.minSampleIntervalSec * 1000) - (Date.now() - m.lastSampleTime);
|
|
return Math.max(0, remaining);
|
|
}
|
|
|
|
module.exports = {
|
|
applyBoundsAndTargets,
|
|
validateFlowBounds,
|
|
getRainIndex,
|
|
getPredictedFlowRate,
|
|
getSampleCooldownMs,
|
|
RAIN_STALE_MS,
|
|
};
|