From 4722bfc2eda9dc575bd56e17f71c40267fe709aa Mon Sep 17 00:00:00 2001 From: lvpeng Date: Mon, 8 Jun 2026 23:28:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=95=E4=BE=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XYParser/XYParserTests/Tests.cpp | 890 +++++++++++++++++++++++++++++++ 1 file changed, 890 insertions(+) diff --git a/XYParser/XYParserTests/Tests.cpp b/XYParser/XYParserTests/Tests.cpp index 3438063..657bb0d 100644 --- a/XYParser/XYParserTests/Tests.cpp +++ b/XYParser/XYParserTests/Tests.cpp @@ -172,6 +172,203 @@ int BuildSineRawValue(int sample_index, int sample_rate, double frequency_hz, do return static_cast(std::lround(std::sin(angle) * amplitude)); } +int BuildCombinedSineRawValue(int sample_index, + int sample_rate, + double primary_frequency_hz, + double primary_amplitude, + double secondary_frequency_hz = 0.0, + double secondary_amplitude = 0.0) +{ + return BuildSineRawValue(sample_index, sample_rate, primary_frequency_hz, primary_amplitude) + + BuildSineRawValue(sample_index, sample_rate, secondary_frequency_hz, secondary_amplitude); +} + +std::vector BuildAlgorithmDataForSingleChannel(int sample_rate, + double primary_frequency_hz, + double primary_amplitude, + double secondary_frequency_hz = 0.0, + double secondary_amplitude = 0.0) +{ + std::vector algorithm_data( + static_cast(sample_rate * XYPARSER_FRAME_DATA_COLUMN_COUNT), 0.0); + for (int sample = 0; sample < sample_rate; ++sample) { + const std::size_t sample_offset = static_cast(sample) * XYPARSER_FRAME_DATA_COLUMN_COUNT; + algorithm_data[sample_offset] = static_cast(BuildCombinedSineRawValue(sample, + sample_rate, + primary_frequency_hz, + primary_amplitude, + secondary_frequency_hz, + secondary_amplitude)); + } + return algorithm_data; +} + +std::vector BuildAlgorithmDataForTwoChannels(int sample_rate, + double channel0_frequency_hz, + double channel0_amplitude, + double channel1_frequency_hz, + double channel1_amplitude) +{ + std::vector algorithm_data( + static_cast(sample_rate * XYPARSER_FRAME_DATA_COLUMN_COUNT), 0.0); + for (int sample = 0; sample < sample_rate; ++sample) { + const std::size_t sample_offset = static_cast(sample) * XYPARSER_FRAME_DATA_COLUMN_COUNT; + algorithm_data[sample_offset] = static_cast( + BuildSineRawValue(sample, sample_rate, channel0_frequency_hz, channel0_amplitude)); + algorithm_data[sample_offset + 1] = static_cast( + BuildSineRawValue(sample, sample_rate, channel1_frequency_hz, channel1_amplitude)); + } + return algorithm_data; +} + +std::vector> BuildFrameSequenceForSingleChannel(std::uint8_t channel_count, + int sample_rate, + double primary_frequency_hz, + double primary_amplitude, + double secondary_frequency_hz = 0.0, + double secondary_amplitude = 0.0) +{ + const int frame_count = sample_rate / static_cast(XYPARSER_SAMPLES_PER_FRAME); + std::vector> frames; + frames.reserve(static_cast(frame_count)); + + for (int frame_index = 0; frame_index < frame_count; ++frame_index) { + std::array, XYPARSER_SAMPLES_PER_FRAME> raw_samples{}; + for (int sample_offset = 0; sample_offset < static_cast(XYPARSER_SAMPLES_PER_FRAME); ++sample_offset) { + const int sample_index = frame_index * static_cast(XYPARSER_SAMPLES_PER_FRAME) + sample_offset; + raw_samples[static_cast(sample_offset)][0] = + BuildCombinedSineRawValue(sample_index, + sample_rate, + primary_frequency_hz, + primary_amplitude, + secondary_frequency_hz, + secondary_amplitude); + } + frames.push_back(BuildFrameWithRawSamples( + channel_count, static_cast(frame_index + 1), raw_samples)); + } + + return frames; +} + +std::vector> BuildFrameSequenceForTwoChannels(std::uint8_t channel_count, + int sample_rate, + double channel0_frequency_hz, + double channel0_amplitude, + double channel1_frequency_hz, + double channel1_amplitude) +{ + const int frame_count = sample_rate / static_cast(XYPARSER_SAMPLES_PER_FRAME); + std::vector> frames; + frames.reserve(static_cast(frame_count)); + + for (int frame_index = 0; frame_index < frame_count; ++frame_index) { + std::array, XYPARSER_SAMPLES_PER_FRAME> raw_samples{}; + for (int sample_offset = 0; sample_offset < static_cast(XYPARSER_SAMPLES_PER_FRAME); ++sample_offset) { + const int sample_index = frame_index * static_cast(XYPARSER_SAMPLES_PER_FRAME) + sample_offset; + raw_samples[static_cast(sample_offset)][0] = + BuildSineRawValue(sample_index, sample_rate, channel0_frequency_hz, channel0_amplitude); + raw_samples[static_cast(sample_offset)][1] = + BuildSineRawValue(sample_index, sample_rate, channel1_frequency_hz, channel1_amplitude); + } + frames.push_back(BuildFrameWithRawSamples( + channel_count, static_cast(frame_index + 1), raw_samples)); + } + + return frames; +} + +int FindFrequencyIndex(const XYParserWelchSummary& summary, double target_frequency_hz) +{ + for (std::uint32_t index = 0; index < summary.frequency_count; ++index) { + if (std::abs(summary.frequencies[index] - target_frequency_hz) < 1e-9) { + return static_cast(index); + } + } + return -1; +} + +int FindPeakPsdIndex(const XYParserWelchSummary& summary, XYParserLeadChannelNumber lead) +{ + if (summary.frequency_count == 0) { + return -1; + } + + const std::size_t lead_index = static_cast(lead); + std::uint32_t peak_index = 0; + double peak_value = summary.psd_values[lead_index][0]; + for (std::uint32_t index = 1; index < summary.frequency_count; ++index) { + if (summary.psd_values[lead_index][index] > peak_value) { + peak_value = summary.psd_values[lead_index][index]; + peak_index = index; + } + } + return static_cast(peak_index); +} + +std::uint16_t MeasureLeadImpedanceForMixedSine(std::uint8_t channel_count, + XYParserLeadChannelNumber lead, + int sample_rate, + double primary_frequency_hz, + double primary_amplitude, + double secondary_frequency_hz, + double secondary_amplitude); + +std::uint16_t MeasureLeadImpedanceForSine(std::uint8_t channel_count, + XYParserLeadChannelNumber lead, + int sample_rate, + double frequency_hz, + double amplitude) +{ + return MeasureLeadImpedanceForMixedSine( + channel_count, lead, sample_rate, frequency_hz, amplitude, 0.0, 0.0); +} + +std::uint16_t MeasureLeadImpedanceForMixedSine(std::uint8_t channel_count, + XYParserLeadChannelNumber lead, + int sample_rate, + double primary_frequency_hz, + double primary_amplitude, + double secondary_frequency_hz, + double secondary_amplitude) +{ + ParserGuard parser(XYParser_CreateParser(channel_count)); + if (parser.get() == nullptr) { + ADD_FAILURE() << "failed to create parser"; + return 0; + } + + XYParser_SetBypassChecksum(parser.get(), 1); + XYParser_SetSampleRate(parser.get(), sample_rate); + XYParser_SetImpedanceDetection(parser.get(), 1); + + const auto frames = BuildFrameSequenceForSingleChannel(channel_count, + sample_rate, + primary_frequency_hz, + primary_amplitude, + secondary_frequency_hz, + secondary_amplitude); + std::array summaries{}; + for (const auto& frame : frames) { + if (XYParser_Feed(parser.get(), + frame.data(), + frame.size(), + summaries.data(), + static_cast(summaries.size())) != 1) { + ADD_FAILURE() << "failed to feed frame for impedance measurement"; + return 0; + } + } + + std::array impedance{}; + if (XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())) != 1) { + ADD_FAILURE() << "failed to read impedance summary"; + return 0; + } + + return impedance[0].impedance_values[static_cast(lead)]; +} + } // namespace /// 测试:创建解析器时拒绝不支持的通道数 @@ -236,6 +433,19 @@ TEST(XYParserApiTests, SerializeTriggerCommandMatchesWirelessEegPacket) EXPECT_TRUE(std::equal(expected.begin(), expected.end(), command.begin())); } +TEST(XYParserApiTests, SerializeTrain1TriggerCommandMatchesWirelessEegPacket) +{ + std::array command{}; + const int command_size = XYParser_SerializeTriggerCommand( + XYPARSER_TRIGGER_TRAIN_1, + command.data(), + command.size()); + + ASSERT_EQ(command_size, static_cast(XYParser_GetTriggerCommandSize())); + const std::array expected = {0x00, 0x00, 0xBC}; + EXPECT_TRUE(std::equal(expected.begin(), expected.end(), command.begin())); +} + TEST(XYParserApiTests, SerializeTriggerCommandRejectsUnsupportedTriggerType) { std::array command{}; @@ -592,6 +802,39 @@ TEST(XYParserApiTests, ImpedanceReturnsOneResultAfterOneSecondFor8Channels) EXPECT_EQ(impedance[0].impedance_values[LeadChannel_FP1], 0); } +TEST(XYParserApiTests, ReadImpedanceConsumesQueuedResults) +{ + ParserGuard parser(XYParser_CreateParser(8)); + ASSERT_NE(parser.get(), nullptr); + + XYParser_SetBypassChecksum(parser.get(), 1); + XYParser_SetSampleRate(parser.get(), 10); + XYParser_SetImpedanceDetection(parser.get(), 1); + + std::array, XYPARSER_SAMPLES_PER_FRAME> raw_samples1{}; + std::array, XYPARSER_SAMPLES_PER_FRAME> raw_samples2{}; + for (int sample = 0; sample < 10; ++sample) { + const int raw_value = BuildSineRawValue(sample, 10, 2.0, 1000000.0); + if (sample < static_cast(XYPARSER_SAMPLES_PER_FRAME)) { + raw_samples1[static_cast(sample)][0] = raw_value; + } else { + raw_samples2[static_cast(sample - XYPARSER_SAMPLES_PER_FRAME)][0] = raw_value; + } + } + + const std::vector frame1 = BuildFrameWithRawSamples(8, 1U, raw_samples1); + const std::vector frame2 = BuildFrameWithRawSamples(8, 2U, raw_samples2); + std::array summaries{}; + std::array impedance{}; + + ASSERT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast(summaries.size())), 1); + ASSERT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast(summaries.size())), 1); + + ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())), 1); + EXPECT_GT(impedance[0].impedance_values[LeadChannel_PO5], 0); + EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())), 0); +} + TEST(XYParserApiTests, ImpedanceUsesUnifiedLeadIndexesFor64Channels) { ParserGuard parser(XYParser_CreateParser(64)); @@ -625,6 +868,212 @@ TEST(XYParserApiTests, ImpedanceUsesUnifiedLeadIndexesFor64Channels) EXPECT_EQ(impedance[0].impedance_values[LeadChannel_FP2], 0); } +TEST(XYParserApiTests, ImpedanceSeparatesDifferentChannelLeadsFor64Channels) +{ + ParserGuard parser(XYParser_CreateParser(64)); + ASSERT_NE(parser.get(), nullptr); + + XYParser_SetBypassChecksum(parser.get(), 1); + XYParser_SetSampleRate(parser.get(), 10); + XYParser_SetImpedanceDetection(parser.get(), 1); + + const auto frames = BuildFrameSequenceForTwoChannels(64, 10, 3.0, 1000000.0, 2.0, 250000.0); + ASSERT_EQ(frames.size(), 2U); + + std::array summaries{}; + std::array impedance{}; + for (const auto& frame : frames) { + ASSERT_EQ(XYParser_Feed(parser.get(), + frame.data(), + frame.size(), + summaries.data(), + static_cast(summaries.size())), + 1); + } + + ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())), 1); + EXPECT_GT(impedance[0].impedance_values[LeadChannel_FP1], 0); + EXPECT_GT(impedance[0].impedance_values[LeadChannel_FP2], 0); + EXPECT_GT(impedance[0].impedance_values[LeadChannel_FP1], impedance[0].impedance_values[LeadChannel_FP2]); + EXPECT_EQ(impedance[0].impedance_values[LeadChannel_PO6], 0); +} + +TEST(XYParserApiTests, ImpedanceDisableClearsHalfWindow) +{ + ParserGuard parser(XYParser_CreateParser(64)); + ASSERT_NE(parser.get(), nullptr); + + XYParser_SetBypassChecksum(parser.get(), 1); + XYParser_SetSampleRate(parser.get(), 10); + XYParser_SetImpedanceDetection(parser.get(), 1); + + const auto full_frames = BuildFrameSequenceForSingleChannel(64, 10, 3.0, 1000000.0); + ASSERT_EQ(full_frames.size(), 2U); + + std::array summaries{}; + std::array impedance{}; + + ASSERT_EQ(XYParser_Feed(parser.get(), + full_frames[0].data(), + full_frames[0].size(), + summaries.data(), + static_cast(summaries.size())), + 1); + EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())), 0); + + XYParser_SetImpedanceDetection(parser.get(), 0); + XYParser_SetImpedanceDetection(parser.get(), 1); + + ASSERT_EQ(XYParser_Feed(parser.get(), + full_frames[0].data(), + full_frames[0].size(), + summaries.data(), + static_cast(summaries.size())), + 1); + EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())), 0); + + ASSERT_EQ(XYParser_Feed(parser.get(), + full_frames[1].data(), + full_frames[1].size(), + summaries.data(), + static_cast(summaries.size())), + 1); + ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())), 1); + EXPECT_GT(impedance[0].impedance_values[LeadChannel_FP1], 0); +} + +TEST(XYParserApiTests, ImpedanceSampleRateChangeClearsHalfWindow) +{ + ParserGuard parser(XYParser_CreateParser(64)); + ASSERT_NE(parser.get(), nullptr); + + XYParser_SetBypassChecksum(parser.get(), 1); + XYParser_SetSampleRate(parser.get(), 10); + XYParser_SetImpedanceDetection(parser.get(), 1); + + const auto frames_10hz_window = BuildFrameSequenceForSingleChannel(64, 10, 3.0, 1000000.0); + ASSERT_EQ(frames_10hz_window.size(), 2U); + + std::array summaries{}; + std::array impedance{}; + + ASSERT_EQ(XYParser_Feed(parser.get(), + frames_10hz_window[0].data(), + frames_10hz_window[0].size(), + summaries.data(), + static_cast(summaries.size())), + 1); + EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())), 0); + + XYParser_SetSampleRate(parser.get(), 20); + + const auto frames_20hz_window = BuildFrameSequenceForSingleChannel(64, 20, 3.0, 1000000.0); + ASSERT_EQ(frames_20hz_window.size(), 4U); + + for (const auto& frame : frames_20hz_window) { + ASSERT_EQ(XYParser_Feed(parser.get(), + frame.data(), + frame.size(), + summaries.data(), + static_cast(summaries.size())), + 1); + } + + ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())), 1); + EXPECT_EQ(impedance[0].sample_rate, 20U); + EXPECT_EQ(impedance[0].window_sample_count, 20U); + EXPECT_GT(impedance[0].impedance_values[LeadChannel_FP1], 0); +} + +TEST(XYParserApiTests, ImpedanceSuppresses50HzLineNoiseFor64Channels) +{ + constexpr int kSampleRate = 200; + constexpr double kAmplitude = 1000000.0; + + const std::uint16_t signal_10hz = MeasureLeadImpedanceForSine( + 64, LeadChannel_FP1, kSampleRate, 10.0, kAmplitude); + const std::uint16_t line_noise_50hz = MeasureLeadImpedanceForSine( + 64, LeadChannel_FP1, kSampleRate, 50.0, kAmplitude); + + EXPECT_GT(signal_10hz, 0); + EXPECT_LT(line_noise_50hz, signal_10hz); +} + +TEST(XYParserApiTests, ImpedanceSuppressesFrequenciesAround50HzAnd100HzFor64Channels) +{ + constexpr int kSampleRate = 200; + constexpr double kAmplitude = 1000000.0; + + const std::uint16_t signal_10hz = MeasureLeadImpedanceForSine( + 64, LeadChannel_FP1, kSampleRate, 10.0, kAmplitude); + const std::uint16_t signal_49hz = MeasureLeadImpedanceForSine( + 64, LeadChannel_FP1, kSampleRate, 49.0, kAmplitude); + const std::uint16_t signal_50hz = MeasureLeadImpedanceForSine( + 64, LeadChannel_FP1, kSampleRate, 50.0, kAmplitude); + const std::uint16_t signal_51hz = MeasureLeadImpedanceForSine( + 64, LeadChannel_FP1, kSampleRate, 51.0, kAmplitude); + const std::uint16_t signal_100hz = MeasureLeadImpedanceForSine( + 64, LeadChannel_FP1, kSampleRate, 100.0, kAmplitude); + + ASSERT_GT(signal_10hz, 0); + EXPECT_LT(signal_49hz, signal_10hz); + EXPECT_LT(signal_50hz, signal_10hz); + EXPECT_LT(signal_51hz, signal_10hz); + EXPECT_LT(signal_100hz, signal_10hz); +} + +TEST(XYParserApiTests, ImpedancePreserves10HzSignalWhenMixedWithStrong50HzLineNoise) +{ + constexpr int kSampleRate = 200; + constexpr double kSignalAmplitude = 1000000.0; + constexpr double kLineNoiseAmplitude = 3000000.0; + + const std::uint16_t signal_10hz = MeasureLeadImpedanceForSine( + 64, LeadChannel_FP1, kSampleRate, 10.0, kSignalAmplitude); + const std::uint16_t line_noise_50hz = MeasureLeadImpedanceForSine( + 64, LeadChannel_FP1, kSampleRate, 50.0, kLineNoiseAmplitude); + const std::uint16_t mixed_signal = MeasureLeadImpedanceForMixedSine( + 64, LeadChannel_FP1, kSampleRate, 10.0, kSignalAmplitude, 50.0, kLineNoiseAmplitude); + + ASSERT_GT(signal_10hz, 0); + EXPECT_LT(line_noise_50hz, signal_10hz); + EXPECT_GT(mixed_signal, line_noise_50hz); + EXPECT_NEAR(static_cast(mixed_signal), + static_cast(signal_10hz), + std::max(1.0, static_cast(signal_10hz) * 0.2)); +} + +TEST(XYParserApiTests, ImpedancePreserves10HzSignalWhenMixedWithStrong49To51HzLineNoise) +{ + constexpr int kSampleRate = 200; + constexpr double kSignalAmplitude = 1000000.0; + constexpr double kLineNoiseAmplitude = 3000000.0; + + const std::uint16_t signal_10hz = MeasureLeadImpedanceForSine( + 64, LeadChannel_FP1, kSampleRate, 10.0, kSignalAmplitude); + ASSERT_GT(signal_10hz, 0); + + for (double line_noise_hz : {49.0, 50.0, 51.0}) { + const std::uint16_t pure_line_noise = MeasureLeadImpedanceForSine( + 64, LeadChannel_FP1, kSampleRate, line_noise_hz, kLineNoiseAmplitude); + const std::uint16_t mixed_signal = MeasureLeadImpedanceForMixedSine( + 64, + LeadChannel_FP1, + kSampleRate, + 10.0, + kSignalAmplitude, + line_noise_hz, + kLineNoiseAmplitude); + + EXPECT_LT(pure_line_noise, signal_10hz) << "line noise hz=" << line_noise_hz; + EXPECT_GT(mixed_signal, pure_line_noise) << "line noise hz=" << line_noise_hz; + EXPECT_NEAR(static_cast(mixed_signal), + static_cast(signal_10hz), + std::max(1.0, static_cast(signal_10hz) * 0.2)) + << "line noise hz=" << line_noise_hz; + } +} + TEST(XYParserApiTests, WelchReturnsOneResultAfterOneSecondFromAlgorithmData) { ParserGuard parser(XYParser_CreateParser(64)); @@ -660,6 +1109,447 @@ TEST(XYParserApiTests, WelchReturnsOneResultAfterOneSecondFromAlgorithmData) EXPECT_GT(welch[0].band_values[0][LeadChannel_FP1], 0.0); } +TEST(XYParserApiTests, WelchDetects50HzPeakFromAlgorithmData) +{ + ParserGuard parser(XYParser_CreateParser(64)); + ASSERT_NE(parser.get(), nullptr); + + constexpr int kSampleRate = 200; + XYParser_SetSampleRate(parser.get(), kSampleRate); + XYParser_SetWelchDetection(parser.get(), 1); + + const std::vector algorithm_data = BuildAlgorithmDataForSingleChannel( + kSampleRate, 50.0, 1000000.0, 10.0, 100000.0); + + std::array welch{}; + EXPECT_EQ(XYParser_FeedAlgorithmData( + parser.get(), + reinterpret_cast(algorithm_data.data()), + algorithm_data.size() * sizeof(double), + nullptr, + 0), + 0); + + ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 1); + ASSERT_EQ(welch[0].ok, 1); + + const int frequency_10hz_index = FindFrequencyIndex(welch[0], 10.0); + const int frequency_50hz_index = FindFrequencyIndex(welch[0], 50.0); + const int peak_index = FindPeakPsdIndex(welch[0], LeadChannel_FP1); + + ASSERT_GE(frequency_10hz_index, 0); + ASSERT_GE(frequency_50hz_index, 0); + ASSERT_GE(peak_index, 0); + + EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast(peak_index)], 50.0); + EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_50hz_index)], + welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_10hz_index)]); + EXPECT_GT(welch[0].band_values[4][LeadChannel_FP1], welch[0].band_values[2][LeadChannel_FP1]); +} + +TEST(XYParserApiTests, WelchDetects49HzPeakFromAlgorithmData) +{ + ParserGuard parser(XYParser_CreateParser(64)); + ASSERT_NE(parser.get(), nullptr); + + constexpr int kSampleRate = 200; + XYParser_SetSampleRate(parser.get(), kSampleRate); + XYParser_SetWelchDetection(parser.get(), 1); + + const std::vector algorithm_data = BuildAlgorithmDataForSingleChannel( + kSampleRate, 49.0, 1000000.0, 10.0, 100000.0); + + std::array welch{}; + EXPECT_EQ(XYParser_FeedAlgorithmData( + parser.get(), + reinterpret_cast(algorithm_data.data()), + algorithm_data.size() * sizeof(double), + nullptr, + 0), + 0); + + ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 1); + ASSERT_EQ(welch[0].ok, 1); + + const int frequency_10hz_index = FindFrequencyIndex(welch[0], 10.0); + const int frequency_49hz_index = FindFrequencyIndex(welch[0], 49.0); + const int peak_index = FindPeakPsdIndex(welch[0], LeadChannel_FP1); + + ASSERT_GE(frequency_10hz_index, 0); + ASSERT_GE(frequency_49hz_index, 0); + ASSERT_GE(peak_index, 0); + + EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast(peak_index)], 49.0); + EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_49hz_index)], + welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_10hz_index)]); + EXPECT_GT(welch[0].band_values[4][LeadChannel_FP1], welch[0].band_values[2][LeadChannel_FP1]); +} + +TEST(XYParserApiTests, WelchReportedFrequenciesDoNotExceed50Hz) +{ + ParserGuard parser(XYParser_CreateParser(64)); + ASSERT_NE(parser.get(), nullptr); + + constexpr int kSampleRate = 200; + XYParser_SetSampleRate(parser.get(), kSampleRate); + XYParser_SetWelchDetection(parser.get(), 1); + + const std::vector algorithm_data = BuildAlgorithmDataForSingleChannel( + kSampleRate, 51.0, 1000000.0, 100.0, 500000.0); + + std::array welch{}; + EXPECT_EQ(XYParser_FeedAlgorithmData( + parser.get(), + reinterpret_cast(algorithm_data.data()), + algorithm_data.size() * sizeof(double), + nullptr, + 0), + 0); + + ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 1); + ASSERT_EQ(welch[0].ok, 1); + ASSERT_GT(welch[0].frequency_count, 0U); + + EXPECT_LE(welch[0].frequencies[welch[0].frequency_count - 1], 50.0); + EXPECT_EQ(FindFrequencyIndex(welch[0], 51.0), -1); + EXPECT_EQ(FindFrequencyIndex(welch[0], 100.0), -1); +} + +TEST(XYParserApiTests, WelchDetects49And50HzPeakWhenMixedWith10Hz) +{ + constexpr int kSampleRate = 200; + + for (double line_noise_hz : {49.0, 50.0}) { + ParserGuard parser(XYParser_CreateParser(64)); + ASSERT_NE(parser.get(), nullptr); + + XYParser_SetSampleRate(parser.get(), kSampleRate); + XYParser_SetWelchDetection(parser.get(), 1); + + const std::vector algorithm_data = BuildAlgorithmDataForSingleChannel( + kSampleRate, line_noise_hz, 1000000.0, 10.0, 100000.0); + + std::array welch{}; + EXPECT_EQ(XYParser_FeedAlgorithmData( + parser.get(), + reinterpret_cast(algorithm_data.data()), + algorithm_data.size() * sizeof(double), + nullptr, + 0), + 0); + + ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 1); + ASSERT_EQ(welch[0].ok, 1); + + const int frequency_10hz_index = FindFrequencyIndex(welch[0], 10.0); + const int line_noise_index = FindFrequencyIndex(welch[0], line_noise_hz); + const int peak_index = FindPeakPsdIndex(welch[0], LeadChannel_FP1); + + ASSERT_GE(frequency_10hz_index, 0) << "line noise hz=" << line_noise_hz; + ASSERT_GE(line_noise_index, 0) << "line noise hz=" << line_noise_hz; + ASSERT_GE(peak_index, 0) << "line noise hz=" << line_noise_hz; + + EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast(peak_index)], line_noise_hz) + << "line noise hz=" << line_noise_hz; + EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast(line_noise_index)], + welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_10hz_index)]) + << "line noise hz=" << line_noise_hz; + EXPECT_GT(welch[0].band_values[4][LeadChannel_FP1], welch[0].band_values[2][LeadChannel_FP1]) + << "line noise hz=" << line_noise_hz; + } +} + +TEST(XYParserApiTests, WelchDoesNotReport51HzButGammaBandRisesWhenMixedWith10Hz) +{ + ParserGuard parser(XYParser_CreateParser(64)); + ASSERT_NE(parser.get(), nullptr); + + constexpr int kSampleRate = 200; + XYParser_SetSampleRate(parser.get(), kSampleRate); + XYParser_SetWelchDetection(parser.get(), 1); + + const std::vector algorithm_data = BuildAlgorithmDataForSingleChannel( + kSampleRate, 51.0, 1000000.0, 10.0, 100000.0); + + std::array welch{}; + EXPECT_EQ(XYParser_FeedAlgorithmData( + parser.get(), + reinterpret_cast(algorithm_data.data()), + algorithm_data.size() * sizeof(double), + nullptr, + 0), + 0); + + ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 1); + ASSERT_EQ(welch[0].ok, 1); + + const int frequency_10hz_index = FindFrequencyIndex(welch[0], 10.0); + const int frequency_50hz_index = FindFrequencyIndex(welch[0], 50.0); + + ASSERT_GE(frequency_10hz_index, 0); + ASSERT_GE(frequency_50hz_index, 0); + + EXPECT_EQ(FindFrequencyIndex(welch[0], 51.0), -1); + EXPECT_GT(welch[0].band_values[4][LeadChannel_FP1], welch[0].band_values[2][LeadChannel_FP1]); + EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_50hz_index)], + welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_10hz_index)]); +} + +TEST(XYParserApiTests, WelchDoesNotReport100HzOrPolluteLowFrequencyBands) +{ + ParserGuard parser(XYParser_CreateParser(64)); + ASSERT_NE(parser.get(), nullptr); + + constexpr int kSampleRate = 200; + XYParser_SetSampleRate(parser.get(), kSampleRate); + XYParser_SetWelchDetection(parser.get(), 1); + + const std::vector algorithm_data = BuildAlgorithmDataForSingleChannel( + kSampleRate, 100.0, 1000000.0, 10.0, 100000.0); + + std::array welch{}; + EXPECT_EQ(XYParser_FeedAlgorithmData( + parser.get(), + reinterpret_cast(algorithm_data.data()), + algorithm_data.size() * sizeof(double), + nullptr, + 0), + 0); + + ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 1); + ASSERT_EQ(welch[0].ok, 1); + + const int frequency_10hz_index = FindFrequencyIndex(welch[0], 10.0); + const int frequency_50hz_index = FindFrequencyIndex(welch[0], 50.0); + + ASSERT_GE(frequency_10hz_index, 0); + ASSERT_GE(frequency_50hz_index, 0); + + EXPECT_EQ(FindFrequencyIndex(welch[0], 100.0), -1); + EXPECT_LE(welch[0].frequencies[welch[0].frequency_count - 1], 50.0); + EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_10hz_index)], 0.0); + EXPECT_LT(welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_50hz_index)], + welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_10hz_index)]); + EXPECT_GT(welch[0].band_values[2][LeadChannel_FP1], welch[0].band_values[0][LeadChannel_FP1]); + EXPECT_GT(welch[0].band_values[2][LeadChannel_FP1], welch[0].band_values[1][LeadChannel_FP1]); +} + +TEST(XYParserApiTests, WelchResetClearsHalfWindowAfterDisableEnable) +{ + ParserGuard parser(XYParser_CreateParser(64)); + ASSERT_NE(parser.get(), nullptr); + + XYParser_SetSampleRate(parser.get(), 10); + XYParser_SetWelchDetection(parser.get(), 1); + + const std::vector full_data = BuildAlgorithmDataForSingleChannel(10, 2.0, 1000000.0); + const std::size_t half_row_count = + static_cast(XYPARSER_SAMPLES_PER_FRAME) * XYPARSER_FRAME_DATA_COLUMN_COUNT; + const std::vector first_half(full_data.begin(), full_data.begin() + static_cast(half_row_count)); + const std::vector second_half(full_data.begin() + static_cast(half_row_count), full_data.end()); + + std::array welch{}; + + EXPECT_EQ(XYParser_FeedAlgorithmData( + parser.get(), + reinterpret_cast(first_half.data()), + first_half.size() * sizeof(double), + nullptr, + 0), + 0); + EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 0); + + XYParser_SetWelchDetection(parser.get(), 0); + XYParser_SetWelchDetection(parser.get(), 1); + + EXPECT_EQ(XYParser_FeedAlgorithmData( + parser.get(), + reinterpret_cast(first_half.data()), + first_half.size() * sizeof(double), + nullptr, + 0), + 0); + EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 0); + + EXPECT_EQ(XYParser_FeedAlgorithmData( + parser.get(), + reinterpret_cast(second_half.data()), + second_half.size() * sizeof(double), + nullptr, + 0), + 0); + ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 1); + EXPECT_EQ(welch[0].ok, 1); +} + +TEST(XYParserApiTests, WelchSeparatesDifferentChannelPeaks) +{ + ParserGuard parser(XYParser_CreateParser(64)); + ASSERT_NE(parser.get(), nullptr); + + constexpr int kSampleRate = 200; + XYParser_SetSampleRate(parser.get(), kSampleRate); + XYParser_SetWelchDetection(parser.get(), 1); + + const std::vector algorithm_data = BuildAlgorithmDataForTwoChannels( + kSampleRate, 10.0, 1000000.0, 20.0, 1000000.0); + + std::array welch{}; + EXPECT_EQ(XYParser_FeedAlgorithmData( + parser.get(), + reinterpret_cast(algorithm_data.data()), + algorithm_data.size() * sizeof(double), + nullptr, + 0), + 0); + + ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 1); + ASSERT_EQ(welch[0].ok, 1); + + const int fp1_peak_index = FindPeakPsdIndex(welch[0], LeadChannel_FP1); + const int fp2_peak_index = FindPeakPsdIndex(welch[0], LeadChannel_FP2); + const int frequency_10hz_index = FindFrequencyIndex(welch[0], 10.0); + const int frequency_20hz_index = FindFrequencyIndex(welch[0], 20.0); + + ASSERT_GE(fp1_peak_index, 0); + ASSERT_GE(fp2_peak_index, 0); + ASSERT_GE(frequency_10hz_index, 0); + ASSERT_GE(frequency_20hz_index, 0); + + EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast(fp1_peak_index)], 10.0); + EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast(fp2_peak_index)], 20.0); + EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_10hz_index)], + welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_20hz_index)]); + EXPECT_GT(welch[0].psd_values[LeadChannel_FP2][static_cast(frequency_20hz_index)], + welch[0].psd_values[LeadChannel_FP2][static_cast(frequency_10hz_index)]); +} + +TEST(XYParserApiTests, WelchPsdIncreasesWithSignalAmplitude) +{ + ParserGuard low_amplitude_parser(XYParser_CreateParser(64)); + ParserGuard high_amplitude_parser(XYParser_CreateParser(64)); + ASSERT_NE(low_amplitude_parser.get(), nullptr); + ASSERT_NE(high_amplitude_parser.get(), nullptr); + + constexpr int kSampleRate = 200; + XYParser_SetSampleRate(low_amplitude_parser.get(), kSampleRate); + XYParser_SetWelchDetection(low_amplitude_parser.get(), 1); + XYParser_SetSampleRate(high_amplitude_parser.get(), kSampleRate); + XYParser_SetWelchDetection(high_amplitude_parser.get(), 1); + + const std::vector low_amplitude_data = BuildAlgorithmDataForSingleChannel( + kSampleRate, 10.0, 100000.0); + const std::vector high_amplitude_data = BuildAlgorithmDataForSingleChannel( + kSampleRate, 10.0, 1000000.0); + + std::array low_welch{}; + std::array high_welch{}; + + EXPECT_EQ(XYParser_FeedAlgorithmData( + low_amplitude_parser.get(), + reinterpret_cast(low_amplitude_data.data()), + low_amplitude_data.size() * sizeof(double), + nullptr, + 0), + 0); + EXPECT_EQ(XYParser_FeedAlgorithmData( + high_amplitude_parser.get(), + reinterpret_cast(high_amplitude_data.data()), + high_amplitude_data.size() * sizeof(double), + nullptr, + 0), + 0); + + ASSERT_EQ(XYParser_ReadWelch(low_amplitude_parser.get(), low_welch.data(), static_cast(low_welch.size())), 1); + ASSERT_EQ(XYParser_ReadWelch(high_amplitude_parser.get(), high_welch.data(), static_cast(high_welch.size())), 1); + + const int low_frequency_10hz_index = FindFrequencyIndex(low_welch[0], 10.0); + const int high_frequency_10hz_index = FindFrequencyIndex(high_welch[0], 10.0); + ASSERT_GE(low_frequency_10hz_index, 0); + ASSERT_GE(high_frequency_10hz_index, 0); + + EXPECT_GT(high_welch[0].psd_values[LeadChannel_FP1][static_cast(high_frequency_10hz_index)], + low_welch[0].psd_values[LeadChannel_FP1][static_cast(low_frequency_10hz_index)]); + EXPECT_GT(high_welch[0].band_values[2][LeadChannel_FP1], + low_welch[0].band_values[2][LeadChannel_FP1]); +} + +TEST(XYParserApiTests, ReadWelchConsumesQueuedResults) +{ + ParserGuard parser(XYParser_CreateParser(64)); + ASSERT_NE(parser.get(), nullptr); + + XYParser_SetSampleRate(parser.get(), 10); + XYParser_SetWelchDetection(parser.get(), 1); + + const std::vector algorithm_data = BuildAlgorithmDataForSingleChannel(10, 2.0, 1000000.0); + std::array welch{}; + + EXPECT_EQ(XYParser_FeedAlgorithmData( + parser.get(), + reinterpret_cast(algorithm_data.data()), + algorithm_data.size() * sizeof(double), + nullptr, + 0), + 0); + + ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 1); + EXPECT_EQ(welch[0].ok, 1); + EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 0); +} + +TEST(XYParserApiTests, Convert8ChTo64ChAlgorithmDataMatchesDirect64Ch) +{ + ParserGuard parser8(XYParser_CreateParser(8)); + ParserGuard parser64(XYParser_CreateParser(64)); + ASSERT_NE(parser8.get(), nullptr); + ASSERT_NE(parser64.get(), nullptr); + + XYParser_SetBypassChecksum(parser8.get(), 1); + XYParser_SetBypassChecksum(parser64.get(), 1); + + std::array leads8{}; + std::array leads64{}; + ASSERT_EQ(XYParser_GetLeadMap(8, leads8.data(), static_cast(leads8.size())), 8); + ASSERT_EQ(XYParser_GetLeadMap(64, leads64.data(), static_cast(leads64.size())), 64); + + std::array, XYPARSER_SAMPLES_PER_FRAME> raw_samples8{}; + std::array, XYPARSER_SAMPLES_PER_FRAME> raw_samples64{}; + for (std::size_t sample_index = 0; sample_index < XYPARSER_SAMPLES_PER_FRAME; ++sample_index) { + for (std::size_t channel_index = 0; channel_index < leads8.size(); ++channel_index) { + const int raw_value = static_cast((sample_index + 1) * 1000 + static_cast(channel_index) * 100); + raw_samples8[sample_index][channel_index] = raw_value; + + for (std::size_t raw64_index = 0; raw64_index < leads64.size(); ++raw64_index) { + if (leads64[raw64_index] == leads8[channel_index]) { + raw_samples64[sample_index][raw64_index] = raw_value; + break; + } + } + } + } + + const std::vector frame8 = BuildFrameWithRawSamples(8, 1U, raw_samples8); + const std::vector frame64 = BuildFrameWithRawSamples(64, 1U, raw_samples64); + + std::array summaries8{}; + std::array summaries64{}; + std::array converted64{}; + + ASSERT_EQ(XYParser_Feed(parser8.get(), frame8.data(), frame8.size(), summaries8.data(), static_cast(summaries8.size())), 1); + ASSERT_EQ(XYParser_Feed(parser64.get(), frame64.data(), frame64.size(), summaries64.data(), static_cast(summaries64.size())), 1); + ASSERT_EQ(XYParser_Convert8ChFramesTo64Ch(summaries8.data(), 1, converted64.data(), 1), 1); + + std::vector converted_algorithm_data(XYPARSER_FRAME_ALGORITHM_VALUE_COUNT, 0.0); + std::vector direct_algorithm_data(XYPARSER_FRAME_ALGORITHM_VALUE_COUNT, 0.0); + ASSERT_EQ(XYParser_ConvertSampleFramesToAlgorithmData(&converted64[0], converted_algorithm_data.data()), 1); + ASSERT_EQ(XYParser_ConvertSampleFramesToAlgorithmData(&summaries64[0], direct_algorithm_data.data()), 1); + + for (std::size_t i = 0; i < converted_algorithm_data.size(); ++i) { + EXPECT_DOUBLE_EQ(converted_algorithm_data[i], direct_algorithm_data[i]) << "index=" << i; + } +} + TEST(XYParserApiTests, WelchFrameFeedDoesNotProduceResults) { ParserGuard parser(XYParser_CreateParser(64));