diff --git a/detection/pom.xml b/detection/pom.xml index b431b7e1..dc7afcc6 100644 --- a/detection/pom.xml +++ b/detection/pom.xml @@ -86,6 +86,7 @@ jakarta.xml.bind-api 2.3.3 + org.glassfish.jaxb jaxb-runtime @@ -124,6 +125,13 @@ 3.10.0 + + + com.njcn.gather + wave-comtrade + 1.0.0 + + diff --git a/entrance/src/main/resources/application.yml b/entrance/src/main/resources/application.yml index 083f861b..df3f7789 100644 --- a/entrance/src/main/resources/application.yml +++ b/entrance/src/main/resources/application.yml @@ -101,3 +101,19 @@ qr: db: type: mysql + + +# 比对录波需要的配置,晚点再做优化 +# 系统配置 +power-quality: + # 文件读取配置 + reading: + encoding: GBK # 文件编码(支持中文) + + # 计算参数 + calculation: + sampling: + default-rate: 256 # 默认采样率(每周波采样点数) + harmonic-times: 50 # 谐波次数 + ib-add: false # 电流基波叠加标志 + uharm-add: false # 电压谐波叠加标志 diff --git a/entrance/src/test/java/com/njcn/AnalysisServiceStreamTest.java b/entrance/src/test/java/com/njcn/AnalysisServiceStreamTest.java new file mode 100644 index 00000000..03203120 --- /dev/null +++ b/entrance/src/test/java/com/njcn/AnalysisServiceStreamTest.java @@ -0,0 +1,116 @@ +package com.njcn; + +import com.njcn.gather.tools.comtrade.comparewave.core.model.CompareWaveDTO; +import com.njcn.gather.tools.comtrade.comparewave.service.ICompareWaveService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; + + +/** + * 流式文件分析测试 + * 测试从本地文件读取并转换为流进行分析 + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = com.njcn.gather.EntranceApplication.class) +public class AnalysisServiceStreamTest { + + @Autowired + private ICompareWaveService compareWaveServiceImpl; + + // 测试文件路径 - 请根据实际情况修改 + private static final String SOURCE_CFG_PATH = "F:\\hatch\\wavecompare\\wave\\PQMonitor_PQM1_000001_20200430_113404_845.cfg"; + private static final String SOURCE_DAT_PATH = "F:\\hatch\\wavecompare\\wave\\PQMonitor_PQM1_000001_20200430_113404_845.dat"; + private static final String TARGET_CFG_PATH = "F:\\hatch\\wavecompare\\wave\\PQMonitor_PQM1_000001_20200430_113407_075.cfg"; + private static final String TARGET_DAT_PATH = "F:\\hatch\\wavecompare\\wave\\PQMonitor_PQM1_000001_20200430_113407_075.dat"; + + // 输出路径 + private static final String OUTPUT_PATH = "./test-output/"; + + /** + * 测试使用文件流进行电能质量分析 + */ + @Test + public void testAnalyzeWithStreams() throws Exception { + System.out.println("========================================"); + System.out.println("开始测试流式文件分析"); + System.out.println("========================================"); + + // 验证文件是否存在 + checkFileExists(SOURCE_CFG_PATH, "源CFG文件"); + checkFileExists(SOURCE_DAT_PATH, "源DAT文件"); + checkFileExists(TARGET_CFG_PATH, "目标CFG文件"); + checkFileExists(TARGET_DAT_PATH, "目标DAT文件"); + + // 读取本地文件并创建输入流 + try (InputStream sourceCfgStream = new FileInputStream(SOURCE_CFG_PATH); + InputStream sourceDatStream = new FileInputStream(SOURCE_DAT_PATH); + InputStream targetCfgStream = new FileInputStream(TARGET_CFG_PATH); + InputStream targetDatStream = new FileInputStream(TARGET_DAT_PATH)) { + + System.out.println("成功创建文件输入流"); + System.out.println("源CFG文件: " + SOURCE_CFG_PATH); + System.out.println("源DAT文件: " + SOURCE_DAT_PATH); + System.out.println("目标CFG文件: " + TARGET_CFG_PATH); + System.out.println("目标DAT文件: " + TARGET_DAT_PATH); + System.out.println("输出路径: " + OUTPUT_PATH); + + // 创建输出目录 + File outputDir = new File(OUTPUT_PATH); + if (!outputDir.exists()) { + outputDir.mkdirs(); + System.out.println("创建输出目录: " + outputDir.getAbsolutePath()); + } + + // 执行分析,使用星型接线方式(0) + System.out.println("\n开始执行电能质量分析(星型接线)..."); + long startTime = System.currentTimeMillis(); + + CompareWaveDTO result = compareWaveServiceImpl.analyzeAndCompareWithStreams( + sourceCfgStream, + sourceDatStream, + targetCfgStream, + targetDatStream, + // 接线方式: 0=星型接线, 1=V型接线 + 0 + ); + + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + // 输出分析结果 + System.out.println("========================================"); + System.out.println("分析完成!"); + System.out.println("总耗时: " + duration + " ms (" + String.format("%.2f", duration / 1000.0) + " 秒)"); + + + + + System.out.println("========================================"); + System.out.println("流式文件分析测试完成!"); + System.out.println("========================================"); + + } + } + + /** + * 检查文件是否存在 + */ + private void checkFileExists(String filePath, String description) { + File file = new File(filePath); + if (!file.exists()) { + System.err.println("警告: " + description + " 不存在: " + filePath); + System.err.println("请确保文件路径正确,或修改测试中的文件路径"); + } else { + System.out.println(description + " 存在: " + filePath); + System.out.println(" 文件大小: " + file.length() + " bytes"); + } + } + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 860a5344..7c447a0e 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,9 @@ 2.3.12.RELEASE + UTF-8 + UTF-8 + UTF-8 @@ -65,6 +68,7 @@ 1.8 1.8 + UTF-8 diff --git a/tools/pom.xml b/tools/pom.xml index 74c6f39f..68751820 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -17,9 +17,7 @@ report-generator - - - + wave-comtrade \ No newline at end of file diff --git a/tools/wave-comtrade/pom.xml b/tools/wave-comtrade/pom.xml new file mode 100644 index 00000000..c043c4dd --- /dev/null +++ b/tools/wave-comtrade/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + + com.njcn.gather + tools + 1.0.0 + + + wave-comtrade + COMTRADE波形文件处理工具 + 专业的COMTRADE格式波形文件读写、解析和转换工具 + + + + + + com.njcn + njcn-common + 0.0.1 + + + + com.njcn + spingboot2.3.12 + 2.3.12 + + + + + com.alibaba + fastjson + 1.2.83 + + + + + org.apache.commons + commons-math3 + 3.6.1 + + + + junit + junit + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.3.12.RELEASE + + + + \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/config/PowerQualityConfig.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/config/PowerQualityConfig.java new file mode 100644 index 00000000..06cd4e87 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/config/PowerQualityConfig.java @@ -0,0 +1,67 @@ +package com.njcn.gather.tools.comtrade.comparewave.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 电能质量分析系统配置类 + * + *

通过Spring Boot配置文件(application.yml)读取电能质量分析相关的配置参数, + * 包括标称值、阈值、读取配置、计算配置和输出配置等。

+ * + *

配置前缀:power-quality

+ * + * @author hongawen + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "power-quality") +public class PowerQualityConfig { + + /** 数据读取配置:包含数据读取相关的参数 */ + private Reading reading; + + /** 计算配置:包含电能质量计算相关的参数 */ + private Calculation calculation; + + /** + * 数据读取配置类 + * 定义数据读取相关的配置参数 + */ + @Data + public static class Reading { + /** 数据编码格式 */ + private String encoding; + } + + /** + * 计算配置类 + * 定义电能质量计算相关的配置参数 + */ + @Data + public static class Calculation { + /** 采样配置 */ + private Sampling sampling; + + /** 谐波分析次数,通常为50次 */ + private Integer harmonicTimes; + + /** 是否合成IB相电流(IB = -(IA + IC)) */ + private Boolean ibAdd; + + /** 是否合成线电压谐波 */ + private Boolean uharmAdd; + + /** + * 采样配置类 + * 定义采样相关的参数 + */ + @Data + public static class Sampling { + /** 默认采样率,每周波采样点数 */ + private Integer defaultRate; + } + } + +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/config/WaveAnalysisConfig.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/config/WaveAnalysisConfig.java new file mode 100644 index 00000000..82fafc12 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/config/WaveAnalysisConfig.java @@ -0,0 +1,31 @@ +package com.njcn.gather.tools.comtrade.comparewave.config; + +import com.njcn.gather.tools.comtrade.comparewave.core.algorithm.FFTProcessor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 波形分析相关的Spring配置 + * + * @author hongawen + * @version 1.0 + * @date 2025/9/2 + */ +@Slf4j +@Configuration +public class WaveAnalysisConfig { + + /** + * FFT处理器Bean + * 统一管理FFT处理器的初始化 + */ + @Bean + public FFTProcessor fftProcessor() { + log.info("初始化FFT处理器..."); + FFTProcessor processor = new FFTProcessor(); + processor.init(); + log.info("FFT处理器初始化完成"); + return processor; + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/Constants.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/Constants.java new file mode 100644 index 00000000..5a7b3558 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/Constants.java @@ -0,0 +1,111 @@ +package com.njcn.gather.tools.comtrade.comparewave.core; + +/** + * 系统常量定义接口 + *

对应C代码:pq.h中的宏定义

+ * + * @author Claude Code + * @since 1.0 + */ +public interface Constants { + + /** + * 数学常量PI - 使用float确保与C代码一致 + */ + float PI = 3.1415926535897932384626433832795f; + + /** + * 最大通道数 + */ + int MAX_CH_NUM = 6; + + /** + * 最大数据长度 + */ + int MAX_DATA_LEN = 20971520; + + /** + * FFT最大点数 + */ + int FFT_MAX_N = 4096; + + /** + * FFT 10周期点数 + */ + int FFT_POINT_10_CYCLE = 2048; + + /** + * FFT点数的2次幂 + */ + int POW2_OF_FFT_POINT = 11; + + /** + * FFT计算点数 + */ + int FFT_CALC_POINT = 1024; + + /** + * 谐波数量 + */ + int HARM_NUM = 512; + + /** + * 最大谐波次数 + */ + int MAX_HARM_TIMES = 127; + + /** + * 默认采样率 + */ + int DEFAULT_SAMPLE_RATE = 256; + + /** + * 最大采样率 + */ + int MAX_SAMPLE_RATE = 512; + + /** + * 计算精度常量 + */ + float CAL_XS = 100.0f; + + /** + * 每分钟秒数 + */ + long SEC_PER_MIN = 60L; + + /** + * 每小时秒数 + */ + long SEC_PER_HOUR = SEC_PER_MIN * 60L; + + /** + * 每天秒数 + */ + long SEC_PER_DAY = SEC_PER_HOUR * 24L; + + /** + * 每年(365天)秒数 + */ + long SEC_PER_365_DAY = SEC_PER_DAY * 365L; + + /** + * 矢量输入类型标识 + */ + int IN_TYPE_VECTOR = 0; + + /** + * 实数输入类型标识 + */ + int IN_TYPE_REAL = 1; + + /** + * 全DFT输出类型标识 + */ + int OUT_TYPE_FULL = 0; + + /** + * 半DFT输出类型标识 + */ + int OUT_TYPE_HALF = 1; +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/FFTProcessor.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/FFTProcessor.java new file mode 100644 index 00000000..15dec01d --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/FFTProcessor.java @@ -0,0 +1,465 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.algorithm; + +import com.njcn.gather.tools.comtrade.comparewave.core.model.Complex; +import lombok.extern.slf4j.Slf4j; + +import static com.njcn.gather.tools.comtrade.comparewave.core.Constants.*; + + +/** + * FFT算法处理器 + *

对应C代码:pqs_fft_lib.c

+ *

重要:所有算法实现必须与C代码完全一致!

+ * + * @author hongawen + * @since 1.0 + */ +@Slf4j +public class FFTProcessor { + + /** FFT旋转因子数组 - 使用float与C代码保持一致 */ + private static float[] FFT_W = new float[FFT_MAX_N * 2]; + private static float[] FFT_W_65536 = new float[65536 * 2]; + + /** DFT旋转因子数组 - 使用float与C代码保持一致 */ + private static float[] DFT_W_5 = new float[5 * 2]; + private static float[] DFT_W_25 = new float[25 * 2]; + private static float[] DFT_W_125 = new float[125 * 2]; + private static float[] DFT_W_800 = new float[800 * 2]; + private static float[] DFT_W_1280 = new float[1280 * 2]; + private static float[] DFT_W_2000 = new float[2000 * 2]; + private static float[] DFT_W_2048 = new float[2048 * 2]; + private static float[] DFT_W_1024 = new float[1024 * 2]; + private static float[] DFT_W_2560 = new float[2560 * 2]; + private static float[] DFT_W_5120 = new float[5120 * 2]; + + /** 临时数组(与C代码对应的静态数组) */ + private static Complex[] FFT_xk1 = new Complex[FFT_MAX_N]; + private static Complex[] FFT_xk2 = new Complex[FFT_MAX_N]; + + /** 初始化标志 */ + private static boolean initialized = false; + + static { + // 静态初始化复数数组 + for (int i = 0; i < FFT_MAX_N; i++) { + FFT_xk1[i] = new Complex(); + FFT_xk2[i] = new Complex(); + } + } + + /** + * FFT初始化方法 + *

对应C代码:FFT_Init

+ *

初始化FFT和DFT所需的旋转因子数组

+ */ + public static void init() { + if (initialized) { + return; + } + + // 初始化FFT旋转因子W + for (int i = 0; i < FFT_MAX_N; i++) { + FFT_W[i * 2] = FloatMath.cos(2 * PI * i / FFT_MAX_N); + FFT_W[i * 2 + 1] = -FloatMath.sin(2 * PI * i / FFT_MAX_N); + } + + for (int i = 0; i < 65536; i++) { + FFT_W_65536[i * 2] = FloatMath.cos(2 * PI * i / 65536); + FFT_W_65536[i * 2 + 1] = -FloatMath.sin(2 * PI * i / 65536); + } + + // 初始化DFT旋转因子W + for (int i = 0; i < 5; i++) { + DFT_W_5[i * 2] = FloatMath.cos(2 * PI * i / 5); + DFT_W_5[i * 2 + 1] = -FloatMath.sin(2 * PI * i / 5); + } + + for (int i = 0; i < 25; i++) { + DFT_W_25[i * 2] = FloatMath.cos(2 * PI * i / 25); + DFT_W_25[i * 2 + 1] = -FloatMath.sin(2 * PI * i / 25); + } + + for (int i = 0; i < 125; i++) { + DFT_W_125[i * 2] = FloatMath.cos(2 * PI * i / 125); + DFT_W_125[i * 2 + 1] = -FloatMath.sin(2 * PI * i / 125); + } + + for (int i = 0; i < 800; i++) { + DFT_W_800[i * 2] = FloatMath.cos(2 * PI * i / 800); + DFT_W_800[i * 2 + 1] = -FloatMath.sin(2 * PI * i / 800); + } + + for (int i = 0; i < 1280; i++) { + DFT_W_1280[i * 2] = FloatMath.cos(2 * PI * i / 1280); + DFT_W_1280[i * 2 + 1] = -FloatMath.sin(2 * PI * i / 1280); + } + + for (int i = 0; i < 2000; i++) { + DFT_W_2000[i * 2] = FloatMath.cos(2 * PI * i / 2000); + DFT_W_2000[i * 2 + 1] = -FloatMath.sin(2 * PI * i / 2000); + } + + for (int i = 0; i < 1024; i++) { + DFT_W_1024[i * 2] = FloatMath.cos(2 * PI * i / 1024); + DFT_W_1024[i * 2 + 1] = -FloatMath.sin(2 * PI * i / 1024); + } + + for (int i = 0; i < 2048; i++) { + DFT_W_2048[i * 2] = FloatMath.cos(2 * PI * i / 2048); + DFT_W_2048[i * 2 + 1] = -FloatMath.sin(2 * PI * i / 2048); + } + + for (int i = 0; i < 2560; i++) { + DFT_W_2560[i * 2] = FloatMath.cos(2 * PI * i / 2560); + DFT_W_2560[i * 2 + 1] = -FloatMath.sin(2 * PI * i / 2560); + } + + for (int i = 0; i < 5120; i++) { + DFT_W_5120[i * 2] = FloatMath.cos(2 * PI * i / 5120); + DFT_W_5120[i * 2 + 1] = -FloatMath.sin(2 * PI * i / 5120); + } + + initialized = true; + } + + /** + * 主FFT计算函数 + *

对应C代码:FFT_Cal

+ * + * @param xk 复数数组,输入输出数据 + * @param N FFT点数 + */ + public static void fftCal(Complex[] xk, int N) { + if (!initialized) { + init(); + } + + switch (N) { + case 800: + fft800Cal(xk); + break; + case 1280: + fft1280Cal(xk); + break; + case 2000: + fft2000Cal(xk); + break; + case 2560: + fft2560Cal(xk); + break; + case 5120: + fft5120Cal(xk); + break; + default: + log.warn("Unsupported FFT size: {}", N); + break; + } + } + + /** + * 2560点FFT计算 + *

对应C代码:FFT_2560_Cal

+ *

使用混合基FFT算法,分解为512×5

+ * + * @param xk 复数数组,输入输出数据 + */ + private static void fft2560Cal(Complex[] xk) { + int SAMPLE_p = 512; + int SAMPLE_q = 5; + Complex[][] xk_pq = new Complex[512][5]; + Complex[] xk_temp = new Complex[512]; + + // 初始化 + for (int i = 0; i < 512; i++) { + for (int j = 0; j < 5; j++) { + xk_pq[i][j] = new Complex(); + } + xk_temp[i] = new Complex(); + } + + // 步骤1: p组q点DFT + for (int i = 0; i < SAMPLE_p; i++) { + for (int j = 0; j < SAMPLE_q; j++) { + int k = j * SAMPLE_p + i; + xk_pq[i][j].setReal(xk[k].getReal()); + xk_pq[i][j].setImag(xk[k].getImag()); + } + xkDft(xk_pq[i], SAMPLE_q, 1, 0); + } + + // 步骤2: 旋转因子W(k,N)相乘 + for (int i = 0; i < SAMPLE_p; i++) { + for (int j = 0; j < SAMPLE_q; j++) { + float cosValue = DFT_W_2560[i * j * 2]; + float sinValue = DFT_W_2560[i * j * 2 + 1]; + float realPart = xk_pq[i][j].getReal(); + float imagPart = xk_pq[i][j].getImag(); + xk_pq[i][j].setReal(cosValue * realPart - sinValue * imagPart); + xk_pq[i][j].setImag(cosValue * imagPart + sinValue * realPart); + } + } + + // 步骤3: q组p点DFT + for (int i = 0; i < SAMPLE_q; i++) { + for (int j = 0; j < SAMPLE_p; j++) { + xk_temp[j] = xk_pq[j][i]; + } + xkFft(xk_temp, SAMPLE_p, 0, 1); + for (int j = 0; j < (SAMPLE_p + 1) / 2; j++) { + int k = SAMPLE_q * j + i; + xk[k].setReal((float)(xk_temp[j].getReal() / (2560 / 2 * 1.4142135))); + xk[k].setImag((float)(xk_temp[j].getImag() / (2560 / 2 * 1.4142135))); + } + } + } + + /** + * 5120点FFT计算 + *

对应C代码:FFT_5120_Cal

+ *

使用混合基FFT算法,分解为1024×5

+ * + * @param xk 复数数组,输入输出数据 + */ + private static void fft5120Cal(Complex[] xk) { + int SAMPLE_p = 1024; + int SAMPLE_q = 5; + Complex[][] xk_pq = new Complex[1024][5]; + Complex[] xk_temp = new Complex[1024]; + + // 初始化 + for (int i = 0; i < 1024; i++) { + for (int j = 0; j < 5; j++) { + xk_pq[i][j] = new Complex(); + } + xk_temp[i] = new Complex(); + } + + // 步骤1: p组q点DFT + for (int i = 0; i < SAMPLE_p; i++) { + for (int j = 0; j < SAMPLE_q; j++) { + int k = j * SAMPLE_p + i; + xk_pq[i][j].setReal(xk[k].getReal()); + xk_pq[i][j].setImag(xk[k].getImag()); + } + xkDft(xk_pq[i], SAMPLE_q, 1, 0); + } + + // 步骤2: 旋转因子W(k,N)相乘 + for (int i = 0; i < SAMPLE_p; i++) { + for (int j = 0; j < SAMPLE_q; j++) { + float cosValue = DFT_W_5120[i * j * 2]; + float sinValue = DFT_W_5120[i * j * 2 + 1]; + float realPart = xk_pq[i][j].getReal(); + float imagPart = xk_pq[i][j].getImag(); + xk_pq[i][j].setReal(cosValue * realPart - sinValue * imagPart); + xk_pq[i][j].setImag(cosValue * imagPart + sinValue * realPart); + } + } + + // 步骤3: q组p点DFT + for (int i = 0; i < SAMPLE_q; i++) { + for (int j = 0; j < SAMPLE_p; j++) { + xk_temp[j] = xk_pq[j][i]; + } + xkFft(xk_temp, SAMPLE_p, 0, 1); + for (int j = 0; j < (SAMPLE_p + 1) / 2; j++) { + int k = SAMPLE_q * j + i; + xk[k].setReal((float)(xk_temp[j].getReal() / (5120 / 2 * 1.4142135))); + xk[k].setImag((float)(xk_temp[j].getImag() / (5120 / 2 * 1.4142135))); + } + } + } + + /** 800点FFT计算实现 */ + private static void fft800Cal(Complex[] xk) { + // 实现800点FFT + } + + /** 1280点FFT计算实现 */ + private static void fft1280Cal(Complex[] xk) { + // TODO: 实现1280点FFT + } + + /** 2000点FFT计算实现 */ + private static void fft2000Cal(Complex[] xk) { + // TODO: 实现2000点FFT + } + + /** + * DFT计算 + *

对应C代码:XK_DFT

+ * + * @param xk 复数数组,输入输出数据 + * @param N DFT点数 + * @param inType 输入类型(0:复数 1:实数) + * @param outType 输出类型(0:全谱 1:半谱) + */ + private static void xkDft(Complex[] xk, int N, int inType, int outType) { + Complex[] dftXk = new Complex[800]; + for (int i = 0; i < 800; i++) { + dftXk[i] = new Complex(); + } + + float[] dftW = null; + switch (N) { + case 5: + dftW = DFT_W_5; + break; + case 25: + dftW = DFT_W_25; + break; + case 800: + dftW = DFT_W_800; + break; + case 1024: + dftW = DFT_W_1024; + break; + case 2048: + dftW = DFT_W_2048; + break; + default: + return; + } + + int xkNum = (outType == 1) ? (N + 1) / 2 : N; + + if (inType == 1) { + // 实数输入 + for (int k = 0; k < xkNum; k++) { + dftXk[k].setReal(0); + dftXk[k].setImag(0); + for (int n = 0; n < N; n++) { + int xsNum = ((k * n) % N) * 2; + float xsR = dftW[xsNum]; + float xsX = dftW[xsNum + 1]; + dftXk[k].setReal(dftXk[k].getReal() + xk[n].getReal() * xsR); + dftXk[k].setImag(dftXk[k].getImag() + xk[n].getReal() * xsX); + } + } + } else { + // 复数输入 + for (int k = 0; k < xkNum; k++) { + dftXk[k].setReal(0); + dftXk[k].setImag(0); + for (int n = 0; n < N; n++) { + int xsNum = ((k * n) % N) * 2; + float xsR = dftW[xsNum]; + float xsX = dftW[xsNum + 1]; + dftXk[k].setReal(dftXk[k].getReal() + + (xk[n].getReal() * xsR - xk[n].getImag() * xsX)); + dftXk[k].setImag(dftXk[k].getImag() + + (xk[n].getReal() * xsX + xk[n].getImag() * xsR)); + } + } + } + + // 复制结果 + for (int k = 0; k < xkNum; k++) { + xk[k].setReal(dftXk[k].getReal()); + xk[k].setImag(dftXk[k].getImag()); + } + } + + /** + * 基2 FFT计算 + *

对应C代码:XK_FFT

+ *

使用Cooley-Tukey基2 FFT算法

+ * + * @param xk 复数数组,输入输出数据 + * @param N FFT点数 + * @param inType 输入类型(0:复数 1:实数) + * @param outType 输出类型(0:全谱 1:半谱) + */ + private static void xkFft(Complex[] xk, int N, int inType, int outType) { + int num = N; + float xk0 = 0, xkn = 0; + + // 实数输入转换 + if (inType == 1) { + num >>= 1; + for (int i = 0; i < num; i++) { + float real = xk[i * 2].getReal(); + float imag = xk[i * 2 + 1].getReal(); + xk[i].setReal(real); + xk[i].setImag(imag); + xk0 += (real + imag); + xkn += (real - imag); + } + } + + // 计算蝶形级数 + int xsNum = FFT_MAX_N * 2 / num; + int M = 0; + for (int i = 2; i <= num; i *= 2) { + M++; + } + + // 位反转 + int J = num / 2; + for (int i = 1; i <= (num - 2); i++) { + if (i < J) { + // 交换xk[i]和xk[J] + Complex temp = new Complex(xk[i]); + xk[i] = new Complex(xk[J]); + xk[J] = temp; + } + int K = num / 2; + while (J >= K) { + J = J - K; + K = K / 2; + } + J = J + K; + } + + // 蝶形计算 + for (int L = 1; L <= M; L++) { + int B = 1 << (L - 1); + for (int j = 0; j < B; j++) { + int P = xsNum * j * (1 << (M - L)); + float xsR = FFT_W[P]; + float xsX = (float)FFT_W[P + 1]; + for (int K = j; K < num; K += (B * 2)) { + float tr = xk[K + B].getReal() * xsR - xk[K + B].getImag() * xsX; + float tx = xk[K + B].getImag() * xsR + xk[K + B].getReal() * xsX; + xk[K + B].setReal(xk[K].getReal() - tr); + xk[K + B].setImag(xk[K].getImag() - tx); + xk[K].setReal(xk[K].getReal() + tr); + xk[K].setImag(xk[K].getImag() + tx); + } + } + } + + // 实数输入的后处理 + if (inType == 1) { + xsNum = xsNum >> 1; + for (int k = 1; k < num; k++) { + FFT_xk1[k].setReal((xk[k].getReal() + xk[num - k].getReal()) * 0.5f); + FFT_xk1[k].setImag((xk[k].getImag() - xk[num - k].getImag()) * 0.5f); + FFT_xk2[k].setReal((xk[k].getImag() + xk[num - k].getImag()) * 0.5f); + FFT_xk2[k].setImag(-(xk[k].getReal() - xk[num - k].getReal()) * 0.5f); + } + + for (int k = 1; k < num; k++) { + float cosValue = FFT_W[k * xsNum]; + float sinValue = FFT_W[k * xsNum + 1]; + float tr = cosValue * FFT_xk2[k].getReal() - sinValue * FFT_xk2[k].getImag(); + float tx = cosValue * FFT_xk2[k].getImag() + sinValue * FFT_xk2[k].getReal(); + xk[k].setReal(tr + FFT_xk1[k].getReal()); + xk[k].setImag(tx + FFT_xk1[k].getImag()); + } + + xk[0].setReal(xk0); + xk[0].setImag(0); + + if (outType == 0) { + xk[num].setReal(xkn); + xk[num].setImag(0); + for (int k = 1; k < num; k++) { + xk[num * 2 - k].setReal(xk[k].getReal()); + xk[num * 2 - k].setImag(-xk[k].getImag()); + } + } + } + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/FloatMath.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/FloatMath.java new file mode 100644 index 00000000..807ddffd --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/FloatMath.java @@ -0,0 +1,149 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.algorithm; + +/** + * 提供float版本的数学函数,避免double精度带来的误差 + * 确保与C代码(使用float)保持完全一致的精度 + * @author hongawen + */ +public class FloatMath { + + /** + * float版本的平方根函数 + */ + public static float sqrt(float value) { + return (float)Math.sqrt(value); + } + + /** + * float版本的正弦函数 + */ + public static float sin(float angle) { + return (float)Math.sin(angle); + } + + /** + * float版本的余弦函数 + */ + public static float cos(float angle) { + return (float)Math.cos(angle); + } + + /** + * float版本的正切函数 + */ + public static float tan(float angle) { + return (float)Math.tan(angle); + } + + /** + * float版本的反正切函数(两个参数) + */ + public static float atan2(float y, float x) { + return (float)Math.atan2(y, x); + } + + /** + * float版本的反正切函数(一个参数) + */ + public static float atan(float value) { + return (float)Math.atan(value); + } + + /** + * float版本的反正弦函数 + */ + public static float asin(float value) { + return (float)Math.asin(value); + } + + /** + * float版本的反余弦函数 + */ + public static float acos(float value) { + return (float)Math.acos(value); + } + + /** + * float版本的指数函数 + */ + public static float exp(float value) { + return (float)Math.exp(value); + } + + /** + * float版本的对数函数 + */ + public static float log(float value) { + return (float)Math.log(value); + } + + /** + * float版本的以10为底的对数函数 + */ + public static float log10(float value) { + return (float)Math.log10(value); + } + + /** + * float版本的幂函数 + */ + public static float pow(float base, float exponent) { + return (float)Math.pow(base, exponent); + } + + /** + * float版本的绝对值函数 + */ + public static float abs(float value) { + return Math.abs(value); + } + + /** + * float版本的向上取整函数 + */ + public static float ceil(float value) { + return (float)Math.ceil(value); + } + + /** + * float版本的向下取整函数 + */ + public static float floor(float value) { + return (float)Math.floor(value); + } + + /** + * float版本的四舍五入函数 + */ + public static float round(float value) { + return Math.round(value); + } + + /** + * float版本的最大值函数 + */ + public static float max(float a, float b) { + return Math.max(a, b); + } + + /** + * float版本的最小值函数 + */ + public static float min(float a, float b) { + return Math.min(a, b); + } + + /** + * 将弧度转换为角度 + */ + public static float toDegrees(float radians) { + return (float)Math.toDegrees(radians); + } + + /** + * 将角度转换为弧度 + */ + public static float toRadians(float degrees) { + return (float)Math.toRadians(degrees); + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/LibraryFunctions.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/LibraryFunctions.java new file mode 100644 index 00000000..73d470b8 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/LibraryFunctions.java @@ -0,0 +1,281 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.algorithm; + +import com.njcn.gather.tools.comtrade.comparewave.core.model.ClockStruct; +import com.njcn.gather.tools.comtrade.comparewave.core.model.DataPq; +import lombok.extern.slf4j.Slf4j; + +import static com.njcn.gather.tools.comtrade.comparewave.core.Constants.*; +import static com.njcn.gather.tools.comtrade.comparewave.core.algorithm.FloatMath.*; + + +/** + * 基础库函数 + *

对应C代码:lib.c

+ *

重要:所有计算必须与C代码完全一致

+ *

提供电能质量分析所需的基础数学计算和工具函数

+ * + * @author hongawen + * @since 1.0 + */ +@Slf4j +public class LibraryFunctions { + + /** 月份天数查找表 */ + private static final int[][] DATE_IN_MONTH = { + {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, + {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} + }; + + /** + * 计算有效值(RMS) + *

对应C代码:rms_cal

+ *

使用滑动窗口计算数据的均方根值

+ * + * @param in 输入数据数组 + * @param out 输出结果数组 + * @param smp 滑动窗口大小 + * @param len 数据长度 + * @return 计算状态码(0表示成功) + */ + public static int rmsCal(float[] in, float[] out, int smp, int len) { + float data; + + // 从第一个完整窗口开始计算 + for (int i = (smp - 1); i < len; i++) { + data = 0; + for (int j = 0; j < smp; j++) { + data += in[i - j] * in[i - j]; + } + out[i] = sqrt(data / smp); + } + + // 用第一个有效值填充前面的位置 + for (int i = 0; i < (smp - 1); i++) { + out[i] = out[smp - 1]; + } + + return 0; + } + + /** + * 判断是否为闰年 + *

对应C代码:look_leap_year

+ *

按照公历闰年规则:4年一闰,百年不闰,四百年又闰

+ * + * @param year 年份 + * @return 是否为闰年 + */ + public static boolean lookLeapYear(int year) { + boolean leapYear = false; + + if ((year % 4) == 0) { + if ((year % 100) == 0) { + if (year % 400 == 0) { + leapYear = true; + } + } else { + leapYear = true; + } + } + + return leapYear; + } + + /** + * 时钟转换为秒 + *

对应C代码:clock_to_second

+ *

将指定日期时间转换为从基准年份开始的秒数

+ * + * @param year 年份 + * @param month 月份 + * @param day 日期 + * @param hour 小时 + * @param minute 分钟 + * @param second 秒数 + * @param sinceYear 基准年份 + * @return 总秒数 + */ + public static int clockToSecond(int year, int month, int day, + int hour, int minute, int second, int sinceYear) { + int leap; + int secs = 0; + int year0 = year; + + // 计算年份差值对应的秒数 + while (year0 > sinceYear) { + year0--; + leap = lookLeapYear(year0) ? 1 : 0; + secs += (int)(SEC_PER_365_DAY + leap * SEC_PER_DAY); + } + + // 计算月份对应的秒数 + leap = lookLeapYear(year) ? 1 : 0; + if (month > 0) { + month--; + } + while (month > 0) { + secs += (int)(SEC_PER_DAY * DATE_IN_MONTH[leap][month - 1]); + month--; + } + + // 计算日期对应的秒数 + if (day > 0) { + day--; + } + secs += (int)(day * SEC_PER_DAY); + + // 计算小时对应的秒数 + secs += (int)(hour * SEC_PER_HOUR); + + // 计算分钟对应的秒数 + secs += (int)(minute * SEC_PER_MIN); + + secs += second; + + return secs; + } + + /** + * 秒转换为时钟 + *

对应C代码:second_to_clock

+ *

将秒数转换为时间结构体,包含北京时间校正

+ * + * @param sec 秒数 + * @return 时间结构体 + */ + public static ClockStruct secondToClock(int sec) { + ClockStruct clock = new ClockStruct(); + + // 调整为北京时区(UTC+8) + long lt = sec - 8 * 60 * 60; + + // 使用Java Calendar进行时间解析 + java.util.Date date = new java.util.Date(lt * 1000L); + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.setTime(date); + + clock.setYear(cal.get(java.util.Calendar.YEAR)); + clock.setMonth(cal.get(java.util.Calendar.MONTH) + 1); + clock.setDay(cal.get(java.util.Calendar.DAY_OF_MONTH)); + clock.setHour(cal.get(java.util.Calendar.HOUR_OF_DAY)); + clock.setMinute(cal.get(java.util.Calendar.MINUTE)); + clock.setSecond(cal.get(java.util.Calendar.SECOND)); + + return clock; + } + + /** + * 生成测试采样数据 + *

对应C代码:smp_test_data_init

+ *

生成标准的正弦波测试数据,包含基波和谐波分量

+ * + * @param dataBuf 数据缓冲区 + */ + public static void smpTestDataInit(DataPq dataBuf) { + float r, ang, wt, t, freq; + + // 初始化测试数据的幅值和相位数组 + float[][] testDataRms = new float[6][50]; + float[][] testDataAng = new float[6][50]; + int[][] smvTestData = new int[2560][6]; + int smvTestDatNum = 2560; + int smpRate = 256; + + // 清零所有数组元素 + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 50; j++) { + testDataRms[i][j] = 0; + testDataAng[i][j] = 0; + } + } + + // 设置三相电压电流的初始相位 + testDataAng[0][0] = 0; + testDataAng[1][0] = -120; + testDataAng[2][0] = 120; + testDataAng[3][0] = 0; + testDataAng[4][0] = -120; + testDataAng[5][0] = 120; + + // 设置基波幅值 + testDataRms[0][0] = 50.000f * CAL_XS; + testDataRms[1][0] = 50.000f * CAL_XS; + testDataRms[2][0] = 50.000f * CAL_XS; + testDataRms[3][0] = 5f * CAL_XS; + testDataRms[4][0] = 5f * CAL_XS; + testDataRms[5][0] = 5f * CAL_XS; + + // 设置谐波分量幅值(1%的基波) + for (int i = 0; i < 6; i++) { + for (int j = 1; j < 50; j++) { + testDataRms[i][j] = (float)(testDataRms[i][0] * 0.01); + testDataAng[i][j] = testDataAng[i][0]; + } + } + + // 根据频域参数生成时域采样数据 + freq = 50.0f; + for (int i = 0; i < 6; i++) { + for (int j = 0; j < smvTestDatNum; j++) { + r = 0; + for (int k = 0; k < 50; k++) { + ang = testDataAng[i][k] * 2.0f * PI / 360.0f; + t = (float)j / (50.0f * smpRate); + wt = 2.0f * PI * freq * (k + 1) * t + ang; + r += sin(wt) * 1.41421356f * testDataRms[i][k]; + } + smvTestData[j][i] = (int)r; + } + } + + // 将生成的数据填入缓冲区 + dataBuf.setSmpNum(0); + dataBuf.setSmpRate(256); + dataBuf.getCfg().setHarmTime(50); + dataBuf.setF(50); + dataBuf.setUn(100.0f); + + for (int j = 0; j < smvTestDatNum; j++) { + for (int i = 0; i < 6; i++) { + dataBuf.getSmpData()[i][j] = smvTestData[j][i]; + } + dataBuf.setSmpNum(dataBuf.getSmpNum() + 1); + } + + for (int i = 0; i < 6; i++) { + dataBuf.getUiGainXs()[i] = 0.01f; + } + } + + /** + * 计算频率(从采样数据) + *

对应C代码:smp_to_freq

+ *

使用过零检测法从采样数据中提取信号频率

+ * + * @param data 采样数据数组 + * @param smpRate 采样率 + * @param smpNum 数据点数 + * @return 信号频率(Hz) + */ + public static float smpToFreq(int[] data, int smpRate, int smpNum) { + // 使用过零检测算法计算频率 + int zeroCrossings = 0; + int lastSign = 0; + + for (int i = 1; i < smpNum; i++) { + int currentSign = data[i] >= 0 ? 1 : -1; + if (i > 0) { + int prevSign = data[i-1] >= 0 ? 1 : -1; + if (prevSign != currentSign) { + zeroCrossings++; + } + } + } + + // 根据过零次数和时间窗口计算频率 + float timeWindow = (float)smpNum / smpRate; + float freq = zeroCrossings / (2.0f * timeWindow); + + return freq; + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/PowerQualityCalculator.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/PowerQualityCalculator.java new file mode 100644 index 00000000..e563ace6 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/PowerQualityCalculator.java @@ -0,0 +1,993 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.algorithm; + +import com.njcn.gather.tools.comtrade.comparewave.core.model.ClockStruct; +import com.njcn.gather.tools.comtrade.comparewave.core.model.Complex; +import com.njcn.gather.tools.comtrade.comparewave.core.model.DataPq; +import com.njcn.gather.tools.comtrade.comparewave.core.model.PqsDataStruct; +import lombok.extern.slf4j.Slf4j; + +import static com.njcn.gather.tools.comtrade.comparewave.core.Constants.*; +import static com.njcn.gather.tools.comtrade.comparewave.core.algorithm.FloatMath.*; + + +/** + * 电能质量计算器 + * 对应C代码:cal.c + * + * 极其重要:必须严格按照C代码的计算顺序和方法! + * @author hongawen + */ +@Slf4j +public class PowerQualityCalculator { + + /** + * FFT结果数据,对应C代码的全局变量 + * 多通道复数数据存储,用于存储各通道的FFT变换结果 + */ + private static Complex[][] fftXkData = new Complex[MAX_CH_NUM][5120 + 1024]; + + /** + * IB相FFT结果数据 + * 用于存储合成IB相的FFT变换结果 + */ + private static Complex[] fftXkDataIb = new Complex[5120 + 1024]; + + static { + // 初始化复数数组 + for (int i = 0; i < MAX_CH_NUM; i++) { + for (int j = 0; j < 5120 + 1024; j++) { + fftXkData[i][j] = new Complex(); + } + } + for (int i = 0; i < 5120 + 1024; i++) { + fftXkDataIb[i] = new Complex(); + } + } + + /** + * 200ms数据计算 + * 对应C代码:pqs_200ms_data_cal + */ + public static void pqs200msDataCal(DataPq pqsBuf, ClockStruct dataTime, int smpWrPoint) { + log.debug("开始200ms计算 - 接线方式: {} ({})", + pqsBuf.getCfg().getLineConfig(), + pqsBuf.getCfg().getLineConfig() == 0 ? "星型" : "V型"); + + /* + * 局部变量声明(严格对应C代码) + * 为确保与原C代码计算结果的一致性,这里的变量声明完全按照C代码结构 + */ + + // FFT和采样相关变量 + int[][] fftSmpdataBuf = new int[8][5120 + 1024]; + Complex[] xkData; + Complex[] vec = new Complex[8]; + Complex seq = new Complex(); + PqsDataStruct pqsDat; + long intRms; + long[] intUi = new long[3]; + int divNum; + int pqsHarmTimes; + int smpPoint; + + // 通道索引和计算参数 + int a, b, c, flagAddIb, flagAddUharm; + int i, j, k, ch, chNum, num, smpdata; + int uaIdx, ubIdx, ucIdx, iaIdx, ibIdx, icIdx; + int FFT_N_NUM, curFreq; + float smpRate; + + // 谐波计算相关变量 + float[] harmRmsVal = new float[6]; + float harmA, harmB, harmC; + float frA, fxA, frB, fxB, frC, fxC; + float ftemp, ffreq, rms, ang, con; + float harmRms, iharmRms, Un, Ux, uiXs; + float ang1Ref = 0, uiMkVal = 0; + float[] oharm = new float[8]; + float[] eharm = new float[8]; + boolean ang1Flg = false; + boolean[] angFlg = new boolean[128]; + + // 功率计算相关变量 + float[] S = new float[5]; + float[] P = new float[5]; + float[] Q = new float[5]; + float[] STotal = new float[5]; + float[] PTotal = new float[5]; + float[] QTotal = new float[5]; + float fw; + float[] fwu = new float[4]; + float[] fws = new float[4]; + float fvar; + float[] fvaru = new float[4]; + float[] fvars = new float[4]; + float fva; + float[] fvau = new float[4]; + float[] fvas = new float[4]; + float diffIuAng = 0; + float[] angRef = new float[128]; + float tempA, tempB, tempV = 0; + + // 初始化vec数组 + for (i = 0; i < 8; i++) { + vec[i] = new Complex(); + } + + // 清零标志数组 + for (i = 0; i < 128; i++) { + angFlg[i] = false; + } + + smpRate = pqsBuf.getSmpRate(); + pqsDat = pqsBuf.getPqData()[pqsBuf.getDataPoint()]; + + /* + * 数据准备阶段 + * smpRate是每周波采样点数,乘以10得到10周波(200ms)的采样点数 + * 计算采样起始点,确保获取完整的10周波数据 + */ + smpPoint = smpWrPoint + pqsBuf.getSmpNum() - (int)(pqsBuf.getSmpRate() * 10); + if (smpPoint >= pqsBuf.getSmpNum()) { + smpPoint -= pqsBuf.getSmpNum(); + } + if (smpPoint < 0) { + smpPoint += pqsBuf.getSmpNum(); + } + + // 通道索引映射:UA、UB、UC、IA、IB、IC + uaIdx = 0; + ubIdx = 1; + ucIdx = 2; + iaIdx = 3; + ibIdx = 4; + icIdx = 5; + + // FFT参数设置:10个周波的采样点数 + FFT_N_NUM = (int)smpRate * 10; + divNum = 1; + + // 标志位设置:是否合成IB相 + flagAddIb = 0; + if (pqsBuf.getCfg().getIbAdd() == 1) { + flagAddIb = 1; + } + + // 标志位设置:是否合成线电压谐波 + flagAddUharm = 0; + if (pqsBuf.getCfg().getUharmAdd() == 1) { + flagAddUharm = 1; + } + + // 复制采样数据 + for (i = 0; i < FFT_N_NUM; i++) { + // 添加边界检查 - 使用smpNum而不是数组长度 + if (smpPoint < 0 || smpPoint >= pqsBuf.getSmpNum()) { + throw new ArrayIndexOutOfBoundsException( + String.format("smpPoint越界: smpPoint=%d, smpNum=%d, i=%d, FFT_N_NUM=%d, smpRate=%f, smpWrPoint=%d", + smpPoint, pqsBuf.getSmpNum(), i, FFT_N_NUM, pqsBuf.getSmpRate(), smpWrPoint)); + } + + fftSmpdataBuf[0][i] = pqsBuf.getSmpData()[uaIdx][smpPoint]; + fftSmpdataBuf[1][i] = pqsBuf.getSmpData()[ubIdx][smpPoint]; + fftSmpdataBuf[2][i] = pqsBuf.getSmpData()[ucIdx][smpPoint]; + fftSmpdataBuf[3][i] = pqsBuf.getSmpData()[iaIdx][smpPoint]; + fftSmpdataBuf[4][i] = pqsBuf.getSmpData()[ibIdx][smpPoint]; + fftSmpdataBuf[5][i] = pqsBuf.getSmpData()[icIdx][smpPoint]; + + if (flagAddIb == 1) { + fftSmpdataBuf[4][i] = (0 - fftSmpdataBuf[3][i]); + fftSmpdataBuf[4][i] += (0 - fftSmpdataBuf[5][i]); + } + + smpPoint = smpPoint + divNum; + if (smpPoint >= pqsBuf.getSmpNum()) { + smpPoint -= pqsBuf.getSmpNum(); + } + } + + // 谐波计算次数 + pqsHarmTimes = pqsBuf.getCfg().getHarmTime(); + + // 断言:谐波次数必须大于0,这样确保uiMkVal会被初始化 + // C代码假设pqsHarmTimes至少为1,否则uiMkVal未初始化 + assert pqsHarmTimes > 0 : "谐波计算次数必须大于0"; + + // 运行时检查(即使断言未启用也会执行) + if (pqsHarmTimes <= 0) { + throw new IllegalArgumentException("谐波计算次数必须大于0,当前值:" + pqsHarmTimes); + } + + // FFT计算 + for (i = 0; i < MAX_CH_NUM; i++) { + smpPoint = smpWrPoint + pqsBuf.getSmpNum() - (int)(pqsBuf.getSmpRate() * 10); + if (smpPoint >= pqsBuf.getSmpNum()) { + smpPoint -= pqsBuf.getSmpNum(); + } + // 注意:C代码这里看起来有bug,div_num总是1 + // C代码中divNum计算结果总是1:(int)pqsBuf.getSmpRate() / (int)pqsBuf.getSmpRate() = 1 + divNum = 1; + + for (j = 0; j < FFT_N_NUM; j++) { + fftXkData[i][j].setReal((float)pqsBuf.getSmpData()[i][smpPoint]); + fftXkData[i][j].setImag(0); + smpPoint = smpPoint + divNum; + if (smpPoint >= pqsBuf.getSmpNum()) { + smpPoint -= pqsBuf.getSmpNum(); + } + } + + FFTProcessor.fftCal(fftXkData[i], FFT_N_NUM); + } + + // 谐波分析 + for (ch = 0; ch < MAX_CH_NUM; ch++) { + uiXs = pqsBuf.getUiGainXs()[ch]; + xkData = fftXkData[ch]; + + // 合成IB相 + if ((flagAddIb == 1) && (ch == 4)) { + for (i = 0; i < FFT_N_NUM; i++) { + fftXkDataIb[i].setReal(0 - (fftXkData[3][i].getReal() + fftXkData[5][i].getReal())); + fftXkDataIb[i].setImag(0 - (fftXkData[3][i].getImag() + fftXkData[5][i].getImag())); + } + xkData = fftXkDataIb; + } + + // 计算基波矢量,对应基波频点(索引10) + vec[ch].setReal(xkData[10].getReal() * uiXs); + vec[ch].setImag(xkData[10].getImag() * uiXs); + + /* + * 谐波分析计算 + * 采用子组方式计算谐波有效值,包含主频点及其邻近频点 + */ + harmRms = 0; + iharmRms = 0; + for (i = 10, num = 0; num < pqsHarmTimes; i += 10, num++) { + /* + * 谐波有效值计算(子组方式) + * 包含主频点(i)及其左右邻近频点(i-1, i+1)的能量 + */ + rms = sqrt( + xkData[i].getReal() * xkData[i].getReal() + xkData[i].getImag() * xkData[i].getImag() + + xkData[i - 1].getReal() * xkData[i - 1].getReal() + xkData[i - 1].getImag() * xkData[i - 1].getImag() + + xkData[i + 1].getReal() * xkData[i + 1].getReal() + xkData[i + 1].getImag() * xkData[i + 1].getImag() + ) * uiXs; + + if (num > 0) { + harmRms += (rms * rms); + } + + /* + * 谐波相位检测 + * 根据通道类型设置检测门限:电压通道和线电压使用Un,电流通道使用In + */ + if ((ch < 3) || (ch == 6)) { + uiMkVal = 0.001f * pqsBuf.getUn(); + } else { + uiMkVal = 0.001f * pqsBuf.getIn(); + } + + if (rms > uiMkVal) { + // 计算谐波相位角度,转换为度数 + ang = 360.0f * atan2(xkData[i].getImag(), xkData[i].getReal()) / (2 * PI); + + // 电流通道进行相位同步修正 + if (ch >= 3) { + ang = ang - diffIuAng * (num + 1); + } + + // 设置相位基准(仅对基波进行一次) + if ((!ang1Flg) && (num == 0)) { + ang1Ref = ang; + ang1Flg = true; + } + + // 相位基准校正和90度偏移校正 + ang = ang - ang1Ref * (num + 1) - num * 90; + } else { + ang = 0; + } + + // 将角度限制在-180到180度范围内 + while (ang > 180) { + ang = ang - 360; + } + while (ang < -180) { + ang = ang + 360; + } + + // 存储谐波有效值和相位 + pqsDat.getFuHarm()[ch][num] = rms; + pqsDat.getFuHarmPhase()[ch][num] = ang; + + /* + * 谐波含有率计算 + * 设置基波门限:电压通道0.5V,电流通道0.01A + */ + if ((ch < 3) || (ch == 6)) { + uiMkVal = 0.500f; + } else { + uiMkVal = 0.010f; + } + + // 计算谐波含有率(谐波/基波 * 100%) + if (pqsDat.getFuHarm()[ch][0] > uiMkVal) { + con = pqsDat.getFuHarm()[ch][num] * 100 / pqsDat.getFuHarm()[ch][0]; + } else { + con = 0; + } + if (con > 300) { + con = 300; + } + pqsDat.getFuHarmCON()[ch][num] = con; + + /* + * 间谐波有效值计算 + * 统计谐波频点周围的非整次谐波分量能量 + */ + rms = 0; + for (j = 2; j < 9; j++) { + ftemp = xkData[i - j].getReal() * xkData[i - j].getReal() + + xkData[i - j].getImag() * xkData[i - j].getImag(); + + // 根据通道类型设置不同的门限值 + if ((ftemp > 0.003) && (i < 3)) { + rms += ftemp; + } + if ((ftemp > 0.001) && (i >= 3)) { + rms += ftemp; + } + } + pqsDat.getInHarm()[ch][num] = sqrt(rms) * uiXs; + iharmRms += (pqsDat.getInHarm()[ch][num] * pqsDat.getInHarm()[ch][num]); + + // 计算间谐波含有率 + if (pqsDat.getFuHarm()[ch][0] > uiMkVal) { + con = pqsDat.getInHarm()[ch][num] * 100 / pqsDat.getFuHarm()[ch][0]; + } else { + con = 0; + } + if (con > 300) { + con = 300; + } + pqsDat.getInHarmCON()[ch][num] = con; + } + + /* + * 总谐波畸变率计算 + * THD = 所有谐波RMS / 基波RMS * 100% + */ + pqsDat.getIHarmRMS()[ch] = sqrt(iharmRms); + pqsDat.getHarmRMS()[ch] = sqrt(harmRms); + + // 计算总谐波畸变率THD + if (pqsDat.getFuHarm()[ch][0] > uiMkVal) { + con = pqsDat.getHarmRMS()[ch] * 100 / pqsDat.getFuHarm()[ch][0]; + } else { + con = 0; + } + if (con > 300) { + con = 300; + } + pqsDat.getHarmTHD()[ch] = con; + } + + /* + * 有效值计算(6通道相电压和相电流) + * 采用时域均方根计算方法 + */ + for (ch = 0; ch < MAX_CH_NUM; ch++) { + uiXs = pqsBuf.getUiGainXs()[ch]; + intRms = 0; + for (i = 0; i < FFT_N_NUM; i++) { + smpdata = fftSmpdataBuf[ch][i]; + intRms += (((long)smpdata) * smpdata); + } + rms = sqrt((float)intRms / FFT_N_NUM); + rms = rms * uiXs; + pqsDat.getRms()[ch] = rms; + } + + /* + * 线电压计算(UAB、UBC、UCA) + * 线电压 = 相电压差值的有效值 + */ + for (i = 0; i < 3; i++) { + intUi[i] = 0; + } + for (i = 0; i < FFT_N_NUM; i++) { + // 读取三相电压采样数据 + // UA相电压 + a = fftSmpdataBuf[0][i]; + // UB相电压 + b = fftSmpdataBuf[1][i]; + // UC相电压 + c = fftSmpdataBuf[2][i]; + + // 计算线电压:UAB = UA - UB + smpdata = (a - b); + intUi[0] += (((long)smpdata) * smpdata); + + // 计算线电压:UBC = UB - UC + smpdata = (b - c); + intUi[1] += (((long)smpdata) * smpdata); + + // 计算线电压:UCA = UC - UA + smpdata = (c - a); + intUi[2] += (((long)smpdata) * smpdata); + } + + // 存储线电压有效值 + // UAB线电压 + rms = sqrt(intUi[0] / FFT_N_NUM) * pqsBuf.getUiGainXs()[0]; + pqsDat.getRms()[6] = rms; + // UBC线电压 + rms = sqrt(intUi[1] / FFT_N_NUM) * pqsBuf.getUiGainXs()[1]; + pqsDat.getRms()[7] = rms; + // UCA线电压 + rms = sqrt(intUi[2] / FFT_N_NUM) * pqsBuf.getUiGainXs()[2]; + pqsDat.getRms()[8] = rms; + + // V型接线特殊处理:线电压直接使用相电压值 + if (pqsBuf.getCfg().getLineConfig() == 1) { + pqsDat.getRms()[6] = pqsDat.getRms()[0]; + pqsDat.getRms()[7] = pqsDat.getRms()[1]; + pqsDat.getRms()[8] = pqsDat.getRms()[2]; + log.debug("V型接线 - 线电压RMS值已更新"); + } else { + log.debug("星型接线 - 保持原始线电压RMS值"); + } + + // 电压偏差计算 + uDevCal(pqsBuf, pqsDat); + + // 序分量计算 + sequenceComponentCal(pqsDat, vec); + + // 功率计算 + powerCalculation(pqsDat, pqsBuf); + + // 线电压谐波计算 + if (flagAddUharm == 1) { + lineVoltageHarmonicCal(pqsDat, pqsHarmTimes, pqsBuf); + } + + // 奇偶次谐波畸变率计算 + oddEvenHarmonicCal(pqsDat, pqsHarmTimes, pqsBuf); + + /* + * 频率计算 + * 使用A相电压数据进行频率检测 + */ + ftemp = LibraryFunctions.smpToFreq( + pqsBuf.getSmpData()[0], + (int)smpRate, + (int)(smpRate * 10) + ); + // 实际频率值 + pqsDat.getFreq()[0] = ftemp; + // 频率偏差(相对于50Hz基准) + pqsDat.getFreq()[1] = ftemp - 50; + + // 更新数据指针和时间戳 + pqsBuf.setDataPoint(pqsBuf.getDataPoint() + 1); + pqsDat.setClocktime(dataTime); + } + + /** + * 电压偏差计算 + * 对应C代码:U_Dev_cal + */ + private static void uDevCal(DataPq pqsBuf, PqsDataStruct pqsDat) { + int ch, rmsC; + float Ux, Un, Udin; + + // 额定电压Udin + Ux = pqsBuf.getUn(); + if ((Ux < 10) || (Ux > 500)) { + Ux = 100; + } + + // 根据接线方式计算相电压标准值 + if (pqsBuf.getCfg().getLineConfig() == 0) { + // 星型接线:相电压 = 线电压 / √3 + Un = Ux * 0.57735f; + } else { + // V型接线:相电压 = 线电压 + Un = Ux; + } + + // 电压偏差计算 + for (ch = 0; ch < 6; ch++) { + if (ch < 3) { + rmsC = ch + 0; + Udin = Un; + } else { + rmsC = ch + 3; + Udin = Ux; + } + + // 正偏差和负偏差RMS记录 + if (pqsDat.getRms()[rmsC] >= Udin) { + pqsDat.getRmsPos()[ch] = pqsDat.getRms()[rmsC]; + } else { + pqsDat.getRmsPos()[ch] = Udin; + } + + if (pqsDat.getRms()[rmsC] <= Udin) { + pqsDat.getRmsNeg()[ch] = pqsDat.getRms()[rmsC]; + } else { + pqsDat.getRmsNeg()[ch] = Udin; + } + + pqsDat.getUDev()[ch] = 0; + pqsDat.getUPosDev()[ch] = 0; + pqsDat.getUNegDev()[ch] = 0; + + if (pqsDat.getRms()[rmsC] > 0.5) { + pqsDat.getUDev()[ch] = (pqsDat.getRms()[rmsC] - Udin) * 100 / Udin; + } + if (pqsDat.getRmsPos()[ch] > 0.5) { + pqsDat.getUPosDev()[ch] = (pqsDat.getRmsPos()[ch] - Udin) * 100 / Udin; + } + if (pqsDat.getRmsNeg()[ch] > 0.5) { + pqsDat.getUNegDev()[ch] = (Udin - pqsDat.getRmsNeg()[ch]) * 100 / Udin; + } + } + } + + /** + * 序分量计算 + * 对应C代码中的序分量计算部分 + */ + private static void sequenceComponentCal(PqsDataStruct pqsDat, Complex[] vec) { + Complex seq = new Complex(); + float rms; + + /* + * 电压序分量计算 + * 零序分量:u0 = (ua + ub + uc) / 3 + */ + seq.setReal(vec[0].getReal() + vec[1].getReal() + vec[2].getReal()); + seq.setImag(vec[0].getImag() + vec[1].getImag() + vec[2].getImag()); + rms = sqrt(seq.getReal() * seq.getReal() + seq.getImag() * seq.getImag()) * (1.0f / 3.0f); + pqsDat.getUiSeq()[0][0] = rms; + + /* + * 正序分量:u1 = (ua + ub*a + uc*a²) / 3 + * 其中a = e^(j2π/3) = -0.5 + j0.866 + */ + seq.setReal(vec[0].getReal() + + (vec[1].getReal() * (-0.5f) - vec[1].getImag() * 0.866f) + + (vec[2].getReal() * (-0.5f) - vec[2].getImag() * (-0.866f))); + seq.setImag(vec[0].getImag() + + (vec[1].getImag() * (-0.5f) + vec[1].getReal() * 0.866f) + + (vec[2].getImag() * (-0.5f) + vec[2].getReal() * (-0.866f))); + rms = sqrt(seq.getReal() * seq.getReal() + seq.getImag() * seq.getImag()) * (1.0f / 3.0f); + pqsDat.getUiSeq()[0][1] = rms; + + /* + * 负序分量:u2 = (ua + uc*a + ub*a²) / 3 + * 交换ub和uc的位置 + */ + seq.setReal(vec[0].getReal() + + (vec[2].getReal() * (-0.5f) - vec[2].getImag() * 0.866f) + + (vec[1].getReal() * (-0.5f) - vec[1].getImag() * (-0.866f))); + seq.setImag(vec[0].getImag() + + (vec[2].getImag() * (-0.5f) + vec[2].getReal() * 0.866f) + + (vec[1].getImag() * (-0.5f) + vec[1].getReal() * (-0.866f))); + rms = sqrt(seq.getReal() * seq.getReal() + seq.getImag() * seq.getImag()) * (1.0f / 3.0f); + pqsDat.getUiSeq()[0][2] = rms; + + // 不平衡度 + pqsDat.getUiSeq()[0][3] = 0; + pqsDat.getUiSeq()[0][4] = 0; + if (pqsDat.getUiSeq()[0][1] > 0.5) { + pqsDat.getUiSeq()[0][3] = pqsDat.getUiSeq()[0][0] * 100 / pqsDat.getUiSeq()[0][1]; + pqsDat.getUiSeq()[0][4] = pqsDat.getUiSeq()[0][2] * 100 / pqsDat.getUiSeq()[0][1]; + if (pqsDat.getUiSeq()[0][3] > 1000) { + pqsDat.getUiSeq()[0][3] = 100; + } + if (pqsDat.getUiSeq()[0][4] > 1000) { + pqsDat.getUiSeq()[0][4] = 100; + } + } + + // 电流序分量(类似计算) + // 零序分量 + seq.setReal(vec[3].getReal() + vec[4].getReal() + vec[5].getReal()); + seq.setImag(vec[3].getImag() + vec[4].getImag() + vec[5].getImag()); + rms = sqrt(seq.getReal() * seq.getReal() + seq.getImag() * seq.getImag()) * (1.0f / 3.0f); + pqsDat.getUiSeq()[1][0] = rms; + + // 正序分量 + seq.setReal(vec[3].getReal() + + (vec[4].getReal() * (-0.5f) - vec[4].getImag() * 0.866f) + + (vec[5].getReal() * (-0.5f) - vec[5].getImag() * (-0.866f))); + seq.setImag(vec[3].getImag() + + (vec[4].getImag() * (-0.5f) + vec[4].getReal() * 0.866f) + + (vec[5].getImag() * (-0.5f) + vec[5].getReal() * (-0.866f))); + rms = sqrt(seq.getReal() * seq.getReal() + seq.getImag() * seq.getImag()) * (1.0f / 3.0f); + pqsDat.getUiSeq()[1][1] = rms; + + // 负序分量 + seq.setReal(vec[3].getReal() + + (vec[5].getReal() * (-0.5f) - vec[5].getImag() * 0.866f) + + (vec[4].getReal() * (-0.5f) - vec[4].getImag() * (-0.866f))); + seq.setImag(vec[3].getImag() + + (vec[5].getImag() * (-0.5f) + vec[5].getReal() * 0.866f) + + (vec[4].getImag() * (-0.5f) + vec[4].getReal() * (-0.866f))); + rms = sqrt(seq.getReal() * seq.getReal() + seq.getImag() * seq.getImag()) * (1.0f / 3.0f); + pqsDat.getUiSeq()[1][2] = rms; + + // 不平衡度 + pqsDat.getUiSeq()[1][3] = 0; + pqsDat.getUiSeq()[1][4] = 0; + if (pqsDat.getUiSeq()[1][1] > 0.1) { + pqsDat.getUiSeq()[1][3] = pqsDat.getUiSeq()[1][0] * 100 / pqsDat.getUiSeq()[1][1]; + pqsDat.getUiSeq()[1][4] = pqsDat.getUiSeq()[1][2] * 100 / pqsDat.getUiSeq()[1][1]; + if (pqsDat.getUiSeq()[1][3] > 1000) { + pqsDat.getUiSeq()[1][3] = 100; + } + if (pqsDat.getUiSeq()[1][4] > 1000) { + pqsDat.getUiSeq()[1][4] = 100; + } + } + } + + /** + * 功率计算 + */ + private static void powerCalculation(PqsDataStruct pqsDat, DataPq pqsBuf) { + float[] S = new float[5]; + float[] P = new float[5]; + float[] Q = new float[5]; + float[] STotal = new float[5]; + float[] PTotal = new float[5]; + float[] QTotal = new float[5]; + int pqsHarmTimes = pqsBuf.getCfg().getHarmTime(); + + // 清零总功率 + for (int j = 0; j < 4; j++) { + STotal[j] = 0; + PTotal[j] = 0; + QTotal[j] = 0; + } + + // 各次谐波功率计算 + for (int i = 0; i < pqsHarmTimes; i++) { + S[0] = pqsDat.getFuHarm()[0][i] * pqsDat.getFuHarm()[3][i]; + S[1] = pqsDat.getFuHarm()[1][i] * pqsDat.getFuHarm()[4][i]; + S[2] = pqsDat.getFuHarm()[2][i] * pqsDat.getFuHarm()[5][i]; + // 三相总视在功率 + S[3] = S[0] + S[1] + S[2]; + + // 根据接线方式计算功率 + if (pqsBuf.getCfg().getLineConfig() == 0) { + P[0] = S[0] * cos((PI / 180) * (pqsDat.getFuHarmPhase()[0][i] - pqsDat.getFuHarmPhase()[3][i])); + P[1] = S[1] * cos((PI / 180) * (pqsDat.getFuHarmPhase()[1][i] - pqsDat.getFuHarmPhase()[4][i])); + P[2] = S[2] * cos((PI / 180) * (pqsDat.getFuHarmPhase()[2][i] - pqsDat.getFuHarmPhase()[5][i])); + P[3] = P[0] + P[1] + P[2]; + + Q[0] = S[0] * sin((PI / 180) * (pqsDat.getFuHarmPhase()[0][i] - pqsDat.getFuHarmPhase()[3][i])); + Q[1] = S[1] * sin((PI / 180) * (pqsDat.getFuHarmPhase()[1][i] - pqsDat.getFuHarmPhase()[4][i])); + Q[2] = S[2] * sin((PI / 180) * (pqsDat.getFuHarmPhase()[2][i] - pqsDat.getFuHarmPhase()[5][i])); + Q[3] = Q[0] + Q[1] + Q[2]; + } else { + // V型接线功率计算:UAB*IA + UCB*IC + P[0] = S[0] * cos((PI / 180) * (pqsDat.getFuHarmPhase()[0][i] - pqsDat.getFuHarmPhase()[3][i])); + P[1] = 0; + P[2] = S[2] * cos((PI / 180) * (pqsDat.getFuHarmPhase()[1][i] - pqsDat.getFuHarmPhase()[5][i] - 180)); + P[3] = P[0] + P[1] + P[2]; + + Q[0] = S[0] * sin((PI / 180) * (pqsDat.getFuHarmPhase()[0][i] - pqsDat.getFuHarmPhase()[3][i])); + Q[1] = 0; + Q[2] = S[2] * sin((PI / 180) * (pqsDat.getFuHarmPhase()[1][i] - pqsDat.getFuHarmPhase()[5][i] - 180)); + Q[3] = Q[0] + Q[1] + Q[2]; + + // 视在功率重新计算 + S[1] = 0; + S[3] = sqrt((P[0] + P[2]) * (P[0] + P[2]) + (Q[0] + Q[2]) * (Q[0] + Q[2])); + } + + for (int j = 0; j < 4; j++) { + PTotal[j] += P[j]; + QTotal[j] += Q[j]; + STotal[j] += S[j]; + // 第i次谐波有功功率 + pqsDat.getHarmP()[j][i] = P[j]; + // 第i次谐波无功功率 + pqsDat.getHarmQ()[j][i] = Q[j]; + // 第i次谐波视在功率 + pqsDat.getHarmS()[j][i] = S[j]; + } + } + + for (int j = 0; j < 4; j++) { + // 各次谐波总有功功率 + pqsDat.getTotalP()[j] = PTotal[j]; + // 各次谐波总无功功率 + pqsDat.getTotalQ()[j] = QTotal[j]; + // 各次谐波总视在功率 + pqsDat.getTotalS()[j] = STotal[j]; + + // 计算功率因数:P/S + if (pqsDat.getTotalS()[j] > 0.1) { + pqsDat.getCosPF()[j] = pqsDat.getTotalP()[j] / pqsDat.getTotalS()[j]; + } else { + pqsDat.getCosPF()[j] = 1; + } + + // 计算位移功率因数:P1/S1(基波功率因数) + if (pqsDat.getHarmS()[j][0] > 0.1) { + pqsDat.getCosDF()[j] = pqsDat.getHarmP()[j][0] / pqsDat.getHarmS()[j][0]; + } else { + pqsDat.getCosDF()[j] = 1; + } + } + + // 谐波功率统计 + harmonicPowerStatistics(pqsDat, pqsHarmTimes); + } + + /** + * 谐波功率统计 + */ + private static void harmonicPowerStatistics(PqsDataStruct pqsDat, int pqsHarmTimes) { + float[] fws = new float[4]; + float[] fwu = new float[4]; + float[] fvars = new float[4]; + float[] fvaru = new float[4]; + float[] fvas = new float[4]; + float[] fvau = new float[4]; + + // 清零 + for (int i = 0; i < 4; i++) { + fws[i] = 0; + fwu[i] = 0; + fvars[i] = 0; + fvaru[i] = 0; + fvas[i] = 0; + fvau[i] = 0; + } + + // 统计各谐波功率 + for (int i = 0; i < 3; i++) { + for (int k = 1; k < pqsHarmTimes; k++) { + float fw = pqsDat.getHarmP()[i][k]; + fws[i] += fw; + if (fw < 0) { + fw = 0 - fw; + } + fwu[i] += fw; + + float fvar = pqsDat.getHarmQ()[i][k]; + fvars[i] += fvar; + if (fvar < 0) { + fvar = 0 - fvar; + } + fvaru[i] += fvar; + + float fva = pqsDat.getHarmS()[i][k]; + fvas[i] += fva; + if (fva < 0) { + fva = 0 - fva; + } + fvau[i] += fva; + } + } + + for (int i = 0; i < 3; i++) { + pqsDat.getHwTotalS()[i] = fws[i]; + pqsDat.getHwTotalU()[i] = fwu[i]; + pqsDat.getHvarTotalS()[i] = fvars[i]; + pqsDat.getHvarTotalU()[i] = fvaru[i]; + pqsDat.getHvaTotalS()[i] = fvas[i]; + pqsDat.getHvaTotalU()[i] = fvau[i]; + } + + // 总计 + pqsDat.getHwTotalS()[3] = pqsDat.getHwTotalS()[0] + pqsDat.getHwTotalS()[1] + pqsDat.getHwTotalS()[2]; + pqsDat.getHwTotalU()[3] = pqsDat.getHwTotalU()[0] + pqsDat.getHwTotalU()[1] + pqsDat.getHwTotalU()[2]; + pqsDat.getHvarTotalS()[3] = pqsDat.getHvarTotalS()[0] + pqsDat.getHvarTotalS()[1] + pqsDat.getHvarTotalS()[2]; + pqsDat.getHvarTotalU()[3] = pqsDat.getHvarTotalU()[0] + pqsDat.getHvarTotalU()[1] + pqsDat.getHvarTotalU()[2]; + pqsDat.getHvaTotalS()[3] = pqsDat.getHvaTotalS()[0] + pqsDat.getHvaTotalS()[1] + pqsDat.getHvaTotalS()[2]; + pqsDat.getHvaTotalU()[3] = pqsDat.getHvaTotalU()[0] + pqsDat.getHvaTotalU()[1] + pqsDat.getHvaTotalU()[2]; + } + + /** + * 线电压谐波计算 + */ + private static void lineVoltageHarmonicCal(PqsDataStruct pqsDat, int pqsHarmTimes, DataPq pqsBuf) { + float[] harmRmsVal = new float[6]; + float harmA, harmB, harmC; + float frA, fxA, frB, fxB, frC, fxC; + float con; + float uiMkVal = 0.100f; + int i, ch; + + for (i = 0; i < pqsHarmTimes; i++) { + // 谐波幅值 + harmA = pqsDat.getFuHarm()[0][i]; + harmB = pqsDat.getFuHarm()[1][i]; + harmC = pqsDat.getFuHarm()[2][i]; + frA = harmA * cos((PI / 180) * pqsDat.getFuHarmPhase()[0][i]); + fxA = harmA * sin((PI / 180) * pqsDat.getFuHarmPhase()[0][i]); + frB = harmB * cos((PI / 180) * pqsDat.getFuHarmPhase()[1][i]); + fxB = harmB * sin((PI / 180) * pqsDat.getFuHarmPhase()[1][i]); + frC = harmC * cos((PI / 180) * pqsDat.getFuHarmPhase()[2][i]); + fxC = harmC * sin((PI / 180) * pqsDat.getFuHarmPhase()[2][i]); + harmA = sqrt(((frA - frB) * (frA - frB)) + ((fxA - fxB) * (fxA - fxB))); + harmB = sqrt(((frB - frC) * (frB - frC)) + ((fxB - fxC) * (fxB - fxC))); + harmC = sqrt(((frC - frA) * (frC - frA)) + ((fxC - fxA) * (fxC - fxA))); + pqsDat.getPpvFuHarm()[0][i] = harmA; + pqsDat.getPpvFuHarm()[1][i] = harmB; + pqsDat.getPpvFuHarm()[2][i] = harmC; + + // 谐波含有率、谐波有效值 + for (ch = 0; ch < 3; ch++) { + if (pqsDat.getPpvFuHarm()[ch][0] > uiMkVal) { + con = pqsDat.getPpvFuHarm()[ch][i] * 100 / pqsDat.getPpvFuHarm()[ch][0]; + } else { + con = 0; + } + if (con > 300) { + con = 300; + } + pqsDat.getPpvFuHarmCON()[ch][i] = con; + if (i != 0) { + harmRmsVal[ch] += (pqsDat.getPpvFuHarm()[ch][i] * pqsDat.getPpvFuHarm()[ch][i]); + } else { + harmRmsVal[ch] = 0; + } + } + } + + // V型接线特殊处理 + if (pqsBuf.getCfg().getLineConfig() == 1) { + for (i = 0; i < pqsHarmTimes; i++) { + for (ch = 0; ch < 3; ch++) { + pqsDat.getPpvFuHarm()[ch][i] = pqsDat.getFuHarm()[ch][i]; + pqsDat.getPpvFuHarmCON()[ch][i] = pqsDat.getFuHarmCON()[ch][i]; + } + } + } + + // 谐波总有效值、总畸变率 + for (ch = 0; ch < 3; ch++) { + pqsDat.getPpvHarmRMS()[ch] = sqrt(harmRmsVal[ch]); + if (pqsDat.getPpvFuHarm()[ch][0] > uiMkVal) { + con = pqsDat.getPpvHarmRMS()[ch] * 100 / pqsDat.getPpvFuHarm()[ch][0]; + } else { + con = 0; + } + if (con > 300) { + con = 300; + } + pqsDat.getPpvHarmTHD()[ch] = con; + } + } + + /** + * 奇偶次谐波畸变率计算 + */ + private static void oddEvenHarmonicCal(PqsDataStruct pqsDat, int pqsHarmTimes, DataPq pqsBuf) { + float[] oharm = new float[6]; + float[] eharm = new float[6]; + float uiMkVal; + float fw; + + // 清零 + for (int i = 0; i < 6; i++) { + oharm[i] = 0; + eharm[i] = 0; + } + + // 相电压和电流 + for (int i = 0; i < 6; i++) { + if ((i < 3) || (i == 6)) { + uiMkVal = 0.10f; + } else { + uiMkVal = 0.01f; + } + + // 奇次 + for (int k = 1; k < (pqsHarmTimes / 2); k++) { + fw = pqsDat.getFuHarm()[i][2 * k]; + oharm[i] += fw * fw; + } + + // 偶次 + for (int k = 0; k < (pqsHarmTimes / 2); k++) { + fw = pqsDat.getFuHarm()[i][2 * k + 1]; + eharm[i] += fw * fw; + } + + if (pqsDat.getFuHarm()[i][0] > uiMkVal) { + oharm[i] = sqrt(oharm[i]) / pqsDat.getFuHarm()[i][0]; + } else { + oharm[i] = 0; + } + if (oharm[i] > 10) { + oharm[i] = 1; + } + + if (pqsDat.getFuHarm()[i][0] > uiMkVal) { + eharm[i] = sqrt(eharm[i]) / pqsDat.getFuHarm()[i][0]; + } else { + eharm[i] = 0; + } + if (eharm[i] > 10) { + eharm[i] = 1; + } + + pqsDat.getHarmOTHD()[i] = oharm[i] * 100; + pqsDat.getHarmETHD()[i] = eharm[i] * 100; + } + + // 线电压奇偶次谐波畸变率 + for (int i = 0; i < 6; i++) { + oharm[i] = 0; + eharm[i] = 0; + } + + for (int i = 0; i < 3; i++) { + if ((i < 3) || (i == 6)) { + uiMkVal = 0.10f; + } else { + uiMkVal = 0.01f; + } + + // 奇次 + for (int k = 1; k < (pqsHarmTimes / 2); k++) { + fw = pqsDat.getPpvFuHarm()[i][2 * k]; + oharm[i] += fw * fw; + } + + // 偶次 + for (int k = 0; k < (pqsHarmTimes / 2); k++) { + fw = pqsDat.getPpvFuHarm()[i][2 * k + 1]; + eharm[i] += fw * fw; + } + + if (pqsDat.getPpvFuHarm()[i][0] > uiMkVal) { + oharm[i] = sqrt(oharm[i]) / pqsDat.getPpvFuHarm()[i][0]; + } else { + oharm[i] = 0; + } + if (oharm[i] > 10) { + oharm[i] = 1; + } + + if (pqsDat.getPpvFuHarm()[i][0] > uiMkVal) { + eharm[i] = sqrt(eharm[i]) / pqsDat.getPpvFuHarm()[i][0]; + } else { + eharm[i] = 0; + } + if (eharm[i] > 10) { + eharm[i] = 1; + } + + pqsDat.getPpvHarmOTHD()[i] = oharm[i] * 100; + pqsDat.getPpvHarmETHD()[i] = eharm[i] * 100; + } + + // 相电压谐波转线电压谐波 + if (pqsBuf.getCfg().getUharmAdd() == 1) { + for (int i = 1; i < pqsHarmTimes; i++) { + for (int ch = 0; ch < 3; ch++) { + pqsDat.getFuHarm()[ch][i] = pqsDat.getPpvFuHarm()[ch][i]; + pqsDat.getFuHarmCON()[ch][i] = pqsDat.getPpvFuHarmCON()[ch][i]; + } + } + for (int ch = 0; ch < 3; ch++) { + pqsDat.getHarmRMS()[ch] = pqsDat.getPpvHarmRMS()[ch]; + pqsDat.getHarmTHD()[ch] = pqsDat.getPpvHarmTHD()[ch]; + pqsDat.getHarmOTHD()[ch] = pqsDat.getPpvHarmOTHD()[ch]; + pqsDat.getHarmETHD()[ch] = pqsDat.getPpvHarmETHD()[ch]; + } + } + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/WaveformAligner.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/WaveformAligner.java new file mode 100644 index 00000000..18a52cda --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/algorithm/WaveformAligner.java @@ -0,0 +1,446 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.algorithm; + +import com.njcn.gather.tools.comtrade.comparewave.core.model.ClockStruct; +import com.njcn.gather.tools.comtrade.comparewave.core.model.DataPq; +import lombok.extern.slf4j.Slf4j; + +/** + * 波形对齐器 + * 对应C代码:main_pro.c中的find_start_pos函数 + *

+ * 实现步骤: + * 1. 根据CFG文件的时间戳,找到两个波形中时间较晚的作为基准 + * 2. 计算时间差,转换为采样点偏移 + * 3. 从计算出的位置开始,在两个波形中分别寻找A相电压从负到正的过零点 + * 4. 以找到的过零点作为真正的对齐位置 + * 5. 从对齐位置开始进行200ms窗口的电能质量计算 + * @author hongawen + */ +@Slf4j +public class WaveformAligner { + + /** + * 对齐结果结构 + * 包含两个波形对齐后的位置信息、时间信息和计算参数 + */ + public static class AlignmentResult { + /** 第一个波形的最终对齐开始位置(过零点位置) */ + private int startPos1; + + /** 第二个波形的最终对齐开始位置(过零点位置) */ + private int startPos2; + + /** 第一个波形基于时间戳计算的初始位置 */ + private int timeStartPos1; + + /** 第二个波形基于时间戳计算的初始位置 */ + private int timeStartPos2; + + /** 第一个波形的计算开始时间 */ + private ClockStruct startTime1; + + /** 第二个波形的计算开始时间 */ + private ClockStruct startTime2; + + /** 可计算的200ms窗口数量,每个窗口包含10个周波 */ + private int calNum; + + /** 对齐操作是否成功 */ + private boolean success; + + /** 对齐失败时的错误信息 */ + private String errorMessage; + + // Getters and Setters + public int getStartPos1() { + return startPos1; + } + + public void setStartPos1(int startPos1) { + this.startPos1 = startPos1; + } + + public int getStartPos2() { + return startPos2; + } + + public void setStartPos2(int startPos2) { + this.startPos2 = startPos2; + } + + public int getTimeStartPos1() { + return timeStartPos1; + } + + public void setTimeStartPos1(int timeStartPos1) { + this.timeStartPos1 = timeStartPos1; + } + + public int getTimeStartPos2() { + return timeStartPos2; + } + + public void setTimeStartPos2(int timeStartPos2) { + this.timeStartPos2 = timeStartPos2; + } + + public ClockStruct getStartTime1() { + return startTime1; + } + + public void setStartTime1(ClockStruct startTime1) { + this.startTime1 = startTime1; + } + + public ClockStruct getStartTime2() { + return startTime2; + } + + public void setStartTime2(ClockStruct startTime2) { + this.startTime2 = startTime2; + } + + public int getCalNum() { + return calNum; + } + + public void setCalNum(int calNum) { + this.calNum = calNum; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + } + + /** + * 寻找波形对齐的起始位置 + * 对应C代码:find_start_pos函数 + * + * @param data1 第一个波形数据 + * @param data2 第二个波形数据 + * @return 对齐结果 + */ + public static AlignmentResult findStartPosition(DataPq data1, DataPq data2) { + AlignmentResult result = new AlignmentResult(); + + try { + // 获取两个波形的记录开始时间戳 + ClockStruct time1 = data1.getLbStartTime(); + ClockStruct time2 = data2.getLbStartTime(); + + if (time1 == null || time2 == null) { + result.setSuccess(false); + result.setErrorMessage("时间戳信息缺失"); + return result; + } + + log.info("波形1开始时间: {}-{}-{} {}:{}:{}.{}", + time1.getYear(), time1.getMonth(), time1.getDay(), + time1.getHour(), time1.getMinute(), time1.getSecond(), time1.getMicroSecond() / 1000); + log.info("波形2开始时间: {}-{}-{} {}:{}:{}.{}", + time2.getYear(), time2.getMonth(), time2.getDay(), + time2.getHour(), time2.getMinute(), time2.getSecond(), time2.getMicroSecond() / 1000); + + // 将时间转换为秒数(从1970年开始) + int seconds1 = LibraryFunctions.clockToSecond( + time1.getYear(), time1.getMonth(), time1.getDay(), + time1.getHour(), time1.getMinute(), time1.getSecond(), 1970); + int seconds2 = LibraryFunctions.clockToSecond( + time2.getYear(), time2.getMonth(), time2.getDay(), + time2.getHour(), time2.getMinute(), time2.getSecond(), 1970); + + /* + * 确定哪个时间更晚作为参考基准 + * ref=1表示time2更晚,ref=0表示time1更晚或相等 + */ + int ref = 0; + int diffMs = 0; + + if (seconds1 < seconds2) { + ref = 1; + } else if (seconds1 == seconds2) { + if (time1.getMicroSecond() / 1000 < time2.getMicroSecond() / 1000) { + ref = 1; + } + } + + // 计算时间差并转换为采样点偏移 + int startCalPos1, startCalPos2; + + if (ref == 1) { + // time2更晚,以time2为参考基准 + diffMs = (seconds2 - seconds1) * 1000 + + (time2.getMicroSecond() / 1000) - (time1.getMicroSecond() / 1000); + startCalPos2 = 0; + + /* + * 采样率转换:diffMs * smpRate / 20 + * 计算原理: + * - smpRate是每周波采样点数 + * - 50Hz电网每秒50个周波 + * - 每ms的采样点数 = smpRate * 50 / 1000 = smpRate / 20 + */ + startCalPos1 = (int) (diffMs * data1.getSmpRate() / 20); + } else { + // time1更晚或相等,以time1为参考基准 + diffMs = (seconds1 - seconds2) * 1000 + + (time1.getMicroSecond() / 1000) - (time2.getMicroSecond() / 1000); + startCalPos1 = 0; + startCalPos2 = (int) (diffMs * data2.getSmpRate() / 20); + } + + // 记录基于时间戳的初始位置 + result.setTimeStartPos1(startCalPos1); + result.setTimeStartPos2(startCalPos2); + + log.info("基于时间戳计算的偏移 - 波形1: {}, 波形2: {}, 时间差: {}ms", + startCalPos1, startCalPos2, diffMs); + + // 检查偏移是否超出数据范围 + if (startCalPos1 >= data1.getSmpNum() || startCalPos2 >= data2.getSmpNum()) { + result.setSuccess(false); + result.setErrorMessage("时间偏移超出数据范围"); + return result; + } + + // 在A相电压通道中寻找从负到正的过零点 + // 对应C代码中的过零点检测逻辑 + + /* + * 寻找波形1的A相电压过零点 + * 在初始位置后的2个周波范围内搜索从负到正的过零点 + */ + // 2个周波的采样点数作为搜索范围 + int searchRange1 = (int) (data1.getSmpRate() * 2); + int zeroCrossingPos1 = findZeroCrossing(data1.getSmpData()[0], startCalPos1, + Math.min(startCalPos1 + searchRange1, data1.getSmpNum() - 1)); + + if (zeroCrossingPos1 == -1) { + result.setSuccess(false); + result.setErrorMessage("波形1未找到A相电压过零点"); + return result; + } + + // 寻找波形2的A相电压过零点 + // 2个周波的采样点数作为搜索范围 + int searchRange2 = (int) (data2.getSmpRate() * 2); + int zeroCrossingPos2 = findZeroCrossing(data2.getSmpData()[0], startCalPos2, + Math.min(startCalPos2 + searchRange2, data2.getSmpNum() - 1)); + + if (zeroCrossingPos2 == -1) { + result.setSuccess(false); + result.setErrorMessage("波形2未找到A相电压过零点"); + return result; + } + + log.info("找到过零点 - 波形1: {}, 波形2: {}", zeroCrossingPos1, zeroCrossingPos2); + + // 更新起始位置为过零点位置 + startCalPos1 = zeroCrossingPos1; + startCalPos2 = zeroCrossingPos2; + + /* + * 检查两个装置时钟同步情况 + * 如果过零点位置相对于时间戳位置的偏移差异过大,需要进行调整 + * 对应C代码中的同步检查逻辑 + */ + int diff1 = startCalPos1 - result.getTimeStartPos1(); + int diff2 = startCalPos2 - result.getTimeStartPos2(); + int diffOffset = Math.abs(diff1 - diff2); + + /* + * 周波边界跨越检测和调整 + * 如果偏差超过半个周波,认为可能跨越了一个周波边界 + */ + if (diffOffset > data1.getSmpRate() / 2) { + if (diff1 > diff2) { + // 波形2需要向前移动一个周波 + startCalPos2 += (int) data2.getSmpRate(); + } else { + // 波形1需要向前移动一个周波 + startCalPos1 += (int) data1.getSmpRate(); + } + log.info("检测到周波边界跨越,调整后位置 - 波形1: {}, 波形2: {}", + startCalPos1, startCalPos2); + } + + // 最终检查位置是否有效 + if (startCalPos1 >= data1.getSmpNum() || startCalPos2 >= data2.getSmpNum()) { + result.setSuccess(false); + result.setErrorMessage("调整后的起始位置超出数据范围"); + return result; + } + + // 设置最终的起始位置 + result.setStartPos1(startCalPos1); + result.setStartPos2(startCalPos2); + + // 计算起始时间 + result.setStartTime1(calculateStartTime(time1, startCalPos1, data1.getSmpRate())); + result.setStartTime2(calculateStartTime(time2, startCalPos2, data2.getSmpRate())); + + /* + * 计算可进行计算的200ms窗口数量 + * 每个窗口需要10个周波的数据用于电能质量分析 + */ + int windowSamples = (int) (data1.getSmpRate() * 10); + int availableWindows1 = (data1.getSmpNum() - startCalPos1) / windowSamples; + int availableWindows2 = (data2.getSmpNum() - startCalPos2) / windowSamples; + int maxWindows = Math.min(availableWindows1, availableWindows2); + + // 限制最大窗口数为100(对应C代码中的MAX_DATA_NUM常量) + if (maxWindows > 100) { + maxWindows = 100; + } + + result.setCalNum(maxWindows); + + if (maxWindows <= 0) { + result.setSuccess(false); + result.setErrorMessage("数据长度不足,无法进行计算"); + return result; + } + + result.setSuccess(true); + + log.info("波形对齐完成 - 最终位置: 波形1={}, 波形2={}, 可计算窗口数: {}", + startCalPos1, startCalPos2, maxWindows); + + return result; + + } catch (Exception e) { + log.error("波形对齐过程出错", e); + result.setSuccess(false); + result.setErrorMessage("对齐过程出错: " + e.getMessage()); + return result; + } + } + + /** + * 寻找从负到正的过零点 + * 对应C代码中的过零点检测逻辑 + * + * @param voltageData 电压数据数组 + * @param startIndex 开始搜索的索引 + * @param endIndex 结束搜索的索引 + * @return 过零点位置,-1表示未找到 + */ + private static int findZeroCrossing(int[] voltageData, int startIndex, int endIndex) { + for (int i = startIndex; i < endIndex - 1; i++) { + /* + * 寻找从负值到正值的过零点 + * 注意:使用严格不等号(< 和 >),避免0值影响判断 + */ + if (voltageData[i] < 0 && voltageData[i + 1] > 0) { + // 返回过零点后的第一个正值位置 + return i + 1; + } + } + // 未找到过零点 + return -1; + } + + /** + * 计算实际的开始时间 + * + * @param baseTime 基准时间 + * @param offsetSamples 采样点偏移 + * @param sampleRate 每周波采样点数 + * @return 计算后的开始时间 + */ + private static ClockStruct calculateStartTime(ClockStruct baseTime, int offsetSamples, float sampleRate) { + ClockStruct startTime = new ClockStruct(); + + // 复制基准时间 + startTime.setYear(baseTime.getYear()); + startTime.setMonth(baseTime.getMonth()); + startTime.setDay(baseTime.getDay()); + startTime.setHour(baseTime.getHour()); + startTime.setMinute(baseTime.getMinute()); + startTime.setSecond(baseTime.getSecond()); + startTime.setMicroSecond(baseTime.getMicroSecond()); + + /* + * 计算偏移时间(毫秒) + * 计算公式:偏移时间 = 偏移采样点数 / 每周波采样点数 * 20ms + * 其中20ms是50Hz电网一个周波的时间周期 + */ + int offsetMs = (int) FloatMath.round((float) offsetSamples / sampleRate * 20.0f); + + // 将偏移时间加到基准时间的毫秒部分 + int totalMs = startTime.getMicroSecond() / 1000 + offsetMs; + + // 处理毫秒溢出 + if (totalMs >= 1000) { + int extraSeconds = totalMs / 1000; + totalMs = totalMs % 1000; + + startTime.setSecond(startTime.getSecond() + extraSeconds); + + // 处理秒溢出(进位到分钟) + if (startTime.getSecond() >= 60) { + startTime.setMinute(startTime.getMinute() + startTime.getSecond() / 60); + startTime.setSecond(startTime.getSecond() % 60); + + // 处理分钟溢出(进位到小时) + if (startTime.getMinute() >= 60) { + startTime.setHour(startTime.getHour() + startTime.getMinute() / 60); + startTime.setMinute(startTime.getMinute() % 60); + + /* + * 处理小时溢出(简化处理,不考虑日期变化) + * 实际应用中可能需要更复杂的日期处理逻辑 + */ + if (startTime.getHour() >= 24) { + startTime.setHour(startTime.getHour() % 24); + } + } + } + } + + startTime.setMicroSecond((int) (totalMs * 1000)); + + return startTime; + } + + /** + * 应用对齐结果到数据缓冲区 + * + * @param data1 第一个波形数据 + * @param data2 第二个波形数据 + * @param result 对齐结果 + */ + public static void applyAlignment(DataPq data1, DataPq data2, AlignmentResult result) { + if (!result.isSuccess()) { + log.error("无法应用对齐结果:{}", result.getErrorMessage()); + return; + } + + // 设置起始计算位置 + data1.setStartCalPos(result.getStartPos1()); + data2.setStartCalPos(result.getStartPos2()); + + // 设置起始计算时间 + data1.setStartCalTime(result.getStartTime1()); + data2.setStartCalTime(result.getStartTime2()); + + // 设置可计算的窗口数量 + data1.setCalNum(result.getCalNum()); + data2.setCalNum(result.getCalNum()); + + log.info("对齐结果已应用到数据缓冲区"); + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/io/ComtradeReader.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/io/ComtradeReader.java new file mode 100644 index 00000000..59dbd848 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/io/ComtradeReader.java @@ -0,0 +1,462 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.io; + +import com.njcn.gather.tools.comtrade.comparewave.core.model.ClockStruct; +import com.njcn.gather.tools.comtrade.comparewave.core.model.ComtradeData; +import com.njcn.gather.tools.comtrade.comparewave.core.model.DataPq; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; + +import static com.njcn.gather.tools.comtrade.comparewave.core.Constants.MAX_CH_NUM; + + +/** + * COMTRADE文件读取器 + *

对应C代码:main_pro.c中的文件读取部分

+ *

重要:必须严格按照C代码的读取逻辑

+ *

用于读取IEEE C37.111标准的COMTRADE格式文件,包括CFG配置文件和DAT数据文件

+ * + * @author hongawen + * @since 1.0 + */ +@Slf4j +public class ComtradeReader { + + /** ASCII格式文件类型常量 */ + private static final String FILE_TYPE_ASCII = "ASCII"; + + /** 二进制格式文件类型常量 */ + private static final String FILE_TYPE_BINARY = "BINARY"; + + /** + * 读取COMTRADE文件(使用InputStream) + *

从输入流中读取CFG配置文件和DAT数据文件

+ * + * @param cfgStream CFG配置文件输入流 + * @param datStream DAT数据文件输入流 + * @param dataBuf 数据缓冲区 + * @param encoding 文件编码格式 + * @return 读取是否成功 + */ + public static boolean readSampleFile(InputStream cfgStream, InputStream datStream, + DataPq dataBuf, String encoding) { + try { + // 从输入流读取CFG配置文件 + ComtradeData.CfgInfo cfgInfo = readCfgFile(cfgStream, encoding); + if (cfgInfo == null) { + log.error("Failed to read CFG from stream"); + return false; + } + + // 获取数据文件类型(ASCII或BINARY) + String dataFileType = cfgInfo.getDataFileType(); + if (dataFileType == null || dataFileType.isEmpty()) { + // CFG文件中未指定类型时使用默认值 + dataFileType = FILE_TYPE_BINARY; + log.info("数据文件类型未指定,默认使用BINARY格式"); + } + + // 从输入流读取DAT数据文件 + boolean success = readDatFileFromStream(datStream, cfgInfo, dataBuf, dataFileType); + if (!success) { + log.error("Failed to read DAT from stream"); + return false; + } + + // 处理CFG信息和数据缓冲区 + return processCfgAndData(cfgInfo, dataBuf); + + } catch (Exception e) { + log.error("Error reading COMTRADE from streams", e); + return false; + } + } + + + /** + * 处理CFG信息和数据缓冲区 + *

提取公共处理逻辑,设置采样参数和数据缓冲区

+ *

注意:此时还没有设置接线方式,需要在外部调用applyConfiguration后才能确定

+ * + * @param cfgInfo CFG配置信息 + * @param dataBuf 数据缓冲区 + * @return 处理是否成功 + */ + private static boolean processCfgAndData(ComtradeData.CfgInfo cfgInfo, DataPq dataBuf) { + try { + // 计算每周波采样点数(对应C代码中smp_rate) + float lineFreq = (float)cfgInfo.getLineFreq(); + if (lineFreq <= 0) { + lineFreq = 50.0f; + } + float samplesPerCycle = (float)cfgInfo.getSampleRate() / lineFreq; + + // 验证采样率限制要求 + if (samplesPerCycle < 128) { + log.error("采样率过低:每周波采样点数={},最小要求128", samplesPerCycle); + throw new IllegalArgumentException("采样率过低,每周波采样点数必须>=128,当前值:" + samplesPerCycle); + } + + // 限制最大采样率 + if (samplesPerCycle > 256) { + log.warn("采样率过高:每周波采样点数={},限制为256", samplesPerCycle); + samplesPerCycle = 256; + } + + dataBuf.setSmpRate(samplesPerCycle); + dataBuf.setF(lineFreq); + + // 设置录波开始时间以支持波形对齐 + dataBuf.setLbStartTime(cfgInfo.getStartTime()); + + // 额定电压电流由外部参数传入,提供更灵活的配置 + + // 输出数据读取统计信息 + log.info("数据读取完成 - 采样点数: {}, 每周波采样点数: {}, 频率: {} Hz", + dataBuf.getSmpNum(), dataBuf.getSmpRate(), dataBuf.getF()); + + // 输出部分采样点用于数据验证 + if (dataBuf.getSmpNum() > 0) { + log.debug("前10个UA采样点: {}, {}, {}, {}, {}, {}, {}, {}, {}, {}", + dataBuf.getSmpData()[0][0], dataBuf.getSmpData()[0][1], + dataBuf.getSmpData()[0][2], dataBuf.getSmpData()[0][3], + dataBuf.getSmpData()[0][4], dataBuf.getSmpData()[0][5], + dataBuf.getSmpData()[0][6], dataBuf.getSmpData()[0][7], + dataBuf.getSmpData()[0][8], dataBuf.getSmpData()[0][9]); + } + + return true; + + } catch (Exception e) { + log.error("Error processing COMTRADE data", e); + return false; + } + } + + /** + * 读取CFG配置文件(从InputStream) + *

解析COMTRADE配置文件,提取通道信息和采样参数

+ * + * @param inputStream CFG文件输入流 + * @param encoding 文件编码格式 + * @return CFG配置信息对象,解析失败返回null + */ + private static ComtradeData.CfgInfo readCfgFile(InputStream inputStream, String encoding) { + ComtradeData.CfgInfo cfgInfo = new ComtradeData.CfgInfo(); + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(inputStream, Charset.forName(encoding)))) { + + String line; + int lineNum = 0; + + // 第1行:站名,装置ID,版本年份 + line = reader.readLine(); + lineNum++; + if (line != null) { + String[] parts = line.split(","); + if (parts.length >= 2) { + cfgInfo.setStationName(parts[0].trim()); + cfgInfo.setRecordDeviceId(parts[1].trim()); + if (parts.length >= 3) { + try { + cfgInfo.setRevYear(Integer.parseInt(parts[2].trim())); + } catch (NumberFormatException e) { + cfgInfo.setRevYear(1999); + } + } + } + } + + // 第2行:通道总数,模拟通道数A,数字通道数D + line = reader.readLine(); + lineNum++; + if (line != null) { + String[] parts = line.split(","); + if (parts.length >= 3) { + cfgInfo.setTotalChannels(Integer.parseInt(parts[0].trim())); + String analogPart = parts[1].trim(); + if (analogPart.endsWith("A")) { + analogPart = analogPart.substring(0, analogPart.length() - 1); + } + cfgInfo.setAnalogChannels(Integer.parseInt(analogPart)); + + String digitalPart = parts[2].trim(); + if (digitalPart.endsWith("D")) { + digitalPart = digitalPart.substring(0, digitalPart.length() - 1); + } + cfgInfo.setDigitalChannels(Integer.parseInt(digitalPart)); + } + } + + // 读取模拟通道信息 + int analogCount = cfgInfo.getAnalogChannels(); + ComtradeData.ChannelInfo[] analogChannels = new ComtradeData.ChannelInfo[analogCount]; + + for (int i = 0; i < analogCount; i++) { + line = reader.readLine(); + lineNum++; + if (line != null) { + analogChannels[i] = parseAnalogChannel(line); + } + } + cfgInfo.setAnalogChannelInfos(analogChannels); + + // 跳过数字通道信息 + int digitalCount = cfgInfo.getDigitalChannels(); + for (int i = 0; i < digitalCount; i++) { + reader.readLine(); + lineNum++; + } + + // 读取电网频率 + line = reader.readLine(); + lineNum++; + if (line != null) { + cfgInfo.setLineFreq(Double.parseDouble(line.trim())); + } + + // 读取采样率信息 + line = reader.readLine(); + lineNum++; + if (line != null) { + cfgInfo.setNrates(Integer.parseInt(line.trim())); + } + + // 读取采样率和采样点数 + line = reader.readLine(); + lineNum++; + if (line != null) { + String[] parts = line.split(","); + if (parts.length >= 2) { + cfgInfo.setSampleRate(Double.parseDouble(parts[0].trim())); + cfgInfo.setTotalSamples(Integer.parseInt(parts[1].trim())); + } + } + + // 读取开始时间 + line = reader.readLine(); + lineNum++; + if (line != null) { + cfgInfo.setStartTime(parseDateTime(line)); + } + + // 读取触发时间 + line = reader.readLine(); + lineNum++; + if (line != null) { + cfgInfo.setTriggerTime(parseDateTime(line)); + } + + // 读取数据文件类型 + line = reader.readLine(); + lineNum++; + if (line != null) { + cfgInfo.setDataFileType(line.trim().toUpperCase()); + } + + // 读取时间倍率 + line = reader.readLine(); + lineNum++; + if (line != null) { + try { + cfgInfo.setTimeMult(Double.parseDouble(line.trim())); + } catch (NumberFormatException e) { + cfgInfo.setTimeMult(1.0); + } + } + + return cfgInfo; + + } catch (IOException e) { + log.error("Error reading CFG from stream", e); + return null; + } + } + + + /** + * 解析模拟通道信息 + */ + private static ComtradeData.ChannelInfo parseAnalogChannel(String line) { + ComtradeData.ChannelInfo channel = new ComtradeData.ChannelInfo(); + String[] parts = line.split(","); + + if (parts.length >= 13) { + channel.setChannelNumber(Integer.parseInt(parts[0].trim())); + channel.setChannelName(parts[1].trim()); + channel.setPhaseId(parts[2].trim()); + channel.setUnit(parts[4].trim()); + channel.setA(Double.parseDouble(parts[5].trim())); + channel.setB(Double.parseDouble(parts[6].trim())); + channel.setSkew(Double.parseDouble(parts[7].trim())); + channel.setMin(Integer.parseInt(parts[8].trim())); + channel.setMax(Integer.parseInt(parts[9].trim())); + channel.setPrimary(Double.parseDouble(parts[10].trim())); + channel.setSecondary(Double.parseDouble(parts[11].trim())); + channel.setPs(parts[12].trim().charAt(0)); + } + + return channel; + } + + /** + * 解析日期时间 + */ + private static ClockStruct parseDateTime(String dateTimeStr) { + ClockStruct clock = new ClockStruct(); + + // 格式: dd/mm/yyyy,hh:mm:ss.ssssss + String[] parts = dateTimeStr.split(","); + if (parts.length >= 2) { + // 解析日期 + String[] dateParts = parts[0].trim().split("/"); + if (dateParts.length >= 3) { + clock.setDay(Integer.parseInt(dateParts[0])); + clock.setMonth(Integer.parseInt(dateParts[1])); + clock.setYear(Integer.parseInt(dateParts[2])); + } + + // 解析时间 + String[] timeParts = parts[1].trim().split(":"); + if (timeParts.length >= 3) { + clock.setHour(Integer.parseInt(timeParts[0])); + clock.setMinute(Integer.parseInt(timeParts[1])); + + // 解析秒和微秒 + String[] secParts = timeParts[2].split("\\."); + clock.setSecond(Integer.parseInt(secParts[0])); + if (secParts.length > 1) { + String microStr = secParts[1]; + // 补齐到6位 + while (microStr.length() < 6) { + microStr += "0"; + } + clock.setMicroSecond(Integer.parseInt(microStr.substring(0, 6))); + } + } + } + + return clock; + } + + /** + * 读取DAT数据文件(从InputStream) + */ + private static boolean readDatFileFromStream(InputStream datStream, ComtradeData.CfgInfo cfgInfo, + DataPq dataBuf, String dataFileType) { + try { + if (FILE_TYPE_ASCII.equals(dataFileType)) { + return readAsciiDatFileFromStream(datStream, cfgInfo, dataBuf); + } else { + return readBinaryDatFileFromStream(datStream, cfgInfo, dataBuf); + } + } catch (Exception e) { + log.error("Error reading DAT from stream", e); + return false; + } + } + + + /** + * 读取ASCII格式的DAT文件(从InputStream) + */ + private static boolean readAsciiDatFileFromStream(InputStream datStream, ComtradeData.CfgInfo cfgInfo, DataPq dataBuf) + throws IOException { + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(datStream))) { + String line; + int sampleIndex = 0; + + while ((line = reader.readLine()) != null && sampleIndex < cfgInfo.getTotalSamples()) { + String[] values = line.split(","); + + // 跳过序号和时间戳(前两列) + int dataStartIndex = 2; + + // 读取模拟通道数据 + for (int ch = 0; ch < cfgInfo.getAnalogChannels() && ch < MAX_CH_NUM; ch++) { + if (dataStartIndex + ch < values.length) { + int rawValue = Integer.parseInt(values[dataStartIndex + ch].trim()); + + // 直接存储原始值,像C代码一样 + dataBuf.getSmpData()[ch][sampleIndex] = rawValue; + } + } + + sampleIndex++; + } + + dataBuf.setSmpNum(sampleIndex); + + // 设置增益系数(从CFG文件的a系数获取) + for (int ch = 0; ch < cfgInfo.getAnalogChannels() && ch < MAX_CH_NUM; ch++) { + ComtradeData.ChannelInfo chInfo = cfgInfo.getAnalogChannelInfos()[ch]; + dataBuf.getUiGainXs()[ch] = (float)chInfo.getA(); // 使用CFG文件中的a系数作为增益 + } + + log.info("ASCII DAT文件读取完成 - 读取了 {} 个采样点", sampleIndex); + + return true; + } + } + + + /** + * 读取二进制格式的DAT文件(从InputStream) + */ + private static boolean readBinaryDatFileFromStream(InputStream datStream, ComtradeData.CfgInfo cfgInfo, DataPq dataBuf) + throws IOException { + + // 计算每个采样记录的字节数 + int analogBytes = cfgInfo.getAnalogChannels() * 2; // 每个模拟通道2字节 + int digitalBytes = (cfgInfo.getDigitalChannels() + 15) / 16 * 2; // 数字通道按16位对齐 + int recordSize = 4 + 4 + analogBytes + digitalBytes; // 序号(4) + 时间戳(4) + 模拟 + 数字 + + byte[] buffer = new byte[recordSize]; + int sampleIndex = 0; + + try (DataInputStream dis = new DataInputStream(new BufferedInputStream(datStream))) { + while (sampleIndex < cfgInfo.getTotalSamples()) { + int bytesRead = dis.read(buffer, 0, recordSize); + if (bytesRead != recordSize) { + break; // 读取完毕或出错 + } + + ByteBuffer bb = ByteBuffer.wrap(buffer); + bb.order(ByteOrder.LITTLE_ENDIAN); // COMTRADE标准使用小端序 + + // 跳过序号和时间戳 + bb.getInt(); // 序号 + bb.getInt(); // 时间戳 + + // 读取模拟通道数据 + for (int ch = 0; ch < cfgInfo.getAnalogChannels() && ch < MAX_CH_NUM; ch++) { + short rawValue = bb.getShort(); + + // 直接存储原始值,像C代码一样 + dataBuf.getSmpData()[ch][sampleIndex] = rawValue; + } + + sampleIndex++; + } + } + + dataBuf.setSmpNum(sampleIndex); + + // 设置增益系数(从CFG文件的a系数获取) + for (int ch = 0; ch < cfgInfo.getAnalogChannels() && ch < MAX_CH_NUM; ch++) { + ComtradeData.ChannelInfo chInfo = cfgInfo.getAnalogChannelInfos()[ch]; + dataBuf.getUiGainXs()[ch] = (float)chInfo.getA(); // 使用CFG文件中的a系数作为增益 + } + + log.info("二进制DAT文件读取完成 - 读取了 {} 个采样点", sampleIndex); + + return true; + } + + +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/io/ResultWriter.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/io/ResultWriter.java new file mode 100644 index 00000000..35326b4c --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/io/ResultWriter.java @@ -0,0 +1,368 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.io; + +import com.njcn.gather.tools.comtrade.comparewave.core.model.PqsDataStruct; +import com.njcn.gather.tools.comtrade.comparewave.core.model.ThresholdConfig; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; + +/** + * 结果输出器 + *

对应C代码:main_pro.c中的结果输出部分

+ *

重要:输出格式必须与C代码完全一致

+ *

负责将电能质量检测数据的比较结果输出为多种格式的文件

+ * + * @author hongawen + * @since 1.0 + */ +@Slf4j +public class ResultWriter { + + /** 三位小数格式化器 */ + private static final DecimalFormat DF3 = new DecimalFormat("0.000"); + + /** 两位小数格式化器 */ + private static final DecimalFormat DF2 = new DecimalFormat("0.00"); + + /** 一位小数格式化器 */ + private static final DecimalFormat DF1 = new DecimalFormat("0.0"); + + /** + * 比较结果数据结构 + *

封装单个检测项目的比较结果信息

+ */ + public static class CompareResult { + /** 检测项目名称 */ + public String itemName; + + /** 源设备数值 */ + public double sourceValue; + + /** 被检设备数值 */ + public double targetValue; + + /** 绝对误差 */ + public double absoluteError; + + /** 相对误差 (%) */ + public double relativeError; + + /** 阈值门限 */ + public double threshold; + + /** 是否超过门限 */ + public boolean isOverLimit; + + public CompareResult(String itemName, double sourceValue, double targetValue, double threshold) { + this.itemName = itemName; + this.sourceValue = sourceValue; + this.targetValue = targetValue; + this.threshold = threshold; + this.absoluteError = Math.abs(targetValue - sourceValue); + + // 计算相对误差百分比 + if (Math.abs(sourceValue) > 0.001) { + this.relativeError = this.absoluteError * 100.0 / Math.abs(sourceValue); + } else { + // 源值过小时,设置相对误差为零以避免除零错误 + this.relativeError = 0; + } + + // 判断是否超过门限值 + this.isOverLimit = this.absoluteError > threshold; + } + } + + /** + * 写入比较结果到多个文件 + *

对应C代码中的结果输出逻辑

+ * + * @param outputPath 输出文件夹路径 + * @param sourceData 源设备数据列表 + * @param targetData 被检设备数据列表 + * @param thresholds 门限配置信息 + * @throws IOException 文件输出异常 + */ + public static void writeCompareResults(String outputPath, + List sourceData, + List targetData, + ThresholdConfig thresholds) throws IOException { + + List allResults = new ArrayList<>(); + + // 比较所有数据点 + int dataPoints = Math.min(sourceData.size(), targetData.size()); + for (int i = 0; i < dataPoints; i++) { + PqsDataStruct source = sourceData.get(i); + PqsDataStruct target = targetData.get(i); + + // 比较有效值 + compareRmsValues(allResults, source, target, thresholds, i); + + // 比较谐波 + compareHarmonics(allResults, source, target, thresholds, i); + + // 比较功率 + comparePower(allResults, source, target, thresholds, i); + + // 比较序分量 + compareSequence(allResults, source, target, thresholds, i); + + // 比较频率 + compareFrequency(allResults, source, target, thresholds, i); + } + + // 写入结果文件 + writeResultFile(outputPath + "/Result.txt", allResults); + + // 写入超限统计 + writeOverLimitSummary(outputPath + "/OverLimit.txt", allResults); + + // 写入CSV格式 + writeCsvResults(outputPath + "/diff_res_min_max.csv", allResults); + + // 写入完成标志 + writeCompleteFlag(outputPath + "/Completed.txt"); + } + + /** + * 比较有效值 + */ + private static void compareRmsValues(List results, + PqsDataStruct source, + PqsDataStruct target, + ThresholdConfig thresholds, + int dataPoint) { + + // 相电压有效值 + for (int i = 0; i < 3; i++) { + String name = String.format("第%d点_U%c_RMS", dataPoint + 1, 'A' + i); + results.add(new CompareResult(name, source.getRms()[i], target.getRms()[i], thresholds.getVoltageRmsThreshold())); + } + + // 相电流有效值 + for (int i = 0; i < 3; i++) { + String name = String.format("第%d点_I%c_RMS", dataPoint + 1, 'A' + i); + results.add(new CompareResult(name, source.getRms()[i + 3], target.getRms()[i + 3], thresholds.getCurrentRmsThreshold())); + } + + // 线电压有效值 + for (int i = 0; i < 3; i++) { + String name = String.format("第%d点_U%c%c_RMS", dataPoint + 1, + 'A' + i, 'B' + ((i + 1) % 3)); + results.add(new CompareResult(name, source.getRms()[i + 6], target.getRms()[i + 6], thresholds.getVoltageRmsThreshold())); + } + } + + /** + * 比较谐波 + */ + private static void compareHarmonics(List results, + PqsDataStruct source, + PqsDataStruct target, + ThresholdConfig thresholds, + int dataPoint) { + + // 比较前50次谐波 + for (int harm = 0; harm < 50; harm++) { + // 电压谐波 + for (int ch = 0; ch < 3; ch++) { + String name = String.format("第%d点_U%c_%d次谐波", dataPoint + 1, 'A' + ch, harm + 1); + results.add(new CompareResult(name, + source.getFuHarm()[ch][harm], + target.getFuHarm()[ch][harm], + // 电压谐波门限: 1%Un + thresholds.getHarmonicVoltageThreshold())); + } + + // 电流谐波 + for (int ch = 0; ch < 3; ch++) { + String name = String.format("第%d点_I%c_%d次谐波", dataPoint + 1, 'A' + ch, harm + 1); + results.add(new CompareResult(name, + source.getFuHarm()[ch + 3][harm], + target.getFuHarm()[ch + 3][harm], + // 电流谐波门限: 3%In + thresholds.getHarmonicCurrentThreshold())); + } + } + + // THD比较 + for (int ch = 0; ch < 6; ch++) { + String prefix = ch < 3 ? "U" : "I"; + char phase = (char)('A' + (ch % 3)); + String name = String.format("第%d点_%c%c_THD", dataPoint + 1, prefix.charAt(0), phase); + results.add(new CompareResult(name, + source.getHarmTHD()[ch], + target.getHarmTHD()[ch], + ch < 3 ? thresholds.getThdVoltageThreshold() : thresholds.getThdCurrentThreshold())); + } + } + + /** + * 比较功率 + */ + private static void comparePower(List results, + PqsDataStruct source, + PqsDataStruct target, + ThresholdConfig thresholds, + int dataPoint) { + + String[] phases = {"A", "B", "C", "总"}; + + for (int i = 0; i < 4; i++) { + // 有功功率 + String nameP = String.format("第%d点_%s_有功功率", dataPoint + 1, phases[i]); + results.add(new CompareResult(nameP, source.getTotalP()[i], target.getTotalP()[i], thresholds.getActivePowerThreshold())); + + // 无功功率 + String nameQ = String.format("第%d点_%s_无功功率", dataPoint + 1, phases[i]); + results.add(new CompareResult(nameQ, source.getTotalQ()[i], target.getTotalQ()[i], thresholds.getReactivePowerThreshold())); + + // 视在功率 + String nameS = String.format("第%d点_%s_视在功率", dataPoint + 1, phases[i]); + results.add(new CompareResult(nameS, source.getTotalS()[i], target.getTotalS()[i], thresholds.getApparentPowerThreshold())); + + // 功率因数 + String namePF = String.format("第%d点_%s_功率因数", dataPoint + 1, phases[i]); + results.add(new CompareResult(namePF, source.getCosPF()[i], target.getCosPF()[i], thresholds.getPowerFactorThreshold())); + } + } + + /** + * 比较序分量 + */ + private static void compareSequence(List results, + PqsDataStruct source, + PqsDataStruct target, + ThresholdConfig thresholds, + int dataPoint) { + + String[] seqNames = {"零序", "正序", "负序"}; + + // 电压序分量 + for (int i = 0; i < 3; i++) { + String name = String.format("第%d点_电压%s分量", dataPoint + 1, seqNames[i]); + results.add(new CompareResult(name, source.getUiSeq()[0][i], target.getUiSeq()[0][i], thresholds.getSequenceVoltageThreshold())); + } + + // 电流序分量 + for (int i = 0; i < 3; i++) { + String name = String.format("第%d点_电流%s分量", dataPoint + 1, seqNames[i]); + results.add(new CompareResult(name, source.getUiSeq()[1][i], target.getUiSeq()[1][i], thresholds.getSequenceCurrentThreshold())); + } + + // 不平衡度 + String nameVUF = String.format("第%d点_电压负序不平衡度", dataPoint + 1); + results.add(new CompareResult(nameVUF, source.getUiSeq()[0][4], target.getUiSeq()[0][4], thresholds.getUnbalanceThreshold())); + + String nameIUF = String.format("第%d点_电流负序不平衡度", dataPoint + 1); + results.add(new CompareResult(nameIUF, source.getUiSeq()[1][4], target.getUiSeq()[1][4], thresholds.getUnbalanceThreshold())); + } + + /** + * 比较频率 + */ + private static void compareFrequency(List results, + PqsDataStruct source, + PqsDataStruct target, + ThresholdConfig thresholds, + int dataPoint) { + + String nameFreq = String.format("第%d点_频率", dataPoint + 1); + results.add(new CompareResult(nameFreq, source.getFreq()[0], target.getFreq()[0], thresholds.getFrequencyThreshold())); + + String nameDev = String.format("第%d点_频率偏差", dataPoint + 1); + results.add(new CompareResult(nameDev, source.getFreq()[1], target.getFreq()[1], thresholds.getFrequencyDeviationThreshold())); + } + + /** + * 写入结果文件 + */ + private static void writeResultFile(String filePath, List results) throws IOException { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) { + writer.write("================================ 电能质量数据比较结果 ================================\n"); + writer.write("项目名称\t\t源值\t\t目标值\t\t绝对误差\t相对误差(%)\t门限\t\t是否超限\n"); + writer.write("====================================================================================\n"); + + for (CompareResult result : results) { + writer.write(String.format("%-30s\t%10s\t%10s\t%10s\t%10s\t%10s\t%s\n", + result.itemName, + DF3.format(result.sourceValue), + DF3.format(result.targetValue), + DF3.format(result.absoluteError), + DF3.format(result.relativeError), + DF3.format(result.threshold), + result.isOverLimit ? "超限" : "正常" + )); + } + + // 统计信息 + long overLimitCount = results.stream().filter(r -> r.isOverLimit).count(); + writer.write("\n====================================================================================\n"); + writer.write(String.format("总项目数: %d, 超限项目数: %d, 超限率: %.2f%%\n", + results.size(), overLimitCount, overLimitCount * 100.0 / results.size())); + } + } + + /** + * 写入超限汇总 + */ + private static void writeOverLimitSummary(String filePath, List results) throws IOException { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) { + writer.write("================================ 超限项目汇总 ================================\n"); + + results.stream() + .filter(r -> r.isOverLimit) + .forEach(r -> { + try { + writer.write(String.format("%s: 源值=%s, 目标值=%s, 误差=%s, 门限=%s\n", + r.itemName, + DF3.format(r.sourceValue), + DF3.format(r.targetValue), + DF3.format(r.absoluteError), + DF3.format(r.threshold) + )); + } catch (IOException e) { + log.error("写入超限项目失败", e); + } + }); + } + } + + /** + * 写入CSV结果 + */ + private static void writeCsvResults(String filePath, List results) throws IOException { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) { + // CSV头 + writer.write("项目名称,源值,目标值,绝对误差,相对误差(%),门限,是否超限\n"); + + for (CompareResult result : results) { + writer.write(String.format("%s,%.3f,%.3f,%.3f,%.3f,%.3f,%s\n", + result.itemName, + result.sourceValue, + result.targetValue, + result.absoluteError, + result.relativeError, + result.threshold, + result.isOverLimit ? "1" : "0" + )); + } + } + } + + /** + * 写入完成标志 + */ + private static void writeCompleteFlag(String filePath) throws IOException { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) { + writer.write("1\n"); + writer.write("比较完成时间: " + new java.util.Date() + "\n"); + } + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/ClockStruct.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/ClockStruct.java new file mode 100644 index 00000000..7bead7d8 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/ClockStruct.java @@ -0,0 +1,90 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 时间结构体 + * 对应C代码:clock_struct + * @author hongawen + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ClockStruct { + + private int year; + private int month; + private int day; + private int hour; + private int minute; + private int second; + private int microSecond; + + /** + * 格式化时间字符串 + */ + public String format() { + return String.format("%04d-%02d-%02d %02d:%02d:%02d.%06d", + year, month, day, hour, minute, second, microSecond); + } + + /** + * 从时间戳创建时间结构体 + *

时间戳单位为微秒

+ * + * @param timestamp 微秒级时间戳 + * @return 时间结构体实例 + */ + public static ClockStruct fromTimestamp(long timestamp) { + // 微秒转换为秒和微秒部分 + long totalSeconds = timestamp / 1_000_000; + int microSeconds = (int) (timestamp % 1_000_000); + + // 使用Java的时间API进行转换 + java.time.Instant instant = java.time.Instant.ofEpochSecond(totalSeconds); + java.time.LocalDateTime dateTime = java.time.LocalDateTime.ofInstant(instant, java.time.ZoneId.systemDefault()); + + return new ClockStruct( + dateTime.getYear(), + dateTime.getMonthValue(), + dateTime.getDayOfMonth(), + dateTime.getHour(), + dateTime.getMinute(), + dateTime.getSecond(), + microSeconds + ); + } + + /** + * 从毫秒时间戳创建时间结构体 + * + * @param timestampMillis 毫秒级时间戳 + * @return 时间结构体实例 + */ + public static ClockStruct fromTimestampMillis(long timestampMillis) { + return fromTimestamp(timestampMillis * 1000); + } + + /** + * 获取当前时间的时间结构体 + * + * @return 当前时间的时间结构体 + */ + public static ClockStruct now() { + java.time.LocalDateTime now = java.time.LocalDateTime.now(); + long nanoTime = System.nanoTime(); + int microSeconds = (int) ((nanoTime / 1000) % 1_000_000); + + return new ClockStruct( + now.getYear(), + now.getMonthValue(), + now.getDayOfMonth(), + now.getHour(), + now.getMinute(), + now.getSecond(), + microSeconds + ); + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/CompareWaveDTO.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/CompareWaveDTO.java new file mode 100644 index 00000000..c391b531 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/CompareWaveDTO.java @@ -0,0 +1,25 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.model; + +import lombok.Data; + +import java.util.List; + +/** + * @author hongawen + * @version 1.0 + * @data 2025/9/2 15:45 + */ +@Data +public class CompareWaveDTO { + + /** + * 标准波形文件解析结果 + */ + private List sourceResults; + /** + * 目标波形文件解析结果 + */ + private List targetResults; + + +} diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/Complex.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/Complex.java new file mode 100644 index 00000000..f6336709 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/Complex.java @@ -0,0 +1,96 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.model; + +import com.njcn.gather.tools.comtrade.comparewave.core.algorithm.FloatMath; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 复数类 + * 对应C代码:fft_vec_struct + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Complex { + + /** + * 实部 + */ + private float real; + + /** + * 虚部 + */ + private float imag; + + /** + * 复制构造函数 + */ + public Complex(Complex other) { + this.real = other.real; + this.imag = other.imag; + } + + /** + * 复数加法 + */ + public Complex add(Complex other) { + return new Complex( + this.real + other.real, + this.imag + other.imag + ); + } + + /** + * 复数减法 + */ + public Complex subtract(Complex other) { + return new Complex( + this.real - other.real, + this.imag - other.imag + ); + } + + /** + * 复数乘法 + * (a+bi)*(c+di) = (ac-bd) + (ad+bc)i + */ + public Complex multiply(Complex other) { + return new Complex( + this.real * other.real - this.imag * other.imag, + this.real * other.imag + this.imag * other.real + ); + } + + /** + * 复数乘以实数 + */ + public Complex multiply(float scalar) { + return new Complex( + this.real * scalar, + this.imag * scalar + ); + } + + /** + * 计算复数的模 + */ + public float magnitude() { + return FloatMath.sqrt(real * real + imag * imag); + } + + /** + * 计算复数的相角(弧度) + */ + public float phase() { + return FloatMath.atan2(imag, real); + } + + /** + * 计算复数的相角(度) + */ + public float phaseDegrees() { + return FloatMath.toDegrees(phase()); + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/ComtradeData.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/ComtradeData.java new file mode 100644 index 00000000..afadf570 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/ComtradeData.java @@ -0,0 +1,103 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * COMTRADE文件数据结构 + * 对应C代码中的COMTRADE相关结构 + * @author hongawen + */ +@Data +@NoArgsConstructor +public class ComtradeData { + + /** + * 通道信息 + */ + @Data + @NoArgsConstructor + public static class ChannelInfo { + private int channelNumber; // 通道号 + private String channelName; // 通道名称 + private String phaseId; // 相别标识 + private String unit; // 单位 + private double a; // 变换系数a + private double b; // 变换系数b + private double skew; // 时滞 + private int min; // 最小值 + private int max; // 最大值 + private double primary; // 一次值 + private double secondary; // 二次值 + private char ps; // P/S标识 + + /** + * 将采样值转换为实际值 + */ + public double convertToReal(int sampleValue) { + return a * sampleValue + b; + } + } + + /** + * CFG配置信息 + */ + @Data + @NoArgsConstructor + public static class CfgInfo { + private String stationName; // 站名 + private String recordDeviceId; // 装置ID + private int revYear; // 标准版本年份 + private int totalChannels; // 总通道数 + private int analogChannels; // 模拟通道数 + private int digitalChannels; // 数字通道数 + private ChannelInfo[] analogChannelInfos; // 模拟通道信息 + private ChannelInfo[] digitalChannelInfos; // 数字通道信息 + private double lineFreq; // 电网频率 + private int nrates; // 采样率数量 + private double sampleRate; // 采样率 + private int totalSamples; // 总采样点数 + private ClockStruct startTime; // 开始时间 + private ClockStruct triggerTime; // 触发时间 + private String dataFileType; // 数据文件类型(ASCII/BINARY) + private double timeMult; // 时间倍率 + } + + /** CFG配置信息 */ + private CfgInfo cfgInfo; + + /** 原始采样数据 */ + private int[][] rawData; + + /** 转换后的实际值数据 */ + private double[][] realData; + + /** 时间戳数组 */ + private long[] timestamps; + + /** + * 初始化 + */ + public void init(int channelCount, int sampleCount) { + cfgInfo = new CfgInfo(); + rawData = new int[channelCount][sampleCount]; + realData = new double[channelCount][sampleCount]; + timestamps = new long[sampleCount]; + } + + /** + * 转换所有原始数据为实际值 + */ + public void convertAllToReal() { + if (cfgInfo == null || cfgInfo.getAnalogChannelInfos() == null) { + return; + } + + for (int ch = 0; ch < cfgInfo.getAnalogChannels(); ch++) { + ChannelInfo chInfo = cfgInfo.getAnalogChannelInfos()[ch]; + for (int i = 0; i < cfgInfo.getTotalSamples(); i++) { + realData[ch][i] = chInfo.convertToReal(rawData[ch][i]); + } + } + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/ConfigStruct.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/ConfigStruct.java new file mode 100644 index 00000000..2ddab776 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/ConfigStruct.java @@ -0,0 +1,192 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 配置结构体 + *

对应C代码:cfg_struct

+ *

用于电能质量检测的配置参数设置

+ * + * @author Claude Code + * @since 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ConfigStruct { + + /** + * 线路配置类型 + *
    + *
  • 0: 星型接线
  • + *
  • 1: V型接线
  • + *
+ */ + private int lineConfig; + + /** + * 是否合成IB相 + *
    + *
  • 0: 不合成
  • + *
  • 1: 合成
  • + *
+ */ + private int ibAdd; + + /** + * 是否合成线电压谐波 + *
    + *
  • 0: 不合成
  • + *
  • 1: 合成
  • + *
+ */ + private int uharmAdd; + + /** + * 谐波分析次数 + *

默认值:50次谐波

+ */ + private int harmTime; + + /** + * 设置默认配置参数 + *

将所有配置项重置为系统默认值

+ */ + public void setDefaults() { + this.lineConfig = LineConfigType.STAR.getValue(); + this.ibAdd = SynthesisFlag.DISABLED.getValue(); + this.uharmAdd = SynthesisFlag.DISABLED.getValue(); + this.harmTime = 50; + } + + /** + * 获取线路配置类型枚举 + * + * @return 线路配置类型 + */ + public LineConfigType getLineConfigType() { + return LineConfigType.fromValue(this.lineConfig); + } + + /** + * 设置线路配置类型 + * + * @param type 线路配置类型 + */ + public void setLineConfigType(LineConfigType type) { + this.lineConfig = type.getValue(); + } + + /** + * 获取IB相合成标志 + * + * @return 合成标志 + */ + public SynthesisFlag getIbAddFlag() { + return SynthesisFlag.fromValue(this.ibAdd); + } + + /** + * 设置IB相合成标志 + * + * @param flag 合成标志 + */ + public void setIbAddFlag(SynthesisFlag flag) { + this.ibAdd = flag.getValue(); + } + + /** + * 获取线电压谐波合成标志 + * + * @return 合成标志 + */ + public SynthesisFlag getUharmAddFlag() { + return SynthesisFlag.fromValue(this.uharmAdd); + } + + /** + * 设置线电压谐波合成标志 + * + * @param flag 合成标志 + */ + public void setUharmAddFlag(SynthesisFlag flag) { + this.uharmAdd = flag.getValue(); + } + + /** + * 验证配置参数的有效性 + * + * @return 配置是否有效 + */ + public boolean isValid() { + try { + LineConfigType.fromValue(this.lineConfig); + SynthesisFlag.fromValue(this.ibAdd); + SynthesisFlag.fromValue(this.uharmAdd); + // 谐波次数范围检查 + return this.harmTime > 0 && this.harmTime <= 127; + } catch (IllegalArgumentException e) { + return false; + } + } + + /** + * 线路配置类型枚举 + */ + public enum LineConfigType { + /** 星型接线 */ + STAR(0), + /** V型接线 */ + V_TYPE(1); + + private final int value; + + LineConfigType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static LineConfigType fromValue(int value) { + for (LineConfigType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Invalid line config type: " + value); + } + } + + /** + * 合成标志枚举 + */ + public enum SynthesisFlag { + /** 禁用 */ + DISABLED(0), + /** 启用 */ + ENABLED(1); + + private final int value; + + SynthesisFlag(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static SynthesisFlag fromValue(int value) { + for (SynthesisFlag flag : values()) { + if (flag.value == value) { + return flag; + } + } + throw new IllegalArgumentException("Invalid synthesis flag: " + value); + } + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/DataPq.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/DataPq.java new file mode 100644 index 00000000..a40354e8 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/DataPq.java @@ -0,0 +1,156 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import static com.njcn.gather.tools.comtrade.comparewave.core.Constants.*; + + +/** + * 主数据缓冲区结构 + *

对应C代码:data_pq

+ *

用于存储和管理电能质量检测的采样数据和分析结果

+ * + * @author hongawen + * @since 1.0 + */ +@Data +@NoArgsConstructor +public class DataPq { + + /** 电能质量数据窗口数量 */ + private static final int PQ_DATA_WINDOW_SIZE = 100; + + /** 三相统计值数组大小 */ + private static final int THREE_PHASE_SIZE = 3; + + /** 默认系统频率 (Hz) */ + private static final float DEFAULT_FREQUENCY = 50.0f; + + /** 默认额定电压 (V) */ + private static final float DEFAULT_NOMINAL_VOLTAGE = 100.0f; + + /** 默认额定电流 (A) */ + private static final float DEFAULT_NOMINAL_CURRENT = 5.0f; + + /** + * 采样数据数组 + *

第一维:通道索引 (0 ~ MAX_CH_NUM-1)

+ *

第二维:数据索引 (0 ~ MAX_DATA_LEN-1)

+ */ + private int[][] smpData = new int[MAX_CH_NUM][MAX_DATA_LEN]; + + /** 实际采样点数 */ + private int smpNum; + + /** 采样率 (Hz) */ + private float smpRate; + + /** 系统频率 (Hz) */ + private float f; + + /** 额定电压 (V) */ + private float un; + + /** 额定电流 (A) */ + private float in; + + /** 电压电流增益系数数组 */ + private float[] uiGainXs = new float[MAX_CH_NUM]; + + /** 系统配置信息 */ + private ConfigStruct cfg; + + /** 电能质量数据数组,存储多个时间窗口的分析结果 */ + private PqsDataStruct[] pqData = new PqsDataStruct[PQ_DATA_WINDOW_SIZE]; + + /** 当前数据点位置索引 */ + private int dataPoint; + + /** 10分钟 PST (短时闪变) 统计值数组 */ + private float[] pstVal10Min = new float[THREE_PHASE_SIZE]; + + /** 10分钟 PLT (长时闪变) 统计值数组 */ + private float[] pltVal10Min = new float[THREE_PHASE_SIZE]; + + /** 10分钟 UDD (电压不平衡度) 统计值数组 */ + private float[] uddVal10Min = new float[THREE_PHASE_SIZE]; + + /** 录波开始时间,从CFG文件中读取 */ + private ClockStruct lbStartTime; + + /** 计算开始位置,波形对齐后的起始位置 */ + private int startCalPos; + + /** 计算开始时间,波形对齐后的起始时间 */ + private ClockStruct startCalTime; + + /** 可计算的200ms窗口数量 */ + private int calNum; + + /** + * 初始化数据结构 + *

设置默认值并初始化所有子结构

+ */ + public void init() { + cfg = new ConfigStruct(); + cfg.setDefaults(); + + // 初始化电能质量数据数组中的每个元素 + for (int i = 0; i < pqData.length; i++) { + pqData[i] = new PqsDataStruct(); + pqData[i].init(); + } + + // 设置采样相关的默认参数 + smpRate = DEFAULT_SAMPLE_RATE; + f = DEFAULT_FREQUENCY; + un = DEFAULT_NOMINAL_VOLTAGE; + in = DEFAULT_NOMINAL_CURRENT; + + // 初始化每个通道的增益系数为1.0 + for (int i = 0; i < MAX_CH_NUM; i++) { + uiGainXs[i] = 1.0f; + } + + dataPoint = 0; + smpNum = 0; + } + + /** + * 添加采样数据到指定通道和位置 + * + * @param channel 通道索引 (0 ~ MAX_CH_NUM-1) + * @param index 数据位置索引 (0 ~ MAX_DATA_LEN-1) + * @param value 采样值 + */ + public void addSampleData(int channel, int index, int value) { + if (channel >= 0 && channel < MAX_CH_NUM && + index >= 0 && index < MAX_DATA_LEN) { + smpData[channel][index] = value; + } + } + + /** + * 获取当前数据点位置的电能质量数据结构 + * + * @return 当前电能质量数据,如果索引无效则返回null + */ + public PqsDataStruct getCurrentPqData() { + if (dataPoint >= 0 && dataPoint < pqData.length) { + return pqData[dataPoint]; + } + return null; + } + + /** + * 移动到下一个数据点位置 + *

当超出数组范围时将循环到索引0

+ */ + public void nextDataPoint() { + dataPoint++; + if (dataPoint >= pqData.length) { + dataPoint = 0; + } + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/PqsDataStruct.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/PqsDataStruct.java new file mode 100644 index 00000000..e469222b --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/PqsDataStruct.java @@ -0,0 +1,200 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import static com.njcn.gather.tools.comtrade.comparewave.core.Constants.*; + + +/** + * 电能质量数据结构 + *

对应C代码:pqs_dat_struct

+ *

重要:所有数组维度和计算逻辑必须与C代码完全一致

+ * + * @author hongawen + * @since 1.0 + */ +@Data +@NoArgsConstructor +public class PqsDataStruct { + + /** 相电压、电流有效值数组 */ + private float[] rms = new float[9]; + + /** 正偏差有效值数组 */ + private float[] rmsPos = new float[6]; + + /** 负偏差有效值数组 */ + private float[] rmsNeg = new float[6]; + + /** 电压偏差数组 */ + private float[] uDev = new float[6]; + + /** 电压正偏差数组 */ + private float[] uPosDev = new float[6]; + + /** 电压负偏差数组 */ + private float[] uNegDev = new float[6]; + + /** 谐波有效值数组 [通道][谐波次数] (最多127次谐波) */ + private float[][] fuHarm = new float[MAX_CH_NUM][MAX_HARM_TIMES]; + + /** 谐波相位数组 [通道][谐波次数] */ + private float[][] fuHarmPhase = new float[MAX_CH_NUM][MAX_HARM_TIMES]; + + /** 谐波含有率数组 [通道][谐波次数] */ + private float[][] fuHarmCON = new float[MAX_CH_NUM][MAX_HARM_TIMES]; + + /** 间谐波有效值数组 [通道][谐波次数] */ + private float[][] inHarm = new float[MAX_CH_NUM][MAX_HARM_TIMES]; + + /** 间谐波含有率数组 [通道][谐波次数] */ + private float[][] inHarmCON = new float[MAX_CH_NUM][MAX_HARM_TIMES]; + + /** 谐波总有效值数组 */ + private float[] harmRMS = new float[MAX_CH_NUM]; + + /** 间谐波总有效值数组 */ + private float[] iHarmRMS = new float[MAX_CH_NUM]; + + /** 总谐波畸变率数组 */ + private float[] harmTHD = new float[MAX_CH_NUM]; + + /** 奇次谐波畸变率数组 */ + private float[] harmOTHD = new float[MAX_CH_NUM]; + + /** 偶次谐波畸变率数组 */ + private float[] harmETHD = new float[MAX_CH_NUM]; + + /** 线电压谐波有效值数组 [3相][谐波次数] */ + private float[][] ppvFuHarm = new float[3][MAX_HARM_TIMES]; + + /** 线电压谐波含有率数组 [3相][谐波次数] */ + private float[][] ppvFuHarmCON = new float[3][MAX_HARM_TIMES]; + + /** 线电压谐波总有效值数组 */ + private float[] ppvHarmRMS = new float[3]; + + /** 线电压总谐波畸变率数组 */ + private float[] ppvHarmTHD = new float[3]; + + /** 线电压奇次谐波畸变率数组 */ + private float[] ppvHarmOTHD = new float[3]; + + /** 线电压偶次谐波畸变率数组 */ + private float[] ppvHarmETHD = new float[3]; + + /** 电压电流序分量数组 [电压/电流][序分量类型] */ + private float[][] uiSeq = new float[2][5]; + + /** 各次谐波有功功率数组 [相别][谐波次数] */ + private float[][] harmP = new float[4][MAX_HARM_TIMES]; + + /** 各次谐波无功功率数组 [相别][谐波次数] */ + private float[][] harmQ = new float[4][MAX_HARM_TIMES]; + + /** 各次谐波视在功率数组 [相别][谐波次数] */ + private float[][] harmS = new float[4][MAX_HARM_TIMES]; + + /** 总有功功率数组 */ + private float[] totalP = new float[4]; + + /** 总无功功率数组 */ + private float[] totalQ = new float[4]; + + /** 总视在功率数组 */ + private float[] totalS = new float[4]; + + /** 功率因数数组 */ + private float[] cosPF = new float[4]; + + /** 位移功率因数数组 */ + private float[] cosDF = new float[4]; + + /** 谐波有功功率数组 (有符号) */ + private float[] hwTotalS = new float[4]; + + /** 谐波有功功率数组 (无符号) */ + private float[] hwTotalU = new float[4]; + + /** 谐波无功功率数组 (有符号) */ + private float[] hvarTotalS = new float[4]; + + /** 谐波无功功率数组 (无符号) */ + private float[] hvarTotalU = new float[4]; + + /** 谐波视在功率数组 (有符号) */ + private float[] hvaTotalS = new float[4]; + + /** 谐波视在功率数组 (无符号) */ + private float[] hvaTotalU = new float[4]; + + /** 频率和频率偏差数组 */ + private float[] freq = new float[2]; + + /** 短时闪变数组 */ + private float[] pst = new float[3]; + + /** 长时闪变数组 */ + private float[] plt = new float[3]; + + /** 电压波动数组 */ + private float[] flt = new float[3]; + + /** 数据时间戳 */ + private ClockStruct clocktime; + + /** + * 初始化数据结构 + *

创建时间戳对象,数组已在声明时自动初始化

+ */ + public void init() { + // 创建时间戳对象,数组已在声明时自动初始化为零值 + clocktime = new ClockStruct(); + } + + /** + * 复制构造函数 + *

深拷贝所有数组和对象

+ * + * @param other 要复制的源对象 + */ + public PqsDataStruct(PqsDataStruct other) { + // 深拷贝一维数组 + this.rms = other.rms.clone(); + this.rmsPos = other.rmsPos.clone(); + this.rmsNeg = other.rmsNeg.clone(); + this.uDev = other.uDev.clone(); + this.uPosDev = other.uPosDev.clone(); + this.uNegDev = other.uNegDev.clone(); + + // 深拷贝二维数组的每个子数组 + for (int i = 0; i < this.fuHarm.length; i++) { + this.fuHarm[i] = other.fuHarm[i].clone(); + this.fuHarmPhase[i] = other.fuHarmPhase[i].clone(); + this.fuHarmCON[i] = other.fuHarmCON[i].clone(); + this.inHarm[i] = other.inHarm[i].clone(); + this.inHarmCON[i] = other.inHarmCON[i].clone(); + } + + // 复制其他一维数组 + this.harmRMS = other.harmRMS.clone(); + this.iHarmRMS = other.iHarmRMS.clone(); + this.harmTHD = other.harmTHD.clone(); + this.harmOTHD = other.harmOTHD.clone(); + this.harmETHD = other.harmETHD.clone(); + + // 复制时间戳对象 + if (other.clocktime != null) { + this.clocktime = new ClockStruct( + other.clocktime.getYear(), + other.clocktime.getMonth(), + other.clocktime.getDay(), + other.clocktime.getHour(), + other.clocktime.getMinute(), + other.clocktime.getSecond(), + other.clocktime.getMicroSecond() + ); + } + } +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/ThresholdConfig.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/ThresholdConfig.java new file mode 100644 index 00000000..9af36311 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/core/model/ThresholdConfig.java @@ -0,0 +1,91 @@ +package com.njcn.gather.tools.comtrade.comparewave.core.model; + +import lombok.Builder; +import lombok.Data; + +/** + * 门限配置封装类 + *

用于传递所有门限值到结果比较方法

+ *

包含电能质量检测中各项参数的阈值配置

+ * + * @author hongawen + * @since 1.0 + */ +@Data +@Builder +public class ThresholdConfig { + /** + * 电压有效值门限 + */ + private double voltageRmsThreshold = 0.01 * 220; + + /** + * 电流有效值门限 + */ + private double currentRmsThreshold = 0.05 * 5; + + /** + * 谐波电压门限 + */ + private double harmonicVoltageThreshold = 0.01 * 220; + + /** + * 谐波电流门限 + */ + private double harmonicCurrentThreshold = 0.03 * 5; + + /** + * THD电压门限 + */ + private double thdVoltageThreshold = 0.1 * 220; + + /** + * THD电流门限 + */ + private double thdCurrentThreshold = 0.1 * 5; + + /** + * 有功功率门限 + */ + private double activePowerThreshold = 0.01; + + /** + * 无功功率门限 + */ + private double reactivePowerThreshold = 0.01; + + /** + * 视在功率门限 + */ + private double apparentPowerThreshold = 0.01; + + /** + * 功率因数门限 + */ + private double powerFactorThreshold = 0.001; + + /** + * 电压序分量门限 + */ + private double sequenceVoltageThreshold = 0.01; + + /** + * 电流序分量门限 + */ + private double sequenceCurrentThreshold = 0.001; + + /** + * 不平衡度门限 + */ + private double unbalanceThreshold = 0.01; + + /** + * 频率门限 + */ + private double frequencyThreshold = 0.001; + + /** + * 频率偏差门限 + */ + private double frequencyDeviationThreshold = 0.001; +} \ No newline at end of file diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/service/ICompareWaveService.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/service/ICompareWaveService.java new file mode 100644 index 00000000..23142b41 --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/service/ICompareWaveService.java @@ -0,0 +1,23 @@ +package com.njcn.gather.tools.comtrade.comparewave.service; + + +import com.njcn.gather.tools.comtrade.comparewave.core.model.CompareWaveDTO; + +import java.io.InputStream; + +/** + * + * 比对录波服务类 + * + * @author hongawen + * @version 1.0 + * @data 2025/9/2 08:34 + */ +public interface ICompareWaveService { + + CompareWaveDTO analyzeAndCompareWithStreams( + InputStream sourceCfgStream, InputStream sourceDatStream, + InputStream targetCfgStream, InputStream targetDatStream, + int lineConfig); + +} diff --git a/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/service/impl/CompareWaveServiceImpl.java b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/service/impl/CompareWaveServiceImpl.java new file mode 100644 index 00000000..5e5a343f --- /dev/null +++ b/tools/wave-comtrade/src/main/java/com/njcn/gather/tools/comtrade/comparewave/service/impl/CompareWaveServiceImpl.java @@ -0,0 +1,279 @@ +package com.njcn.gather.tools.comtrade.comparewave.service.impl; + +import com.njcn.gather.tools.comtrade.comparewave.config.PowerQualityConfig; +import com.njcn.gather.tools.comtrade.comparewave.core.algorithm.PowerQualityCalculator; +import com.njcn.gather.tools.comtrade.comparewave.core.algorithm.WaveformAligner; +import com.njcn.gather.tools.comtrade.comparewave.core.io.ComtradeReader; +import com.njcn.gather.tools.comtrade.comparewave.core.model.ClockStruct; +import com.njcn.gather.tools.comtrade.comparewave.core.model.CompareWaveDTO; +import com.njcn.gather.tools.comtrade.comparewave.core.model.DataPq; +import com.njcn.gather.tools.comtrade.comparewave.core.model.PqsDataStruct; +import com.njcn.gather.tools.comtrade.comparewave.service.ICompareWaveService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * 波形比对分析服务实现类 + * + *

提供电能质量波形数据的分析和比对功能,支持:

+ *
    + *
  • COMTRADE格式文件的流式读取和解析
  • + *
  • 波形对齐算法,确保比对数据的时序一致性
  • + *
  • 电能质量参数计算(谐波、间谐波、THD、功率等)
  • + *
  • 多种接线方式支持(星型、V型)
  • + *
  • 结果比对分析和报告生成
  • + *
+ * + * @author hongawen + * @version 1.0 + * @since 2025-09-02 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CompareWaveServiceImpl implements ICompareWaveService { + + /** + * 电能质量配置参数 + * + *

包含分析计算所需的各项配置:

+ *
    + *
  • 谐波分析参数(谐波次数、计算方式等)
  • + *
  • 门限配置(RMS、THD、功率等门限值)
  • + *
  • 采样参数(默认采样率、编码格式等)
  • + *
  • 输出配置(结果文件路径等)
  • + *
+ */ + private final PowerQualityConfig powerQualityConfig; + + + /** + * 执行电能质量分析和比较(使用InputStream) + * + * @param sourceCfgStream 源CFG文件流 + * @param sourceDatStream 源DAT文件流 + * @param targetCfgStream 目标CFG文件流 + * @param targetDatStream 目标DAT文件流 + * @param lineConfig 接线方式 0=星型接线, 1=V型接线 + * @return 解析后的数据结果 + */ + @Override + public CompareWaveDTO analyzeAndCompareWithStreams( + InputStream sourceCfgStream, InputStream sourceDatStream, + InputStream targetCfgStream, InputStream targetDatStream, int lineConfig) { + + CompareWaveDTO compareWaveDTO = new CompareWaveDTO(); + + // 使用局部变量确保线程安全 + DataPq sourceDataBuf = new DataPq(); + DataPq targetDataBuf = new DataPq(); + try { + log.info("开始电能质量分析(使用流模式)..."); + + // 初始化数据缓冲区 + sourceDataBuf.init(); + targetDataBuf.init(); + + // 设置配置参数(包括接线方式和额定值) + applyConfiguration(sourceDataBuf, lineConfig); + applyConfiguration(targetDataBuf, lineConfig); + + // 读取源文件流 + log.info("读取源文件流..."); + boolean sourceSuccess = ComtradeReader.readSampleFile( + sourceCfgStream, sourceDatStream, sourceDataBuf, + powerQualityConfig.getReading().getEncoding() + ); + + if (!sourceSuccess) { + log.error("读取源文件流失败"); + return compareWaveDTO; + } + + // 读取目标文件流 + log.info("读取目标文件流..."); + boolean targetSuccess = ComtradeReader.readSampleFile( + targetCfgStream, targetDatStream, targetDataBuf, + powerQualityConfig.getReading().getEncoding() + ); + + if (!targetSuccess) { + log.error("读取目标文件流失败"); + return compareWaveDTO; + } + + // 执行后续的对齐、计算和比较(复用现有逻辑) + return performAnalysisAndComparison(sourceDataBuf, targetDataBuf); + + } catch (Exception e) { + log.error("分析过程中发生错误", e); + return compareWaveDTO; + } + } + + /** + * 应用配置参数(带接线方式和额定值) + * + * @param dataBuf 数据缓冲区 + * @param lineConfig 接线方式(0: 星型接线, 1: V型接线) + */ + private void applyConfiguration(DataPq dataBuf, int lineConfig) { + // 设置计算参数 + dataBuf.getCfg().setHarmTime(powerQualityConfig.getCalculation().getHarmonicTimes()); + // 使用传入的接线方式 + dataBuf.getCfg().setLineConfig(lineConfig); + dataBuf.getCfg().setIbAdd(powerQualityConfig.getCalculation().getIbAdd() ? 1 : 0); + dataBuf.getCfg().setUharmAdd(powerQualityConfig.getCalculation().getUharmAdd() ? 1 : 0); + log.info("配置已应用 - 接线方式: {} ({}), 额定电压: {}V, 额定电流: {}A", + lineConfig, + lineConfig == 0 ? "星型接线" : "V型接线"); + + // 设置默认采样率 + if (dataBuf.getSmpRate() == 0) { + dataBuf.setSmpRate(powerQualityConfig.getCalculation().getSampling().getDefaultRate()); + } + } + + /** + * 执行分析和比较的核心逻辑 + * + * @param sourceDataBuf 源数据缓冲区 + * @param targetDataBuf 目标数据缓冲区 + * @return 是否成功 + */ + private CompareWaveDTO performAnalysisAndComparison(DataPq sourceDataBuf, DataPq targetDataBuf) { + CompareWaveDTO compareWaveDTO = new CompareWaveDTO(); + try { + // 执行波形对齐 + log.info("开始波形对齐..."); + WaveformAligner.AlignmentResult alignmentResult = WaveformAligner.findStartPosition(sourceDataBuf, targetDataBuf); + + if (!alignmentResult.isSuccess()) { + log.error("波形对齐失败: {}", alignmentResult.getErrorMessage()); + return compareWaveDTO; + } + + // 应用对齐结果 + WaveformAligner.applyAlignment(sourceDataBuf, targetDataBuf, alignmentResult); + + log.info("波形对齐完成 - 源文件起始位置: {}, 目标文件起始位置: {}, 可计算窗口数: {}", + alignmentResult.getStartPos1(), alignmentResult.getStartPos2(), alignmentResult.getCalNum()); + + // 执行电能质量计算 + log.info("开始计算电能质量参数..."); + List sourceResults = calculatePowerQuality(sourceDataBuf); + List targetResults = calculatePowerQuality(targetDataBuf); + compareWaveDTO.setSourceResults(sourceResults); + compareWaveDTO.setTargetResults(targetResults); + } catch (Exception e) { + log.error("分析和比较过程中发生错误", e); + } + return compareWaveDTO; + } + + /** + * 计算电能质量参数 + * 使用对齐后的起始位置和窗口数量 + */ + private List calculatePowerQuality(DataPq dataBuf) { + List results = new ArrayList<>(); + + // 获取对齐后的参数 + int startCalPos = dataBuf.getStartCalPos(); + int calNum = dataBuf.getCalNum(); + ClockStruct startCalTime = dataBuf.getStartCalTime(); + + // 如果没有设置对齐参数,使用默认值(兼容性) + if (startCalTime == null) { + startCalPos = 0; + calNum = Math.min(dataBuf.getSmpNum() / (int)(dataBuf.getSmpRate() * 10), 100); + startCalTime = new ClockStruct(); + startCalTime.setYear(2024); + startCalTime.setMonth(1); + startCalTime.setDay(1); + startCalTime.setHour(0); + startCalTime.setMinute(0); + startCalTime.setSecond(0); + startCalTime.setMicroSecond(0); + } + + // 计算周期:每200ms(10个周波)计算一次 + int samplesPerWindow = (int)(dataBuf.getSmpRate() * 10); // 10个周波的采样点数 + + log.info("使用对齐参数 - 起始位置: {}, 窗口数: {}, 每周波采样点数: {}", + startCalPos, calNum, dataBuf.getSmpRate()); + + // 重置数据点位置 + dataBuf.setDataPoint(0); + + // 对每个窗口进行计算 + for (int window = 0; window < calNum; window++) { + // 计算当前窗口的结束位置 + // 对应C代码:pqs_200ms_data_cal(pq, &data_time, pq->start_cal_pos + (i+1)*pq->smp_rate*10) + int smpWrPoint = startCalPos + (window + 1) * samplesPerWindow; + + // 检查是否超出数据范围 + if (smpWrPoint > dataBuf.getSmpNum()) { + log.warn("窗口 {} 超出数据范围,跳过", window); + break; + } + + // 计算当前窗口的时间戳 + ClockStruct dataTime = new ClockStruct(); + dataTime.setYear(startCalTime.getYear()); + dataTime.setMonth(startCalTime.getMonth()); + dataTime.setDay(startCalTime.getDay()); + dataTime.setHour(startCalTime.getHour()); + dataTime.setMinute(startCalTime.getMinute()); + dataTime.setSecond(startCalTime.getSecond()); + dataTime.setMicroSecond(startCalTime.getMicroSecond()); + + // 添加时间偏移(每个窗口200ms) + int totalMs = dataTime.getMicroSecond() / 1000 + window * 200; + if (totalMs >= 1000) { + dataTime.setSecond(dataTime.getSecond() + totalMs / 1000); + totalMs = totalMs % 1000; + + // 处理秒溢出 + if (dataTime.getSecond() >= 60) { + dataTime.setMinute(dataTime.getMinute() + dataTime.getSecond() / 60); + dataTime.setSecond(dataTime.getSecond() % 60); + + // 处理分钟溢出 + if (dataTime.getMinute() >= 60) { + dataTime.setHour(dataTime.getHour() + dataTime.getMinute() / 60); + dataTime.setMinute(dataTime.getMinute() % 60); + } + } + } + dataTime.setMicroSecond(totalMs * 1000); + + log.debug("计算窗口 {}: smpWrPoint={}, 时间={}-{}-{} {}:{}:{}.{}", + window, smpWrPoint, dataTime.getYear(), dataTime.getMonth(), dataTime.getDay(), + dataTime.getHour(), dataTime.getMinute(), dataTime.getSecond(), totalMs); + + // 执行200ms数据计算 + PowerQualityCalculator.pqs200msDataCal(dataBuf, dataTime, smpWrPoint); + + // 获取计算结果 + PqsDataStruct result = dataBuf.getPqData()[window]; + results.add(result); + + // 日志输出部分结果用于调试 + if (window == 0) { + log.info("第一个窗口计算结果 - UA有效值: {}, UB有效值: {}, UC有效值: {}", + result.getRms()[0], result.getRms()[1], result.getRms()[2]); + log.info("第一个窗口计算结果 - IA有效值: {}, IB有效值: {}, IC有效值: {}", + result.getRms()[3], result.getRms()[4], result.getRms()[5]); + } + } + + return results; + } + +}