using System.Collections; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// A high-performance circular buffer for double values optimized for SIMD operations. /// Uses pinned memory and maintains running sum for O(1) average calculations. /// /// /// Key characteristics: /// - Fixed capacity set at construction /// - Pinned memory for SIMD compatibility /// - O(1) Add and Sum operations via running sum /// - SIMD-accelerated Min/Max operations /// - Direct span access when buffer is contiguous /// - Thread-unsafe for maximum performance /// [SkipLocalsInit] public sealed class RingBuffer : IEnumerable { private readonly double[] _buffer; private int _head; private int _count; private double _sum; private int _savedHead; private int _savedCount; private double _savedSum; private double _savedValue; /// /// Immutable snapshot token for multi-buffer scenarios. /// Allows capturing and restoring buffer state without using the built-in single snapshot. /// [StructLayout(LayoutKind.Auto)] public readonly record struct SnapshotToken(int Head, int Count, double Sum, double Value); /// /// Creates a new RingBuffer with the specified capacity. /// Uses pinned memory for SIMD compatibility. /// /// Maximum number of elements (must be > 0) public RingBuffer(int capacity) { if (capacity <= 0) { throw new ArgumentException("Capacity must be greater than 0", nameof(capacity)); } Capacity = capacity; _buffer = GC.AllocateArray(capacity, pinned: true); _head = 0; _count = 0; _sum = 0; } /// /// Maximum number of elements the buffer can hold. /// public int Capacity { get; } /// /// Current number of elements in the buffer. /// public int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count; } /// /// True if the buffer is full (Count == Capacity). /// public bool IsFull { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count == Capacity; } /// /// Running sum of all elements in the buffer. /// O(1) operation using maintained running sum. /// public double Sum { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _sum; } /// /// Recalculates the sum by iterating over all elements using SIMD acceleration. /// Useful for correcting floating-point drift after many updates. /// Uses GetSequencedSpans to avoid allocation when buffer wraps. /// public double RecalculateSum() { GetSequencedSpans(out var first, out var second); _sum = SumSpanSimd(first) + SumSpanSimd(second); return _sum; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static double SumSpanSimd(ReadOnlySpan span) { if (span.IsEmpty) { return 0.0; } int vectorSize = Vector.Count; var acc = Vector.Zero; int i = 0; if (span.Length >= vectorSize) { ref double spanRef = ref MemoryMarshal.GetReference(span); for (; i <= span.Length - vectorSize; i += vectorSize) { acc += Unsafe.As>(ref Unsafe.Add(ref spanRef, i)); } } // Horizontal sum of SIMD accumulator double sum = 0.0; for (int j = 0; j < vectorSize; j++) { sum += acc[j]; } // Scalar tail for (; i < span.Length; i++) { sum += span[i]; } return sum; } /// /// Average of all elements in the buffer. /// Returns if buffer is empty. /// public double Average { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count > 0 ? _sum / _count : double.NaN; } /// /// Gets the newest (most recently added) value. /// Returns double.NaN if buffer is empty. /// public double Newest { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { if (_count == 0) { return double.NaN; } int idx = (_head - 1 + Capacity) % Capacity; return _buffer[idx]; } } /// /// Gets the oldest value in the buffer. /// Returns double.NaN if buffer is empty. /// public double Oldest { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { if (_count == 0) { return double.NaN; } int start = _count == Capacity ? _head : 0; return _buffer[start]; } } /// /// Gets the index in the internal buffer where the oldest element is located. /// public int StartIndex { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count == Capacity ? _head : 0; } /// /// Gets a read-only span over the internal buffer array for direct SIMD access. /// public ReadOnlySpan InternalBuffer { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _buffer.AsSpan(); } /// /// Adds a value to the buffer. /// If full, the oldest value is overwritten and its value is subtracted from the sum. /// Returns the value that was removed (0 if buffer was not full). /// /// Value to add /// The removed oldest value, or 0 if buffer was not full [MethodImpl(MethodImplOptions.AggressiveInlining)] public double Add(double value) { double removed = 0; if (_count == Capacity) { removed = _buffer[_head]; _sum = Math.FusedMultiplyAdd(-1.0, removed, _sum + value); } else { _count++; _sum += value; } _buffer[_head] = value; _head = (_head + 1) % Capacity; return removed; } /// /// Adds a value with support for bar correction semantics. /// /// Value to add /// True for new bar, false for update to current bar [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(double value, bool isNew) { if (isNew || _count == 0) { Add(value); } else { UpdateNewest(value); } } /// /// Updates the newest (most recently added) value. /// This is used for bar correction (isNew=false semantics). /// /// New value to replace the newest [MethodImpl(MethodImplOptions.AggressiveInlining)] public void UpdateNewest(double value) { if (_count == 0) { return; } int idx = (_head - 1 + Capacity) % Capacity; double oldValue = _buffer[idx]; _sum = Math.FusedMultiplyAdd(-1.0, oldValue, _sum + value); _buffer[idx] = value; } /// /// Gets or sets element at the specified index (0 = oldest, Count-1 = newest). /// Supports negative indexing via Index type. /// public double this[Index index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => GetAt(index); [MethodImpl(MethodImplOptions.AggressiveInlining)] set => SetAt(index, value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetAt(Index index) { int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value; ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count, nameof(index)); int start = _count == Capacity ? _head : 0; int bufferIdx = (start + actualIndex) % Capacity; return _buffer[bufferIdx]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetAt(Index index, double value) { int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value; ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count, nameof(index)); int start = _count == Capacity ? _head : 0; int bufferIdx = (start + actualIndex) % Capacity; double oldValue = _buffer[bufferIdx]; _sum = Math.FusedMultiplyAdd(-1.0, oldValue, _sum + value); _buffer[bufferIdx] = value; } /// /// Returns a span over the buffer contents. /// If buffer is contiguous, returns direct span (SIMD-friendly). /// If wrapped, returns span over a copy. /// /// /// Allocation Warning: When the buffer wraps around (i.e., when data spans /// from the end of the internal array back to the beginning), this method allocates a new /// array via to return contiguous data. For allocation-free iteration /// over wrapped buffers, use instead. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan GetSpan() { if (_count == 0) { return ReadOnlySpan.Empty; } int start = _count == Capacity ? _head : 0; if (start + _count <= Capacity) { return new ReadOnlySpan(_buffer, start, _count); } return new ReadOnlySpan(ToArray()); } /// /// Returns a span over the entire internal buffer (for advanced SIMD use). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan GetInternalSpan() => _buffer.AsSpan(); /// /// Gets the two sequential spans that make up the buffer contents in chronological order (Oldest to Newest). /// The first segment of data. /// The second segment of data (empty if buffer is contiguous). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void GetSequencedSpans(out ReadOnlySpan first, out ReadOnlySpan second) { if (_count == 0) { first = default; second = default; return; } int start = _count == Capacity ? _head : 0; int firstLen = Math.Min(_count, Capacity - start); first = new ReadOnlySpan(_buffer, start, firstLen); second = _count > firstLen ? new ReadOnlySpan(_buffer, 0, _count - firstLen) : default; } /// /// Returns the maximum value in the buffer using SIMD acceleration. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public double Max() { if (_count == 0) { return double.NaN; } return MaxSimd(); } /// /// Returns the minimum value in the buffer using SIMD acceleration. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public double Min() { if (_count == 0) { return double.NaN; } return MinSimd(); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double MaxSimd() { GetSequencedSpans(out var first, out var second); var vectorSize = Vector.Count; var maxVector = new Vector(double.MinValue); double max = double.MinValue; // Process first span with SIMD int i = 0; if (first.Length >= vectorSize) { ref double firstRef = ref MemoryMarshal.GetReference(first); for (; i <= first.Length - vectorSize; i += vectorSize) { maxVector = Vector.Max(maxVector, Unsafe.As>(ref Unsafe.Add(ref firstRef, i))); } } // Scalar remainder of first span for (; i < first.Length; i++) { max = Math.Max(max, first[i]); } // Process second span with SIMD (if wrapped) i = 0; if (second.Length >= vectorSize) { ref double secondRef = ref MemoryMarshal.GetReference(second); for (; i <= second.Length - vectorSize; i += vectorSize) { maxVector = Vector.Max(maxVector, Unsafe.As>(ref Unsafe.Add(ref secondRef, i))); } } // Scalar remainder of second span for (; i < second.Length; i++) { max = Math.Max(max, second[i]); } // Reduce vector to scalar for (int j = 0; j < vectorSize; j++) { max = Math.Max(max, maxVector[j]); } return max; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double MinSimd() { GetSequencedSpans(out var first, out var second); var vectorSize = Vector.Count; var minVector = new Vector(double.MaxValue); double min = double.MaxValue; // Process first span with SIMD int i = 0; if (first.Length >= vectorSize) { ref double firstRef = ref MemoryMarshal.GetReference(first); for (; i <= first.Length - vectorSize; i += vectorSize) { minVector = Vector.Min(minVector, Unsafe.As>(ref Unsafe.Add(ref firstRef, i))); } } // Scalar remainder of first span for (; i < first.Length; i++) { min = Math.Min(min, first[i]); } // Process second span with SIMD (if wrapped) i = 0; if (second.Length >= vectorSize) { ref double secondRef = ref MemoryMarshal.GetReference(second); for (; i <= second.Length - vectorSize; i += vectorSize) { minVector = Vector.Min(minVector, Unsafe.As>(ref Unsafe.Add(ref secondRef, i))); } } // Scalar remainder of second span for (; i < second.Length; i++) { min = Math.Min(min, second[i]); } // Reduce vector to scalar for (int j = 0; j < vectorSize; j++) { min = Math.Min(min, minVector[j]); } return min; } /// /// Clears all elements from the buffer. /// public void Clear() { Array.Clear(_buffer, 0, _buffer.Length); _head = 0; _count = 0; _sum = 0; } /// /// Copies the buffer elements to a new array in chronological order. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public double[] ToArray() { if (_count == 0) { return Array.Empty(); } double[] array = new double[_count]; CopyTo(array, 0); return array; } /// /// Copies elements to destination array starting at destinationIndex. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CopyTo(double[] destination, int destinationIndex) { if (_count == 0) { return; } int start = _count == Capacity ? _head : 0; if (start + _count <= Capacity) { Array.Copy(_buffer, start, destination, destinationIndex, _count); } else { int firstPartLength = Capacity - start; Array.Copy(_buffer, start, destination, destinationIndex, firstPartLength); Array.Copy(_buffer, 0, destination, destinationIndex + firstPartLength, _count - firstPartLength); } } /// /// Copies elements to a destination span in chronological order. /// Destination must have at least Count elements. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CopyTo(Span destination) { if (_count == 0) { return; } int start = _count == Capacity ? _head : 0; if (start + _count <= Capacity) { _buffer.AsSpan(start, _count).CopyTo(destination); } else { int firstPartLength = Capacity - start; _buffer.AsSpan(start, firstPartLength).CopyTo(destination); _buffer.AsSpan(0, _count - firstPartLength).CopyTo(destination.Slice(firstPartLength)); } } /// /// Creates a copy of the current state for bar correction support. /// public RingBuffer Clone() { var clone = new RingBuffer(Capacity); Array.Copy(_buffer, clone._buffer, Capacity); clone._head = _head; clone._count = _count; clone._sum = _sum; return clone; } /// /// Copies state from another RingBuffer. /// Both buffers must have the same capacity. /// public void CopyFrom(RingBuffer source) { if (source.Capacity != Capacity) { throw new ArgumentException("Source buffer must have same capacity", nameof(source)); } Array.Copy(source._buffer, _buffer, Capacity); _head = source._head; _count = source._count; _sum = source._sum; } /// /// Captures the current state of the buffer. /// Must be called BEFORE adding a new value if you intend to Restore later. /// Saves the value at _head position (which will be overwritten by the next Add). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Snapshot() { _savedHead = _head; _savedCount = _count; _savedSum = _sum; // Save the value that will be overwritten by the next Add() // When buffer is full, Add() will overwrite _buffer[_head] (the oldest value) // When buffer is not full, _buffer[_head] is undefined but we save it anyway _savedValue = _buffer[_head]; } /// /// Restores the buffer to the state captured by Snapshot. /// This restores the buffer to its state before the last Add() operation. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Restore() { _head = _savedHead; _count = _savedCount; _sum = _savedSum; // Restore the value at _head position that was saved before the Add() _buffer[_head] = _savedValue; } /// /// Creates a snapshot token that captures the current buffer state. /// Use this for multi-buffer scenarios where you need to snapshot multiple buffers atomically. /// Must be called BEFORE adding a new value if you intend to restore later. /// /// An immutable token containing the buffer state. [MethodImpl(MethodImplOptions.AggressiveInlining)] public SnapshotToken GetSnapshot() { return new SnapshotToken(_head, _count, _sum, _buffer[_head]); } /// /// Restores the buffer to the state captured in the provided snapshot token. /// Use this for multi-buffer scenarios where you need to restore multiple buffers atomically. /// /// The snapshot token to restore from. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RestoreSnapshot(SnapshotToken token) { _head = token.Head; _count = token.Count; _sum = token.Sum; _buffer[_head] = token.Value; } /// /// Returns an enumerator that iterates through the buffer in chronological order. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Enumerator GetEnumerator() => new(this); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// High-performance enumerator for the RingBuffer. /// public struct Enumerator : IEnumerator, IEquatable { private readonly RingBuffer _buffer; private readonly int _start; private readonly int _count; private int _index; private double _current; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal Enumerator(RingBuffer buffer) { _buffer = buffer; _count = buffer._count; _start = buffer._count == buffer.Capacity ? buffer._head : 0; _index = -1; _current = 0.0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { if (_index + 1 >= _count) { return false; } _index++; int bufferIdx = (_start + _index) % _buffer.Capacity; _current = _buffer._buffer[bufferIdx]; return true; } public readonly double Current => _current; readonly object IEnumerator.Current => Current; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _index = -1; _current = 0.0; } public readonly void Dispose() { } public readonly bool Equals(Enumerator other) => ReferenceEquals(_buffer, other._buffer) && _start == other._start && _count == other._count && _index == other._index && _current.Equals(other._current); public readonly override bool Equals(object? obj) => obj is Enumerator other && Equals(other); public readonly override int GetHashCode() => HashCode.Combine(RuntimeHelpers.GetHashCode(_buffer), _start, _count, _index, _current); public static bool operator ==(Enumerator left, Enumerator right) => left.Equals(right); public static bool operator !=(Enumerator left, Enumerator right) => !left.Equals(right); } }