波形算法调整:增加降采样处理和多段分析处理

This commit is contained in:
2025-09-10 14:53:17 +08:00
parent 694f12bc29
commit 90eb90554b
5 changed files with 498 additions and 175 deletions

View File

@@ -295,10 +295,12 @@ public class WaveformAligner {
/* /*
* 计算可进行计算的200ms窗口数量 * 计算可进行计算的200ms窗口数量
* 每个窗口需要10个周波的数据用于电能质量分析 * 每个窗口需要10个周波的数据用于电能质量分析
* 重要:使用降采样后的采样率(getSmpRate)
*/ */
int windowSamples = (int) (data1.getFactSmpRate() * 10); int windowSamples1 = (int) (data1.getSmpRate() * 10); // 使用降采样后的采样率
int availableWindows1 = (data1.getSmpNum() - startCalPos1) / windowSamples; int windowSamples2 = (int) (data2.getSmpRate() * 10); // 每个文件可能有不同的采样率
int availableWindows2 = (data2.getSmpNum() - startCalPos2) / windowSamples; int availableWindows1 = (data1.getSmpNum() - startCalPos1) / windowSamples1;
int availableWindows2 = (data2.getSmpNum() - startCalPos2) / windowSamples2;
int maxWindows = Math.min(availableWindows1, availableWindows2); int maxWindows = Math.min(availableWindows1, availableWindows2);
// 限制最大窗口数为100对应C代码中的MAX_DATA_NUM常量 // 限制最大窗口数为100对应C代码中的MAX_DATA_NUM常量

View File

@@ -18,31 +18,37 @@ import static com.njcn.gather.tools.comtrade.comparewave.core.Constants.MAX_CH_N
* <p>对应C代码main_pro.c中的文件读取部分</p> * <p>对应C代码main_pro.c中的文件读取部分</p>
* <p><b>重要必须严格按照C代码的读取逻辑</b></p> * <p><b>重要必须严格按照C代码的读取逻辑</b></p>
* <p>用于读取IEEE C37.111标准的COMTRADE格式文件包括CFG配置文件和DAT数据文件</p> * <p>用于读取IEEE C37.111标准的COMTRADE格式文件包括CFG配置文件和DAT数据文件</p>
* *
* @author hongawen * @author hongawen
* @since 1.0 * @since 1.0
*/ */
@Slf4j @Slf4j
public class ComtradeReader { public class ComtradeReader {
/** ASCII格式文件类型常量 */ /**
* ASCII格式文件类型常量
*/
private static final String FILE_TYPE_ASCII = "ASCII"; private static final String FILE_TYPE_ASCII = "ASCII";
/** 二进制格式文件类型常量 */ /**
* 二进制格式文件类型常量
*/
private static final String FILE_TYPE_BINARY = "BINARY"; private static final String FILE_TYPE_BINARY = "BINARY";
/** /**
* 读取COMTRADE文件使用InputStream * 读取COMTRADE文件使用InputStream
* <p>从输入流中读取CFG配置文件和DAT数据文件</p> * <p>从输入流中读取CFG配置文件和DAT数据文件</p>
* * <p>重要实现了与C代码一致的降采样算法确保采样率统一</p>
* @param cfgStream CFG配置文件输入流 *
* @param datStream DAT数据文件输入流 * @param cfgStream CFG配置文件输入流
* @param dataBuf 数据缓冲区 * @param datStream DAT数据文件输入流
* @param encoding 文件编码格式 * @param dataBuf 数据缓冲区
* @param encoding 文件编码格式
* @param targetSampleRate 目标采样率(点/周波0表示自动选择
* @return 读取是否成功 * @return 读取是否成功
*/ */
public static boolean readSampleFile(InputStream cfgStream, InputStream datStream, public static boolean readSampleFile(InputStream cfgStream, InputStream datStream,
DataPq dataBuf, String encoding) { DataPq dataBuf, String encoding, float targetSampleRate) {
try { try {
// 从输入流读取CFG配置文件 // 从输入流读取CFG配置文件
ComtradeData.CfgInfo cfgInfo = readCfgFile(cfgStream, encoding); ComtradeData.CfgInfo cfgInfo = readCfgFile(cfgStream, encoding);
@@ -50,7 +56,7 @@ public class ComtradeReader {
log.error("Failed to read CFG from stream"); log.error("Failed to read CFG from stream");
return false; return false;
} }
// 获取数据文件类型ASCII或BINARY // 获取数据文件类型ASCII或BINARY
String dataFileType = cfgInfo.getDataFileType(); String dataFileType = cfgInfo.getDataFileType();
if (dataFileType == null || dataFileType.isEmpty()) { if (dataFileType == null || dataFileType.isEmpty()) {
@@ -58,101 +64,92 @@ public class ComtradeReader {
dataFileType = FILE_TYPE_BINARY; dataFileType = FILE_TYPE_BINARY;
log.info("数据文件类型未指定默认使用BINARY格式"); log.info("数据文件类型未指定默认使用BINARY格式");
} }
// 从输入流读取DAT数据文件 // 计算有效的目标采样率对应C代码中的out_smp计算逻辑
boolean success = readDatFileFromStream(datStream, cfgInfo, dataBuf, dataFileType); float effectiveTargetRate = calculateEffectiveTargetSampleRate(cfgInfo, targetSampleRate);
// 从输入流读取DAT数据文件包含降采样处理
boolean success = readDatFileFromStream(datStream, cfgInfo, dataBuf, dataFileType, effectiveTargetRate);
if (!success) { if (!success) {
log.error("Failed to read DAT from stream"); log.error("Failed to read DAT from stream");
return false; return false;
} }
// 处理CFG信息和数据缓冲区 // 处理CFG信息和数据缓冲区
return processCfgAndData(cfgInfo, dataBuf); return processCfgAndData(cfgInfo, dataBuf, effectiveTargetRate);
} catch (Exception e) { } catch (Exception e) {
log.error("Error reading COMTRADE from streams", e); log.error("Error reading COMTRADE from streams", e);
return false; return false;
} }
} }
/** /**
* 处理CFG信息和数据缓冲区 * 处理CFG信息和数据缓冲区
* <p>提取公共处理逻辑,设置采样参数和数据缓冲区</p> * <p>提取公共处理逻辑,设置采样参数和数据缓冲区</p>
* <p>注意此时还没有设置接线方式需要在外部调用applyConfiguration后才能确定</p> * <p>注意此时还没有设置接线方式需要在外部调用applyConfiguration后才能确定</p>
* *
* @param cfgInfo CFG配置信息 * @param cfgInfo CFG配置信息
* @param dataBuf 数据缓冲区 * @param dataBuf 数据缓冲区
* @param effectiveTargetRate 有效的目标采样率(仅用于日志输出)
* @return 处理是否成功 * @return 处理是否成功
*/ */
private static boolean processCfgAndData(ComtradeData.CfgInfo cfgInfo, DataPq dataBuf) { private static boolean processCfgAndData(ComtradeData.CfgInfo cfgInfo, DataPq dataBuf, float effectiveTargetRate) {
try { try {
// 计算每周波采样点数对应C代码中smp_rate // 采样率已在DAT文件读取时设置考虑了多段统一降采样
float lineFreq = (float)cfgInfo.getLineFreq(); // 不再覆盖仅用effectiveTargetRate作为日志参考
float lineFreq = (float) cfgInfo.getLineFreq();
if (lineFreq <= 0) { if (lineFreq <= 0) {
lineFreq = 50.0f; lineFreq = 50.0f;
} }
float samplesPerCycle = (float)cfgInfo.getSampleRate() / lineFreq;
dataBuf.setFactSmpRate(samplesPerCycle);
// 验证采样率限制要求
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.setF(lineFreq);
// 设置录波开始时间以支持波形对齐 // 设置录波开始时间以支持波形对齐
dataBuf.setLbStartTime(cfgInfo.getStartTime()); dataBuf.setLbStartTime(cfgInfo.getStartTime());
// 额定电压电流由外部参数传入,提供更灵活的配置 // 额定电压电流由外部参数传入,提供更灵活的配置
// 输出数据读取统计信息 // 输出数据读取统计信息
log.info("数据读取完成 - 采样点数: {}, 每周波采样点数: {}, 频率: {} Hz", log.info("数据读取完成 - 采样点数: {}, 每周波采样点数: {}, 频率: {} Hz",
dataBuf.getSmpNum(), dataBuf.getSmpRate(), dataBuf.getF()); dataBuf.getSmpNum(), dataBuf.getSmpRate(), dataBuf.getF());
// 输出部分采样点用于数据验证 // 输出部分采样点用于数据验证
if (dataBuf.getSmpNum() > 0) { if (dataBuf.getSmpNum() > 0) {
log.debug("前10个UA采样点: {}, {}, {}, {}, {}, {}, {}, {}, {}, {}", log.debug("前10个UA采样点: {}, {}, {}, {}, {}, {}, {}, {}, {}, {}",
dataBuf.getSmpData()[0][0], dataBuf.getSmpData()[0][1], dataBuf.getSmpData()[0][0], dataBuf.getSmpData()[0][1],
dataBuf.getSmpData()[0][2], dataBuf.getSmpData()[0][3], dataBuf.getSmpData()[0][2], dataBuf.getSmpData()[0][3],
dataBuf.getSmpData()[0][4], dataBuf.getSmpData()[0][5], dataBuf.getSmpData()[0][4], dataBuf.getSmpData()[0][5],
dataBuf.getSmpData()[0][6], dataBuf.getSmpData()[0][7], dataBuf.getSmpData()[0][6], dataBuf.getSmpData()[0][7],
dataBuf.getSmpData()[0][8], dataBuf.getSmpData()[0][9]); dataBuf.getSmpData()[0][8], dataBuf.getSmpData()[0][9]);
} }
return true; return true;
} catch (Exception e) { } catch (Exception e) {
log.error("Error processing COMTRADE data", e); log.error("Error processing COMTRADE data", e);
return false; return false;
} }
} }
/** /**
* 读取CFG配置文件从InputStream * 读取CFG配置文件从InputStream
* <p>解析COMTRADE配置文件提取通道信息和采样参数</p> * <p>解析COMTRADE配置文件提取通道信息和采样参数</p>
* *
* @param inputStream CFG文件输入流 * @param inputStream CFG文件输入流
* @param encoding 文件编码格式 * @param encoding 文件编码格式
* @return CFG配置信息对象解析失败返回null * @return CFG配置信息对象解析失败返回null
*/ */
private static ComtradeData.CfgInfo readCfgFile(InputStream inputStream, String encoding) { private static ComtradeData.CfgInfo readCfgFile(InputStream inputStream, String encoding) {
ComtradeData.CfgInfo cfgInfo = new ComtradeData.CfgInfo(); ComtradeData.CfgInfo cfgInfo = new ComtradeData.CfgInfo();
try (BufferedReader reader = new BufferedReader( try (BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream, Charset.forName(encoding)))) { new InputStreamReader(inputStream, Charset.forName(encoding)))) {
String line; String line;
int lineNum = 0; int lineNum = 0;
// 第1行站名装置ID版本年份 // 第1行站名装置ID版本年份
line = reader.readLine(); line = reader.readLine();
lineNum++; lineNum++;
@@ -170,7 +167,7 @@ public class ComtradeReader {
} }
} }
} }
// 第2行通道总数模拟通道数A数字通道数D // 第2行通道总数模拟通道数A数字通道数D
line = reader.readLine(); line = reader.readLine();
lineNum++; lineNum++;
@@ -183,7 +180,7 @@ public class ComtradeReader {
analogPart = analogPart.substring(0, analogPart.length() - 1); analogPart = analogPart.substring(0, analogPart.length() - 1);
} }
cfgInfo.setAnalogChannels(Integer.parseInt(analogPart)); cfgInfo.setAnalogChannels(Integer.parseInt(analogPart));
String digitalPart = parts[2].trim(); String digitalPart = parts[2].trim();
if (digitalPart.endsWith("D")) { if (digitalPart.endsWith("D")) {
digitalPart = digitalPart.substring(0, digitalPart.length() - 1); digitalPart = digitalPart.substring(0, digitalPart.length() - 1);
@@ -191,11 +188,11 @@ public class ComtradeReader {
cfgInfo.setDigitalChannels(Integer.parseInt(digitalPart)); cfgInfo.setDigitalChannels(Integer.parseInt(digitalPart));
} }
} }
// 读取模拟通道信息 // 读取模拟通道信息
int analogCount = cfgInfo.getAnalogChannels(); int analogCount = cfgInfo.getAnalogChannels();
ComtradeData.ChannelInfo[] analogChannels = new ComtradeData.ChannelInfo[analogCount]; ComtradeData.ChannelInfo[] analogChannels = new ComtradeData.ChannelInfo[analogCount];
for (int i = 0; i < analogCount; i++) { for (int i = 0; i < analogCount; i++) {
line = reader.readLine(); line = reader.readLine();
lineNum++; lineNum++;
@@ -204,60 +201,75 @@ public class ComtradeReader {
} }
} }
cfgInfo.setAnalogChannelInfos(analogChannels); cfgInfo.setAnalogChannelInfos(analogChannels);
// 跳过数字通道信息 // 跳过数字通道信息
int digitalCount = cfgInfo.getDigitalChannels(); int digitalCount = cfgInfo.getDigitalChannels();
for (int i = 0; i < digitalCount; i++) { for (int i = 0; i < digitalCount; i++) {
reader.readLine(); reader.readLine();
lineNum++; lineNum++;
} }
// 读取电网频率 // 读取电网频率
line = reader.readLine(); line = reader.readLine();
lineNum++; lineNum++;
if (line != null) { if (line != null) {
cfgInfo.setLineFreq(Double.parseDouble(line.trim())); cfgInfo.setLineFreq(Double.parseDouble(line.trim()));
} }
// 读取采样率信息 // 读取采样率信息(段数)
line = reader.readLine(); line = reader.readLine();
lineNum++; lineNum++;
if (line != null) { if (line != null) {
cfgInfo.setNrates(Integer.parseInt(line.trim())); cfgInfo.setNrates(Integer.parseInt(line.trim()));
} }
// 读取采样率和采样点数 // 读取每段的采样率和采样点数对应C代码的rate[]和fd_data_num[]
line = reader.readLine(); int nrates = cfgInfo.getNrates();
lineNum++; if (nrates <= 0) nrates = 1; // 默认至少1段
if (line != null) {
String[] parts = line.split(","); double[] sampleRates = new double[nrates];
if (parts.length >= 2) { int[] segmentSamples = new int[nrates];
cfgInfo.setSampleRate(Double.parseDouble(parts[0].trim())); int totalSamples = 0;
cfgInfo.setTotalSamples(Integer.parseInt(parts[1].trim()));
for (int i = 0; i < nrates; i++) {
line = reader.readLine();
lineNum++;
if (line != null) {
String[] parts = line.split(",");
if (parts.length >= 2) {
sampleRates[i] = Double.parseDouble(parts[0].trim());
segmentSamples[i] = Integer.parseInt(parts[1].trim());
totalSamples += segmentSamples[i];
}
} }
} }
cfgInfo.setSampleRates(sampleRates);
cfgInfo.setSegmentSamples(segmentSamples);
cfgInfo.setSampleRate(sampleRates[0]); // 兼容性:主采样率设为第一段
cfgInfo.setTotalSamples(totalSamples);
// 读取开始时间 // 读取开始时间
line = reader.readLine(); line = reader.readLine();
lineNum++; lineNum++;
if (line != null) { if (line != null) {
cfgInfo.setStartTime(parseDateTime(line)); cfgInfo.setStartTime(parseDateTime(line));
} }
// 读取触发时间 // 读取触发时间
line = reader.readLine(); line = reader.readLine();
lineNum++; lineNum++;
if (line != null) { if (line != null) {
cfgInfo.setTriggerTime(parseDateTime(line)); cfgInfo.setTriggerTime(parseDateTime(line));
} }
// 读取数据文件类型 // 读取数据文件类型
line = reader.readLine(); line = reader.readLine();
lineNum++; lineNum++;
if (line != null) { if (line != null) {
cfgInfo.setDataFileType(line.trim().toUpperCase()); cfgInfo.setDataFileType(line.trim().toUpperCase());
} }
// 读取时间倍率 // 读取时间倍率
line = reader.readLine(); line = reader.readLine();
lineNum++; lineNum++;
@@ -268,23 +280,23 @@ public class ComtradeReader {
cfgInfo.setTimeMult(1.0); cfgInfo.setTimeMult(1.0);
} }
} }
return cfgInfo; return cfgInfo;
} catch (IOException e) { } catch (IOException e) {
log.error("Error reading CFG from stream", e); log.error("Error reading CFG from stream", e);
return null; return null;
} }
} }
/** /**
* 解析模拟通道信息 * 解析模拟通道信息
*/ */
private static ComtradeData.ChannelInfo parseAnalogChannel(String line) { private static ComtradeData.ChannelInfo parseAnalogChannel(String line) {
ComtradeData.ChannelInfo channel = new ComtradeData.ChannelInfo(); ComtradeData.ChannelInfo channel = new ComtradeData.ChannelInfo();
String[] parts = line.split(","); String[] parts = line.split(",");
if (parts.length >= 13) { if (parts.length >= 13) {
channel.setChannelNumber(Integer.parseInt(parts[0].trim())); channel.setChannelNumber(Integer.parseInt(parts[0].trim()));
channel.setChannelName(parts[1].trim()); channel.setChannelName(parts[1].trim());
@@ -299,16 +311,16 @@ public class ComtradeReader {
channel.setSecondary(Double.parseDouble(parts[11].trim())); channel.setSecondary(Double.parseDouble(parts[11].trim()));
channel.setPs(parts[12].trim().charAt(0)); channel.setPs(parts[12].trim().charAt(0));
} }
return channel; return channel;
} }
/** /**
* 解析日期时间 * 解析日期时间
*/ */
private static ClockStruct parseDateTime(String dateTimeStr) { private static ClockStruct parseDateTime(String dateTimeStr) {
ClockStruct clock = new ClockStruct(); ClockStruct clock = new ClockStruct();
// 格式: dd/mm/yyyy,hh:mm:ss.ssssss // 格式: dd/mm/yyyy,hh:mm:ss.ssssss
String[] parts = dateTimeStr.split(","); String[] parts = dateTimeStr.split(",");
if (parts.length >= 2) { if (parts.length >= 2) {
@@ -319,13 +331,13 @@ public class ComtradeReader {
clock.setMonth(Integer.parseInt(dateParts[1])); clock.setMonth(Integer.parseInt(dateParts[1]));
clock.setYear(Integer.parseInt(dateParts[2])); clock.setYear(Integer.parseInt(dateParts[2]));
} }
// 解析时间 // 解析时间
String[] timeParts = parts[1].trim().split(":"); String[] timeParts = parts[1].trim().split(":");
if (timeParts.length >= 3) { if (timeParts.length >= 3) {
clock.setHour(Integer.parseInt(timeParts[0])); clock.setHour(Integer.parseInt(timeParts[0]));
clock.setMinute(Integer.parseInt(timeParts[1])); clock.setMinute(Integer.parseInt(timeParts[1]));
// 解析秒和微秒 // 解析秒和微秒
String[] secParts = timeParts[2].split("\\."); String[] secParts = timeParts[2].split("\\.");
clock.setSecond(Integer.parseInt(secParts[0])); clock.setSecond(Integer.parseInt(secParts[0]));
@@ -339,124 +351,435 @@ public class ComtradeReader {
} }
} }
} }
return clock; return clock;
} }
/** /**
* 读取DAT数据文件从InputStream * 读取DAT数据文件从InputStream
*/ */
private static boolean readDatFileFromStream(InputStream datStream, ComtradeData.CfgInfo cfgInfo, private static boolean readDatFileFromStream(InputStream datStream, ComtradeData.CfgInfo cfgInfo,
DataPq dataBuf, String dataFileType) { DataPq dataBuf, String dataFileType, float targetSampleRate) {
try { try {
if (FILE_TYPE_ASCII.equals(dataFileType)) { if (FILE_TYPE_ASCII.equals(dataFileType)) {
return readAsciiDatFileFromStream(datStream, cfgInfo, dataBuf); return readAsciiDatFileFromStream(datStream, cfgInfo, dataBuf, targetSampleRate);
} else { } else {
return readBinaryDatFileFromStream(datStream, cfgInfo, dataBuf); return readBinaryDatFileFromStream(datStream, cfgInfo, dataBuf, targetSampleRate);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Error reading DAT from stream", e); log.error("Error reading DAT from stream", e);
return false; return false;
} }
} }
/**
* 找出多段数据中的最低采样率
* 对应C代码寻找所有段中最低的采样率
*/
private static float findMinimumSampleRate(ComtradeData.CfgInfo cfgInfo, float targetSampleRate) {
float lineFreq = (float) cfgInfo.getLineFreq();
if (lineFreq <= 0) {
lineFreq = 50.0f;
}
float minSamplesPerCycle = Float.MAX_VALUE;
// 如果有多段数据
if (cfgInfo.getNrates() > 0 && cfgInfo.getSampleRates() != null) {
for (int i = 0; i < cfgInfo.getNrates(); i++) {
float samplesPerCycle = (float) (cfgInfo.getSampleRates()[i] / lineFreq);
if (samplesPerCycle < minSamplesPerCycle) {
minSamplesPerCycle = samplesPerCycle;
}
}
} else {
// 单段数据,使用主采样率
minSamplesPerCycle = (float) (cfgInfo.getSampleRate() / lineFreq);
}
// 如果最低采样率仍然高于目标256则返回目标值
// 否则返回实际最低值
return Math.min(minSamplesPerCycle, targetSampleRate);
}
/** /**
* 读取ASCII格式的DAT文件从InputStream * 读取ASCII格式的DAT文件从InputStream
*/ */
private static boolean readAsciiDatFileFromStream(InputStream datStream, ComtradeData.CfgInfo cfgInfo, DataPq dataBuf) private static boolean readAsciiDatFileFromStream(InputStream datStream, ComtradeData.CfgInfo cfgInfo,
throws IOException { DataPq dataBuf, float targetSampleRate) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(datStream))) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(datStream))) {
String line; String line;
// 输出索引
int sampleIndex = 0; int sampleIndex = 0;
// 输入索引
while ((line = reader.readLine()) != null && sampleIndex < cfgInfo.getTotalSamples()) { int inputIndex = 0;
String[] values = line.split(",");
// 找出最低采样率作为统一目标
// 跳过序号和时间戳(前两列) float unifiedSampleRate = findMinimumSampleRate(cfgInfo, targetSampleRate);
int dataStartIndex = 2; float lineFreq = (float) cfgInfo.getLineFreq();
if (lineFreq <= 0) {
// 读取模拟通道数据 lineFreq = 50.0f;
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()); // 处理多段数据
if (cfgInfo.getNrates() > 0 && cfgInfo.getSampleRates() != null) {
// 直接存储原始值像C代码一样 // 多段数据处理
dataBuf.getSmpData()[ch][sampleIndex] = rawValue; for (int seg = 0; seg < cfgInfo.getNrates(); seg++) {
float segmentSamplesPerCycle = (float) (cfgInfo.getSampleRates()[seg] / lineFreq);
int segmentSamples = cfgInfo.getSegmentSamples()[seg];
// 计算当前段的降采样模数
int downsampleMod = (segmentSamplesPerCycle > unifiedSampleRate) ?
Math.round(segmentSamplesPerCycle / unifiedSampleRate) : 1;
log.info("段{} - 原始: {}点/周波, 统一目标: {}点/周波, 模数: {}",
seg, segmentSamplesPerCycle, unifiedSampleRate, downsampleMod);
// 读取当前段的数据
int segmentInputIndex = 0;
while (segmentInputIndex < segmentSamples && (line = reader.readLine()) != null) {
// 降采样每隔downsampleMod个点取一个
if (segmentInputIndex % downsampleMod == 0) {
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());
dataBuf.getSmpData()[ch][sampleIndex] = rawValue;
}
}
sampleIndex++;
}
segmentInputIndex++;
inputIndex++;
} }
} }
} else {
sampleIndex++; // 单段数据处理(保持原有逻辑)
float originalSamplesPerCycle = (float) cfgInfo.getSampleRate() / lineFreq;
int downsampleMod = (originalSamplesPerCycle > targetSampleRate) ?
Math.round(originalSamplesPerCycle / targetSampleRate) : 1;
log.info("ASCII采样处理 - 原始: {}点/周波, 目标: {}点/周波, 模数: {}",
originalSamplesPerCycle, targetSampleRate, downsampleMod);
while ((line = reader.readLine()) != null && inputIndex < cfgInfo.getTotalSamples()) {
// 降采样每隔downsampleMod个点取一个对应C代码i % mod == 0
if (inputIndex % downsampleMod == 0) {
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++;
}
inputIndex++;
}
} }
dataBuf.setSmpNum(sampleIndex); dataBuf.setSmpNum(sampleIndex);
// 设置采样率为统一的降采样后的值
dataBuf.setSmpRate(unifiedSampleRate);
// 设置增益系数从CFG文件的a系数获取 // 设置增益系数从CFG文件的a系数获取
for (int ch = 0; ch < cfgInfo.getAnalogChannels() && ch < MAX_CH_NUM; ch++) { for (int ch = 0; ch < cfgInfo.getAnalogChannels() && ch < MAX_CH_NUM; ch++) {
ComtradeData.ChannelInfo chInfo = cfgInfo.getAnalogChannelInfos()[ch]; ComtradeData.ChannelInfo chInfo = cfgInfo.getAnalogChannelInfos()[ch];
dataBuf.getUiGainXs()[ch] = (float)chInfo.getA(); // 使用CFG文件中的a系数作为增益 dataBuf.getUiGainXs()[ch] = (float) chInfo.getA(); // 使用CFG文件中的a系数作为增益
} }
log.info("ASCII DAT文件读取完成 - 读取了 {} 个采样点", sampleIndex); log.info("ASCII DAT文件读取完成 - 原始点数: {}, 实际读取点数: {}, 统一采样率: {}点/周波",
inputIndex, sampleIndex, unifiedSampleRate);
return true; return true;
} }
} }
/** /**
* 读取二进制格式的DAT文件从InputStream * 读取二进制格式的DAT文件从InputStream
*/ */
private static boolean readBinaryDatFileFromStream(InputStream datStream, ComtradeData.CfgInfo cfgInfo, DataPq dataBuf) private static boolean readBinaryDatFileFromStream(InputStream datStream, ComtradeData.CfgInfo cfgInfo,
throws IOException { DataPq dataBuf, float targetSampleRate) throws IOException {
// 计算每个采样记录的字节数 // 计算每个采样记录的字节数// 每个模拟通道2字节
int analogBytes = cfgInfo.getAnalogChannels() * 2; // 每个模拟通道2字节 int analogBytes = cfgInfo.getAnalogChannels() * 2;
int digitalBytes = (cfgInfo.getDigitalChannels() + 15) / 16 * 2; // 数字通道按16位对齐 // 数字通道按16位对齐
int recordSize = 4 + 4 + analogBytes + digitalBytes; // 序号(4) + 时间戳(4) + 模拟 + 数字 int digitalBytes = (cfgInfo.getDigitalChannels() + 15) / 16 * 2;
// 序号(4) + 时间戳(4) + 模拟 + 数字
int recordSize = 4 + 4 + analogBytes + digitalBytes;
// 找出最低采样率作为统一目标
float unifiedSampleRate = findMinimumSampleRate(cfgInfo, targetSampleRate);
float lineFreq = (float) cfgInfo.getLineFreq();
if (lineFreq <= 0) {
lineFreq = 50.0f;
}
byte[] buffer = new byte[recordSize]; byte[] buffer = new byte[recordSize];
int sampleIndex = 0; int sampleIndex = 0; // 输出索引
int inputIndex = 0; // 输入索引
try (DataInputStream dis = new DataInputStream(new BufferedInputStream(datStream))) { try (DataInputStream dis = new DataInputStream(new BufferedInputStream(datStream))) {
while (sampleIndex < cfgInfo.getTotalSamples()) { // 处理多段数据
int bytesRead = dis.read(buffer, 0, recordSize); if (cfgInfo.getNrates() > 0 && cfgInfo.getSampleRates() != null) {
if (bytesRead != recordSize) { // 多段数据处理
break; // 读取完毕或出错 for (int seg = 0; seg < cfgInfo.getNrates(); seg++) {
float segmentSamplesPerCycle = (float) (cfgInfo.getSampleRates()[seg] / lineFreq);
int segmentSamples = cfgInfo.getSegmentSamples()[seg];
// 计算当前段的降采样模数
int downsampleMod = (segmentSamplesPerCycle > unifiedSampleRate) ?
Math.round(segmentSamplesPerCycle / unifiedSampleRate) : 1;
log.info("二进制段{} - 原始: {}点/周波, 统一目标: {}点/周波, 模数: {}",
seg, segmentSamplesPerCycle, unifiedSampleRate, downsampleMod);
// 读取当前段的数据
int segmentInputIndex = 0;
while (segmentInputIndex < segmentSamples) {
int bytesRead = dis.read(buffer, 0, recordSize);
if (bytesRead != recordSize) {
break; // 读取完毕或出错
}
// 降采样每隔downsampleMod个点取一个
if (segmentInputIndex % downsampleMod == 0) {
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();
dataBuf.getSmpData()[ch][sampleIndex] = rawValue;
}
sampleIndex++;
}
segmentInputIndex++;
inputIndex++;
}
} }
} else {
ByteBuffer bb = ByteBuffer.wrap(buffer); // 单段数据处理(保持原有逻辑)
bb.order(ByteOrder.LITTLE_ENDIAN); // COMTRADE标准使用小端序 float originalSamplesPerCycle = (float) cfgInfo.getSampleRate() / lineFreq;
int downsampleMod = (originalSamplesPerCycle > targetSampleRate) ?
// 跳过序号和时间戳 Math.round(originalSamplesPerCycle / targetSampleRate) : 1;
bb.getInt(); // 序号
bb.getInt(); // 时间戳 log.info("二进制采样处理 - 原始: {}点/周波, 目标: {}点/周波, 模数: {}",
originalSamplesPerCycle, targetSampleRate, downsampleMod);
// 读取模拟通道数据
for (int ch = 0; ch < cfgInfo.getAnalogChannels() && ch < MAX_CH_NUM; ch++) { while (inputIndex < cfgInfo.getTotalSamples()) {
short rawValue = bb.getShort(); int bytesRead = dis.read(buffer, 0, recordSize);
if (bytesRead != recordSize) {
// 直接存储原始值像C代码一样 break; // 读取完毕或出错
dataBuf.getSmpData()[ch][sampleIndex] = rawValue; }
// 降采样每隔downsampleMod个点取一个对应C代码i % mod == 0
if (inputIndex % downsampleMod == 0) {
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++;
}
inputIndex++;
} }
sampleIndex++;
} }
} }
dataBuf.setSmpNum(sampleIndex); dataBuf.setSmpNum(sampleIndex);
// 设置采样率为统一的降采样后的值
dataBuf.setSmpRate(unifiedSampleRate);
// 设置增益系数从CFG文件的a系数获取 // 设置增益系数从CFG文件的a系数获取
for (int ch = 0; ch < cfgInfo.getAnalogChannels() && ch < MAX_CH_NUM; ch++) { for (int ch = 0; ch < cfgInfo.getAnalogChannels() && ch < MAX_CH_NUM; ch++) {
ComtradeData.ChannelInfo chInfo = cfgInfo.getAnalogChannelInfos()[ch]; ComtradeData.ChannelInfo chInfo = cfgInfo.getAnalogChannelInfos()[ch];
dataBuf.getUiGainXs()[ch] = (float)chInfo.getA(); // 使用CFG文件中的a系数作为增益 dataBuf.getUiGainXs()[ch] = (float) chInfo.getA(); // 使用CFG文件中的a系数作为增益
} }
log.info("二进制DAT文件读取完成 - 读取了 {} 个采样点", sampleIndex); log.info("二进制DAT文件读取完成 - 原始点数: {}, 实际读取点数: {}, 统一采样率: {}点/周波",
inputIndex, sampleIndex, unifiedSampleRate);
return true; return true;
} }
/**
* 计算有效的目标采样率对应C代码中的out_smp逻辑
* <p>按照C代码第345-346行限制最大采样率为256点/周波</p>
*
* @param cfgInfo CFG配置信息
* @param requestedTargetRate 请求的目标采样率0表示自动选择
* @return 有效的目标采样率
*/
private static float calculateEffectiveTargetSampleRate(ComtradeData.CfgInfo cfgInfo, float requestedTargetRate) {
// 计算原始每周波采样点数
float lineFreq = (float) cfgInfo.getLineFreq();
if (lineFreq <= 0) {
lineFreq = 50.0f;
}
float samplesPerCycle = (float) cfgInfo.getSampleRate() / lineFreq;
// 验证最小采样率要求对应C代码第333-335行
if (samplesPerCycle < 128) {
log.error("采样率过低:每周波{}点最小要求128点", samplesPerCycle);
throw new IllegalArgumentException("采样率过低,每周波采样点数必须>=128当前" + samplesPerCycle);
}
// 使用指定目标采样率,否则使用原始采样率
float targetRate = (requestedTargetRate > 0) ? requestedTargetRate : samplesPerCycle;
// 对应C代码第345-346行限制最大采样率为256点/周波
if (targetRate > 256) {
log.info("采样率{}超过256限制为256点/周波", targetRate);
targetRate = 256;
}
// 目标采样率不能高于原始采样率(不能上采样)
if (targetRate > samplesPerCycle) {
targetRate = samplesPerCycle;
}
log.info("采样率确定 - 原始: {}点/周波, 目标: {}点/周波", samplesPerCycle, targetRate);
return targetRate;
}
/**
* 兼容性方法:不指定目标采样率的读取接口
*/
public static boolean readSampleFile(InputStream cfgStream, InputStream datStream,
DataPq dataBuf, String encoding) {
return readSampleFile(cfgStream, datStream, dataBuf, encoding, 0);
}
/**
* 读取两个COMTRADE文件并统一采样率对应C代码的两文件比较逻辑
* <p>实现了与C代码一致的双文件采样率统一策略</p>
* <ol>
* <li>先读取两个CFG文件</li>
* <li>计算两者的最小采样率但不超过256点/周波)</li>
* <li>将两个文件都降采样到统一的采样率</li>
* </ol>
*
* @param cfg1Stream 文件1的CFG流
* @param dat1Stream 文件1的DAT流
* @param dataBuf1 文件1的数据缓冲区
* @param cfg2Stream 文件2的CFG流
* @param dat2Stream 文件2的DAT流
* @param dataBuf2 文件2的数据缓冲区
* @param encoding 文件编码
* @return 读取是否成功
*/
public static boolean readTwoFilesWithUnifiedSampleRate(
InputStream cfg1Stream, InputStream dat1Stream, DataPq dataBuf1,
InputStream cfg2Stream, InputStream dat2Stream, DataPq dataBuf2,
String encoding) {
try {
// 步骤1读取两个CFG文件
ComtradeData.CfgInfo cfg1 = readCfgFile(cfg1Stream, encoding);
ComtradeData.CfgInfo cfg2 = readCfgFile(cfg2Stream, encoding);
if (cfg1 == null || cfg2 == null) {
log.error("CFG文件读取失败");
return false;
}
// 步骤2计算统一的目标采样率对应C代码中的out_smp计算
float unifiedSampleRate = calculateUnifiedSampleRate(cfg1, cfg2);
log.info("双文件采样率统一 - 文件1原始: {}, 文件2原始: {}, 统一目标: {}",
calculateOriginalSampleRate(cfg1), calculateOriginalSampleRate(cfg2), unifiedSampleRate);
// 步骤3读取两个文件都使用统一的采样率
boolean success1 = readDatFileFromStream(dat1Stream, cfg1, dataBuf1,
cfg1.getDataFileType() != null ? cfg1.getDataFileType() : FILE_TYPE_BINARY, unifiedSampleRate);
boolean success2 = readDatFileFromStream(dat2Stream, cfg2, dataBuf2,
cfg2.getDataFileType() != null ? cfg2.getDataFileType() : FILE_TYPE_BINARY, unifiedSampleRate);
if (!success1 || !success2) {
log.error("DAT文件读取失败");
return false;
}
// 步骤4处理CFG信息
boolean result1 = processCfgAndData(cfg1, dataBuf1, unifiedSampleRate);
boolean result2 = processCfgAndData(cfg2, dataBuf2, unifiedSampleRate);
return result1 && result2;
} catch (Exception e) {
log.error("双文件读取过程中发生错误", e);
return false;
}
}
/**
* 计算两个文件的统一采样率对应C代码第327-346行的逻辑
*/
private static float calculateUnifiedSampleRate(ComtradeData.CfgInfo cfg1, ComtradeData.CfgInfo cfg2) {
float rate1 = calculateOriginalSampleRate(cfg1);
float rate2 = calculateOriginalSampleRate(cfg2);
// 取较小的采样率对应C代码第339-342行
float minRate = Math.min(rate1, rate2);
// 对应C代码第345-346行限制最大采样率为256点/周波
if (minRate > 256) {
log.info("统一采样率{}超过256限制为256点/周波", minRate);
minRate = 256;
}
log.info("双文件采样率统一 - 文件1: {}点/周波, 文件2: {}点/周波, 统一为: {}点/周波",
rate1, rate2, minRate);
return minRate;
}
/**
* 计算原始采样率(每周波采样点数)
*/
private static float calculateOriginalSampleRate(ComtradeData.CfgInfo cfgInfo) {
float lineFreq = (float) cfgInfo.getLineFreq();
if (lineFreq <= 0) {
lineFreq = 50.0f;
}
return (float) cfgInfo.getSampleRate() / lineFreq;
}
} }

View File

@@ -55,7 +55,9 @@ public class ComtradeData {
private ChannelInfo[] digitalChannelInfos; // 数字通道信息 private ChannelInfo[] digitalChannelInfos; // 数字通道信息
private double lineFreq; // 电网频率 private double lineFreq; // 电网频率
private int nrates; // 采样率数量 private int nrates; // 采样率数量
private double sampleRate; // 采样率 private double[] sampleRates; // 每段采样率数组对应C代码rate[]
private int[] segmentSamples; // 每段数据点数组对应C代码fd_data_num[]
private double sampleRate; // 主采样率(兼容性保留)
private int totalSamples; // 总采样点数 private int totalSamples; // 总采样点数
private ClockStruct startTime; // 开始时间 private ClockStruct startTime; // 开始时间
private ClockStruct triggerTime; // 触发时间 private ClockStruct triggerTime; // 触发时间

View File

@@ -43,10 +43,7 @@ public class DataPq {
/** 实际采样点数 */ /** 实际采样点数 */
private int smpNum; private int smpNum;
/** 实际采样率 */ /** 每周波采样点数对应C代码中的smp_rate */
private float factSmpRate;
/** 改良后的采样率 */
private float smpRate; private float smpRate;
/** 系统频率 (Hz) */ /** 系统频率 (Hz) */

View File

@@ -1,6 +1,5 @@
package com.njcn.gather.tools.comtrade.comparewave.service.impl; package com.njcn.gather.tools.comtrade.comparewave.service.impl;
import com.alibaba.fastjson.JSON;
import com.njcn.gather.tools.comtrade.comparewave.config.PowerQualityConfig; 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.PowerQualityCalculator;
import com.njcn.gather.tools.comtrade.comparewave.core.algorithm.WaveformAligner; import com.njcn.gather.tools.comtrade.comparewave.core.algorithm.WaveformAligner;