Skip to content

Commit 5c04cdb

Browse files
committed
Первый заход в алгоритм фазовой демодуляции
1 parent a2936ba commit 5c04cdb

3 files changed

Lines changed: 327 additions & 2 deletions

File tree

MathCore.DSP/Samples/Extensions/SampleSI16Ex.cs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace MathCore.DSP.Samples.Extensions;
1+
using System.Runtime.CompilerServices;
2+
3+
namespace MathCore.DSP.Samples.Extensions;
24

35
public static class SampleSI16Ex
46
{
@@ -9,6 +11,70 @@ public static class SampleSI16Ex
911
/// <returns>Возвращает массив вещественных значений отсчётов демодулированного сигнала</returns>
1012
public static float[] PhaseDemodulation(this Span<SampleSI16> samples, double f0, double fd)
1113
{
12-
throw new NotImplementedException();
14+
if (samples.IsEmpty) return [];
15+
if (samples.Length == 1) return [0f];
16+
17+
var result = new float[samples.Length];
18+
result[0] = 0f; // Первый отсчёт всегда 0, так как нет предыдущего для вычисления производной
19+
20+
// Вычисляем массив фаз
21+
var phases = new float[samples.Length];
22+
for (var i = 0; i < samples.Length; i++)
23+
{
24+
phases[i] = GetPhase(samples[i]);
25+
}
26+
27+
// Разворачиваем фазы (phase unwrapping)
28+
UnwrapPhases(phases);
29+
30+
// Вычисляем мгновенную частоту как производную фазы
31+
var dt = 1.0 / fd;
32+
var scale_factor = (float)(fd / (2.0 * Math.PI));
33+
34+
for (var i = 1; i < samples.Length; i++)
35+
{
36+
// Производная фазы дает мгновенную частоту в рад/с
37+
// Делим на 2π для перевода в Гц
38+
var instantaneous_frequency = (phases[i] - phases[i - 1]) * scale_factor;
39+
40+
// Вычитаем центральную частоту f0
41+
result[i] = instantaneous_frequency - (float)f0;
42+
}
43+
44+
return result;
45+
}
46+
47+
/// <summary>Быстрое вычисление фазы с использованием статических функций SampleSI16</summary>
48+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
49+
private static float GetPhase(SampleSI16 sample)
50+
{
51+
// Используем уже оптимизированную функцию GetArg из SampleSI16
52+
return SampleSI16.GetArg(sample);
53+
}
54+
55+
/// <summary>Разворачивание фаз - устранение скачков ±2π</summary>
56+
private static void UnwrapPhases(Span<float> phases)
57+
{
58+
if (phases.Length <= 1) return;
59+
60+
const float two_pi = (float)(2.0 * Math.PI);
61+
const float pi = (float)Math.PI;
62+
63+
for (var i = 1; i < phases.Length; i++)
64+
{
65+
var diff = phases[i] - phases[i - 1];
66+
67+
// Приводим разность к диапазону (-π, π]
68+
while (diff > pi)
69+
{
70+
diff -= two_pi;
71+
phases[i] -= two_pi;
72+
}
73+
while (diff <= -pi)
74+
{
75+
diff += two_pi;
76+
phases[i] += two_pi;
77+
}
78+
}
1379
}
1480
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
using MathCore.DSP.Samples;
2+
using MathCore.DSP.Samples.Extensions;
3+
4+
namespace MathCore.DSP.Tests.Samples.Extensions;
5+
6+
[TestClass]
7+
public class SampleSI16ExTests
8+
{
9+
/// <summary>Тест фазовой демодуляции для пустого массива</summary>
10+
[TestMethod]
11+
public void PhaseDemodulation_EmptyArray_ReturnsEmpty()
12+
{
13+
// Arrange
14+
var samples = Span<SampleSI16>.Empty;
15+
const double f0 = 1000.0;
16+
const double fd = 48000.0;
17+
18+
// Act
19+
var result = samples.PhaseDemodulation(f0, fd);
20+
21+
// Assert
22+
Assert.AreEqual(0, result.Length);
23+
}
24+
25+
/// <summary>Тест фазовой демодуляции для массива с одним элементом</summary>
26+
[TestMethod]
27+
public void PhaseDemodulation_SingleElement_ReturnsZero()
28+
{
29+
// Arrange
30+
var samples = new SampleSI16[] { new(100, 50) }.AsSpan();
31+
const double f0 = 1000.0;
32+
const double fd = 48000.0;
33+
34+
// Act
35+
var result = samples.PhaseDemodulation(f0, fd);
36+
37+
// Assert
38+
Assert.AreEqual(1, result.Length);
39+
Assert.AreEqual(0f, result[0]);
40+
}
41+
42+
/// <summary>Тест фазовой демодуляции для постоянного сигнала</summary>
43+
[TestMethod]
44+
public void PhaseDemodulation_ConstantSignal_ReturnsNearZeros()
45+
{
46+
// Arrange - создаем постоянный сигнал
47+
var samples = new SampleSI16[]
48+
{
49+
new(100, 0),
50+
new(100, 0),
51+
new(100, 0),
52+
new(100, 0),
53+
new(100, 0)
54+
}.AsSpan();
55+
56+
const double f0 = 1000.0;
57+
const double fd = 48000.0;
58+
59+
// Act
60+
var result = samples.PhaseDemodulation(f0, fd);
61+
62+
// Assert
63+
Assert.AreEqual(5, result.Length);
64+
Assert.AreEqual(0f, result[0]); // Первый всегда 0
65+
66+
// Для постоянного сигнала мгновенная частота должна быть близка к нулю
67+
// после вычитания центральной частоты результат должен быть близок к -f0
68+
for (var i = 1; i < result.Length; i++)
69+
Assert.IsTrue(Math.Abs(result[i] + f0) < 50,
70+
$"Sample {i}: expected ~{-f0}, got {result[i]}");
71+
}
72+
73+
/// <summary>Тест фазовой демодуляции для синусоидального сигнала с известной частотой</summary>
74+
[TestMethod]
75+
public void PhaseDemodulation_SinusoidalSignal_ReturnsExpectedFrequency()
76+
{
77+
// Arrange - создаем синусоидальный сигнал с частотой f_signal
78+
const double fd = 48000.0;
79+
const double f0 = 1000.0; // Центральная частота
80+
const double f_signal = 1500.0; // Частота сигнала
81+
const int samples_count = 200;
82+
83+
var samples = new SampleSI16[samples_count];
84+
var dt = 1.0 / fd;
85+
86+
for (var i = 0; i < samples_count; i++)
87+
{
88+
var t = i * dt;
89+
var angle = 2.0 * Math.PI * f_signal * t;
90+
var amplitude = 100.0;
91+
92+
samples[i] = new SampleSI16(
93+
(sbyte)(amplitude * Math.Cos(angle)),
94+
(sbyte)(amplitude * Math.Sin(angle))
95+
);
96+
}
97+
98+
// Act
99+
var result = samples.AsSpan().PhaseDemodulation(f0, fd);
100+
101+
// Assert
102+
Assert.AreEqual(samples_count, result.Length);
103+
Assert.AreEqual(0f, result[0]); // Первый всегда 0
104+
105+
// Проверяем, что результат близок к ожидаемой частоте (f_signal - f0)
106+
var expected_frequency = f_signal - f0;
107+
var tolerance = 100.0; // Допуск в Гц
108+
109+
// Проверяем стабильную часть сигнала (пропускаем начальные образцы)
110+
var stable_samples = 0;
111+
for (var i = 20; i < result.Length - 20; i++) // Пропускаем края для стабилизации
112+
if (Math.Abs(result[i] - expected_frequency) < tolerance)
113+
stable_samples++;
114+
115+
// Проверяем, что большинство образцов дает правильную частоту
116+
var expected_stable_count = (result.Length - 40) * 0.8; // 80% образцов должны быть стабильными
117+
Assert.IsTrue(stable_samples > expected_stable_count,
118+
$"Expected at least {expected_stable_count} stable samples, got {stable_samples}");
119+
}
120+
121+
/// <summary>Тест производительности фазовой демодуляции</summary>
122+
[TestMethod]
123+
public void PhaseDemodulation_Performance_CompletesQuickly()
124+
{
125+
// Arrange - большой массив для тестирования производительности
126+
const int samples_count = 100_000;
127+
var samples = new SampleSI16[samples_count];
128+
var random = new Random(42); // Фиксированное семя для воспроизводимости
129+
130+
for (var i = 0; i < samples_count; i++)
131+
samples[i] = new SampleSI16(
132+
(sbyte)random.Next(-128, 128),
133+
(sbyte)random.Next(-128, 128)
134+
);
135+
136+
const double f0 = 1000.0;
137+
const double fd = 48000.0;
138+
139+
// Act
140+
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
141+
var result = samples.AsSpan().PhaseDemodulation(f0, fd);
142+
stopwatch.Stop();
143+
144+
// Assert
145+
Assert.AreEqual(samples_count, result.Length);
146+
147+
// Проверяем, что выполняется достаточно быстро (менее 200 мс для 100k образцов)
148+
Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200,
149+
$"Деmodulation took {stopwatch.ElapsedMilliseconds} ms, expected < 200 ms");
150+
151+
Console.WriteLine($"Фазовая демодуляция {samples_count} образцов заняла {stopwatch.ElapsedMilliseconds} мс");
152+
}
153+
154+
/// <summary>Тест проверки правильности unwrap фазы</summary>
155+
[TestMethod]
156+
public void PhaseDemodulation_PhaseUnwrap_WorksCorrectly()
157+
{
158+
// Arrange - создаем сигнал с плавно изменяющейся фазой, но с перескоками ±2π
159+
const double fd = 1000.0;
160+
const double f0 = 0.0; // Нулевая центральная частота для простоты
161+
162+
var samples = new SampleSI16[]
163+
{
164+
new(100, 0), // фаза ≈ 0
165+
new(71, 71), // фаза ≈ π/4
166+
new(0, 100), // фаза ≈ π/2
167+
new(-71, 71), // фаза ≈ 3π/4
168+
new(-100, 0), // фаза ≈ π
169+
new(-71, -71), // фаза ≈ 5π/4 (но с unwrap должна быть ≈ -3π/4)
170+
new(0, -100), // фаза ≈ 3π/2 (но с unwrap должна быть ≈ -π/2)
171+
new(71, -71), // фаза ≈ 7π/4 (но с unwrap должна быть ≈ -π/4)
172+
}.AsSpan();
173+
174+
// Act
175+
var result = samples.PhaseDemodulation(f0, fd);
176+
177+
// Assert
178+
Assert.AreEqual(samples.Length, result.Length);
179+
Assert.AreEqual(0f, result[0]); // Первый всегда 0
180+
181+
// Проверяем, что результаты имеют разумные значения без больших скачков
182+
for (var i = 1; i < result.Length; i++)
183+
Assert.IsTrue(Math.Abs(result[i]) < 1000,
184+
$"Sample {i}: frequency {result[i]} Hz seems too high");
185+
}
186+
}

docs/PhaseDemodulation.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Фазовая демодуляция для SampleSI16
2+
3+
## Описание
4+
5+
Реализован высокопроизводительный алгоритм фазовой демодуляции радиосигнала для квадратурных отсчетов типа `SampleSI16`. Алгоритм предназначен для использования в системах реального времени.
6+
7+
## Алгоритм
8+
9+
### Входные параметры
10+
- `samples` - последовательность квадратурных отсчетов (I/Q) типа `SampleSI16`
11+
- `f0` - центральная частота фазовой модуляции (Гц)
12+
- `fd` - частота дискретизации (Гц)
13+
14+
### Выходные данные
15+
- Массив `float[]` с демодулированными значениями мгновенной частоты (Гц)
16+
17+
### Основные этапы
18+
19+
1. **Вычисление фазы** - для каждого квадратурного отсчета вычисляется фаза с помощью `atan2(Q, I)`
20+
21+
2. **Разворачивание фазы (Phase Unwrapping)** - устранение скачков ±2π для получения непрерывной фазы
22+
23+
3. **Вычисление мгновенной частоты** - как производная фазы по времени:
24+
```
25+
instantaneous_frequency = (phase[i] - phase[i-1]) * fd / (2π)
26+
```
27+
28+
4. **Вычитание центральной частоты** - для получения демодулированного сигнала:
29+
```
30+
result[i] = instantaneous_frequency - f0
31+
```
32+
33+
## Оптимизации
34+
35+
- Использование `MethodImpl(MethodImplOptions.AggressiveInlining)` для критических функций
36+
- Минимизация выделения памяти (только один выделенный массив для результата и временный массив фаз)
37+
- Использование оптимизированных функций `SampleSI16.GetArg()` для вычисления фазы
38+
- Эффективный алгоритм unwrapping фазы с линейной сложностью O(n)
39+
40+
## Производительность
41+
42+
Алгоритм обрабатывает 100 000 образцов за время менее 200 мс на современном оборудовании, что обеспечивает работу в режиме реального времени.
43+
44+
## Пример использования
45+
46+
```csharp
47+
using MathCore.DSP.Samples;
48+
using MathCore.DSP.Samples.Extensions;
49+
50+
// Создаем массив квадратурных отсчетов
51+
var samples = new SampleSI16[]
52+
{
53+
new(100, 0), // I=100, Q=0
54+
new(71, 71), // I=71, Q=71
55+
// ... другие отсчеты
56+
};
57+
58+
// Параметры демодуляции
59+
const double f0 = 1000.0; // Центральная частота 1 кГц
60+
const double fd = 48000.0; // Частота дискретизации 48 кГц
61+
62+
// Выполняем фазовую демодуляцию
63+
var demodulated = samples.AsSpan().PhaseDemodulation(f0, fd);
64+
65+
// demodulated[0] всегда равен 0
66+
// demodulated[1..] содержат мгновенные частоты в Гц
67+
```
68+
69+
## Ограничения
70+
71+
- Первый элемент результирующего массива всегда равен 0, так как для вычисления производной требуется предыдущий отсчет
72+
- Точность демодуляции зависит от отношения сигнал/шум входных данных
73+
- Алгоритм предполагает, что входной сигнал имеет достаточную амплитуду для корректного вычисления фазы

0 commit comments

Comments
 (0)