波形比对算法迁移成功

This commit is contained in:
2025-09-02 15:53:35 +08:00
parent 8963b20dd3
commit da3373c710
26 changed files with 4836 additions and 3 deletions

View File

@@ -86,6 +86,7 @@
<artifactId>jakarta.xml.bind-api</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
@@ -124,6 +125,13 @@
<version>3.10.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>wave-comtrade</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>

View File

@@ -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 # 电压谐波叠加标志

View File

@@ -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");
}
}
}

View File

@@ -33,6 +33,9 @@
<properties>
<spring-boot.version>2.3.12.RELEASE</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
</properties>
<dependencyManagement>
@@ -65,6 +68,7 @@
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>

View File

@@ -17,9 +17,7 @@
<modules>
<module>report-generator</module>
<!-- 未来可以添加更多工具子模块 -->
<!-- <module>data-generator</module> -->
<!-- <module>file-processor</module> -->
<module>wave-comtrade</module>
</modules>
</project>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>tools</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>wave-comtrade</artifactId>
<name>COMTRADE波形文件处理工具</name>
<description>专业的COMTRADE格式波形文件读写、解析和转换工具</description>
<dependencies>
<!-- 通用工具包 -->
<dependency>
<groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- Apache Commons Math for mathematical operations -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.12.RELEASE</version>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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;
/**
* 电能质量分析系统配置类
*
* <p>通过Spring Boot配置文件application.yml读取电能质量分析相关的配置参数
* 包括标称值、阈值、读取配置、计算配置和输出配置等。</p>
*
* <p>配置前缀power-quality</p>
*
* @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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,111 @@
package com.njcn.gather.tools.comtrade.comparewave.core;
/**
* 系统常量定义接口
* <p>对应C代码pq.h中的宏定义</p>
*
* @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;
}

View File

@@ -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算法处理器
* <p>对应C代码pqs_fft_lib.c</p>
* <p><b>重要所有算法实现必须与C代码完全一致</b></p>
*
* @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初始化方法
* <p>对应C代码FFT_Init</p>
* <p>初始化FFT和DFT所需的旋转因子数组</p>
*/
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计算函数
* <p>对应C代码FFT_Cal</p>
*
* @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计算
* <p>对应C代码FFT_2560_Cal</p>
* <p>使用混合基FFT算法分解为512×5</p>
*
* @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计算
* <p>对应C代码FFT_5120_Cal</p>
* <p>使用混合基FFT算法分解为1024×5</p>
*
* @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计算
* <p>对应C代码XK_DFT</p>
*
* @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计算
* <p>对应C代码XK_FFT</p>
* <p>使用Cooley-Tukey基2 FFT算法</p>
*
* @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());
}
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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.*;
/**
* 基础库函数
* <p>对应C代码lib.c</p>
* <p><b>重要所有计算必须与C代码完全一致</b></p>
* <p>提供电能质量分析所需的基础数学计算和工具函数</p>
*
* @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
* <p>对应C代码rms_cal</p>
* <p>使用滑动窗口计算数据的均方根值</p>
*
* @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;
}
/**
* 判断是否为闰年
* <p>对应C代码look_leap_year</p>
* <p>按照公历闰年规则4年一闰百年不闰四百年又闰</p>
*
* @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;
}
/**
* 时钟转换为秒
* <p>对应C代码clock_to_second</p>
* <p>将指定日期时间转换为从基准年份开始的秒数</p>
*
* @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;
}
/**
* 秒转换为时钟
* <p>对应C代码second_to_clock</p>
* <p>将秒数转换为时间结构体,包含北京时间校正</p>
*
* @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;
}
/**
* 生成测试采样数据
* <p>对应C代码smp_test_data_init</p>
* <p>生成标准的正弦波测试数据,包含基波和谐波分量</p>
*
* @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;
}
}
/**
* 计算频率(从采样数据)
* <p>对应C代码smp_to_freq</p>
* <p>使用过零检测法从采样数据中提取信号频率</p>
*
* @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;
}
}

View File

@@ -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代码这里看起来有bugdiv_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];
}
}
}
}

View File

@@ -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函数
* <p>
* 实现步骤:
* 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("对齐结果已应用到数据缓冲区");
}
}

View File

@@ -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文件读取器
* <p>对应C代码main_pro.c中的文件读取部分</p>
* <p><b>重要必须严格按照C代码的读取逻辑</b></p>
* <p>用于读取IEEE C37.111标准的COMTRADE格式文件包括CFG配置文件和DAT数据文件</p>
*
* @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
* <p>从输入流中读取CFG配置文件和DAT数据文件</p>
*
* @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信息和数据缓冲区
* <p>提取公共处理逻辑,设置采样参数和数据缓冲区</p>
* <p>注意此时还没有设置接线方式需要在外部调用applyConfiguration后才能确定</p>
*
* @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
* <p>解析COMTRADE配置文件提取通道信息和采样参数</p>
*
* @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;
}
}

View File

@@ -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;
/**
* 结果输出器
* <p>对应C代码main_pro.c中的结果输出部分</p>
* <p><b>重要输出格式必须与C代码完全一致</b></p>
* <p>负责将电能质量检测数据的比较结果输出为多种格式的文件</p>
*
* @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");
/**
* 比较结果数据结构
* <p>封装单个检测项目的比较结果信息</p>
*/
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;
}
}
/**
* 写入比较结果到多个文件
* <p>对应C代码中的结果输出逻辑</p>
*
* @param outputPath 输出文件夹路径
* @param sourceData 源设备数据列表
* @param targetData 被检设备数据列表
* @param thresholds 门限配置信息
* @throws IOException 文件输出异常
*/
public static void writeCompareResults(String outputPath,
List<PqsDataStruct> sourceData,
List<PqsDataStruct> targetData,
ThresholdConfig thresholds) throws IOException {
List<CompareResult> 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<CompareResult> 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<CompareResult> 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<CompareResult> 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<CompareResult> 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<CompareResult> 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<CompareResult> 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<CompareResult> 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<CompareResult> 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");
}
}
}

View File

@@ -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);
}
/**
* 从时间戳创建时间结构体
* <p>时间戳单位为微秒</p>
*
* @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
);
}
}

View File

@@ -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<PqsDataStruct> sourceResults;
/**
* 目标波形文件解析结果
*/
private List<PqsDataStruct> targetResults;
}

View File

@@ -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());
}
}

View File

@@ -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]);
}
}
}
}

View File

@@ -0,0 +1,192 @@
package com.njcn.gather.tools.comtrade.comparewave.core.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 配置结构体
* <p>对应C代码cfg_struct</p>
* <p>用于电能质量检测的配置参数设置</p>
*
* @author Claude Code
* @since 1.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ConfigStruct {
/**
* 线路配置类型
* <ul>
* <li>0: 星型接线</li>
* <li>1: V型接线</li>
* </ul>
*/
private int lineConfig;
/**
* 是否合成IB相
* <ul>
* <li>0: 不合成</li>
* <li>1: 合成</li>
* </ul>
*/
private int ibAdd;
/**
* 是否合成线电压谐波
* <ul>
* <li>0: 不合成</li>
* <li>1: 合成</li>
* </ul>
*/
private int uharmAdd;
/**
* 谐波分析次数
* <p>默认值50次谐波</p>
*/
private int harmTime;
/**
* 设置默认配置参数
* <p>将所有配置项重置为系统默认值</p>
*/
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);
}
}
}

View File

@@ -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.*;
/**
* 主数据缓冲区结构
* <p>对应C代码data_pq</p>
* <p>用于存储和管理电能质量检测的采样数据和分析结果</p>
*
* @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;
/**
* 采样数据数组
* <p>第一维:通道索引 (0 ~ MAX_CH_NUM-1)</p>
* <p>第二维:数据索引 (0 ~ MAX_DATA_LEN-1)</p>
*/
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;
/**
* 初始化数据结构
* <p>设置默认值并初始化所有子结构</p>
*/
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;
}
/**
* 移动到下一个数据点位置
* <p>当超出数组范围时将循环到索引0</p>
*/
public void nextDataPoint() {
dataPoint++;
if (dataPoint >= pqData.length) {
dataPoint = 0;
}
}
}

View File

@@ -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.*;
/**
* 电能质量数据结构
* <p>对应C代码pqs_dat_struct</p>
* <p><b>重要所有数组维度和计算逻辑必须与C代码完全一致</b></p>
*
* @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;
/**
* 初始化数据结构
* <p>创建时间戳对象,数组已在声明时自动初始化</p>
*/
public void init() {
// 创建时间戳对象,数组已在声明时自动初始化为零值
clocktime = new ClockStruct();
}
/**
* 复制构造函数
* <p>深拷贝所有数组和对象</p>
*
* @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()
);
}
}
}

View File

@@ -0,0 +1,91 @@
package com.njcn.gather.tools.comtrade.comparewave.core.model;
import lombok.Builder;
import lombok.Data;
/**
* 门限配置封装类
* <p>用于传递所有门限值到结果比较方法</p>
* <p>包含电能质量检测中各项参数的阈值配置</p>
*
* @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;
}

View File

@@ -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);
}

View File

@@ -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;
/**
* 波形比对分析服务实现类
*
* <p>提供电能质量波形数据的分析和比对功能,支持:</p>
* <ul>
* <li>COMTRADE格式文件的流式读取和解析</li>
* <li>波形对齐算法,确保比对数据的时序一致性</li>
* <li>电能质量参数计算谐波、间谐波、THD、功率等</li>
* <li>多种接线方式支持星型、V型</li>
* <li>结果比对分析和报告生成</li>
* </ul>
*
* @author hongawen
* @version 1.0
* @since 2025-09-02
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CompareWaveServiceImpl implements ICompareWaveService {
/**
* 电能质量配置参数
*
* <p>包含分析计算所需的各项配置:</p>
* <ul>
* <li>谐波分析参数(谐波次数、计算方式等)</li>
* <li>门限配置RMS、THD、功率等门限值</li>
* <li>采样参数(默认采样率、编码格式等)</li>
* <li>输出配置(结果文件路径等)</li>
* </ul>
*/
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<PqsDataStruct> sourceResults = calculatePowerQuality(sourceDataBuf);
List<PqsDataStruct> targetResults = calculatePowerQuality(targetDataBuf);
compareWaveDTO.setSourceResults(sourceResults);
compareWaveDTO.setTargetResults(targetResults);
} catch (Exception e) {
log.error("分析和比较过程中发生错误", e);
}
return compareWaveDTO;
}
/**
* 计算电能质量参数
* 使用对齐后的起始位置和窗口数量
*/
private List<PqsDataStruct> calculatePowerQuality(DataPq dataBuf) {
List<PqsDataStruct> 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);
}
// 计算周期每200ms10个周波计算一次
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;
}
}