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