style patterns

This commit is contained in:
Miha Kralj
2026-01-25 16:01:45 -08:00
parent 2836f253c4
commit e59665c8f0
399 changed files with 6892 additions and 1323 deletions
+14 -1
View File
@@ -52,7 +52,9 @@ public abstract class BiInputIndicatorBase : AbstractBase
protected BiInputIndicatorBase(int period, string name)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_buffer = new RingBuffer(period);
Name = name;
@@ -176,9 +178,13 @@ public abstract class BiInputIndicatorBase : AbstractBase
double error = ComputeError(actualVal, predictedVal);
if (isNew)
{
ProcessNewBar(error);
}
else
{
ProcessBarCorrection(error);
}
double mean = _buffer.Count > 0 ? _state.Sum / _buffer.Count : error;
double result = PostProcess(mean);
@@ -248,7 +254,9 @@ public abstract class BiInputIndicatorBase : AbstractBase
BiInputBatchDelegate batchMethod)
{
if (actual.Count != predicted.Count)
{
throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted));
}
int len = actual.Count;
var t = new List<long>(len);
@@ -276,8 +284,13 @@ public abstract class BiInputIndicatorBase : AbstractBase
int period)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException("All spans must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
}
}
}
+11 -3
View File
@@ -38,7 +38,9 @@ public sealed class MonotonicDeque
public MonotonicDeque(int period)
{
if (period <= 0)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0");
}
_period = period;
_deque = new int[period];
@@ -155,7 +157,10 @@ public sealed class MonotonicDeque
public void RebuildMax(double[] buffer, long currentIndex, int count)
{
Reset();
if (count == 0) return;
if (count == 0)
{
return;
}
long startLogical = currentIndex - count + 1;
for (int i = 0; i < count; i++)
@@ -176,7 +181,10 @@ public sealed class MonotonicDeque
public void RebuildMin(double[] buffer, long currentIndex, int count)
{
Reset();
if (count == 0) return;
if (count == 0)
{
return;
}
long startLogical = currentIndex - count + 1;
for (int i = 0; i < count; i++)
@@ -186,4 +194,4 @@ public sealed class MonotonicDeque
PushMin(logicalIndex, buffer[bufIdx], buffer);
}
}
}
}
+47 -10
View File
@@ -46,7 +46,9 @@ public sealed class RingBuffer : IEnumerable<double>
public RingBuffer(int capacity)
{
if (capacity <= 0)
{
throw new ArgumentException("Capacity must be greater than 0", nameof(capacity));
}
Capacity = capacity;
_buffer = GC.AllocateArray<double>(capacity, pinned: true);
@@ -130,7 +132,11 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (_count == 0) return double.NaN;
if (_count == 0)
{
return double.NaN;
}
int idx = (_head - 1 + Capacity) % Capacity;
return _buffer[idx];
}
@@ -145,7 +151,11 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (_count == 0) return double.NaN;
if (_count == 0)
{
return double.NaN;
}
int start = _count == Capacity ? _head : 0;
return _buffer[start];
}
@@ -224,7 +234,10 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UpdateNewest(double value)
{
if (_count == 0) return;
if (_count == 0)
{
return;
}
int idx = (_head - 1 + Capacity) % Capacity;
double oldValue = _buffer[idx];
@@ -283,7 +296,10 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<double> GetSpan()
{
if (_count == 0) return ReadOnlySpan<double>.Empty;
if (_count == 0)
{
return ReadOnlySpan<double>.Empty;
}
int start = _count == Capacity ? _head : 0;
@@ -332,7 +348,11 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Max()
{
if (_count == 0) return double.NaN;
if (_count == 0)
{
return double.NaN;
}
return MaxSimd();
}
@@ -342,7 +362,11 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Min()
{
if (_count == 0) return double.NaN;
if (_count == 0)
{
return double.NaN;
}
return MinSimd();
}
@@ -461,7 +485,10 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double[] ToArray()
{
if (_count == 0) return Array.Empty<double>();
if (_count == 0)
{
return Array.Empty<double>();
}
double[] array = new double[_count];
CopyTo(array, 0);
@@ -474,7 +501,10 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CopyTo(double[] destination, int destinationIndex)
{
if (_count == 0) return;
if (_count == 0)
{
return;
}
int start = _count == Capacity ? _head : 0;
@@ -497,7 +527,10 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CopyTo(Span<double> destination)
{
if (_count == 0) return;
if (_count == 0)
{
return;
}
int start = _count == Capacity ? _head : 0;
@@ -533,7 +566,9 @@ public sealed class RingBuffer : IEnumerable<double>
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;
@@ -632,7 +667,9 @@ public sealed class RingBuffer : IEnumerable<double>
public bool MoveNext()
{
if (_index + 1 >= _count)
{
return false;
}
_index++;
int bufferIdx = (_start + _index) % _buffer.Capacity;
@@ -668,4 +705,4 @@ public sealed class RingBuffer : IEnumerable<double>
public static bool operator ==(Enumerator left, Enumerator right) => left.Equals(right);
public static bool operator !=(Enumerator left, Enumerator right) => !left.Equals(right);
}
}
}
+281 -27
View File
@@ -39,11 +39,15 @@ public static class ErrorHelpers
Span<double> output)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -53,7 +57,9 @@ public static class ErrorHelpers
{
int processedCount = ComputeSignedErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
if (processedCount == len)
{
return; // All processed via SIMD
}
// Continue with scalar for remaining elements (NaN was detected)
ComputeSignedErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted);
return;
@@ -74,11 +80,15 @@ public static class ErrorHelpers
Span<double> output)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -88,7 +98,9 @@ public static class ErrorHelpers
{
int processedCount = ComputeAbsoluteErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
if (processedCount == len)
{
return; // All processed via SIMD
}
// Continue with scalar for remaining elements (NaN was detected)
ComputeAbsoluteErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted);
return;
@@ -109,11 +121,15 @@ public static class ErrorHelpers
Span<double> output)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -123,7 +139,9 @@ public static class ErrorHelpers
{
int processedCount = ComputeSquaredErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
if (processedCount == len)
{
return; // All processed via SIMD
}
// Continue with scalar for remaining elements (NaN was detected)
ComputeSquaredErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted);
return;
@@ -145,11 +163,15 @@ public static class ErrorHelpers
Span<double> output)
{
if (actual.Length != predicted.Length || actual.Length != weights.Length || actual.Length != output.Length)
{
throw new ArgumentException("All spans must have the same length", nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -165,9 +187,32 @@ public static class ErrorHelpers
double pred = predicted[i];
double wgt = weights[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(wgt)) currentValidWeight = wgt; else wgt = currentValidWeight;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
if (double.IsFinite(wgt))
{
currentValidWeight = wgt;
}
else
{
wgt = currentValidWeight;
}
double diff = act - pred;
output[i] = wgt * diff * diff;
@@ -186,11 +231,15 @@ public static class ErrorHelpers
double epsilon = 1e-10)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -227,11 +276,15 @@ public static class ErrorHelpers
double epsilon = 1e-10)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -244,8 +297,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double denominator = (Math.Abs(act) + Math.Abs(pred)) / 2.0;
output[i] = denominator < epsilon
@@ -265,11 +333,15 @@ public static class ErrorHelpers
Span<double> output)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -282,8 +354,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
// log(cosh(x)) ≈ |x| - log(2) for large |x|, numerically stable
@@ -303,11 +390,15 @@ public static class ErrorHelpers
double delta = 1.0)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -321,8 +412,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
double ratio = diff / delta;
@@ -345,11 +451,15 @@ public static class ErrorHelpers
double c = 4.685)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -363,8 +473,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
double absDiff = Math.Abs(diff);
@@ -396,11 +521,15 @@ public static class ErrorHelpers
double delta = 1.0)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -414,8 +543,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
double absDiff = Math.Abs(diff);
@@ -438,13 +582,20 @@ public static class ErrorHelpers
int resyncInterval = 1000)
{
if (errors.Length != output.Length)
{
throw new ArgumentException("Spans must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = errors.Length;
if (len == 0)
{
return;
}
double[]? rented = null;
@@ -477,7 +628,10 @@ public static class ErrorHelpers
buffer[bufferIndex] = error;
bufferIndex++;
if (bufferIndex >= period) bufferIndex = 0;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
output[i] = sum / period;
@@ -486,7 +640,11 @@ public static class ErrorHelpers
{
tickCount = 0;
double recalcSum = 0;
for (int k = 0; k < period; k++) recalcSum += buffer[k];
for (int k = 0; k < period; k++)
{
recalcSum += buffer[k];
}
sum = recalcSum;
}
}
@@ -511,13 +669,20 @@ public static class ErrorHelpers
int resyncInterval = 1000)
{
if (squaredErrors.Length != output.Length)
{
throw new ArgumentException("Spans must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = squaredErrors.Length;
if (len == 0)
{
return;
}
double[]? rented = null;
@@ -550,7 +715,10 @@ public static class ErrorHelpers
buffer[bufferIndex] = sqError;
bufferIndex++;
if (bufferIndex >= period) bufferIndex = 0;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
output[i] = Math.Sqrt(sum / period);
@@ -559,7 +727,11 @@ public static class ErrorHelpers
{
tickCount = 0;
double recalcSum = 0;
for (int k = 0; k < period; k++) recalcSum += buffer[k];
for (int k = 0; k < period; k++)
{
recalcSum += buffer[k];
}
sum = recalcSum;
}
}
@@ -586,13 +758,20 @@ public static class ErrorHelpers
int resyncInterval = 1000)
{
if (weightedSquaredErrors.Length != output.Length || weightedSquaredErrors.Length != weights.Length)
{
throw new ArgumentException("Spans must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = weightedSquaredErrors.Length;
if (len == 0)
{
return;
}
double[]? rentedErrors = null;
double[]? rentedWeights = null;
@@ -637,7 +816,10 @@ public static class ErrorHelpers
weightBuffer[bufferIndex] = wgt;
bufferIndex++;
if (bufferIndex >= period) bufferIndex = 0;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
output[i] = sumWeights > 1e-10 ? Math.Sqrt(sumErrors / sumWeights) : 0.0;
@@ -686,11 +868,15 @@ public static class ErrorHelpers
Span<double> predictedOut)
{
if (actual.Length != predicted.Length || actual.Length != actualOut.Length || actual.Length != predictedOut.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(predictedOut));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -700,8 +886,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual;
if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted;
if (double.IsFinite(act))
{
lastValidActual = act;
}
else
{
act = lastValidActual;
}
if (double.IsFinite(pred))
{
lastValidPredicted = pred;
}
else
{
pred = lastValidPredicted;
}
actualOut[i] = act;
predictedOut[i] = pred;
@@ -717,7 +918,9 @@ public static class ErrorHelpers
for (int i = 0; i < span.Length; i++)
{
if (double.IsFinite(span[i]))
{
return span[i];
}
}
return 0.0;
}
@@ -751,14 +954,18 @@ public static class ErrorHelpers
// MoveMask returns a bitmask; all-ones means all finite (mask == 0b1111 for 4 doubles)
int mask = Avx.MoveMask(combined);
if (mask != 0b1111)
{
return false;
}
}
// Scalar tail
for (int i = vectorEnd; i < len; i++)
{
if (!double.IsFinite(actual[i]) || !double.IsFinite(predicted[i]))
{
return false;
}
}
return true;
}
@@ -767,7 +974,9 @@ public static class ErrorHelpers
for (int i = 0; i < len; i++)
{
if (!double.IsFinite(actual[i]) || !double.IsFinite(predicted[i]))
{
return false;
}
}
return true;
}
@@ -890,8 +1099,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
output[i] = act - pred;
}
@@ -1099,8 +1323,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
output[i] = Math.Abs(act - pred);
}
@@ -1156,8 +1395,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
output[i] = diff * diff;
@@ -1181,4 +1435,4 @@ public static class ErrorHelpers
}
#endregion
}
}
+3 -1
View File
@@ -112,7 +112,9 @@ public class SimdExtensionsTests
{
double[] data = new double[1000];
for (int i = 0; i < data.Length; i++)
{
data[i] = i + 1.0;
}
var span = new ReadOnlySpan<double>(data);
const double expected = 1000.0 * 1001.0 / 2.0;
@@ -992,4 +994,4 @@ public class SimdScalarFallbackTests
Assert.Throws<ArgumentException>(() => SimdExtensions.Subtract(left, right, result));
}
}
}
+159 -25
View File
@@ -20,7 +20,9 @@ public static class SimdExtensions
for (int i = 0; i < span.Length; i++)
{
if (!double.IsFinite(span[i]))
{
return true;
}
}
return false;
}
@@ -30,7 +32,10 @@ public static class SimdExtensions
{
double scalar = 0.0;
for (int i = 0; i < span.Length; i++)
{
scalar += span[i];
}
return scalar;
}
@@ -38,13 +43,17 @@ public static class SimdExtensions
internal static double MinScalar(ReadOnlySpan<double> span)
{
if (span.Length == 0)
{
throw new ArgumentException("Span must not be empty", nameof(span));
}
double min = span[0];
for (int i = 1; i < span.Length; i++)
{
if (span[i] < min)
{
min = span[i];
}
}
return min;
}
@@ -53,13 +62,17 @@ public static class SimdExtensions
internal static double MaxScalar(ReadOnlySpan<double> span)
{
if (span.Length == 0)
{
throw new ArgumentException("Span must not be empty", nameof(span));
}
double max = span[0];
for (int i = 1; i < span.Length; i++)
{
if (span[i] > max)
{
max = span[i];
}
}
return max;
}
@@ -69,7 +82,9 @@ public static class SimdExtensions
{
// Match VarianceSIMD behavior: return 0.0 for length <= 1 to avoid divide-by-zero
if (span.Length <= 1)
{
return 0.0;
}
double sumSquares = 0.0;
for (int i = 0; i < span.Length; i++)
@@ -84,14 +99,23 @@ public static class SimdExtensions
internal static (double Min, double Max) MinMaxScalar(ReadOnlySpan<double> span)
{
if (span.Length == 0)
{
throw new ArgumentException("Span must not be empty", nameof(span));
}
double scalarMin = span[0];
double scalarMax = span[0];
for (int i = 1; i < span.Length; i++)
{
if (span[i] < scalarMin) scalarMin = span[i];
if (span[i] > scalarMax) scalarMax = span[i];
if (span[i] < scalarMin)
{
scalarMin = span[i];
}
if (span[i] > scalarMax)
{
scalarMax = span[i];
}
}
return (scalarMin, scalarMax);
}
@@ -105,7 +129,10 @@ public static class SimdExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ContainsNonFinite(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return false;
if (span.IsEmpty)
{
return false;
}
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
@@ -120,19 +147,25 @@ public static class SimdExtensions
// NaN check: NaN != NaN, so Vector.Equals(v, v) will be false for NaN lanes
var nanCheck = Vector.Equals(vector, vector);
if (!nanCheck.Equals(Vector<long>.AllBitsSet))
{
return true;
}
// Infinity check: |v| > MaxValue (Infinity has magnitude > MaxValue)
var absVec = Vector.Abs(vector);
var infCheck = Vector.GreaterThan(absVec, maxValue);
if (!infCheck.Equals(Vector<long>.Zero))
{
return true;
}
}
for (; i < span.Length; i++)
{
if (!double.IsFinite(span[i]))
{
return true;
}
}
return false;
@@ -151,7 +184,10 @@ public static class SimdExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double SumSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return 0.0;
if (span.IsEmpty)
{
return 0.0;
}
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
@@ -167,16 +203,22 @@ public static class SimdExtensions
double result = 0.0;
for (int j = 0; j < vectorSize; j++)
{
result += sum[j];
}
for (; i < span.Length; i++)
{
result += span[i];
}
// Lazy check: if result is non-finite AND input contained non-finite values, return NaN
// NaN + anything = NaN, Inf + anything finite = Inf
// If result is infinite from overflow (no input NaN/Inf), return as-is
if (!double.IsFinite(result) && span.ContainsNonFinite())
{
return double.NaN;
}
return result;
}
@@ -194,11 +236,21 @@ public static class SimdExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double MinSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return double.NaN;
if (span.Length == 1) return span[0];
if (span.IsEmpty)
{
return double.NaN;
}
if (span.Length == 1)
{
return span[0];
}
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return double.NaN;
if (span.ContainsNonFinite())
{
return double.NaN;
}
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
@@ -216,13 +268,17 @@ public static class SimdExtensions
for (int j = 1; j < vectorSize; j++)
{
if (minVec[j] < result)
{
result = minVec[j];
}
}
for (; i < span.Length; i++)
{
if (span[i] < result)
{
result = span[i];
}
}
return result;
@@ -239,11 +295,21 @@ public static class SimdExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double MaxSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return double.NaN;
if (span.Length == 1) return span[0];
if (span.IsEmpty)
{
return double.NaN;
}
if (span.Length == 1)
{
return span[0];
}
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return double.NaN;
if (span.ContainsNonFinite())
{
return double.NaN;
}
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
@@ -261,13 +327,17 @@ public static class SimdExtensions
for (int j = 1; j < vectorSize; j++)
{
if (maxVec[j] > result)
{
result = maxVec[j];
}
}
for (; i < span.Length; i++)
{
if (span[i] > result)
{
result = span[i];
}
}
return result;
@@ -284,7 +354,10 @@ public static class SimdExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double AverageSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return double.NaN;
if (span.IsEmpty)
{
return double.NaN;
}
// SumSIMD already guards against non-finite, which will propagate NaN
return span.SumSIMD() / span.Length;
}
@@ -300,13 +373,20 @@ public static class SimdExtensions
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
{
// Match VarianceScalar behavior: return 0.0 for length <= 1 to avoid inconsistency
if (span.Length <= 1) return 0.0;
if (span.Length <= 1)
{
return 0.0;
}
double m;
if (mean.HasValue)
{
// Mean provided externally - need explicit non-finite check
if (span.ContainsNonFinite()) return double.NaN;
if (span.ContainsNonFinite())
{
return double.NaN;
}
m = mean.Value;
}
else
@@ -317,7 +397,10 @@ public static class SimdExtensions
}
// If mean is NaN (from input NaN or explicit NaN mean), return NaN
if (!double.IsFinite(m)) return double.NaN;
if (!double.IsFinite(m))
{
return double.NaN;
}
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
@@ -335,7 +418,9 @@ public static class SimdExtensions
double result = 0.0;
for (int j = 0; j < vectorSize; j++)
{
result += sumSq[j];
}
for (; i < span.Length; i++)
{
@@ -368,11 +453,21 @@ public static class SimdExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return (double.NaN, double.NaN);
if (span.Length == 1) return (span[0], span[0]);
if (span.IsEmpty)
{
return (double.NaN, double.NaN);
}
if (span.Length == 1)
{
return (span[0], span[0]);
}
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return (double.NaN, double.NaN);
if (span.ContainsNonFinite())
{
return (double.NaN, double.NaN);
}
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
@@ -392,14 +487,28 @@ public static class SimdExtensions
double max = maxVec[0];
for (int j = 1; j < vectorSize; j++)
{
if (minVec[j] < min) min = minVec[j];
if (maxVec[j] > max) max = maxVec[j];
if (minVec[j] < min)
{
min = minVec[j];
}
if (maxVec[j] > max)
{
max = maxVec[j];
}
}
for (; i < span.Length; i++)
{
if (span[i] < min) min = span[i];
if (span[i] > max) max = span[i];
if (span[i] < min)
{
min = span[i];
}
if (span[i] > max)
{
max = span[i];
}
}
return (min, max);
@@ -416,7 +525,9 @@ public static class SimdExtensions
public static void Add(ReadOnlySpan<double> left, ReadOnlySpan<double> right, Span<double> result)
{
if (left.Length != right.Length || left.Length != result.Length)
{
throw new ArgumentException("All spans must have the same length", nameof(result));
}
int i = 0;
if (Vector.IsHardwareAccelerated && left.Length >= Vector<double>.Count)
@@ -444,7 +555,9 @@ public static class SimdExtensions
public static void Scale(ReadOnlySpan<double> source, double scalar, Span<double> result)
{
if (source.Length != result.Length)
{
throw new ArgumentException("Source and result spans must have the same length", nameof(result));
}
int i = 0;
if (Vector.IsHardwareAccelerated && source.Length >= Vector<double>.Count)
@@ -472,7 +585,9 @@ public static class SimdExtensions
public static void Subtract(ReadOnlySpan<double> left, ReadOnlySpan<double> right, Span<double> result)
{
if (left.Length != right.Length || left.Length != result.Length)
{
throw new ArgumentException("All spans must have the same length", nameof(result));
}
int i = 0;
if (Vector.IsHardwareAccelerated && left.Length >= Vector<double>.Count)
@@ -500,9 +615,14 @@ public static class SimdExtensions
public static double DotProduct(this ReadOnlySpan<double> a, ReadOnlySpan<double> b)
{
if (a.Length != b.Length)
{
throw new ArgumentException("Spans must have equal length", nameof(b));
}
if (a.IsEmpty) return 0.0;
if (a.IsEmpty)
{
return 0.0;
}
int len = a.Length;
@@ -513,19 +633,33 @@ public static class SimdExtensions
ref double bRef = ref MemoryMarshal.GetReference(b);
double sum = aRef * bRef;
if (len > 1) sum += Unsafe.Add(ref aRef, 1) * Unsafe.Add(ref bRef, 1);
if (len > 2) sum += Unsafe.Add(ref aRef, 2) * Unsafe.Add(ref bRef, 2);
if (len > 1)
{
sum += Unsafe.Add(ref aRef, 1) * Unsafe.Add(ref bRef, 1);
}
if (len > 2)
{
sum += Unsafe.Add(ref aRef, 2) * Unsafe.Add(ref bRef, 2);
}
return sum;
}
if (Avx512F.IsSupported)
{
return DotProductAvx512(a, b);
}
if (Avx2.IsSupported)
{
return DotProductAvx2(a, b);
}
if (AdvSimd.Arm64.IsSupported)
{
return DotProductNeon(a, b);
}
double s1 = 0, s2 = 0, s3 = 0, s4 = 0;
ref double ar = ref MemoryMarshal.GetReference(a);
@@ -779,4 +913,4 @@ public static class SimdExtensions
return sum;
}
}
}
+1 -1
View File
@@ -508,4 +508,4 @@ public class TBarTests
// (90 + 120 + 60) / 3 = 270 / 3 = 90
Assert.Equal(90.0, bar.OHL3);
}
}
}
+1 -1
View File
@@ -53,4 +53,4 @@ public readonly record struct TBar(long Time, double Open, double High, double L
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]";
}
}
+13 -3
View File
@@ -64,7 +64,9 @@ public struct TBarSeriesEnumerator : IEnumerator<TBar>, IEquatable<TBarSeriesEnu
public bool MoveNext()
{
if (_index + 1 >= _count)
{
return false;
}
_index++;
_current = new TBar(_t[_index], _o[_index], _h[_index], _l[_index], _c[_index], _v[_index]);
@@ -323,9 +325,14 @@ public class TBarSeries : IReadOnlyList<TBar>
{
int len = t.Length;
if (o.Length != len || h.Length != len || l.Length != len || c.Length != len || v.Length != len)
{
throw new ArgumentException("All spans must have the same length", nameof(t));
}
if (len == 0) return;
if (len == 0)
{
return;
}
int oldCount = _c.Count;
int newCount = oldCount + len;
@@ -366,7 +373,10 @@ public class TBarSeries : IReadOnlyList<TBar>
public void AddRange(ReadOnlySpan<TBar> bars)
{
int len = bars.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
int oldCount = _c.Count;
int newCount = oldCount + len;
@@ -417,4 +427,4 @@ public class TBarSeries : IReadOnlyList<TBar>
IEnumerator<TBar> IEnumerable<TBar>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
+1 -1
View File
@@ -49,4 +49,4 @@ public interface ITValuePublisher
/// </summary>
event TValuePublishedHandler? Pub;
}
#pragma warning restore MA0046
#pragma warning restore MA0046
+3 -1
View File
@@ -41,7 +41,9 @@ public struct TSeriesEnumerator : IEnumerator<TValue>, IEquatable<TSeriesEnumera
public bool MoveNext()
{
if (_index + 1 >= _count)
{
return false;
}
_index++;
_current = new TValue(_t[_index], _v[_index]);
@@ -227,4 +229,4 @@ public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
+1 -1
View File
@@ -373,4 +373,4 @@ public class TValueTests
Assert.Equal(long.MaxValue, tValue.Time);
}
}
}
+25
View File
@@ -52,7 +52,9 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable
public string ToString(string? format, IFormatProvider? formatProvider)
{
if (!string.IsNullOrEmpty(format))
{
throw new NotSupportedException($"Custom format '{format}' is not supported by TValue. Use ToString() for the default format.");
}
return ToString();
}
@@ -70,7 +72,9 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable
// This is a heuristic check; actual buffer-overflow protection is performed
// by the explicit length checks that guard each write operation below.
if (destination.Length < 24)
{
return false;
}
// Write opening bracket
destination[0] = '[';
@@ -78,12 +82,18 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable
// Format datetime: yyyy-MM-dd HH:mm:ss (19 chars)
if (!AsDateTime.TryFormat(destination.Slice(pos), out int dtChars, "yyyy-MM-dd HH:mm:ss", provider))
{
return false;
}
pos += dtChars;
// Write separator
if (pos + 2 > destination.Length)
{
return false;
}
destination[pos++] = ',';
destination[pos++] = ' ';
@@ -91,20 +101,29 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable
if (double.IsPositiveInfinity(Value))
{
if (pos + 1 > destination.Length)
{
return false;
}
destination[pos++] = (char)0x221E; // 
}
else if (double.IsNegativeInfinity(Value))
{
if (pos + 2 > destination.Length)
{
return false;
}
destination[pos++] = '-';
destination[pos++] = (char)0x221E; // -
}
else if (double.IsNaN(Value))
{
if (pos + 3 > destination.Length)
{
return false;
}
destination[pos++] = 'N';
destination[pos++] = 'a';
destination[pos++] = 'N';
@@ -112,13 +131,19 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable
else
{
if (!Value.TryFormat(destination.Slice(pos), out int valueChars, "F2", provider))
{
return false;
}
pos += valueChars;
}
// Write closing bracket
if (pos + 1 > destination.Length)
{
return false;
}
destination[pos++] = ']';
charsWritten = pos;