wifi-densepose/ui/observatory/js/demo-data.js

1795 lines
75 KiB
JavaScript

/**
* Demo Data Generator — RuView Observatory
*
* Generates synthetic CSI data matching the SensingUpdate contract.
* 12 scenarios covering all edge module categories.
* Each person includes pose, facing, and scenario-specific motion data.
* Auto-cycles with cosine crossfade transitions.
*
* V2: Enhanced with temporally-correlated noise, spatially-coherent fields,
* physiologically accurate vital signs, and realistic behavioral patterns.
*/
const SCENARIOS = [
'empty_room',
'single_breathing',
'two_walking',
'fall_event',
'sleep_monitoring',
'intrusion_detect',
'gesture_control',
'crowd_occupancy',
'search_rescue',
'elderly_care',
'fitness_tracking',
'security_patrol',
];
const CROSSFADE_DURATION = 2; // seconds
// ---------------------------------------------------------------------------
// Noise & utility functions (module-private)
// ---------------------------------------------------------------------------
/** Seeded PRNG for deterministic per-scenario noise. */
function _mulberry32(seed) {
return function () {
let t = (seed += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
/**
* Temporally-correlated noise (1st-order IIR low-pass filtered white noise).
* Returns a function noise(t) that produces smooth, non-teleporting values
* in approximately [-amplitude, +amplitude].
* `smoothing` controls correlation: higher = smoother (0.9-0.99 typical).
*/
function _makeCorrelatedNoise(seed, smoothing = 0.95, amplitude = 1) {
const rng = _mulberry32(seed);
let state = 0;
let lastT = -1;
return function (t) {
// Step the filter forward for each new time tick
const steps = Math.max(1, Math.round((t - lastT) * 60)); // ~60 Hz internal
for (let i = 0; i < Math.min(steps, 120); i++) {
state = smoothing * state + (1 - smoothing) * (rng() * 2 - 1);
}
lastT = t;
return state * amplitude;
};
}
/**
* Perlin-like 1D noise via sine harmonics.
* Deterministic, smooth, and cheap.
*/
function _harmonicNoise(t, seed, octaves = 3) {
let v = 0, amp = 1, freq = 1;
for (let i = 0; i < octaves; i++) {
v += amp * Math.sin(t * freq + seed * (i + 1) * 1.618);
amp *= 0.5;
freq *= 2.17;
}
return v;
}
/** Smooth step (hermite interpolation) */
function _smoothstep(edge0, edge1, x) {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
/** Clamp */
function _clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
/** Lerp */
function _lerp(a, b, t) { return a + (b - a) * t; }
/** Gaussian blob value at distance d with given sigma */
function _gaussian(d, sigma) {
return Math.exp(-(d * d) / (2 * sigma * sigma));
}
// ---------------------------------------------------------------------------
// Noise bank — pre-allocated correlated noise channels per scenario
// Each scenario gets its own set of noise functions so they don't interfere.
// ---------------------------------------------------------------------------
const _noiseBanks = {};
function _getNoiseBank(scenario) {
if (!_noiseBanks[scenario]) {
const idx = SCENARIOS.indexOf(scenario);
const base = (idx + 1) * 1000;
_noiseBanks[scenario] = {
rssi: _makeCorrelatedNoise(base + 1, 0.97, 1.5),
breath: _makeCorrelatedNoise(base + 2, 0.92, 0.3),
hr: _makeCorrelatedNoise(base + 3, 0.94, 1.0),
motion: _makeCorrelatedNoise(base + 4, 0.90, 0.5),
field: _makeCorrelatedNoise(base + 5, 0.96, 0.2),
pos1: _makeCorrelatedNoise(base + 6, 0.98, 0.15),
pos2: _makeCorrelatedNoise(base + 7, 0.98, 0.15),
env: _makeCorrelatedNoise(base + 8, 0.99, 1.0),
spike: _makeCorrelatedNoise(base + 9, 0.80, 1.0),
};
}
return _noiseBanks[scenario];
}
export class DemoDataGenerator {
constructor() {
this._scenarioIndex = 0;
this._elapsed = 0;
this._paused = false;
this._prevFrame = null;
this._currFrame = null;
this._cycleDuration = 30;
this._autoMode = true;
}
get currentScenario() {
return SCENARIOS[this._scenarioIndex];
}
get paused() { return this._paused; }
set paused(v) { this._paused = v; }
cycleScenario() {
this._scenarioIndex = (this._scenarioIndex + 1) % SCENARIOS.length;
this._elapsed = 0;
}
setScenario(name) {
const idx = SCENARIOS.indexOf(name);
if (idx >= 0) {
this._scenarioIndex = idx;
this._autoMode = false;
this._elapsed = 0;
} else if (name === 'auto') {
this._autoMode = true;
}
}
setCycleDuration(seconds) {
this._cycleDuration = Math.max(5, seconds);
}
/** Call each frame; returns blended SensingUpdate object */
update(dt) {
if (this._paused) {
return this._currFrame || this._generate(this._scenarioIndex, this._elapsed);
}
this._elapsed += dt;
// Auto-cycle
if (this._autoMode && this._elapsed >= this._cycleDuration) {
this._elapsed -= this._cycleDuration;
this._scenarioIndex = (this._scenarioIndex + 1) % SCENARIOS.length;
}
const t = this._elapsed;
const frame = this._generate(this._scenarioIndex, t);
// Crossfade near transition boundaries
if (this._autoMode && t < CROSSFADE_DURATION) {
const prevIdx = (this._scenarioIndex - 1 + SCENARIOS.length) % SCENARIOS.length;
const prevFrame = this._generate(prevIdx, this._cycleDuration - CROSSFADE_DURATION + t);
const alpha = 0.5 + 0.5 * Math.cos(Math.PI * (1 - t / CROSSFADE_DURATION));
this._currFrame = this._blend(prevFrame, frame, alpha);
} else {
this._currFrame = frame;
}
return this._currFrame;
}
// ---- Scenario generators ----
_generate(scenarioIdx, t) {
const name = SCENARIOS[scenarioIdx];
switch (name) {
case 'empty_room': return this._emptyRoom(t);
case 'single_breathing': return this._singleBreathing(t);
case 'two_walking': return this._twoWalking(t);
case 'fall_event': return this._fallEvent(t);
case 'sleep_monitoring': return this._sleepMonitoring(t);
case 'intrusion_detect': return this._intrusionDetect(t);
case 'gesture_control': return this._gestureControl(t);
case 'crowd_occupancy': return this._crowdOccupancy(t);
case 'search_rescue': return this._searchRescue(t);
case 'elderly_care': return this._elderlyCare(t);
case 'fitness_tracking': return this._fitnessTracking(t);
case 'security_patrol': return this._securityPatrol(t);
default: return this._emptyRoom(t);
}
}
// ---- Base template ----
_baseFrame(overrides) {
return {
type: 'sensing_update',
timestamp: Date.now() / 1000,
source: 'demo',
scenario: SCENARIOS[this._scenarioIndex],
nodes: [{ node_id: 1, rssi_dbm: -45, position: [2, 0, 1.5], amplitude: new Float32Array(64), subcarrier_count: 64 }],
features: { mean_rssi: -45, variance: 0.3, std: 0.55, motion_band_power: 0.02, breathing_band_power: 0.01, dominant_freq_hz: 0.05, spectral_power: 0.03 },
classification: { motion_level: 'absent', presence: false, confidence: 0.92 },
signal_field: { grid_size: [20, 1, 20], values: this._flatField(0.05) },
vital_signs: { breathing_rate_bpm: 0, heart_rate_bpm: 0, breathing_confidence: 0, heart_rate_confidence: 0 },
persons: [],
estimated_persons: 0,
edge_modules: {},
_observatory: { subcarrier_iq: [], per_subcarrier_variance: new Float32Array(64).fill(0.02) },
...overrides,
};
}
// ========================================================================
// 1. Empty Room — environmental noise, interference spikes, day/night drift
// ========================================================================
_emptyRoom(t) {
const n = _getNoiseBank('empty_room');
// Day/night RSSI drift: slow sinusoidal cycle over the scenario duration
const dayNightDrift = Math.sin(t * 0.08) * 3;
// Occasional microwave/device interference spike
const spikeRaw = n.spike(t);
const interferenceSpike = spikeRaw > 0.7 ? (spikeRaw - 0.7) * 15 : 0;
// Subtle HVAC cycling
const hvacCycle = Math.sin(t * 0.4) * 0.5 + Math.sin(t * 1.1) * 0.2;
const baseRssi = -45 + dayNightDrift + n.rssi(t) + interferenceSpike;
const amplitude = new Float32Array(64);
for (let i = 0; i < 64; i++) {
// Base floor with harmonic variation per subcarrier
const subNoise = _harmonicNoise(t, i * 0.37, 2) * 0.02;
// Interference affects specific subcarrier bands (like a microwave in 2.4GHz)
const microBand = (i >= 20 && i <= 35) ? interferenceSpike * 0.03 : 0;
amplitude[i] = 0.1 + subNoise + microBand + Math.abs(hvacCycle) * 0.01;
}
// Signal field with subtle ripple patterns (standing waves in empty room)
const vals = [];
for (let iz = 0; iz < 20; iz++) {
for (let ix = 0; ix < 20; ix++) {
const standingWave = Math.sin(ix * 0.8 + t * 0.3) * Math.sin(iz * 0.6 + t * 0.2) * 0.015;
const fieldNoise = _harmonicNoise(t + ix * 0.5 + iz * 0.7, ix + iz * 20, 2) * 0.008;
const ripple = interferenceSpike > 0
? _gaussian(Math.sqrt((ix - 10) ** 2 + (iz - 10) ** 2), 8) * interferenceSpike * 0.02
: 0;
vals.push(_clamp(0.05 + standingWave + fieldNoise + ripple, 0, 1));
}
}
return this._baseFrame({
nodes: [{ node_id: 1, rssi_dbm: baseRssi, position: [2, 0, 1.5], amplitude, subcarrier_count: 64 }],
features: {
mean_rssi: baseRssi,
variance: 0.3 + Math.abs(n.env(t)) * 0.15 + interferenceSpike * 0.5,
std: 0.55 + interferenceSpike * 0.2,
motion_band_power: 0.02 + interferenceSpike * 0.08 + Math.abs(hvacCycle) * 0.005,
breathing_band_power: 0.01 + Math.abs(hvacCycle) * 0.003,
dominant_freq_hz: interferenceSpike > 0.5 ? 2.45 : 0.05 + Math.abs(hvacCycle) * 0.02,
spectral_power: 0.03 + interferenceSpike * 0.1,
},
signal_field: { grid_size: [20, 1, 20], values: vals },
edge_modules: {
environment: {
interference_detected: interferenceSpike > 0.5,
interference_band: interferenceSpike > 0.5 ? '2.4GHz_microwave' : 'none',
ambient_drift: dayNightDrift.toFixed(2),
},
},
});
}
// ========================================================================
// 2. Single Breathing — HRV, respiratory sinus arrhythmia, natural irregularity
// ========================================================================
_singleBreathing(t) {
const n = _getNoiseBank('single_breathing');
// Natural breathing: ~16 BPM but with irregularity
// Breathing rate varies slightly over time (14.5-17.5)
const breathRateBase = 16 + _harmonicNoise(t, 1.23, 2) * 1.5;
const breathFreq = breathRateBase / 60;
// Accumulate phase for non-uniform period
const breathPhase = Math.sin(2 * Math.PI * breathFreq * t + n.breath(t) * 0.4);
// Inhale is slightly shorter than exhale (1:1.5 ratio via asymmetric wave)
const breathSignal = breathPhase > 0
? Math.sin(Math.asin(breathPhase) * 1.3)
: breathPhase * 0.85;
// Heart Rate Variability (HRV): base 72 BPM, varies 68-76
// Respiratory Sinus Arrhythmia: HR increases on inhale, decreases on exhale
const rsaEffect = breathSignal * 3.0; // +/-3 BPM with breathing
const hrvWander = _harmonicNoise(t, 7.77, 3) * 2.0; // slow HRV drift
const instantHR = 72 + rsaEffect + hrvWander + n.hr(t) * 0.5;
const hrFreq = instantHR / 60;
const hrPhase = Math.sin(2 * Math.PI * hrFreq * t);
const amplitude = new Float32Array(64);
for (let i = 0; i < 64; i++) {
const subBase = 0.4 + 0.2 * Math.sin(t * 0.5 + i * 0.15);
const breathMod = breathSignal * 0.08 * (1 + 0.3 * Math.sin(i * 0.4)); // subcarrier-dependent
const hrMod = hrPhase * 0.015 * (i > 20 && i < 45 ? 1.5 : 0.5); // HR stronger in mid-band
amplitude[i] = subBase + breathMod + hrMod + _harmonicNoise(t, i * 0.13, 2) * 0.01;
}
const rssiBase = -42 + breathSignal * 1.5 + n.rssi(t) * 0.5;
return this._baseFrame({
nodes: [{ node_id: 1, rssi_dbm: rssiBase, position: [2, 0, 1.5], amplitude, subcarrier_count: 64 }],
features: {
mean_rssi: rssiBase,
variance: 1.8 + breathSignal * 0.3 + Math.abs(n.motion(t)) * 0.1,
std: 1.34 + Math.abs(breathSignal) * 0.1,
motion_band_power: 0.04 + Math.abs(breathSignal) * 0.02,
breathing_band_power: 0.12 + breathSignal * 0.04,
dominant_freq_hz: breathFreq,
spectral_power: 0.18 + Math.abs(hrPhase) * 0.03,
},
classification: { motion_level: 'present_still', presence: true, confidence: 0.88 + breathSignal * 0.03 },
signal_field: { grid_size: [20, 1, 20], values: this._presenceField(10, 10, 2.5, t) },
vital_signs: {
breathing_rate_bpm: breathRateBase,
heart_rate_bpm: instantHR,
breathing_confidence: 0.85 + breathSignal * 0.05,
heart_rate_confidence: 0.75 + hrPhase * 0.05,
hrv_ms: 35 + _harmonicNoise(t, 3.14, 2) * 15, // RMSSD-like HRV metric
rsa_active: true,
},
persons: [{ id: 'p0', position: [0 + n.pos1(t) * 0.05, 0, 0 + n.pos2(t) * 0.05], motion_score: 15, pose: 'standing', facing: 0 }],
estimated_persons: 1,
edge_modules: {
vital_trend: { status: 'normal', trend: 'stable', hrv_quality: 'good' },
cardiac_detail: { rsa_amplitude_bpm: Math.abs(rsaEffect).toFixed(1), hrv_rmssd_ms: (35 + _harmonicNoise(t, 3.14, 2) * 15).toFixed(0) },
},
});
}
// ========================================================================
// 3. Two Walking — collision avoidance, phone pause, confidence dip at crossing
// ========================================================================
_twoWalking(t) {
const n = _getNoiseBank('two_walking');
// Person 1: walks a figure-8 with speed variation
const p1speed = 0.5 + _harmonicNoise(t, 1.1, 2) * 0.1; // natural speed var
const p1phase = t * p1speed;
let p1x = Math.sin(p1phase) * 2.5;
let p1z = Math.sin(p1phase * 0.7) * Math.cos(p1phase * 0.35) * 1.8;
// Person 2: walks an ellipse, pauses at t~10-12 (checking phone)
const phonePause = (t >= 10 && t < 12);
const p2speedMod = phonePause ? 0.05 : 1.0; // nearly stopped during phone check
const p2speed = (0.4 + _harmonicNoise(t, 2.2, 2) * 0.08) * p2speedMod;
const p2phase = t * 0.4 + 1 + (phonePause ? 0 : _harmonicNoise(t, 3.3, 2) * 0.1);
let p2x = -Math.sin(p2phase) * 2;
let p2z = Math.cos(p2phase * 0.75 + 2) * 1.5;
// Collision avoidance: repulsion when persons are close
const dx = p1x - p2x;
const dz = p1z - p2z;
const dist = Math.sqrt(dx * dx + dz * dz);
const minDist = 0.8;
if (dist < minDist * 3 && dist > 0.01) {
const repulsion = Math.max(0, 1 - dist / (minDist * 3)) * 0.6;
const nx = dx / dist, nz = dz / dist;
p1x += nx * repulsion;
p1z += nz * repulsion;
p2x -= nx * repulsion;
p2z -= nz * repulsion;
}
// Confidence dip when persons are close (tracking confusion)
const proxConfidence = dist < 1.5 ? 0.65 + dist * 0.1 : 0.82;
const matchConfidence = dist < 1.2 ? 0.6 + dist * 0.2 : 0.91;
const p1facing = Math.atan2(
Math.cos(p1phase) * p1speed * 2.5,
Math.cos(p1phase * 0.7) * 0.7 * Math.cos(p1phase * 0.35) * 1.8
);
const p2facing = phonePause
? Math.PI * 0.8 // looking down at phone
: Math.atan2(-Math.cos(p2phase) * p2speed * 2, -Math.sin(p2phase * 0.75 + 2) * 0.75 * 1.5);
const p1ms = 160 + _harmonicNoise(t, 4.4, 2) * 20;
const p2ms = phonePause ? 8 + Math.abs(n.motion(t)) * 5 : 140 + _harmonicNoise(t, 5.5, 2) * 20;
const amplitude = new Float32Array(64);
for (let i = 0; i < 64; i++) {
amplitude[i] = 0.3 + 0.3 * Math.abs(Math.sin(t * 2 + i * 0.3))
+ _harmonicNoise(t, i * 0.17, 2) * 0.02;
}
return this._baseFrame({
nodes: [{ node_id: 1, rssi_dbm: -40 + Math.sin(t * 1.2) * 4 + n.rssi(t), position: [2, 0, 1.5], amplitude, subcarrier_count: 64 }],
features: {
mean_rssi: -40 + Math.sin(t * 1.2) * 4 + n.rssi(t),
variance: 3.5 + Math.sin(t * 0.8) * 1.2 + (dist < 1.5 ? 2 : 0),
std: 1.87 + (dist < 1.5 ? 0.5 : 0),
motion_band_power: 0.25 + Math.abs(Math.sin(t * 1.5)) * 0.15 * (phonePause ? 0.3 : 1),
breathing_band_power: 0.06,
dominant_freq_hz: 1.2 + Math.sin(t * 0.5) * 0.3,
spectral_power: 0.45,
},
classification: { motion_level: phonePause ? 'present_still' : 'active', presence: true, confidence: proxConfidence },
signal_field: { grid_size: [20, 1, 20], values: this._twoPresenceField(10 + p1x * 2, 10 + p1z * 2, 10 + p2x * 2, 10 + p2z * 2, t) },
vital_signs: { breathing_rate_bpm: 18, heart_rate_bpm: 85, breathing_confidence: 0.4, heart_rate_confidence: 0.35 },
persons: [
{ id: 'p0', position: [p1x, 0, p1z], motion_score: p1ms, pose: 'walking', facing: p1facing },
{ id: 'p1', position: [p2x, 0, p2z], motion_score: p2ms, pose: phonePause ? 'standing' : 'walking', facing: p2facing },
],
estimated_persons: 2,
edge_modules: {
person_match: { matched: 2, confidence: matchConfidence, proximity_warning: dist < 1.5 },
tracking: { id_swap_risk: dist < 1.0, nearest_pair_dist: dist.toFixed(2) },
},
});
}
// ========================================================================
// 4. Fall Event — pre-fall stumble, impact spike, micro-movements, shock HR
// ========================================================================
_fallEvent(t) {
const n = _getNoiseBank('fall_event');
// Timeline: 0-3 normal walk, 3-5 stumble, 5-5.8 fall, 5.8-8 micro-movement, 8+ still
const stumbleStart = 3, fallStart = 5, fallEnd = 5.8;
const microEnd = 8, stillPhase = t >= microEnd;
const preStumble = t < stumbleStart;
const stumbling = t >= stumbleStart && t < fallStart;
const inFall = t >= fallStart && t < fallEnd;
const microMovement = t >= fallEnd && t < microEnd;
const postFall = t >= fallEnd;
// Pre-fall stumble: unsteady gait (asymmetric, wobbly)
const stumbleIntensity = stumbling ? _smoothstep(stumbleStart, fallStart, t) : 0;
const wobble = stumbling ? Math.sin(t * 8) * stumbleIntensity * 0.4 : 0;
// Fall impact spike: sharp gaussian at moment of impact
const impactT = (fallStart + fallEnd) / 2;
const impactSpike = Math.exp(-((t - impactT) ** 2) / 0.04) * 1.0;
// Post-fall micro-movements (trying to get up)
const microIntensity = microMovement
? (1 - _smoothstep(fallEnd, microEnd, t)) * 0.3
: 0;
const microSignal = microMovement
? Math.sin(t * 3) * microIntensity + Math.sin(t * 5.5) * microIntensity * 0.4
: 0;
// Heart rate: normal 72, elevated post-fall shock response 100-110 BPM
let hrRate = 72;
if (stumbling) hrRate = 72 + stumbleIntensity * 15; // anxiety rising
else if (inFall) hrRate = 90 + impactSpike * 30;
else if (postFall) hrRate = 108 - _smoothstep(fallEnd, fallEnd + 20, t) * 30; // slowly comes down
hrRate += n.hr(t) * 1.5;
// Breathing: elevated post-fall
let breathRate = 16;
if (postFall) breathRate = 24 - _smoothstep(fallEnd, fallEnd + 15, t) * 8;
breathRate += n.breath(t) * 0.5;
// Position: walking -> stumble -> fall -> ground
let px = 0.3, pz = 0.2, py = 0, pose = 'standing', ms = 20;
if (preStumble) {
px = Math.sin(t * 0.4) * 1.5;
pz = t * 0.3 - 1;
pose = 'walking';
ms = 80;
} else if (stumbling) {
const st = (t - stumbleStart) / (fallStart - stumbleStart);
px = Math.sin(stumbleStart * 0.4) * 1.5 + wobble + st * 0.5;
pz = (stumbleStart * 0.3 - 1) + st * 0.3;
pose = 'walking'; // stumbling but still upright
ms = 120 + stumbleIntensity * 80;
} else if (inFall) {
pose = 'falling';
ms = 255;
py = 0;
} else if (microMovement) {
pose = 'fallen';
ms = _clamp(microIntensity * 100, 3, 40);
px += microSignal * 0.1;
} else {
pose = 'fallen';
ms = 2 + Math.abs(n.motion(t)) * 1;
}
const motionPower = preStumble ? 0.08
: stumbling ? 0.15 + stumbleIntensity * 0.3
: inFall ? 0.6 + impactSpike * 0.4
: microMovement ? 0.05 + microIntensity * 0.15
: 0.02 + Math.abs(n.motion(t)) * 0.005;
const amplitude = new Float32Array(64);
for (let i = 0; i < 64; i++) {
const base = postFall && !microMovement ? 0.15 : 0.3;
amplitude[i] = base + impactSpike * 0.5 + microSignal * 0.1
+ Math.sin(t * 0.5 + i * 0.1) * 0.1 * (1 - (stillPhase ? 0.7 : 0))
+ _harmonicNoise(t, i * 0.19, 2) * 0.01;
}
const rssi = -43 + impactSpike * 8 + wobble * 2 + n.rssi(t) * 0.8;
return this._baseFrame({
nodes: [{ node_id: 1, rssi_dbm: rssi, position: [2, 0, 1.5], amplitude, subcarrier_count: 64 }],
features: {
mean_rssi: rssi,
variance: postFall && !microMovement ? 0.5 : (1.5 + impactSpike * 6 + stumbleIntensity * 2),
std: postFall && !microMovement ? 0.7 : (1.22 + impactSpike * 2),
motion_band_power: motionPower,
breathing_band_power: postFall ? 0.08 + Math.abs(n.breath(t)) * 0.02 : 0.1,
dominant_freq_hz: inFall ? 3.5 : (stumbling ? 1.8 + wobble : 0.15),
spectral_power: inFall ? 0.9 : (postFall ? 0.1 : 0.2 + stumbleIntensity * 0.3),
},
classification: {
motion_level: postFall && !microMovement ? 'present_still' : (inFall || stumbling ? 'active' : 'present_still'),
presence: true,
confidence: inFall ? 0.55 : (stumbling ? 0.7 : (postFall ? 0.6 : 0.85)),
fall_detected: inFall || postFall,
pre_fall_warning: stumbling,
},
signal_field: { grid_size: [20, 1, 20], values: this._presenceField(10 + px, 10 + pz, postFall ? 1.5 + microIntensity : 2.5, t) },
vital_signs: {
breathing_rate_bpm: breathRate,
heart_rate_bpm: hrRate,
breathing_confidence: postFall ? 0.5 + _smoothstep(fallEnd, fallEnd + 5, t) * 0.2 : 0.8,
heart_rate_confidence: postFall ? 0.4 + _smoothstep(fallEnd, fallEnd + 5, t) * 0.2 : 0.7,
},
persons: [{ id: 'p0', position: [px, py, pz], motion_score: ms, pose, facing: 0.5, fallProgress: inFall ? (t - fallStart) / (fallEnd - fallStart) : (postFall ? 1 : 0) }],
estimated_persons: 1,
edge_modules: {
fall_detect: {
detected: inFall || postFall,
severity: inFall ? 'critical' : (microMovement ? 'monitoring_movement' : (stillPhase ? 'monitoring_still' : 'none')),
impact_time: postFall ? (fallEnd - fallStart).toFixed(2) : (inFall ? (t - fallStart).toFixed(2) : '0'),
pre_fall_stumble: stumbling,
post_fall_movement: microMovement,
shock_hr_bpm: postFall ? hrRate.toFixed(0) : null,
},
},
});
}
// ========================================================================
// 5. Sleep Monitoring — sleep stages, REM, position changes, apnea buildup
// ========================================================================
_sleepMonitoring(t) {
const n = _getNoiseBank('sleep_monitoring');
// Sleep stages timeline (30s cycle compressed):
// 0-4: light sleep (stage 1-2)
// 4-10: deep sleep (stage 3-4)
// 10-14: REM sleep
// 14-16: position change
// 16-18: light sleep again
// 18-22: apnea warning signs (breathing gets irregular)
// 22-26: apnea event
// 26-30: recovery
const cycleT = t % 30;
let sleepStage = 'light';
let breathRateBase = 14;
let movementLevel = 0.03;
let hrBase = 64;
let eyeMovementArtifact = 0;
let positionChangeActive = false;
if (cycleT < 4) {
// Light sleep: more body movement, higher breath rate
sleepStage = 'light';
breathRateBase = 14 + _harmonicNoise(t, 1.1, 2) * 1.5;
movementLevel = 0.06 + Math.abs(n.motion(t)) * 0.03;
hrBase = 64 + _harmonicNoise(t, 2.2, 2) * 3;
} else if (cycleT < 10) {
// Deep sleep: minimal movement, slow breathing, low HR
sleepStage = 'deep';
breathRateBase = 10 + _harmonicNoise(t, 1.3, 2) * 0.5;
movementLevel = 0.01;
hrBase = 56 + _harmonicNoise(t, 2.4, 2) * 1;
} else if (cycleT < 14) {
// REM: rapid eye movement creates signal artifacts, HR more variable
sleepStage = 'REM';
breathRateBase = 16 + _harmonicNoise(t, 1.5, 2) * 2;
movementLevel = 0.02;
hrBase = 68 + _harmonicNoise(t, 2.6, 3) * 5; // more variable in REM
// Eye movement artifact: high-frequency bursts
const remBurst = Math.sin(t * 12) * Math.sin(t * 7.3) * 0.5;
eyeMovementArtifact = Math.max(0, remBurst) * 0.08;
} else if (cycleT < 16) {
// Position change: brief movement spike
sleepStage = 'light';
positionChangeActive = true;
const changeProgress = (cycleT - 14) / 2;
movementLevel = changeProgress < 0.5
? _smoothstep(0, 0.5, changeProgress) * 0.5
: _smoothstep(1, 0.5, changeProgress) * 0.5;
breathRateBase = 16;
hrBase = 68;
} else if (cycleT < 18) {
sleepStage = 'light';
breathRateBase = 13;
movementLevel = 0.04;
hrBase = 62;
} else if (cycleT < 22) {
// Pre-apnea: breathing becomes irregular
sleepStage = 'light';
const irregularity = _smoothstep(18, 22, cycleT);
breathRateBase = 12 - irregularity * 6; // slowing down
// Breathing becomes chaotic before stopping
const chaotic = irregularity * Math.sin(t * 3 + Math.sin(t * 1.7) * 2) * 0.4;
breathRateBase = Math.max(3, breathRateBase + chaotic * 5);
movementLevel = 0.02;
hrBase = 60 - irregularity * 4;
} else if (cycleT < 26) {
// Full apnea
sleepStage = 'apnea';
breathRateBase = 0 + Math.abs(n.breath(t)) * 0.5; // near-zero
movementLevel = 0.01;
hrBase = 54 + _smoothstep(22, 26, cycleT) * 8; // HR rises during apnea (stress)
} else {
// Recovery: gasp, then return to normal
sleepStage = 'light';
const recovery = _smoothstep(26, 28, cycleT);
breathRateBase = 6 + recovery * 10; // gasping then normalizing
movementLevel = cycleT < 27 ? 0.15 : 0.04; // body startles
hrBase = 70 - recovery * 6;
}
const inApnea = sleepStage === 'apnea';
const breathFreq = breathRateBase / 60;
const breathPhase = Math.sin(2 * Math.PI * breathFreq * t + n.breath(t) * 0.3);
const breathSignal = inApnea ? n.breath(t) * 0.05 : breathPhase;
// Lying position: slight shifts over time, bigger shift during position change
const posAngle = positionChangeActive
? Math.PI / 2 + _smoothstep(14, 16, cycleT) * Math.PI * 0.3
: Math.PI / 2 + Math.sin(t * 0.02) * 0.1;
const lyingX = 3.5 + (positionChangeActive ? Math.sin(cycleT * 2) * 0.3 : 0);
const amplitude = new Float32Array(64);
for (let i = 0; i < 64; i++) {
const base = 0.25 + breathSignal * 0.04 * (1 - (inApnea ? 0.9 : 0));
const rem = eyeMovementArtifact * (i > 30 && i < 50 ? 1.5 : 0.3); // REM artifact in upper band
amplitude[i] = base + rem + movementLevel * Math.sin(t * 0.8 + i * 0.1)
+ _harmonicNoise(t, i * 0.11, 2) * 0.005;
}
return this._baseFrame({
nodes: [{ node_id: 1, rssi_dbm: -44 + breathSignal * 0.5 + n.rssi(t) * 0.3, position: [2, 0, 1.5], amplitude, subcarrier_count: 64 }],
features: {
mean_rssi: -44 + breathSignal * 0.5 + n.rssi(t) * 0.3,
variance: inApnea ? 0.15 : (0.6 + movementLevel * 3),
std: inApnea ? 0.39 : (0.77 + movementLevel),
motion_band_power: movementLevel + eyeMovementArtifact * 0.5,
breathing_band_power: inApnea ? 0.02 : (0.1 + Math.abs(breathSignal) * 0.05),
dominant_freq_hz: breathFreq,
spectral_power: 0.08 + eyeMovementArtifact * 0.3,
},
classification: { motion_level: movementLevel > 0.1 ? 'active' : 'present_still', presence: true, confidence: 0.9, apnea_detected: inApnea },
signal_field: { grid_size: [20, 1, 20], values: this._presenceField(15, 13, 1.8 + movementLevel * 2, t) },
vital_signs: {
breathing_rate_bpm: breathRateBase,
heart_rate_bpm: hrBase + n.hr(t) * 1,
breathing_confidence: inApnea ? 0.35 : (0.85 + breathSignal * 0.05),
heart_rate_confidence: 0.82,
},
persons: [{ id: 'p0', position: [lyingX, 0.45, -3.5 + n.pos1(t) * 0.1], motion_score: _clamp(movementLevel * 100, 1, 50), pose: 'lying', facing: posAngle }],
estimated_persons: 1,
edge_modules: {
sleep_staging: {
stage: sleepStage,
stage_duration_s: cycleT.toFixed(1),
position_change: positionChangeActive,
rem_density: eyeMovementArtifact > 0.02 ? 'high' : 'low',
},
sleep_apnea: {
state: inApnea ? 'apnea_event' : (cycleT >= 18 && cycleT < 22 ? 'pre_apnea_warning' : 'normal'),
duration_s: inApnea ? (cycleT - 22).toFixed(1) : 0,
events_total: inApnea ? 1 : 0,
breathing_irregularity: cycleT >= 18 && cycleT < 22 ? _smoothstep(18, 22, cycleT).toFixed(2) : '0',
},
cardiac_arrhythmia: { rhythm: 'sinus', hr_variability: (4.2 + _harmonicNoise(t, 8.8, 2) * 2).toFixed(1) },
},
});
}
// ========================================================================
// 6. Intrusion Detection — door pressure, cautious movement, drawer search
// ========================================================================
_intrusionDetect(t) {
const n = _getNoiseBank('intrusion_detect');
// Timeline:
// 0-2: baseline (quiet room)
// 2-3: door opens (pressure change, environmental shift)
// 3-6: cautious entry (pause-move-pause)
// 6-10: checking corners
// 10-14: checks room, pauses
// 14-22: searches drawers near desk (oscillating position)
// 22+: settles, loiters
const doorOpen = t >= 2 && t < 3;
const entered = t >= 3;
const cautiousEntry = t >= 3 && t < 6;
const checkingCorners = t >= 6 && t < 10;
const settledSearch = t >= 14 && t < 22;
const loitering = t >= 22;
// Environmental baseline shift when door opens
const doorPressure = doorOpen ? Math.sin((t - 2) * Math.PI) * 0.4 : 0;
// Cautious movement: pause-move-pause pattern
let px, pz, facing, ms, pose;
if (!entered) {
px = -5.5; pz = -2; facing = 0; ms = 0; pose = 'absent';
} else if (cautiousEntry) {
// Pause-move-pause pattern
const entryT = t - 3;
const movePhase = entryT % 1.5;
const isMoving = movePhase > 0.6 && movePhase < 1.3; // move for 0.7s, pause for 0.8s
const progress = Math.min(1, entryT / 3);
px = -4.5 + progress * 3;
pz = -1 + progress * 0.8;
// Slight position jitter during pauses (looking around)
if (!isMoving) {
px += Math.sin(t * 4) * 0.05;
facing = Math.sin(t * 2) * 0.5 + 0.8; // head scanning
} else {
facing = Math.atan2(3, 0.8); // heading into room
}
ms = isMoving ? 100 : 8;
pose = 'crouching';
} else if (checkingCorners) {
// Move to corners, pause at each
const cornerT = (t - 6) / 4;
const cornerIdx = Math.floor(cornerT * 3) % 3;
const corners = [[-2, -0.5], [0, 1], [2, 0]];
const corner = corners[cornerIdx];
const inTransit = (cornerT * 3) % 1 < 0.6;
px = _lerp(corner[0], corners[(cornerIdx + 1) % 3][0], inTransit ? (cornerT * 3 % 1) / 0.6 : 0);
pz = _lerp(corner[1], corners[(cornerIdx + 1) % 3][1], inTransit ? (cornerT * 3 % 1) / 0.6 : 0);
facing = inTransit ? Math.atan2(corners[(cornerIdx + 1) % 3][0] - corner[0], corners[(cornerIdx + 1) % 3][1] - corner[1]) : Math.sin(t * 3) * Math.PI; // scanning while paused
ms = inTransit ? 120 : 10;
pose = 'crouching';
} else if (settledSearch) {
// Oscillating near desk area, opening drawers
const searchT = t - 14;
const deskX = 1.5, deskZ = -0.5;
px = deskX + Math.sin(searchT * 1.2) * 0.6; // back and forth along desk
pz = deskZ + Math.cos(searchT * 0.8) * 0.3;
// Periodic reaching motion (drawer open/close every ~2s)
const reaching = Math.sin(searchT * Math.PI) > 0.7;
facing = reaching ? 0 : Math.PI * 0.5;
ms = reaching ? 80 : 30;
pose = reaching ? 'reaching' : 'standing';
} else if (loitering) {
px = 0.5 + n.pos1(t) * 0.2;
pz = 0.5 + n.pos2(t) * 0.2;
facing = Math.sin(t * 0.3) * Math.PI;
ms = 12 + Math.abs(n.motion(t)) * 8;
pose = 'standing';
} else {
// 10-14: general room check
const checkT = (t - 10) / 4;
px = -1 + Math.sin(checkT * Math.PI * 2) * 2;
pz = Math.cos(checkT * Math.PI * 2) * 1.5;
facing = Math.atan2(Math.cos(checkT * Math.PI * 2) * 2, -Math.sin(checkT * Math.PI * 2) * 1.5);
ms = 90;
pose = 'walking';
}
const isMovingNow = ms > 50;
const amplitude = new Float32Array(64);
for (let i = 0; i < 64; i++) {
amplitude[i] = entered
? 0.35 + 0.15 * Math.sin(t * 1.5 + i * 0.2) + _harmonicNoise(t, i * 0.14, 2) * 0.01
: 0.1 + doorPressure * 0.05 + _harmonicNoise(t, i * 0.14, 2) * 0.008;
}
const rssiBase = entered ? -38 + Math.sin(t * 2) * 3 + n.rssi(t) : -46 + doorPressure * 2 + n.rssi(t) * 0.3;
return this._baseFrame({
nodes: [{ node_id: 1, rssi_dbm: rssiBase, position: [2, 0, 1.5], amplitude, subcarrier_count: 64 }],
features: {
mean_rssi: rssiBase,
variance: isMovingNow ? 4.5 : (entered ? 1.0 : 0.2 + Math.abs(doorPressure) * 1.5),
std: isMovingNow ? 2.1 : 0.45,
motion_band_power: isMovingNow ? 0.4 : (entered ? 0.03 + (settledSearch ? 0.08 : 0) : 0.01 + Math.abs(doorPressure) * 0.15),
breathing_band_power: entered && !isMovingNow ? 0.08 : 0.01,
dominant_freq_hz: isMovingNow ? 1.8 : (doorOpen ? 0.5 : 0.1),
spectral_power: isMovingNow ? 0.55 : 0.03 + Math.abs(doorPressure) * 0.2,
},
classification: {
motion_level: isMovingNow ? 'active' : (entered ? 'present_still' : 'absent'),
presence: entered || doorOpen,
confidence: entered ? 0.78 + (loitering ? 0.1 : 0) : (doorOpen ? 0.45 : 0.95),
intrusion: entered,
perimeter_breach: t >= 3 && t < 5,
door_event: doorOpen,
},
signal_field: { grid_size: [20, 1, 20], values: entered ? this._presenceField(10 + px, 10 + pz, 2, t) : this._flatField(0.04 + Math.abs(doorPressure) * 0.06) },
vital_signs: {
breathing_rate_bpm: entered && !isMovingNow ? 20 + n.breath(t) : 0,
heart_rate_bpm: entered ? 90 + _harmonicNoise(t, 4.4, 2) * 5 : 0, // elevated from adrenaline
breathing_confidence: entered && !isMovingNow ? 0.6 : 0,
heart_rate_confidence: entered && !isMovingNow ? 0.4 : 0,
},
persons: entered ? [{ id: 'p0', position: [px, 0, pz], motion_score: ms, pose, facing }] : [],
estimated_persons: entered ? 1 : 0,
edge_modules: {
intrusion: {
detected: entered,
zone: cautiousEntry ? 'perimeter' : (settledSearch ? 'desk_area' : 'interior'),
threat_level: cautiousEntry ? 'high' : (settledSearch ? 'high' : (loitering ? 'medium' : 'none')),
behavior_pattern: cautiousEntry ? 'cautious_entry' : (checkingCorners ? 'corner_check' : (settledSearch ? 'searching' : (loitering ? 'loitering' : 'none'))),
},
loitering: { detected: loitering || settledSearch, duration_s: loitering ? (t - 22).toFixed(1) : (settledSearch ? (t - 14).toFixed(1) : 0) },
door_sensor: { open_event: doorOpen, pressure_delta: doorPressure.toFixed(3) },
},
});
}
// ========================================================================
// 7. Gesture Control — distinct gesture signatures, recognition feedback
// ========================================================================
_gestureControl(t) {
const n = _getNoiseBank('gesture_control');
const gestureCycle = 7; // seconds per gesture
const gesturePhase = Math.floor(t / gestureCycle) % 4;
const gestures = ['wave', 'swipe_left', 'circle', 'point'];
const gestureT = t % gestureCycle;
const isGesturing = gestureT >= 1.5 && gestureT < 5;
const gestureProgress = isGesturing ? (gestureT - 1.5) / 3.5 : 0;
const gestureEnvelope = isGesturing ? Math.sin(gestureProgress * Math.PI) : 0;
// Recognition feedback: brief confidence spike when gesture completes (at ~80% progress)
const recognitionMoment = isGesturing && gestureProgress > 0.75 && gestureProgress < 0.85;
const recognitionBoost = recognitionMoment ? 0.15 : 0;
// Gesture-specific signal characteristics
let gestureSignal = 0;
let dominantFreq = 0.2;
let motionScore = 10;
let gestureDetail = {};
const g = gestures[gesturePhase];
if (isGesturing) {
switch (g) {
case 'wave':
// Fast oscillation (hand waving back and forth)
gestureSignal = Math.sin(t * 14) * gestureEnvelope * 0.5
+ Math.sin(t * 21) * gestureEnvelope * 0.2; // harmonics
dominantFreq = 4.0 + _harmonicNoise(t, 6.6, 2) * 0.3;
motionScore = 150 * gestureEnvelope;
gestureDetail = { oscillation_hz: 7, amplitude: gestureEnvelope.toFixed(2) };
break;
case 'swipe_left':
// Clear directional shift: signal ramps in one direction
gestureSignal = (gestureProgress - 0.5) * 2 * gestureEnvelope * 0.6;
dominantFreq = 2.0;
motionScore = 180 * gestureEnvelope;
gestureDetail = { direction: 'left', displacement: gestureSignal.toFixed(3) };
break;
case 'circle':
// Rotating phase pattern
const circleAngle = gestureProgress * Math.PI * 2 * 1.5; // 1.5 rotations
gestureSignal = Math.sin(circleAngle) * gestureEnvelope * 0.4;
const phaseRotation = Math.cos(circleAngle) * gestureEnvelope * 0.4;
dominantFreq = 3.0;
motionScore = 130 * gestureEnvelope;
gestureDetail = { rotation_angle: (circleAngle * 180 / Math.PI).toFixed(0), phase_i: gestureSignal.toFixed(3), phase_q: phaseRotation.toFixed(3) };
break;
case 'point':
// Quick, decisive: sharp onset, brief hold, sharp offset
const pointEnvelope = gestureProgress < 0.2
? _smoothstep(0, 0.2, gestureProgress) // fast rise
: (gestureProgress < 0.6 ? 1.0 : _smoothstep(1, 0.6, gestureProgress)); // hold then drop
gestureSignal = pointEnvelope * 0.55;
dominantFreq = 1.5; // lower freq, more impulse-like
motionScore = 200 * pointEnvelope;
gestureDetail = { sharpness: pointEnvelope > 0.9 ? 'locked' : 'transitioning' };
break;
}
}
const amplitude = new Float32Array(64);
for (let i = 0; i < 64; i++) {
const base = 0.3 + _harmonicNoise(t, i * 0.13, 2) * 0.01;
// Each gesture affects subcarriers differently
let gestMod = 0;
if (isGesturing) {
if (g === 'wave') gestMod = Math.sin(t * 14 + i * 0.5) * gestureEnvelope * 0.15;
else if (g === 'swipe_left') gestMod = gestureSignal * (i / 64) * 0.2; // gradient across band
else if (g === 'circle') gestMod = Math.sin(t * 8 + i * 0.3) * gestureEnvelope * 0.12;
else if (g === 'point') gestMod = gestureSignal * 0.2 * (i > 25 && i < 40 ? 1.5 : 0.5);
}
amplitude[i] = base + gestMod;
}
const rssi = -41 + gestureEnvelope * 3 + n.rssi(t) * 0.5;
const confidence = isGesturing ? 0.7 + gestureEnvelope * 0.15 + recognitionBoost : 0;
return this._baseFrame({
nodes: [{ node_id: 1, rssi_dbm: rssi, position: [2, 0, 1.5], amplitude, subcarrier_count: 64 }],
features: {
mean_rssi: rssi,
variance: 1.2 + gestureEnvelope * 2.5,
std: 1.1 + gestureEnvelope * 0.5,
motion_band_power: 0.05 + Math.abs(gestureSignal) * 0.6,
breathing_band_power: 0.08,
dominant_freq_hz: dominantFreq,
spectral_power: 0.15 + gestureEnvelope * 0.4,
},
classification: {
motion_level: isGesturing ? 'active' : 'present_still',
presence: true,
confidence: 0.85,
gesture: isGesturing ? g : null,
gesture_recognized: recognitionMoment,
},
signal_field: { grid_size: [20, 1, 20], values: this._presenceField(10, 10, 2 + gestureEnvelope * 0.8, t) },
vital_signs: { breathing_rate_bpm: 16 + n.breath(t) * 0.5, heart_rate_bpm: 74 + n.hr(t) * 1, breathing_confidence: 0.7, heart_rate_confidence: 0.65 },
persons: [{ id: 'p0', position: [0 + n.pos1(t) * 0.03, 0, 0.5 + n.pos2(t) * 0.03], motion_score: motionScore, pose: 'gesturing', facing: Math.PI, gestureType: g, gestureIntensity: gestureEnvelope, gestureDetail }],
estimated_persons: 1,
edge_modules: {
gesture: {
detected: isGesturing,
type: isGesturing ? g : 'none',
confidence: confidence,
dtw_distance: isGesturing ? (12.3 - recognitionBoost * 8) : 999,
recognition_feedback: recognitionMoment ? 'MATCHED' : null,
detail: gestureDetail,
},
},
});
}
// ========================================================================
// 8. Crowd Occupancy — clustering, stationary person, rushing, entry/exit
// ========================================================================
_crowdOccupancy(t) {
const n = _getNoiseBank('crowd_occupancy');
// Points of interest for clustering
const poi = [
{ x: -2, z: -1.5, label: 'display' }, // display/kiosk
{ x: 2, z: 1, label: 'counter' }, // service counter
{ x: 0, z: 0, label: 'center' }, // open area
];
// 5 people with distinct behaviors
const persons = [];
const count = 5;
// Person 0: sits stationary at desk
{
const px = -1.5 + n.pos1(t) * 0.02;
const pz = 1.5 + n.pos2(t) * 0.02;
persons.push({ id: 'p0', position: [px, 0, pz], motion_score: 3, pose: 'sitting', facing: Math.PI * 0.5 + _harmonicNoise(t, 11.1, 2) * 0.1 });
}
// Person 1: browses near display (clusters near POI 0)
{
const browseT = t * 0.3;
const px = poi[0].x + Math.sin(browseT) * 0.8 + _harmonicNoise(t, 12.1, 2) * 0.1;
const pz = poi[0].z + Math.cos(browseT * 0.7) * 0.6;
const facing = Math.atan2(poi[0].x - px, poi[0].z - pz);
persons.push({ id: 'p1', position: [px, 0, pz], motion_score: 40 + Math.abs(_harmonicNoise(t, 13.1, 2)) * 20, pose: 'walking', facing });
}
// Person 2: rushes through (faster speed, enters and exits)
{
const rushCycle = 20;
const rushT = t % rushCycle;
const inSpace = rushT > 2 && rushT < 15;
const rushProgress = inSpace ? (rushT - 2) / 13 : 0;
const rushSpeed = 1.5 + _harmonicNoise(t, 14.1, 2) * 0.2;
const px = inSpace ? -4 + rushProgress * 8 : -5;
const pz = inSpace ? -0.5 + Math.sin(rushProgress * Math.PI * 0.5) * 0.8 : -3;
if (inSpace) {
persons.push({ id: 'p2', position: [px, 0, pz], motion_score: 220, pose: 'walking', facing: 0.1, speed: rushSpeed });
}
}
// Person 3: walks between display and counter (clusters near POIs)
{
const walkT = t * 0.15;
const poiIdx = Math.floor(walkT) % 2;
const target = poiIdx === 0 ? poi[0] : poi[1];
const progress = walkT % 1;
const other = poiIdx === 0 ? poi[1] : poi[0];
const px = _lerp(other.x, target.x, _smoothstep(0, 0.7, progress))
+ _harmonicNoise(t, 15.1, 2) * 0.15;
const pz = _lerp(other.z, target.z, _smoothstep(0, 0.7, progress))
+ _harmonicNoise(t, 16.1, 2) * 0.15;
const facing = Math.atan2(target.x - other.x, target.z - other.z);
const nearPoi = progress > 0.7;
persons.push({ id: 'p3', position: [px, 0, pz], motion_score: nearPoi ? 15 : 100, pose: nearPoi ? 'standing' : 'walking', facing });
}
// Person 4: enters/exits periodically
{
const cycleLen = 25;
const ct = t % cycleLen;
const entering = ct < 3;
const inside = ct >= 3 && ct < 18;
const exiting = ct >= 18 && ct < 21;
if (entering || inside || exiting) {
let px, pz;
if (entering) {
px = -4.5 + (ct / 3) * 3;
pz = -2 + (ct / 3) * 1;
} else if (exiting) {
const ep = (ct - 18) / 3;
px = 1 + ep * 3;
pz = 0.5 + ep * 1;
} else {
px = poi[2].x + Math.sin(t * 0.2) * 1.5;
pz = poi[2].z + Math.cos(t * 0.15) * 1;
}
persons.push({ id: 'p4', position: [px, 0, pz], motion_score: (entering || exiting) ? 130 : 70, pose: 'walking', facing: entering ? 0.3 : (exiting ? -0.3 : Math.atan2(Math.cos(t * 0.2), -Math.sin(t * 0.15))) });
}
}
const actualCount = persons.length;
const amplitude = new Float32Array(64);
for (let i = 0; i < 64; i++) {
amplitude[i] = 0.35 + 0.25 * Math.abs(Math.sin(t * 1.5 + i * 0.2))
+ _harmonicNoise(t, i * 0.16, 2) * 0.015;
}
// Signal field with congestion patterns around POIs
const vals = [];
for (let iz = 0; iz < 20; iz++) {
for (let ix = 0; ix < 20; ix++) {
let v = 0;
for (const p of persons) {
const dx = (ix - 10) / 3 - p.position[0];
const dz = (iz - 10) / 3 - p.position[2];
v += _gaussian(Math.sqrt(dx * dx + dz * dz), 0.9) * 0.4;
}
// POI congestion haze
for (const p of poi) {
const dx = (ix - 10) / 3 - p.x;
const dz = (iz - 10) / 3 - p.z;
v += _gaussian(Math.sqrt(dx * dx + dz * dz), 2.0) * 0.08;
}
vals.push(_clamp(v + _harmonicNoise(t + ix * 0.3, iz * 0.4, 2) * 0.01, 0, 1));
}
}
return this._baseFrame({
nodes: [{ node_id: 1, rssi_dbm: -37 + Math.sin(t * 0.9) * 5 + n.rssi(t), position: [2, 0, 1.5], amplitude, subcarrier_count: 64 }],
features: {
mean_rssi: -37 + Math.sin(t * 0.9) * 5 + n.rssi(t),
variance: 5.2 + Math.sin(t * 0.6) * 1.5 + actualCount * 0.3,
std: 2.28 + actualCount * 0.1,
motion_band_power: 0.25 + actualCount * 0.05,
breathing_band_power: 0.04,
dominant_freq_hz: 1.5,
spectral_power: 0.45 + actualCount * 0.03,
},
classification: { motion_level: 'active', presence: true, confidence: 0.76 - (actualCount > 4 ? 0.05 : 0) },
signal_field: { grid_size: [20, 1, 20], values: vals },
vital_signs: { breathing_rate_bpm: 0, heart_rate_bpm: 0, breathing_confidence: 0.15, heart_rate_confidence: 0.1 },
persons,
estimated_persons: actualCount,
edge_modules: {
occupancy: {
count: actualCount,
zones: {
display: persons.filter(p => Math.sqrt((p.position[0] - poi[0].x) ** 2 + (p.position[2] - poi[0].z) ** 2) < 2).length,
counter: persons.filter(p => Math.sqrt((p.position[0] - poi[1].x) ** 2 + (p.position[2] - poi[1].z) ** 2) < 2).length,
center: persons.filter(p => Math.sqrt((p.position[0] - poi[2].x) ** 2 + (p.position[2] - poi[2].z) ** 2) < 2).length,
},
density: (actualCount / 20).toFixed(2), // per sq meter
congestion_zones: actualCount > 3 ? ['display'] : [],
},
customer_flow: {
entries: Math.floor(t / 25) + 1,
exits: Math.floor(t / 25),
dwell_avg_s: 145 + _harmonicNoise(t, 17.1, 2) * 20,
},
},
});
}
// ========================================================================
// 9. Search & Rescue — scanning, false positives, triangulation, gradual lock-on
// ========================================================================
_searchRescue(t) {
const n = _getNoiseBank('search_rescue');
// Timeline:
// 0-4: scanning phase (signal sweeps, no detection)
// 4-7: first false positive (ghost echo)
// 7-10: second scan, another brief false positive
// 10-14: genuine signal detected, gradual lock-on
// 14-20: confirmed detection, vital extraction (confidence building)
// 20+: stable monitoring
const scanning = t < 4;
const falsePos1 = t >= 4 && t < 7;
const scan2 = t >= 7 && t < 10;
const falsePos2 = t >= 7 && t < 8.5;
const genuineDetect = t >= 10;
const lockingOn = t >= 10 && t < 14;
const confirmed = t >= 14;
const stableMonitor = t >= 20;
// Scan sweep effect (nodes cycle through angles)
const scanAngle = t * 0.8;
// Triangulation: 3 sensor nodes with different signal strengths
const targetPos = [3.5, 0, 0];
const nodePositions = [[2, 0, 1.5], [-2, 0, 1.5], [0, 0, -2]];
const nodes = [];
for (let ni = 0; ni < 3; ni++) {
const npos = nodePositions[ni];
const dist = Math.sqrt((npos[0] - targetPos[0]) ** 2 + (npos[2] - targetPos[2]) ** 2);
const baseSignal = -62 - dist * 3; // signal attenuation with distance
const scanMod = scanning ? Math.sin(scanAngle + ni * 2.1) * 4 : 0;
const falseSignal = (falsePos1 && ni === 0) ? Math.sin((t - 4) * 3) * 3 : 0;
const genuineSignal = genuineDetect ? _smoothstep(10, 14, t) * 5 : 0;
const amp = new Float32Array(64);
for (let i = 0; i < 64; i++) {
amp[i] = 0.08 + (genuineDetect ? 0.06 * _smoothstep(10, 16, t) : 0)
* Math.sin(t * 0.4 + i * 0.15 + ni)
+ _harmonicNoise(t, i * 0.12 + ni * 100, 2) * 0.008;
}
nodes.push({
node_id: ni + 1,
rssi_dbm: baseSignal + scanMod + falseSignal + genuineSignal + n.rssi(t) * 0.5,
position: npos,
amplitude: amp,
subcarrier_count: 64,
distance_estimate: genuineDetect ? (dist + (1 - _smoothstep(10, 20, t)) * 3).toFixed(2) : null,
});
}
// Confidence builds gradually during lock-on
let confidence;
if (scanning) confidence = 0.08 + Math.abs(n.motion(t)) * 0.05;
else if (falsePos1) confidence = 0.25 + Math.sin((t - 4) * 2) * 0.15; // fluctuating
else if (scan2 && !falsePos2) confidence = 0.1;
else if (falsePos2) confidence = 0.2 + Math.sin((t - 7) * 3) * 0.1;
else if (lockingOn) confidence = 0.2 + _smoothstep(10, 14, t) * 0.3;
else if (confirmed && !stableMonitor) confidence = 0.5 + _smoothstep(14, 20, t) * 0.2;
else confidence = 0.7 + n.env(t) * 0.03;
// Vital sign extraction: gradual confidence over 10+ seconds after detection
const vitalConfidence = confirmed ? _smoothstep(14, 25, t) : 0;
const breathRate = genuineDetect ? 10 + _harmonicNoise(t, 3.3, 2) * 0.5 : 0;
const breathPhase = Math.sin(2 * Math.PI * (breathRate / 60) * t);
// Detected persons
let detected = false;
let triageColor = 'unknown';
if (falsePos1) { detected = true; triageColor = 'unknown'; }
else if (falsePos2) { detected = true; triageColor = 'unknown'; }
else if (genuineDetect) { detected = true; triageColor = confidence > 0.5 ? 'yellow' : 'unknown'; }
const persons = detected && genuineDetect
? [{ id: 'p0', position: targetPos, motion_score: 2, pose: 'lying', facing: 0, signal_strength: confidence }]
: (detected ? [{ id: 'ghost', position: [falsePos1 ? 1 : -1, 0, falsePos2 ? 2 : -1], motion_score: 1, pose: 'unknown', facing: 0 }] : []);
return this._baseFrame({
nodes,
features: {
mean_rssi: nodes[0].rssi_dbm,
variance: genuineDetect ? 0.4 + breathPhase * 0.1 * vitalConfidence : 0.15,
std: 0.63,
motion_band_power: 0.01 + (scanning ? Math.abs(Math.sin(scanAngle)) * 0.02 : 0),
breathing_band_power: genuineDetect ? 0.05 * vitalConfidence + breathPhase * 0.02 * vitalConfidence : 0.005,
dominant_freq_hz: genuineDetect && vitalConfidence > 0.3 ? 0.167 : (scanning ? 0.8 : 0.02),
spectral_power: 0.04 + (scanning ? 0.03 : 0),
},
classification: {
motion_level: genuineDetect ? 'present_still' : 'absent',
presence: detected,
confidence,
through_wall: true,
triage_color: triageColor,
false_positive: (falsePos1 || falsePos2) && !genuineDetect,
scan_phase: scanning ? 'sweeping' : (lockingOn ? 'locking_on' : (confirmed ? 'confirmed' : 'searching')),
},
signal_field: { grid_size: [20, 1, 20], values: this._searchRescueField(t, scanning, genuineDetect, confidence) },
vital_signs: {
breathing_rate_bpm: genuineDetect && vitalConfidence > 0.2 ? breathRate : 0,
heart_rate_bpm: genuineDetect && vitalConfidence > 0.4 ? 55 + n.hr(t) * 2 : 0,
breathing_confidence: genuineDetect ? vitalConfidence * 0.85 : 0,
heart_rate_confidence: genuineDetect ? vitalConfidence * 0.5 : 0,
},
persons,
estimated_persons: detected ? 1 : 0,
edge_modules: {
wifi_mat: {
mode: scanning ? 'scanning' : (lockingOn ? 'locking_on' : (confirmed ? 'monitoring' : 'search')),
survivors_detected: genuineDetect ? 1 : 0,
triage: genuineDetect && confidence > 0.5 ? 'delayed' : 'searching',
signal_through_material: 'concrete_30cm',
false_positives_filtered: (falsePos1 || falsePos2) ? 1 : 0,
triangulation_nodes: genuineDetect ? 3 : 0,
vital_extraction_confidence: (vitalConfidence * 100).toFixed(0) + '%',
},
},
});
}
// ========================================================================
// 10. Elderly Care — gait asymmetry, gradual transitions, rest & recover
// ========================================================================
_elderlyCare(t) {
const n = _getNoiseBank('elderly_care');
// Timeline:
// 0-12: walking with gait analysis
// 12-14: slowing down
// 14-16: reaching for chair (transitional)
// 16-18: sitting transition
// 18-24: resting (HR comes down)
// 24+: light activity while seated
const walkPhase = t < 12;
const slowingDown = t >= 12 && t < 14;
const reachingChair = t >= 14 && t < 16;
const sittingTransition = t >= 16 && t < 18;
const resting = t >= 18 && t < 24;
const seated = t >= 18;
// Walking speed decreases gradually
const walkSpeed = walkPhase ? 0.6 - _smoothstep(8, 12, t) * 0.2 : (slowingDown ? 0.3 * (1 - _smoothstep(12, 14, t)) : 0);
// Gait analysis: slight asymmetry in step timing (right step ~5% longer)
const stepFreq = walkPhase ? 1.4 + _harmonicNoise(t, 1.1, 2) * 0.05 : 0;
const stepPhaseR = Math.sin(2 * Math.PI * stepFreq * t);
const stepPhaseL = Math.sin(2 * Math.PI * stepFreq * t + Math.PI + 0.15); // asymmetry
const stepAsymmetry = Math.abs(stepPhaseR) - Math.abs(stepPhaseL);
// Position
let px, pz, facing, ms, pose;
if (walkPhase) {
const wp = t * walkSpeed;
px = Math.sin(wp * 0.25) * 2;
pz = Math.cos(wp * 0.15) * 1.2;
facing = Math.atan2(Math.cos(wp * 0.25) * 0.25 * walkSpeed, -Math.sin(wp * 0.15) * 0.15 * walkSpeed);
ms = 60 + stepAsymmetry * 10;
pose = 'walking';
} else if (slowingDown) {
const sp = _smoothstep(12, 14, t);
px = _lerp(Math.sin(12 * 0.6 * 0.25) * 2, 1, sp);
pz = _lerp(Math.cos(12 * 0.6 * 0.15) * 1.2, -1.5, sp);
facing = Math.atan2(1 - px, -1.5 - pz);
ms = 30 * (1 - sp);
pose = 'walking';
} else if (reachingChair) {
const rp = _smoothstep(14, 16, t);
px = 1 + Math.sin(t * 2) * 0.05 * (1 - rp); // slight unsteadiness reaching
pz = -1.5;
facing = Math.PI * 0.25;
ms = 20 * (1 - rp);
pose = 'reaching';
} else if (sittingTransition) {
const sp = _smoothstep(16, 18, t);
px = 1;
pz = -1.5;
facing = Math.PI * 0.25;
ms = 15 * (1 - sp);
pose = sp > 0.5 ? 'sitting' : 'reaching';
} else {
px = 1 + n.pos1(t) * 0.02;
pz = -1.5 + n.pos2(t) * 0.02;
facing = Math.PI * 0.25 + _harmonicNoise(t, 5.5, 2) * 0.1;
ms = 5 + Math.abs(n.motion(t)) * 3;
pose = 'sitting';
}
// Heart rate: walking ~82, elevated slightly from walking exertion,
// then gradually comes down during rest (physiological recovery)
let hrBase;
if (walkPhase) hrBase = 82 + walkSpeed * 5;
else if (slowingDown || reachingChair) hrBase = 78 - _smoothstep(12, 16, t) * 5;
else if (resting) hrBase = 73 - _smoothstep(18, 24, t) * 5; // slow recovery
else hrBase = 68;
hrBase += n.hr(t) * 1.5;
// Breathing: correlated with HR
let breathRate;
if (walkPhase) breathRate = 18 + walkSpeed * 3;
else if (seated) breathRate = 14 - _smoothstep(18, 24, t) * 2;
else breathRate = 16;
breathRate += n.breath(t) * 0.5;
// Blood pressure proxy: HR/breathing correlation
const hrBreathRatio = hrBase / breathRate;
const amplitude = new Float32Array(64);
for (let i = 0; i < 64; i++) {
amplitude[i] = 0.3 + (walkPhase ? 0.15 : 0.06) * Math.sin(t * 0.8 + i * 0.12)
+ stepAsymmetry * 0.02
+ _harmonicNoise(t, i * 0.11, 2) * 0.008;
}
return this._baseFrame({
nodes: [{ node_id: 1, rssi_dbm: -41 + Math.sin(t * 0.5) * 2 + n.rssi(t) * 0.5, position: [2, 0, 1.5], amplitude, subcarrier_count: 64 }],
features: {
mean_rssi: -41 + Math.sin(t * 0.5) * 2 + n.rssi(t) * 0.5,
variance: walkPhase ? 2.2 + stepAsymmetry * 0.5 : 0.8,
std: walkPhase ? 1.48 : 0.89,
motion_band_power: walkPhase ? 0.15 + Math.abs(stepPhaseR) * 0.05 : (ms > 10 ? 0.05 : 0.02),
breathing_band_power: 0.1 + Math.abs(n.breath(t)) * 0.02,
dominant_freq_hz: walkPhase ? stepFreq * 0.5 : 0.23,
spectral_power: 0.22,
},
classification: { motion_level: walkPhase ? 'active' : (ms > 15 ? 'active' : 'present_still'), presence: true, confidence: 0.88 },
signal_field: { grid_size: [20, 1, 20], values: this._presenceField(10 + px * 2, 10 + pz * 2, 2.5, t) },
vital_signs: {
breathing_rate_bpm: breathRate,
heart_rate_bpm: hrBase,
breathing_confidence: 0.82,
heart_rate_confidence: 0.78,
hr_breath_ratio: hrBreathRatio.toFixed(2),
},
persons: [{ id: 'p0', position: [px, 0, pz], motion_score: ms, pose, facing }],
estimated_persons: 1,
edge_modules: {
gait_analysis: {
step_frequency: walkPhase ? stepFreq.toFixed(2) : 0,
stride_length_m: walkPhase ? (0.48 + _harmonicNoise(t, 7.7, 2) * 0.02).toFixed(3) : 0,
symmetry: walkPhase ? (0.82 + stepAsymmetry * 0.1).toFixed(3) : null,
asymmetry_side: walkPhase ? 'right_longer' : null,
fall_risk: walkPhase ? 'low' : 'none',
},
vital_trend: {
status: 'normal',
hr_trend: resting ? 'recovering' : (walkPhase ? 'elevated' : 'stable'),
recovery_phase: resting,
bp_proxy: hrBreathRatio > 5.5 ? 'elevated' : 'normal',
},
pattern_sequence: {
activity: walkPhase ? 'walking' : (reachingChair || sittingTransition ? 'transitioning' : 'resting'),
transition: reachingChair ? 'reaching_chair' : (sittingTransition ? 'sitting_down' : (slowingDown ? 'slowing' : null)),
routine_deviation: false,
},
},
});
}
// ========================================================================
// 11. Fitness Tracking — warm-up, intensity ramp, rest intervals, HR lag
// ========================================================================
_fitnessTracking(t) {
const n = _getNoiseBank('fitness_tracking');
// Timeline:
// 0-3: warm-up (slow movements, gradually increasing)
// 3-9: jumping jacks (high intensity)
// 9-12: rest interval
// 12-18: squats (medium intensity)
// 18-21: rest interval
// 21-27: jumping jacks again (peak intensity)
// 27-30: cool-down
const block = t % 30;
let exerciseType = 'rest';
let targetIntensity = 0; // 0-1 target exertion
let actualMotion = 0;
if (block < 3) {
// Warm-up: ramp from 0 to 0.4
exerciseType = 'warmup';
targetIntensity = _smoothstep(0, 3, block) * 0.4;
actualMotion = targetIntensity * 0.8;
} else if (block < 9) {
// Jumping jacks
exerciseType = 'jumping_jacks';
targetIntensity = 0.7 + _smoothstep(3, 5, block) * 0.2;
// Rhythmic motion with 2 Hz cadence
actualMotion = targetIntensity * (0.7 + 0.3 * Math.abs(Math.sin(t * Math.PI * 2)));
} else if (block < 12) {
// Rest
exerciseType = 'rest';
targetIntensity = 0.1 * (1 - _smoothstep(9, 11, block));
actualMotion = 0.05 + Math.abs(n.motion(t)) * 0.03; // slight fidgeting
} else if (block < 18) {
// Squats: slower, deeper movement
exerciseType = 'squats';
targetIntensity = 0.6 + _smoothstep(12, 14, block) * 0.15;
// Slower cadence (~0.5 Hz), smooth up/down
const squatPhase = Math.sin(t * Math.PI * 0.5);
actualMotion = targetIntensity * (0.5 + 0.5 * Math.abs(squatPhase));
} else if (block < 21) {
// Rest
exerciseType = 'rest';
targetIntensity = 0.1 * (1 - _smoothstep(18, 20, block));
actualMotion = 0.05;
} else if (block < 27) {
// Jumping jacks peak
exerciseType = 'jumping_jacks';
targetIntensity = 0.85 + _smoothstep(21, 23, block) * 0.15;
actualMotion = targetIntensity * (0.7 + 0.3 * Math.abs(Math.sin(t * Math.PI * 2.2)));
} else {
// Cool-down
exerciseType = 'cooldown';
targetIntensity = 0.3 * (1 - _smoothstep(27, 30, block));
actualMotion = targetIntensity * 0.5;
}
// HR lags behind exertion by ~5-8 seconds (physiological delay)
// Simulate with a slow-tracking variable
const hrTarget = 70 + targetIntensity * 90; // 70 rest -> 160 max
// IIR-filtered HR that follows target with delay
const hrLagFactor = 0.92; // higher = more lag
const hrDelayed = hrTarget + (70 - hrTarget) * Math.exp(-t * 0.15) * (exerciseType === 'rest' ? 0.5 : 0.2);
// Use harmonic noise to approximate the lag behavior in a stateless way
const hrSmooth = hrTarget - _harmonicNoise(t - 3, 8.8, 2) * 5 * targetIntensity;
const hrRate = _clamp(hrSmooth + n.hr(t) * 2, 60, 185);
// Breathing also lags but less
const breathTarget = 14 + targetIntensity * 20;
const breathRate = _clamp(breathTarget + n.breath(t) * 1.5 - _harmonicNoise(t - 1, 9.9, 2) * 2, 12, 40);
const repCount = Math.floor(t * (exerciseType === 'jumping_jacks' ? 1.0 : (exerciseType === 'squats' ? 0.25 : 0)));
// Vertical motion for exercises
const verticalPos = exerciseType === 'jumping_jacks'
? Math.abs(Math.sin(t * Math.PI * 2)) * 0.15
: (exerciseType === 'squats' ? -Math.abs(Math.sin(t * Math.PI * 0.5)) * 0.3 : 0);
const amplitude = new Float32Array(64);
for (let i = 0; i < 64; i++) {
amplitude[i] = 0.35 + 0.3 * actualMotion * Math.abs(Math.sin(t * 2.5 + i * 0.25))
+ _harmonicNoise(t, i * 0.15, 2) * 0.01;
}
const ms = _clamp(actualMotion * 255, 5, 255);
return this._baseFrame({
nodes: [{ node_id: 1, rssi_dbm: -39 + actualMotion * 6 + n.rssi(t), position: [2, 0, 1.5], amplitude, subcarrier_count: 64 }],
features: {
mean_rssi: -39 + actualMotion * 6 + n.rssi(t),
variance: 2 + actualMotion * 4,
std: 1.4 + actualMotion * 1.5,
motion_band_power: 0.05 + actualMotion * 0.55,
breathing_band_power: 0.08 + targetIntensity * 0.1,
dominant_freq_hz: exerciseType === 'jumping_jacks' ? 2.0 : (exerciseType === 'squats' ? 0.5 : 0.2),
spectral_power: 0.1 + actualMotion * 0.5,
},
classification: { motion_level: actualMotion > 0.3 ? 'active' : (actualMotion > 0.1 ? 'present_still' : 'present_still'), presence: true, confidence: 0.8 },
signal_field: { grid_size: [20, 1, 20], values: this._presenceField(10, 10, 2.5 + actualMotion * 1.5, t) },
vital_signs: {
breathing_rate_bpm: breathRate,
heart_rate_bpm: hrRate,
breathing_confidence: 0.7,
heart_rate_confidence: 0.65,
hr_zone: hrRate > 155 ? 'anaerobic' : (hrRate > 130 ? 'threshold' : (hrRate > 110 ? 'aerobic' : 'warmup')),
},
persons: [{
id: 'p0',
position: [0 + n.pos1(t) * 0.05, verticalPos, 0 + n.pos2(t) * 0.05],
motion_score: ms,
pose: exerciseType === 'rest' || exerciseType === 'cooldown' ? 'standing' : 'exercising',
facing: 0,
exerciseType,
exercisePhase: exerciseType === 'jumping_jacks' ? 'high_cadence' : (exerciseType === 'squats' ? 'controlled' : 'recovery'),
}],
estimated_persons: 1,
edge_modules: {
breathing_sync: { cadence: breathRate.toFixed(1), sync_quality: actualMotion > 0.5 ? 0.7 : 0.85 },
gesture: { type: exerciseType !== 'rest' && exerciseType !== 'cooldown' ? 'exercise_rep' : 'none', count: repCount },
vital_trend: {
status: hrRate > 170 ? 'warning' : (hrRate > 140 ? 'elevated' : 'normal'),
hr_zone: hrRate > 155 ? 'anaerobic' : (hrRate > 130 ? 'threshold' : (hrRate > 110 ? 'aerobic' : (hrRate > 90 ? 'warmup' : 'resting'))),
hr_lag_s: exerciseType === 'rest' ? 'recovering' : 'tracking',
intensity: (targetIntensity * 100).toFixed(0) + '%',
workout_phase: exerciseType,
},
},
});
}
// ========================================================================
// 12. Security Patrol — checkpoint pauses, speed variation, anomaly buildup
// ========================================================================
_securityPatrol(t) {
const n = _getNoiseBank('security_patrol');
// Patrol route: rectangular with checkpoint pauses at corners
const patrolSpeed = 0.18; // slightly slower for realism
const rawPatrolT = (t * patrolSpeed) % 1; // 0..1 around route
// Checkpoint pauses: guard slows/pauses at each corner (0.25, 0.5, 0.75, 1.0)
// Remap rawPatrolT to account for pauses
const cornerDuration = 0.04; // proportion of circuit spent pausing at each corner
let patrolT = rawPatrolT;
let atCheckpoint = false;
let checkpointCorner = -1;
const corners = [0, 0.25, 0.5, 0.75];
for (let ci = 0; ci < 4; ci++) {
const c = corners[ci];
const next = ci < 3 ? corners[ci + 1] : 1;
if (rawPatrolT >= c && rawPatrolT < c + cornerDuration) {
patrolT = c;
atCheckpoint = true;
checkpointCorner = ci;
break;
}
}
// Speed variation: faster on long stretches, slower near corners
let px, pz, facing;
if (patrolT < 0.25) {
const p = patrolT / 0.25;
px = -3 + p * 6; pz = -2; facing = 0;
} else if (patrolT < 0.5) {
const p = (patrolT - 0.25) / 0.25;
px = 3; pz = -2 + p * 4; facing = Math.PI * 0.5;
} else if (patrolT < 0.75) {
const p = (patrolT - 0.5) / 0.25;
px = 3 - p * 6; pz = 2; facing = Math.PI;
} else {
const p = (patrolT - 0.75) / 0.25;
px = -3; pz = 2 - p * 4; facing = Math.PI * 1.5;
}
// At checkpoint: guard looks around (facing oscillates)
if (atCheckpoint) {
facing += Math.sin(t * 3) * 0.8; // scanning left-right
}
// Add natural movement noise
px += n.pos1(t) * 0.05;
pz += n.pos2(t) * 0.05;
const guardSpeed = atCheckpoint ? 5 : (80 + _harmonicNoise(t, 10.1, 2) * 20);
const zone = px > 0 ? (pz > 0 ? 'NE' : 'SE') : (pz > 0 ? 'NW' : 'SW');
// Anomaly: starts as faint signal, builds confidence, guard responds
const anomalyCycle = t % 25;
const anomalyFaint = anomalyCycle >= 14 && anomalyCycle < 17; // first hints
const anomalyBuilding = anomalyCycle >= 17 && anomalyCycle < 19; // confidence builds
const anomalyConfirmed = anomalyCycle >= 19 && anomalyCycle < 22; // confirmed, guard responds
const anomalyActive = anomalyFaint || anomalyBuilding || anomalyConfirmed;
let anomalyScore = 0;
if (anomalyFaint) anomalyScore = 0.15 + _smoothstep(14, 17, anomalyCycle) * 0.2;
else if (anomalyBuilding) anomalyScore = 0.35 + _smoothstep(17, 19, anomalyCycle) * 0.35;
else if (anomalyConfirmed) anomalyScore = 0.7 + _smoothstep(19, 20, anomalyCycle) * 0.15;
// Anomaly position (opposite quadrant)
const ax = -px * 0.5 + Math.sin(t * 0.3) * 0.3;
const az = -pz * 0.4 + Math.cos(t * 0.25) * 0.2;
// Guard changes path toward anomaly when confirmed
if (anomalyConfirmed) {
const redirectStrength = _smoothstep(19, 20, anomalyCycle);
px = _lerp(px, ax, redirectStrength * 0.4);
pz = _lerp(pz, az, redirectStrength * 0.4);
facing = Math.atan2(ax - px, az - pz);
}
const amplitude = new Float32Array(64);
for (let i = 0; i < 64; i++) {
amplitude[i] = 0.3 + 0.15 * Math.sin(t * 1.0 + i * 0.15)
+ _harmonicNoise(t, i * 0.13, 2) * 0.01;
}
const persons = [{
id: 'guard',
position: [px, 0, pz],
motion_score: guardSpeed,
pose: atCheckpoint ? 'standing' : (anomalyConfirmed ? 'alert' : 'walking'),
facing,
}];
if (anomalyActive) {
persons.push({
id: 'anomaly',
position: [ax, 0, az],
motion_score: anomalyFaint ? 8 : (anomalyBuilding ? 15 : 25),
pose: 'crouching',
facing: Math.atan2(px - ax, pz - az),
signal_confidence: anomalyScore,
});
}
return this._baseFrame({
nodes: [
{ node_id: 1, rssi_dbm: -40 + Math.sin(t * 0.8) * 3 + n.rssi(t) * 0.5, position: [4, 2, -4], amplitude, subcarrier_count: 64 },
{ node_id: 2, rssi_dbm: -42 + Math.sin(t * 0.6) * 2 + n.env(t) * 0.3, position: [-4, 2, 4], amplitude: new Float32Array(amplitude), subcarrier_count: 64 },
],
features: {
mean_rssi: -40 + Math.sin(t * 0.8) * 3 + n.rssi(t) * 0.5,
variance: 2.5 + Math.sin(t * 0.5) * 0.8 + (anomalyActive ? anomalyScore * 2 : 0),
std: 1.58 + anomalyScore * 0.5,
motion_band_power: atCheckpoint ? 0.03 : 0.18,
breathing_band_power: 0.06,
dominant_freq_hz: atCheckpoint ? 0.1 : 0.8,
spectral_power: 0.3 + anomalyScore * 0.2,
},
classification: {
motion_level: atCheckpoint ? 'present_still' : 'active',
presence: true,
confidence: 0.85,
anomaly_zone: anomalyActive,
anomaly_confidence: anomalyScore,
},
signal_field: {
grid_size: [20, 1, 20],
values: anomalyActive
? this._twoPresenceField(10 + px * 1.5, 10 + pz * 1.5, 10 + ax * 1.5, 10 + az * 1.5, t)
: this._presenceField(10 + px * 1.5, 10 + pz * 1.5, 2.5, t),
},
vital_signs: {
breathing_rate_bpm: 16 + n.breath(t) * 0.5,
heart_rate_bpm: 78 + (anomalyConfirmed ? 12 : 0) + n.hr(t) * 1,
breathing_confidence: 0.6,
heart_rate_confidence: 0.5,
},
persons,
estimated_persons: anomalyActive ? 2 : 1,
edge_modules: {
behavioral_profiler: {
guard_zone: zone,
coverage_pct: 72 + _harmonicNoise(t, 11.1, 2) * 3,
anomaly_score: anomalyScore,
checkpoint_active: atCheckpoint,
checkpoint_corner: checkpointCorner >= 0 ? ['SW', 'SE', 'NE', 'NW'][checkpointCorner] : null,
guard_response: anomalyConfirmed ? 'investigating' : (anomalyBuilding ? 'alerted' : 'patrolling'),
},
perimeter_breach: {
detected: anomalyScore > 0.5,
confidence: anomalyScore > 0.5 ? anomalyScore : 0,
zone: anomalyActive ? (zone === 'NE' ? 'SW' : 'NE') : 'none',
first_detected_s: anomalyFaint ? (anomalyCycle - 14).toFixed(1) : null,
buildup_phase: anomalyFaint ? 'faint' : (anomalyBuilding ? 'building' : (anomalyConfirmed ? 'confirmed' : 'none')),
},
},
});
}
// ---- Helpers ----
_flatField(base) {
const vals = [];
// Spatially coherent noise: smooth gradient + gentle ripple
for (let iz = 0; iz < 20; iz++) {
for (let ix = 0; ix < 20; ix++) {
const gradient = Math.sin(ix * 0.3) * Math.sin(iz * 0.25) * 0.01;
const ripple = _harmonicNoise(ix * 0.5 + iz * 0.7, ix + iz * 20, 2) * 0.005;
vals.push(_clamp(base + gradient + ripple, 0, 1));
}
}
return vals;
}
_presenceField(cx, cz, radius, t) {
const vals = [];
for (let iz = 0; iz < 20; iz++) {
for (let ix = 0; ix < 20; ix++) {
const dx = ix - cx, dz = iz - cz;
const d = Math.sqrt(dx * dx + dz * dz);
// Spatially coherent noise (smooth, not random per cell)
const noise = _harmonicNoise(t * 0.5 + ix * 0.4 + iz * 0.3, ix + iz * 20, 2) * 0.015;
const v = _gaussian(d, radius) * 0.7 + noise;
vals.push(_clamp(v, 0, 1));
}
}
return vals;
}
_twoPresenceField(x1, z1, x2, z2, t) {
const vals = [];
for (let iz = 0; iz < 20; iz++) {
for (let ix = 0; ix < 20; ix++) {
const d1 = Math.sqrt((ix - x1) ** 2 + (iz - z1) ** 2);
const d2 = Math.sqrt((ix - x2) ** 2 + (iz - z2) ** 2);
const v1 = _gaussian(d1, 1.7) * 0.6;
const v2 = _gaussian(d2, 1.7) * 0.55;
const noise = _harmonicNoise(t * 0.5 + ix * 0.4 + iz * 0.3, ix + iz * 20, 2) * 0.012;
vals.push(_clamp(v1 + v2 + noise, 0, 1));
}
}
return vals;
}
/** Search & rescue field with scanning sweep and gradual target lock */
_searchRescueField(t, scanning, detected, confidence) {
const vals = [];
const targetCx = 14, targetCz = 10;
for (let iz = 0; iz < 20; iz++) {
for (let ix = 0; ix < 20; ix++) {
let v = 0;
// Scan sweep (rotating beam)
if (scanning) {
const scanAngle = t * 0.8;
const cellAngle = Math.atan2(iz - 10, ix - 10);
const angleDiff = Math.abs(((cellAngle - scanAngle + Math.PI) % (2 * Math.PI)) - Math.PI);
v += _gaussian(angleDiff, 0.5) * 0.15;
}
// Target presence (gradually intensifying)
if (detected) {
const d = Math.sqrt((ix - targetCx) ** 2 + (iz - targetCz) ** 2);
v += _gaussian(d, 3.5 - confidence * 2) * confidence * 0.7;
}
// Background noise
v += _harmonicNoise(t * 0.3 + ix * 0.5 + iz * 0.6, ix + iz * 20, 2) * 0.01;
vals.push(_clamp(v, 0, 1));
}
}
return vals;
}
_generateIQ(count, scale, t) {
const iq = [];
for (let i = 0; i < count; i++) {
const phase = t * 0.5 + i * 0.2 + Math.sin(t * 0.3 + i * 0.1) * 0.5;
const amp = scale * (0.5 + 0.5 * Math.sin(t * 0.2 + i * 0.15));
iq.push({ i: amp * Math.cos(phase), q: amp * Math.sin(phase) });
}
return iq;
}
_generateVariance(count, scale, t) {
const v = new Float32Array(count);
for (let i = 0; i < count; i++) v[i] = scale * (0.3 + 0.7 * Math.abs(Math.sin(t * 0.4 + i * 0.25)));
return v;
}
_blend(a, b, alpha) {
const beta = 1 - alpha;
const result = JSON.parse(JSON.stringify(b));
if (a.features && b.features) {
for (const key of Object.keys(b.features)) {
if (typeof b.features[key] === 'number' && typeof a.features[key] === 'number')
result.features[key] = a.features[key] * beta + b.features[key] * alpha;
}
}
if (a.signal_field?.values && b.signal_field?.values) {
const len = Math.min(a.signal_field.values.length, b.signal_field.values.length);
for (let i = 0; i < len; i++) result.signal_field.values[i] = a.signal_field.values[i] * beta + b.signal_field.values[i] * alpha;
}
if (a.vital_signs && b.vital_signs) {
for (const key of Object.keys(b.vital_signs)) {
if (typeof b.vital_signs[key] === 'number' && typeof a.vital_signs[key] === 'number')
result.vital_signs[key] = a.vital_signs[key] * beta + b.vital_signs[key] * alpha;
}
}
if (a.nodes?.[0]?.amplitude && b.nodes?.[0]?.amplitude) {
const ampA = a.nodes[0].amplitude, ampB = b.nodes[0].amplitude;
const len = Math.min(ampA.length, ampB.length);
const blended = new Float32Array(len);
for (let i = 0; i < len; i++) blended[i] = ampA[i] * beta + ampB[i] * alpha;
result.nodes[0].amplitude = blended;
}
return result;
}
}