Livt packages

Livt.Signal

Version 0.1.1

Q1.15 sample streams, filters, windows, rate conversion, FFTs, oscillators, and mixers for Livt.

Published files (80)

tests/transform/SequentialFftQ15Fixture.lvt

namespace Livt.Signal.Tests.Transform

using Livt.Signal.Transform

/**
 * Emits one fixed complex Q1.15 frame while honoring backpressure.
 */
component ComplexQ15VectorSource<COUNT: int>
{
	stream: IComplexSampleStream<logic[16]>
	realValues: int[COUNT]
	imagValues: int[COUNT]
	index: int
	public accepted: int

	/**
	 * Binds a borrowed stream and fixed real and imaginary vectors.
	 */
	new(stream: flip IComplexSampleStream<logic[16]>,
		realValues: const int[COUNT], imagValues: const int[COUNT])
	{
		this.stream = stream
		this.realValues = realValues
		this.imagValues = imagValues
	}

	/**
	 * Emits the frame with one idle cycle between accepted tokens.
	 */
	process Send()
	{
		state Emit {
			stream.real = realValues[index] as logic[16]
			stream.imag = imagValues[index] as logic[16]
			stream.valid = true
			stream.first = index == 0
			stream.last = index == COUNT - 1
			goto AwaitAccept
		}

		state AwaitAccept {
			if (stream.ready) {
				stream.valid = false
				accepted++

				if (index == COUNT - 1) {
					goto Idle
				} else {
					index++
					goto Gap
				}
			}
		}

		state Gap {
			stream.valid = false
			stream.first = false
			stream.last = false
			goto Emit
		}

		state Idle {
			stream.valid = false
			stream.first = false
			stream.last = false
			goto Idle
		}
	}
}

/**
 * Retains one bounded complex output frame for contract assertions.
 */
component ComplexQ15VectorObserver<COUNT: int>
{
	stream: IComplexSampleStream<logic[16]>
	realValues: logic[COUNT, 16]
	imagValues: logic[COUNT, 16]

	public ready: in bool
	public valid: out bool
	public accepted: int
	public firstSeen: bool
	public lastSeen: bool

	/**
	 * Binds the borrowed output stream.
	 */
	new(stream: IComplexSampleStream<logic[16]>)
	{
		this.stream = stream
	}

	/**
	 * Drives capacity and exposes retained output validity.
	 */
	process Observe[]()
	{
		stream.ready = this.context.rst == 0b0 && ready
		valid = stream.valid
	}

	/**
	 * Captures each accepted complex output and its generated markers.
	 */
	process Capture()
	{
		state {
			if (stream.valid && stream.ready) {
				realValues[accepted] = stream.real
				imagValues[accepted] = stream.imag

				if (accepted == 0 && stream.first) {
					firstSeen = true
				}

				if (accepted == COUNT - 1 && stream.last) {
					lastSeen = true
				}

				accepted++
			}
		}
	}

	/**
	 * Returns one retained real component as a signed raw Q1.15 value.
	 */
	public fn GetReal(index: int) int
	{
		return realValues[index] as int
	}

	/**
	 * Returns one retained imaginary component as a signed raw Q1.15 value.
	 */
	public fn GetImag(index: int) int
	{
		return imagValues[index] as int
	}
}

/**
 * Fixed-vector harness for one sequential FFT or IFFT configuration.
 */
component SequentialFftQ15Fixture<COUNT: int,
	DIRECTION: FftDirection = FftDirection.Forward,
	STAGE_SHIFT_MASK: int = -1,
	INPUT_ORDER: FftOrder = FftOrder.Natural,
	OUTPUT_ORDER: FftOrder = FftOrder.Natural>
{
	inputStream: IComplexSampleStream<logic[16]>
	outputStream: IComplexSampleStream<logic[16]>
	source: ComplexQ15VectorSource<COUNT>
	transform: SequentialFftQ15<COUNT, DIRECTION, STAGE_SHIFT_MASK,
		INPUT_ORDER, OUTPUT_ORDER>
	observer: ComplexQ15VectorObserver<COUNT>

	public outputReady: in bool
	public outputValid: out bool
	public sourceAccepted: out int
	public accepted: out int
	public firstSeen: out bool
	public lastSeen: out bool
	public frameLengthError: out bool

	/**
	 * Constructs the fixed source, transform, and retained observer.
	 */
	new(realValues: const int[COUNT], imagValues: const int[COUNT])
	{
		source = new ComplexQ15VectorSource<COUNT>(inputStream,
			realValues, imagValues)
		transform = new SequentialFftQ15<COUNT, DIRECTION,
			STAGE_SHIFT_MASK, INPUT_ORDER, OUTPUT_ORDER>(inputStream,
			outputStream)
		observer = new ComplexQ15VectorObserver<COUNT>(outputStream)
	}

	/**
	 * Connects fixture controls and observations.
	 */
	process Connect[]()
	{
		observer.ready = outputReady
		outputValid = observer.valid
		sourceAccepted = source.accepted
		accepted = observer.accepted
		firstSeen = observer.firstSeen
		lastSeen = observer.lastSeen
		frameLengthError = transform.frameLengthError
	}

	/**
	 * Returns one retained real output component.
	 */
	public fn GetReal(index: int) int
	{
		return observer.GetReal(index)
	}

	/**
	 * Returns one retained imaginary output component.
	 */
	public fn GetImag(index: int) int
	{
		return observer.GetImag(index)
	}

	/**
	 * Returns the published transform stage count.
	 */
	public fn GetStages() int
	{
		return transform.STAGES
	}

	/**
	 * Returns the published transform butterfly count.
	 */
	public fn GetButterflyCount() int
	{
		return transform.BUTTERFLY_COUNT
	}
}

/**
 * Generates one complex impulse frame of a compile-time length.
 */
component ComplexQ15ImpulseSource<SAMPLES: int>
{
	stream: IComplexSampleStream<logic[16]>
	index: int

	/**
	 * Binds the generated impulse to a borrowed producer endpoint.
	 */
	new(stream: flip IComplexSampleStream<logic[16]>)
	{
		this.stream = stream
	}

	/**
	 * Generates one natural-order impulse frame.
	 */
	process Generate()
	{
		state Emit {
			if (index == 0) {
				stream.real = 16384 as logic[16]
			} else {
				stream.real = 0x0000
			}

			stream.imag = 0x0000
			stream.valid = true
			stream.first = index == 0
			stream.last = index == SAMPLES - 1
			goto AwaitAccept
		}

		state AwaitAccept {
			if (stream.ready) {
				stream.valid = false

				if (index == SAMPLES - 1) {
					goto Idle
				} else {
					index++
					goto Emit
				}
			}
		}

		state Idle {
			stream.valid = false
			goto Idle
		}
	}
}

/**
 * Checks every complex impulse output against its normalized expected value.
 */
component ComplexQ15ImpulseObserver<SAMPLES: int>
{
	stream: IComplexSampleStream<logic[16]>

	public ready: in bool
	public accepted: int
	public mismatches: int
	public firstSeen: bool
	public lastSeen: bool

	/**
	 * Binds the checker to a borrowed consumer endpoint.
	 */
	new(stream: IComplexSampleStream<logic[16]>)
	{
		this.stream = stream
	}

	/**
	 * Drives consumer capacity without adding a registered handshake boundary.
	 */
	process Observe[]()
	{
		stream.ready = this.context.rst == 0b0 && ready
	}

	/**
	 * Checks normalized impulse bins and output frame markers.
	 */
	process Check()
	{
		state {
			if (stream.valid && stream.ready) {
				if ((stream.real as int) != 16384 / SAMPLES
					|| (stream.imag as int) != 0) {
					mismatches++
				}

				if (accepted == 0 && stream.first) {
					firstSeen = true
				}

				if (accepted == SAMPLES - 1 && stream.last) {
					lastSeen = true
				}

				accepted++
			}
		}
	}
}

/**
 * Generates an impulse and checks every output against its normalized value.
 */
component SequentialFftImpulseFixture<SAMPLES: int>
{
	inputStream: IComplexSampleStream<logic[16]>
	outputStream: IComplexSampleStream<logic[16]>
	source: ComplexQ15ImpulseSource<SAMPLES>
	transform: SequentialFftQ15<SAMPLES>
	observer: ComplexQ15ImpulseObserver<SAMPLES>

	public ready: in bool
	public accepted: out int
	public mismatches: out int
	public firstSeen: out bool
	public lastSeen: out bool

	/**
	 * Owns one default transform between generated and checked endpoints.
	 */
	new()
	{
		source = new ComplexQ15ImpulseSource<SAMPLES>(inputStream)
		transform = new SequentialFftQ15<SAMPLES>(inputStream, outputStream)
		observer = new ComplexQ15ImpulseObserver<SAMPLES>(outputStream)
	}

	/**
	 * Connects checker capacity and observations.
	 */
	process Connect[]()
	{
		observer.ready = ready
		accepted = observer.accepted
		mismatches = observer.mismatches
		firstSeen = observer.firstSeen
		lastSeen = observer.lastSeen
	}
}

/**
 * Emits one early-terminated frame followed by one valid impulse frame.
 */
component MalformedThenImpulseSource
{
	stream: IComplexSampleStream<logic[16]>
	frame: int
	index: int

	/**
	 * Binds the recovery sequence to a borrowed producer endpoint.
	 */
	new(stream: flip IComplexSampleStream<logic[16]>)
	{
		this.stream = stream
	}

	/**
	 * Generates a three-token malformed frame and then a valid eight-token frame.
	 */
	process Send()
	{
		state Emit {
			if (frame == 1 && index == 0) {
				stream.real = 16384 as logic[16]
			} else {
				stream.real = 0x0000
			}

			stream.imag = 0x0000
			stream.valid = true
			stream.first = index == 0
			stream.last = (frame == 0 && index == 2)
				|| (frame == 1 && index == 7)
			goto AwaitAccept
		}

		state AwaitAccept {
			if (stream.ready) {
				stream.valid = false

				if (frame == 0 && index == 2) {
					frame = 1
					index = 0
					goto Gap
				} elif (frame == 1 && index == 7) {
					goto Idle
				} else {
					index++
					goto Gap
				}
			}
		}

		state Gap {
			stream.valid = false
			stream.first = false
			stream.last = false
			goto Emit
		}

		state Idle {
			stream.valid = false
			goto Idle
		}
	}
}

/**
 * Checks malformed-frame reporting and recovery at the next first marker.
 */
component SequentialFftRecoveryFixture
{
	inputStream: IComplexSampleStream<logic[16]>
	outputStream: IComplexSampleStream<logic[16]>
	source: MalformedThenImpulseSource
	transform: SequentialFftQ15<8>
	observer: ComplexQ15ImpulseObserver<8>

	public ready: in bool
	public accepted: out int
	public mismatches: out int
	public errorSeen: bool

	/**
	 * Owns the malformed source, transform, and recovery checker.
	 */
	new()
	{
		source = new MalformedThenImpulseSource(inputStream)
		transform = new SequentialFftQ15<8>(inputStream, outputStream)
		observer = new ComplexQ15ImpulseObserver<8>(outputStream)
	}

	/**
	 * Connects output capacity and retained observations.
	 */
	process Connect[]()
	{
		observer.ready = ready
		accepted = observer.accepted
		mismatches = observer.mismatches
	}

	/**
	 * Retains the transform's one-cycle malformed-frame diagnostic.
	 */
	process WatchError()
	{
		state {
			if (transform.frameLengthError) {
				errorSeen = true
			}
		}
	}
}

/**
 * Composes a normalized forward FFT with an unscaled inverse FFT.
 */
component SequentialFftRoundTripFixture
{
	const TONE_REAL: int[8] = [8192, 5793, 0, -5793,
		-8192, -5793, 0, 5793]
	const TONE_IMAG: int[8] = [0, 5793, 8192, 5793,
		0, -5793, -8192, -5793]

	inputStream: IComplexSampleStream<logic[16]>
	frequencyStream: IComplexSampleStream<logic[16]>
	outputStream: IComplexSampleStream<logic[16]>
	source: ComplexQ15VectorSource<8>
	forward: SequentialFftQ15<8>
	inverse: SequentialFftQ15<8, FftDirection.Inverse, 0>
	observer: ComplexQ15VectorObserver<8>

	public ready: in bool
	public accepted: out int

	/**
	 * Owns the source, both transforms, and retained output observer.
	 */
	new()
	{
		source = new ComplexQ15VectorSource<8>(inputStream,
			TONE_REAL, TONE_IMAG)
		forward = new SequentialFftQ15<8>(inputStream, frequencyStream)
		inverse = new SequentialFftQ15<8, FftDirection.Inverse, 0>(
			frequencyStream, outputStream)
		observer = new ComplexQ15VectorObserver<8>(outputStream)
	}

	/**
	 * Connects output capacity and accepted-result count.
	 */
	process Connect[]()
	{
		observer.ready = ready
		accepted = observer.accepted
	}

	/**
	 * Returns one retained real round-trip component.
	 */
	public fn GetReal(index: int) int
	{
		return observer.GetReal(index)
	}

	/**
	 * Returns one retained imaginary round-trip component.
	 */
	public fn GetImag(index: int) int
	{
		return observer.GetImag(index)
	}
}