/** * 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; } }