diff options
| author | grothedev <grothedev@gmail.com> | 2026-05-29 21:49:20 -0400 |
|---|---|---|
| committer | grothedev <grothedev@gmail.com> | 2026-05-29 21:49:20 -0400 |
| commit | 6196004b51a6850909c154f5402ff4858eab479a (patch) | |
| tree | 126b8bb1600d0a656e0df016e25d08c390f3540e /src/test-data-generators.js | |
| parent | 27dc5849c3eaf4824d79938e7077abdbe2c82e24 (diff) | |
mv web stuff to root project dirHEADprototypeframeworkmain
Diffstat (limited to 'src/test-data-generators.js')
| -rw-r--r-- | src/test-data-generators.js | 530 |
1 files changed, 530 insertions, 0 deletions
diff --git a/src/test-data-generators.js b/src/test-data-generators.js new file mode 100644 index 0000000..02bc0ad --- /dev/null +++ b/src/test-data-generators.js @@ -0,0 +1,530 @@ +/** + * Test Data Generators - Classes for generating fake/test data patterns + * + * These generators produce various types of synthetic data for testing + * and visualizing the waterfall graphs with realistic patterns. + */ + +/** + * Base class for all data generators + */ +class DataGenerator { + constructor(config = {}) { + this.sampleRate = config.sampleRate || 100; // Samples per second + this.amplitude = config.amplitude || 1.0; + this.offset = config.offset || 0.0; + this.time = 0; + } + + /** + * Generate a single sample at the current time + * @returns {number} The generated value + */ + sample() { + throw new Error('sample() must be implemented by subclass'); + } + + /** + * Generate an array of samples + * @param {number} count - Number of samples to generate + * @returns {Array<number>} Array of generated values + */ + generateSamples(count) { + const samples = []; + for (let i = 0; i < count; i++) { + samples.push(this.sample()); + this.time += 1 / this.sampleRate; + } + return samples; + } + + /** + * Generate a line of points for waterfall display + * @param {number} pointCount - Number of points in the line + * @param {number} width - Width of the display area + * @returns {Array<{x: number, y: number}>} Array of points + */ + generateLine(pointCount, width) { + const points = []; + const samples = this.generateSamples(pointCount); + + for (let i = 0; i < pointCount; i++) { + const x = (i / pointCount) * width; + const y = samples[i] * this.amplitude + this.offset; + points.push({ x, y }); + } + + return points; + } + + reset() { + this.time = 0; + } +} + +/** + * Sine Wave Generator - Classic sinusoidal wave + */ +export class SineWaveGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.frequency = config.frequency || 1.0; // Hz + this.phase = config.phase || 0.0; // Radians + } + + sample() { + const value = Math.sin(2 * Math.PI * this.frequency * this.time + this.phase); + return value; + } +} + +/** + * Square Wave Generator - Digital-style square wave + */ +export class SquareWaveGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.frequency = config.frequency || 1.0; + this.dutyCycle = config.dutyCycle || 0.5; // 0.0 to 1.0 + } + + sample() { + const period = 1 / this.frequency; + const phase = (this.time % period) / period; + return phase < this.dutyCycle ? 1.0 : -1.0; + } +} + +/** + * Sawtooth Wave Generator - Linear ramp wave + */ +export class SawtoothWaveGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.frequency = config.frequency || 1.0; + } + + sample() { + const period = 1 / this.frequency; + const phase = (this.time % period) / period; + return 2 * phase - 1; // -1 to 1 + } +} + +/** + * Triangle Wave Generator - Linear up/down wave + */ +export class TriangleWaveGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.frequency = config.frequency || 1.0; + } + + sample() { + const period = 1 / this.frequency; + const phase = (this.time % period) / period; + return phase < 0.5 + ? 4 * phase - 1 + : 3 - 4 * phase; + } +} + +/** + * White Noise Generator - Random noise + */ +export class WhiteNoiseGenerator extends DataGenerator { + sample() { + return Math.random() * 2 - 1; // -1 to 1 + } +} + +/** + * Pink Noise Generator - 1/f noise (more realistic than white noise) + */ +export class PinkNoiseGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + // Paul Kellet's refined method + this.b0 = 0; + this.b1 = 0; + this.b2 = 0; + this.b3 = 0; + this.b4 = 0; + this.b5 = 0; + this.b6 = 0; + } + + sample() { + const white = Math.random() * 2 - 1; + this.b0 = 0.99886 * this.b0 + white * 0.0555179; + this.b1 = 0.99332 * this.b1 + white * 0.0750759; + this.b2 = 0.96900 * this.b2 + white * 0.1538520; + this.b3 = 0.86650 * this.b3 + white * 0.3104856; + this.b4 = 0.55000 * this.b4 + white * 0.5329522; + this.b5 = -0.7616 * this.b5 - white * 0.0168980; + const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362; + this.b6 = white * 0.115926; + return pink * 0.11; // Normalize + } +} + +/** + * Perlin Noise Generator - Smooth, continuous noise + */ +export class PerlinNoiseGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.frequency = config.frequency || 1.0; + this.octaves = config.octaves || 4; + this.persistence = config.persistence || 0.5; + } + + // Simple 1D Perlin-like noise + noise(x) { + const i = Math.floor(x); + const f = x - i; + + // Fade curve + const u = f * f * (3 - 2 * f); + + // Hash function for pseudo-random gradients + const hash = (n) => { + n = (n << 13) ^ n; + return (1.0 - ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0); + }; + + return (1 - u) * hash(i) + u * hash(i + 1); + } + + sample() { + let value = 0; + let amplitude = 1; + let frequency = this.frequency; + let maxValue = 0; + + for (let i = 0; i < this.octaves; i++) { + value += this.noise(this.time * frequency) * amplitude; + maxValue += amplitude; + amplitude *= this.persistence; + frequency *= 2; + } + + return value / maxValue; + } +} + +/** + * Pulse/Spike Generator - Random spikes/pulses + */ +export class PulseGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.pulseRate = config.pulseRate || 0.05; // Probability per sample + this.pulseWidth = config.pulseWidth || 0.01; // Duration in seconds + this.pulseAmplitude = config.pulseAmplitude || 1.0; + this.currentPulse = null; + } + + sample() { + // Check if we're in a pulse + if (this.currentPulse) { + if (this.time >= this.currentPulse.endTime) { + this.currentPulse = null; + } else { + return this.pulseAmplitude; + } + } + + // Random chance to start new pulse + if (Math.random() < this.pulseRate) { + this.currentPulse = { + startTime: this.time, + endTime: this.time + this.pulseWidth, + }; + return this.pulseAmplitude; + } + + return 0; + } +} + +/** + * Burst Generator - Bursts of activity with quiet periods + */ +export class BurstGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.burstDuration = config.burstDuration || 1.0; // Seconds + this.quietDuration = config.quietDuration || 2.0; // Seconds + this.burstFrequency = config.burstFrequency || 5.0; // Hz during burst + this.currentState = 'quiet'; + this.stateStartTime = 0; + } + + sample() { + const elapsed = this.time - this.stateStartTime; + + // State transitions + if (this.currentState === 'quiet' && elapsed >= this.quietDuration) { + this.currentState = 'burst'; + this.stateStartTime = this.time; + } else if (this.currentState === 'burst' && elapsed >= this.burstDuration) { + this.currentState = 'quiet'; + this.stateStartTime = this.time; + } + + // Generate value based on state + if (this.currentState === 'burst') { + return Math.sin(2 * Math.PI * this.burstFrequency * this.time); + } else { + return 0; + } + } +} + +/** + * Chirp Generator - Frequency sweep signal + */ +export class ChirpGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.startFreq = config.startFreq || 0.5; // Hz + this.endFreq = config.endFreq || 10.0; // Hz + this.duration = config.duration || 5.0; // Seconds + } + + sample() { + const t = this.time % this.duration; + const progress = t / this.duration; + const freq = this.startFreq + (this.endFreq - this.startFreq) * progress; + return Math.sin(2 * Math.PI * freq * t); + } +} + +/** + * Composite Generator - Combine multiple generators + */ +export class CompositeGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.generators = config.generators || []; + this.weights = config.weights || this.generators.map(() => 1.0); + } + + sample() { + let sum = 0; + let weightSum = 0; + + for (let i = 0; i < this.generators.length; i++) { + sum += this.generators[i].sample() * this.weights[i]; + weightSum += this.weights[i]; + } + + return weightSum > 0 ? sum / weightSum : 0; + } + + generateSamples(count) { + const samples = []; + for (let i = 0; i < count; i++) { + samples.push(this.sample()); + // Advance all child generators + this.generators.forEach(gen => gen.time += 1 / gen.sampleRate); + } + return samples; + } +} + +/** + * FM (Frequency Modulation) Generator - One signal modulates another + */ +export class FMGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.carrierFreq = config.carrierFreq || 5.0; // Hz + this.modulatorFreq = config.modulatorFreq || 0.5; // Hz + this.modulationIndex = config.modulationIndex || 2.0; + } + + sample() { + const modulator = Math.sin(2 * Math.PI * this.modulatorFreq * this.time); + const instantFreq = this.carrierFreq + this.modulationIndex * modulator; + return Math.sin(2 * Math.PI * instantFreq * this.time); + } +} + +/** + * Exponential Decay Generator - Exponentially decaying signal + */ +export class ExponentialDecayGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.decayRate = config.decayRate || 1.0; // 1/seconds + this.oscillationFreq = config.oscillationFreq || 5.0; // Hz + } + + sample() { + const envelope = Math.exp(-this.decayRate * this.time); + const oscillation = Math.sin(2 * Math.PI * this.oscillationFreq * this.time); + return envelope * oscillation; + } +} + +/** + * Step Function Generator - Random walk / brownian motion + */ +export class RandomWalkGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.stepSize = config.stepSize || 0.1; + this.currentValue = 0; + this.bounds = config.bounds || { min: -5, max: 5 }; + } + + sample() { + // Random step + const step = (Math.random() - 0.5) * this.stepSize; + this.currentValue += step; + + // Apply bounds + this.currentValue = Math.max(this.bounds.min, Math.min(this.bounds.max, this.currentValue)); + + return this.currentValue; + } +} + +// ============================================================================ +// Example Usage and Presets +// ============================================================================ + +/** + * Factory function to create common test scenarios + */ +export class TestDataFactory { + static createSimpleSine(amplitude = 30) { + return new SineWaveGenerator({ + frequency: 2.0, + amplitude: amplitude, + sampleRate: 100, + }); + } + + static createNoisySine(amplitude = 30) { + const sine = new SineWaveGenerator({ + frequency: 2.0, + amplitude: amplitude * 0.8, + sampleRate: 100, + }); + + const noise = new WhiteNoiseGenerator({ + amplitude: amplitude * 0.2, + sampleRate: 100, + }); + + return new CompositeGenerator({ + generators: [sine, noise], + weights: [1.0, 1.0], + }); + } + + static createComplexPattern(amplitude = 30) { + const low = new SineWaveGenerator({ + frequency: 0.5, + amplitude: amplitude * 0.4, + sampleRate: 100, + }); + + const mid = new SineWaveGenerator({ + frequency: 3.0, + amplitude: amplitude * 0.3, + sampleRate: 100, + }); + + const high = new SineWaveGenerator({ + frequency: 8.0, + amplitude: amplitude * 0.2, + sampleRate: 100, + }); + + const noise = new PinkNoiseGenerator({ + amplitude: amplitude * 0.1, + sampleRate: 100, + }); + + return new CompositeGenerator({ + generators: [low, mid, high, noise], + weights: [1.0, 1.0, 1.0, 1.0], + }); + } + + static createBurstySignal(amplitude = 30) { + return new BurstGenerator({ + amplitude: amplitude, + burstDuration: 0.5, + quietDuration: 1.5, + burstFrequency: 10.0, + sampleRate: 100, + }); + } + + static createSmoothNoise(amplitude = 30) { + return new PerlinNoiseGenerator({ + amplitude: amplitude, + frequency: 2.0, + octaves: 3, + persistence: 0.5, + sampleRate: 100, + }); + } + + static createFrequencySweep(amplitude = 30) { + return new ChirpGenerator({ + amplitude: amplitude, + startFreq: 0.5, + endFreq: 10.0, + duration: 3.0, + sampleRate: 100, + }); + } + + static createModulatedSignal(amplitude = 30) { + return new FMGenerator({ + amplitude: amplitude, + carrierFreq: 5.0, + modulatorFreq: 0.3, + modulationIndex: 3.0, + sampleRate: 100, + }); + } + + static createRandomWalk(amplitude = 30) { + return new RandomWalkGenerator({ + stepSize: 0.5, + bounds: { min: -amplitude, max: amplitude }, + sampleRate: 100, + }); + } +} + +/** + * Example: How to use with WaterfallGraph + * + * // Create a generator + * const generator = TestDataFactory.createComplexPattern(30); + * + * // In your graph's addLine method: + * addLine(time, graphIdx) { + * const line = { + * points: generator.generateLine(this.pointsPerLine, this.width), + * yOffset: 0, + * color: this.generateColor(time), + * }; + * this.lines.push(line); + * } + * + * // Or generate custom samples: + * const samples = generator.generateSamples(100); + * const points = samples.map((y, i) => ({ + * x: (i / samples.length) * width, + * y: y + * })); + */ |
