Skip to content

Commit 69a70ba

Browse files
committed
Добавлен адаптивный фильтр AverageAdaptiveValue и тесты
Реализован класс AverageAdaptiveValue для адаптивного скользящего среднего с автоматическим обнаружением резких скачков статистики и ускоренной адаптацией. Класс поддерживает сериализацию, деконструкцию, неявные преобразования, хранит min/max/интервал значений и параметры адаптации. Добавлен полный набор юнит-тестов, покрывающих все сценарии использования, включая реальные кейсы и стресс-тесты.
1 parent 80e9fb0 commit 69a70ba

2 files changed

Lines changed: 892 additions & 0 deletions

File tree

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
using System.Globalization;
2+
using System.Runtime.Serialization;
3+
using System.Security.Permissions;
4+
5+
namespace MathCore.Values;
6+
7+
/// <summary>Адаптивное скользящее среднее с обнаружением скачков статистики</summary>
8+
/// <remarks>
9+
/// Алгоритм автоматически обнаруживает резкие изменения в потоке данных и ускоряет адаптацию.
10+
/// В стационарном режиме обеспечивает плавное усреднение шумов и флуктуаций.
11+
/// </remarks>
12+
/// <example>
13+
/// Пример использования для усреднения скорости чтения файла с сетевого диска:
14+
/// <code><![CDATA[
15+
/// var speed_average = new AverageAdaptiveValue(
16+
/// BaseFactor: 0.15, // Плавное усреднение в стабильном режиме
17+
/// FastFactor: 0.5, // Быстрая адаптация при изменении скорости сети
18+
/// JumpThreshold: 3.0, // Порог обнаружения скачка (3σ)
19+
/// MinSamplesForDetection: 10 // Начать обнаружение после 10 измерений
20+
/// );
21+
///
22+
/// while (reading)
23+
/// {
24+
/// var bytes_read = stream.Read(buffer, 0, 4096);
25+
/// var speed = bytes_read / elapsed_time.TotalSeconds;
26+
/// var avg_speed = speed_average.AddValue(speed);
27+
/// Console.WriteLine($"Текущая скорость: {avg_speed:F2} Б/с");
28+
/// }
29+
/// ]]></code>
30+
///
31+
/// Пример использования для индикатора вертикальной скорости самолёта:
32+
/// <code><![CDATA[
33+
/// var vspeed_indicator = new AverageAdaptiveValue(
34+
/// BaseFactor: 0.05, // Плавное усреднение колебаний стрелки
35+
/// FastFactor: 0.6, // Быстрая реакция на смену режима полёта
36+
/// JumpThreshold: 2.5, // Чувствительность к резким изменениям
37+
/// MinSamplesForDetection: 5 // Быстрое обнаружение смены режима
38+
/// );
39+
///
40+
/// void UpdateVerticalSpeed(double current_vspeed)
41+
/// {
42+
/// var smoothed_vspeed = vspeed_indicator.AddValue(current_vspeed);
43+
/// indicator.SetValue(smoothed_vspeed);
44+
/// }
45+
/// ]]></code>
46+
/// </example>
47+
[Serializable]
48+
public class AverageAdaptiveValue : ISerializable, IValue<double>, IResettable
49+
{
50+
/* --------------------------------------------------------------------------------------------- */
51+
52+
/// <summary>Номер итерации усреднения</summary>
53+
private int _N;
54+
55+
/// <summary>Текущее значение среднего</summary>
56+
private double _Value;
57+
58+
/// <summary>Текущее значение дисперсии</summary>
59+
private double _Value2;
60+
61+
/// <summary>Начальное значение</summary>
62+
private readonly double _StartValue;
63+
64+
/// <summary>Базовый фактор сглаживания для стационарного режима</summary>
65+
private double _BaseFactor;
66+
67+
/// <summary>Фактор сглаживания при обнаружении скачка</summary>
68+
private double _FastFactor;
69+
70+
/// <summary>Порог обнаружения скачка (в количестве стандартных отклонений)</summary>
71+
private double _JumpThreshold;
72+
73+
/// <summary>Минимальное количество точек для начала обнаружения скачков</summary>
74+
private int _MinSamplesForDetection;
75+
76+
public double _Min = double.PositiveInfinity;
77+
78+
public double _Max = double.NegativeInfinity;
79+
80+
/* --------------------------------------------------------------------------------------------- */
81+
82+
/// <summary>Начальное значение</summary>
83+
public double StartValue => _StartValue;
84+
85+
/// <summary>Базовый фактор сглаживания (для стационарного режима, 0-1)</summary>
86+
public double BaseFactor
87+
{
88+
get => _BaseFactor;
89+
set => _BaseFactor = value is > 0 and < 1
90+
? value
91+
: throw new ArgumentOutOfRangeException(nameof(value), "Фактор должен быть в диапазоне (0, 1)");
92+
}
93+
94+
/// <summary>Фактор сглаживания при обнаружении скачка (0-1, должен быть больше BaseFactor)</summary>
95+
public double FastFactor
96+
{
97+
get => _FastFactor;
98+
set => _FastFactor = value is > 0 and < 1
99+
? value
100+
: throw new ArgumentOutOfRangeException(nameof(value), "Фактор должен быть в диапазоне (0, 1)");
101+
}
102+
103+
/// <summary>Порог обнаружения скачка (в количестве стандартных отклонений)</summary>
104+
public double JumpThreshold
105+
{
106+
get => _JumpThreshold;
107+
set => _JumpThreshold = value > 0
108+
? value
109+
: throw new ArgumentOutOfRangeException(nameof(value), "Порог должен быть положительным");
110+
}
111+
112+
/// <summary>Минимальное количество точек для начала обнаружения скачков</summary>
113+
public int MinSamplesForDetection
114+
{
115+
get => _MinSamplesForDetection;
116+
set => _MinSamplesForDetection = value > 0
117+
? value
118+
: throw new ArgumentOutOfRangeException(nameof(value), "Должно быть положительным");
119+
}
120+
121+
/// <summary>Текущее значение среднего</summary>
122+
public double Value { get => _Value; set => AddValue(value); }
123+
124+
/// <summary>Дисперсия значений</summary>
125+
public double Dispersion => _Value2 - _Value * _Value;
126+
127+
/// <summary>Стандартное отклонение</summary>
128+
public double StandardDeviation => Math.Sqrt(Math.Max(0, Dispersion));
129+
130+
/// <summary>Количество точек усреднения</summary>
131+
public int ValuesCount => _N;
132+
133+
/// <summary>Минимальное значение</summary>
134+
public double Min => double.IsPositiveInfinity(_Min) ? double.NaN : _Min;
135+
136+
/// <summary>Максимальное значение</summary>
137+
public double Max => double.IsNegativeInfinity(_Max) ? double.NaN : _Max;
138+
139+
/// <summary>Интервал значений [Min, Max]</summary>
140+
public Interval Interval => double.IsPositiveInfinity(_Min) || double.IsNegativeInfinity(_Max)
141+
? new()
142+
: new(_Min, _Max, true);
143+
144+
/* --------------------------------------------------------------------------------------------- */
145+
146+
/// <summary>Инициализация адаптивного скользящего среднего</summary>
147+
/// <param name="BaseFactor">Базовый фактор сглаживания (по умолчанию 0.1)</param>
148+
/// <param name="FastFactor">Быстрый фактор при скачке (по умолчанию 0.5)</param>
149+
/// <param name="JumpThreshold">Порог скачка в σ (по умолчанию 3.0)</param>
150+
/// <param name="MinSamplesForDetection">Минимум точек для обнаружения (по умолчанию 5)</param>
151+
public AverageAdaptiveValue(
152+
double BaseFactor = 0.1,
153+
double FastFactor = 0.5,
154+
double JumpThreshold = 3.0,
155+
int MinSamplesForDetection = 5)
156+
{
157+
_BaseFactor = BaseFactor is > 0 and < 1
158+
? BaseFactor
159+
: throw new ArgumentOutOfRangeException(nameof(BaseFactor), "Должен быть в диапазоне (0, 1)");
160+
161+
_FastFactor = FastFactor is > 0 and < 1
162+
? FastFactor
163+
: throw new ArgumentOutOfRangeException(nameof(FastFactor), "Должен быть в диапазоне (0, 1)");
164+
165+
_JumpThreshold = JumpThreshold > 0
166+
? JumpThreshold
167+
: throw new ArgumentOutOfRangeException(nameof(JumpThreshold), "Должен быть положительным");
168+
169+
_MinSamplesForDetection = MinSamplesForDetection > 0
170+
? MinSamplesForDetection
171+
: throw new ArgumentOutOfRangeException(nameof(MinSamplesForDetection), "Должно быть положительным");
172+
173+
_N = 0;
174+
_StartValue = double.NaN;
175+
}
176+
177+
/// <summary>Инициализация адаптивного скользящего среднего с начальным значением</summary>
178+
/// <param name="StartValue">Начальное значение</param>
179+
/// <param name="BaseFactor">Базовый фактор сглаживания (по умолчанию 0.1)</param>
180+
/// <param name="FastFactor">Быстрый фактор при скачке (по умолчанию 0.5)</param>
181+
/// <param name="JumpThreshold">Порог скачка в σ (по умолчанию 3.0)</param>
182+
/// <param name="MinSamplesForDetection">Минимум точек для обнаружения (по умолчанию 5)</param>
183+
public AverageAdaptiveValue(
184+
double StartValue,
185+
double BaseFactor = 0.1,
186+
double FastFactor = 0.5,
187+
double JumpThreshold = 3.0,
188+
int MinSamplesForDetection = 5)
189+
{
190+
_BaseFactor = BaseFactor is > 0 and < 1
191+
? BaseFactor
192+
: throw new ArgumentOutOfRangeException(nameof(BaseFactor), "Должен быть в диапазоне (0, 1)");
193+
194+
_FastFactor = FastFactor is > 0 and < 1
195+
? FastFactor
196+
: throw new ArgumentOutOfRangeException(nameof(FastFactor), "Должен быть в диапазоне (0, 1)");
197+
198+
_JumpThreshold = JumpThreshold > 0
199+
? JumpThreshold
200+
: throw new ArgumentOutOfRangeException(nameof(JumpThreshold), "Должен быть положительным");
201+
202+
_MinSamplesForDetection = MinSamplesForDetection > 0
203+
? MinSamplesForDetection
204+
: throw new ArgumentOutOfRangeException(nameof(MinSamplesForDetection), "Должно быть положительным");
205+
206+
_N = 1;
207+
_StartValue = StartValue;
208+
_Value = StartValue;
209+
_Value2 = StartValue * StartValue;
210+
_Min = StartValue;
211+
_Max = StartValue;
212+
}
213+
214+
/* --------------------------------------------------------------------------------------------- */
215+
216+
/// <summary>Добавить значение к усреднению с адаптивной реакцией на скачки</summary>
217+
/// <param name="Value">Добавляемое значение</param>
218+
/// <returns>Новое значение среднего</returns>
219+
public double AddValue(double Value)
220+
{
221+
// Обновление min/max
222+
if (Value > _Max)
223+
_Max = Value;
224+
225+
if (Value < _Min)
226+
_Min = Value;
227+
228+
if (_N < 1)
229+
{
230+
// Первое значение - инициализация
231+
_Value = Value;
232+
_Value2 = Value * Value;
233+
_N = 1;
234+
return _Value;
235+
}
236+
237+
// Определение фактора сглаживания
238+
var factor = _BaseFactor;
239+
240+
// Обнаружение скачка, если накоплено достаточно данных
241+
if (_N >= _MinSamplesForDetection)
242+
{
243+
var std_dev = StandardDeviation;
244+
245+
if (std_dev > 1e-10) // Проверка, что дисперсия не нулевая
246+
{
247+
var deviation = Math.Abs(Value - _Value);
248+
var normalized_deviation = deviation / std_dev;
249+
250+
// Если отклонение превышает порог - используем быстрый фактор
251+
if (normalized_deviation > _JumpThreshold)
252+
factor = _FastFactor;
253+
}
254+
}
255+
256+
// Обновление среднего и дисперсии по формуле экспоненциального сглаживания
257+
_Value = (1 - factor) * _Value + factor * Value;
258+
_Value2 = (1 - factor) * _Value2 + factor * Value * Value;
259+
_N++;
260+
261+
return _Value;
262+
}
263+
264+
/// <summary>Добавить значение к усреднению</summary>
265+
/// <param name="Value">Добавляемое значение</param>
266+
public void Add(double Value) => AddValue(Value);
267+
268+
/// <summary>Сбросить состояние</summary>
269+
public void Reset()
270+
{
271+
_Value2 = 0;
272+
273+
if (double.IsNaN(_StartValue))
274+
{
275+
_N = 0;
276+
_Value = 0;
277+
}
278+
else
279+
{
280+
_N = 1;
281+
_Value = _StartValue;
282+
_Value2 = _StartValue * _StartValue;
283+
}
284+
285+
_Min = double.PositiveInfinity;
286+
_Max = double.NegativeInfinity;
287+
}
288+
289+
/* --------------------------------------------------------------------------------------------- */
290+
291+
/// <summary>Преобразование в строку</summary>
292+
/// <returns>Текстовое представление</returns>
293+
public override string ToString() => _Value.ToString(CultureInfo.CurrentCulture);
294+
295+
/// <summary>Преобразование в строку с форматированием</summary>
296+
/// <param name="Format">Формат</param>
297+
/// <returns>Текстовое представление</returns>
298+
public string ToString(string Format) => _Value.ToString(Format);
299+
300+
/* --------------------------------------------------------------------------------------------- */
301+
302+
/// <summary>Деконструкция на среднее и дисперсию</summary>
303+
/// <param name="Mean">Среднее значение</param>
304+
/// <param name="Variance">Дисперсия</param>
305+
public void Deconstruct(out double Mean, out double Variance)
306+
{
307+
Mean = _Value;
308+
Variance = Dispersion;
309+
}
310+
311+
/* --------------------------------------------------------------------------------------------- */
312+
313+
/// <summary>Оператор неявного приведения к типу вещественного числа</summary>
314+
/// <param name="Value">Усредняемое значение</param>
315+
public static implicit operator double(AverageAdaptiveValue Value) => Value.Value;
316+
317+
/// <summary>Оператор неявного приведения вещественного числа к адаптивному среднему</summary>
318+
/// <param name="Data">Вещественное число</param>
319+
public static implicit operator AverageAdaptiveValue(double Data) => new(StartValue: Data);
320+
321+
/// <summary>Оператор неявного приведения к интервалу</summary>
322+
/// <param name="Value">Усредняемое значение</param>
323+
public static implicit operator Interval(AverageAdaptiveValue Value) => Value.Interval;
324+
325+
/* --------------------------------------------------------------------------------------------- */
326+
327+
#region ISerializable Members
328+
329+
/// <summary>Новая адаптивная усредняемая величина (десериализация)</summary>
330+
/// <param name="Info">Сериализационная информация</param>
331+
/// <param name="Context">Контекст сериализации</param>
332+
protected AverageAdaptiveValue(SerializationInfo Info, StreamingContext Context)
333+
{
334+
ArgumentNullException.ThrowIfNull(Info);
335+
336+
_Value = Info.GetDouble("Value");
337+
_Value2 = Info.GetDouble("Value2");
338+
_N = Info.GetInt32("N");
339+
_StartValue = Info.GetDouble("StartValue");
340+
_BaseFactor = Info.GetDouble("BaseFactor");
341+
_FastFactor = Info.GetDouble("FastFactor");
342+
_JumpThreshold = Info.GetDouble("JumpThreshold");
343+
_MinSamplesForDetection = Info.GetInt32("MinSamplesForDetection");
344+
_Min = Info.GetDouble("Min");
345+
_Max = Info.GetDouble("Max");
346+
}
347+
348+
/// <inheritdoc />
349+
#if !NET8_0_OR_GREATER
350+
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
351+
#endif
352+
void ISerializable.GetObjectData(SerializationInfo Info, StreamingContext Context)
353+
{
354+
ArgumentNullException.ThrowIfNull(Info);
355+
GetObjectData(Info, Context);
356+
}
357+
358+
/// <summary>Получить состояние объекта</summary>
359+
/// <param name="Info">Объект сериализации</param>
360+
/// <param name="Context">Контекст операции сериализации</param>
361+
/// <exception cref="ArgumentNullException">Если <paramref name="Info"/> is null</exception>
362+
#if !NET8_0_OR_GREATER
363+
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
364+
#endif
365+
// ReSharper disable once UnusedParameter.Global
366+
protected virtual void GetObjectData(SerializationInfo Info, StreamingContext Context)
367+
{
368+
ArgumentNullException.ThrowIfNull(Info);
369+
370+
Info.AddValue("Value", _Value);
371+
Info.AddValue("Value2", _Value2);
372+
Info.AddValue("N", _N);
373+
Info.AddValue("StartValue", _StartValue);
374+
Info.AddValue("BaseFactor", _BaseFactor);
375+
Info.AddValue("FastFactor", _FastFactor);
376+
Info.AddValue("JumpThreshold", _JumpThreshold);
377+
Info.AddValue("MinSamplesForDetection", _MinSamplesForDetection);
378+
Info.AddValue("Min", _Min);
379+
Info.AddValue("Max", _Max);
380+
}
381+
382+
#endregion
383+
384+
/* --------------------------------------------------------------------------------------------- */
385+
}

0 commit comments

Comments
 (0)