Browse Source

优化光学算法

master
zhaohe 2 months ago
parent
commit
c9fafaa239
  1. 6
      src/main/java/a8k/app/constant/OptConstant.java
  2. 29
      src/main/java/a8k/app/dao/AppStatePersistenceDao.java
  3. 3
      src/main/java/a8k/app/dao/type/db/OptCfg.java
  4. 1
      src/main/java/a8k/app/dao/type/db/OptRawScanData.java
  5. 17
      src/main/java/a8k/app/dao/type/db/ReactionReport.java
  6. 11
      src/main/java/a8k/app/factory/A8kOptProcessExceptionFactory.java
  7. 30
      src/main/java/a8k/app/i18n/Internationalization.java
  8. 265
      src/main/java/a8k/app/optalgo/A8kOptCurveAnalyzer.java
  9. 49
      src/main/java/a8k/app/optalgo/A8kPeakAnalyzer.java
  10. 106
      src/main/java/a8k/app/optalgo/algo/LinearRegressionCalculator.java
  11. 1
      src/main/java/a8k/app/optalgo/type/A8kOptProcessException.java
  12. 12
      src/main/java/a8k/app/optalgo/type/PeakPresetPosConfig.java
  13. 2
      src/main/java/a8k/app/optalgo/type/ReactionResultStatus.java
  14. 8
      src/main/java/a8k/app/service/data/ProjIdCardInfoMgrService.java
  15. 3
      src/main/java/a8k/app/service/data/ReactionRecordMgrService.java
  16. 47
      src/main/java/a8k/app/service/lowerctrl/OptScanModuleLowerCtrlService.java
  17. 2
      src/main/java/a8k/app/service/module/OptScanCtrlModule.java
  18. 8
      src/main/java/a8k/app/service/module/SamplePreProcessModule.java
  19. 11
      src/main/java/a8k/app/type/a8k/container/A8kOptPeakContainer.java
  20. 4
      src/main/java/a8k/app/type/a8k/opt/A8kOptPeak.java
  21. 1
      src/main/java/a8k/app/type/a8k/opt/PeakName.java
  22. 4
      src/main/java/a8k/app/type/a8k/optfn/A8kOptX.java
  23. 13
      src/main/java/a8k/app/type/a8k/state/SampleInfo.java
  24. 4
      src/main/java/a8k/extui/mgr/ExtApiPageGroupCfgMgr.java
  25. 70
      src/main/java/a8k/extui/page/optalgotest/OptAlgoTestPage.java
  26. 3
      src/main/java/a8k/extui/page/test/codetest/OptFormulaTestPageV2.java
  27. 76
      src/main/resources/db/zapp_a8k_project_opt_config.csv
  28. BIN
      tools/20250427-F-光学光学报告.xlsx
  29. BIN
      tools/20250427-T-光学光学报告-2.xlsx

6
src/main/java/a8k/app/constant/OptConstant.java

@ -1,8 +1,12 @@
package a8k.app.constant; package a8k.app.constant;
public class OptConstant { public class OptConstant {
static public final Integer OPT_F_RESULT_TOLERATE = 300;
static public final Integer OPT_T_RESULT_TOLERATE = 700; static public final Integer OPT_T_RESULT_TOLERATE = 700;
static public final Integer OPT_T_TARGET_VAL = 2300; static public final Integer OPT_T_TARGET_VAL = 2300;
static public final Integer OPT_T_FINAL_TARGET_VAL = 3600;
static public final Integer OPT_F_TARGET_VAL = 3600; static public final Integer OPT_F_TARGET_VAL = 3600;
static public final Integer OPT_F_RESULT_TOLERATE = 300;
static public final Integer OPT_F_FINAL_TARGET_VAL = 3600;
static public final Integer OPT_F_OVERFLOW_VAL = 3900;
} }

29
src/main/java/a8k/app/dao/AppStatePersistenceDao.java

@ -0,0 +1,29 @@
package a8k.app.dao;
import a8k.app.dao.type.db.KeyVal;
import a8k.app.utils.ZSqlite;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class AppStatePersistenceDao extends ZSqlite<KeyVal> {
@Resource
JdbcTemplate jdbcTemplate;
@PostConstruct
void init() {
init(jdbcTemplate, "zapp_state_persistence_dao", KeyVal.class, false);
}
void storage(String key, String val) {
if (key == null || val == null) {
log.error("key or val is null, key: {}, val: {}", key, val);
return;
}
}
}

3
src/main/java/a8k/app/dao/type/db/OptCfg.java

@ -1,8 +1,10 @@
package a8k.app.dao.type.db; package a8k.app.dao.type.db;
import a8k.app.type.a8k.opt.A8kOptType; import a8k.app.type.a8k.opt.A8kOptType;
import a8k.app.type.a8k.opt.PeakName;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
public class OptCfg implements Serializable { public class OptCfg implements Serializable {
public int id = 0; public int id = 0;
@ -19,4 +21,5 @@ public class OptCfg implements Serializable {
public Integer optScanPeakNum; //子项目 实际扫描到的峰的数量 public Integer optScanPeakNum; //子项目 实际扫描到的峰的数量
public Integer peakNameRefNum; //子项目 峰的数量 public Integer peakNameRefNum; //子项目 峰的数量
public List<PeakName> peakConfig;
} }

1
src/main/java/a8k/app/dao/type/db/OptRawScanData.java

@ -8,6 +8,7 @@ public class OptRawScanData {
public Integer lasterGain; public Integer lasterGain;
public Integer scanGain; public Integer scanGain;
public Integer[] rawData; public Integer[] rawData;
public Boolean overflow;
public String toString() { public String toString() {
return ZJsonHelper.objectToJson(this); return ZJsonHelper.objectToJson(this);

17
src/main/java/a8k/app/dao/type/db/ReactionReport.java

@ -6,6 +6,7 @@ import a8k.app.type.a8k.optfn.A8kResultUnitConverterFn;
import a8k.app.type.a8k.BloodType; import a8k.app.type.a8k.BloodType;
import a8k.app.type.a8k.opt.A8kOptType; import a8k.app.type.a8k.opt.A8kOptType;
import a8k.app.type.a8k.proj.ProjInfo; import a8k.app.type.a8k.proj.ProjInfo;
import a8k.app.type.a8k.state.SampleInfo;
import a8k.app.utils.ZJsonHelper; import a8k.app.utils.ZJsonHelper;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -88,6 +89,8 @@ public class ReactionReport implements Serializable { //记录单个反应板的
public String sampleUserid = ""; //用户输入的样本ID不做逻辑只做展示 public String sampleUserid = ""; //用户输入的样本ID不做逻辑只做展示
@Schema(description = "样本ID(系统生成用于追溯)") @Schema(description = "样本ID(系统生成用于追溯)")
public String sampleId = ""; // 样本ID,由系统生成 public String sampleId = ""; // 样本ID,由系统生成
// @Schema(description = "是否是急诊", example = "false")
// public Boolean sampleIsEmergency = false; // 是否急诊
//项目信息 //项目信息
@Schema(description = "项目名称", example = "Tn-I/CK-MB/Myoglobin") @Schema(description = "项目名称", example = "Tn-I/CK-MB/Myoglobin")
@ -130,6 +133,8 @@ public class ReactionReport implements Serializable { //记录单个反应板的
@JsonIgnore @JsonIgnore
public List<OptScanResult> detailOptData = new ArrayList<>();// 扫描上下文数据(包含原始结果扫描参数扫描原始数据) public List<OptScanResult> detailOptData = new ArrayList<>();// 扫描上下文数据(包含原始结果扫描参数扫描原始数据)
// public ProjExtInfoCard projExtInfoCard;
public OptScanResult getOptData(int subProjIndex) { public OptScanResult getOptData(int subProjIndex) {
A8kOptType subProjOptType = getSubProjOptType(subProjIndex); A8kOptType subProjOptType = getSubProjOptType(subProjIndex);
@ -150,5 +155,17 @@ public class ReactionReport implements Serializable { //记录单个反应板的
return projInfo.buildIn.optcfg.get(subProjIndex).subProjName; return projInfo.buildIn.optcfg.get(subProjIndex).subProjName;
} }
@JsonIgnore
public SampleInfo getSampleInfo() {
return new SampleInfo(
sampleId,
false,
false,//在最终结果中添加急诊标识位
sampleBloodType,
sampleUserid,
sampleId
);
}
} }

11
src/main/java/a8k/app/factory/A8kOptProcessExceptionFactory.java

@ -0,0 +1,11 @@
package a8k.app.factory;
import a8k.app.i18n.Internationalization;
import a8k.app.optalgo.type.A8kOptProcessException;
import a8k.app.optalgo.type.ReactionResultStatus;
public class A8kOptProcessExceptionFactory {
static public A8kOptProcessException create(ReactionResultStatus status) {
return new A8kOptProcessException(status, Internationalization.reactionResultStatus2String(status));
}
}

30
src/main/java/a8k/app/i18n/Internationalization.java

@ -1,5 +1,6 @@
package a8k.app.i18n; package a8k.app.i18n;
import a8k.app.optalgo.type.ReactionResultStatus;
import a8k.app.type.a8k.BloodType; import a8k.app.type.a8k.BloodType;
import a8k.app.type.error.ConsumablesScanReportErrorType; import a8k.app.type.error.ConsumablesScanReportErrorType;
import a8k.app.hardware.type.A8kEcode; import a8k.app.hardware.type.A8kEcode;
@ -120,4 +121,33 @@ public class Internationalization {
case FECES -> "粪便"; case FECES -> "粪便";
}; };
} }
public static String reactionResultStatus2String(ReactionResultStatus status) {
return switch (status) {
case DISABLED -> "未启用";
case SUCCESS -> "成功";
case ERROR -> "结果异常";
case ERROR_RESULT_OUT_OF_RANGE -> "结果超出范围";
case ERROR_RESULT_EXCEED_THE_UPPER_LIMIT -> "超过上限";
case ERROR_RESULT_BELOW_THE_LOWER_LIMIT -> "低于下限";
case ERROR_X_EXCEED_THE_UPPER_LIMIT -> "超过上限";
case ERROR_X_BELOW_THE_LOWER_LIMIT -> "低于下限";
case ERROR_QUALITY_PEAK_DETECTION -> "质峰检测错误";
case ERROR_LOST_PEAK_T4 -> "T4峰丢失";
case ERROR_LOST_PEAK_R -> "R峰丢失";
case ERROR_LOST_PEAK_H -> "H峰丢失";
case ERROR_LOST_PEAK_T -> "T峰丢失";
case ERROR_LOST_PEAK_C -> "C峰丢失";
case ERROR_COMPUTE_R_FAIL -> "计算R失败";
case ERROR_COMPUTE_AR_FAIL -> "计算AR失败";
case ERROR_COMPUTE_ATR_FAIL -> "计算ATR失败";
case ERROR_COMPUTE_RFR_FAIL -> "计算RFR失败";
case ERROR_COMPUTE_T4R_FAIL -> "计算T4R失败";
case ERROR_COMPUTE_T4T3R_FAIL -> "计算T4T3R失败";
case ERROR_UNKNOWN_X -> "未知X";
};
}
} }

265
src/main/java/a8k/app/optalgo/A8kOptCurveAnalyzer.java

@ -1,17 +1,23 @@
package a8k.app.optalgo; package a8k.app.optalgo;
import a8k.app.dao.type.db.OptCfg;
import a8k.app.optalgo.algo.LeastSquare; import a8k.app.optalgo.algo.LeastSquare;
import a8k.app.optalgo.algo.LinearRegressionCalculator;
import a8k.app.optalgo.algo.SubSampling; import a8k.app.optalgo.algo.SubSampling;
import a8k.app.optalgo.algo.SupperSampling; import a8k.app.optalgo.algo.SupperSampling;
import a8k.app.optalgo.type.*;
import a8k.app.dao.type.db.OptCfg;
import a8k.app.optalgo.type.A8kOptPeakInfo;
import a8k.app.optalgo.type.PeakPresetPosConfig;
import a8k.app.optalgo.version.OptAlogVersion; import a8k.app.optalgo.version.OptAlogVersion;
import a8k.app.type.a8k.container.A8kOptPeakContainer; import a8k.app.type.a8k.container.A8kOptPeakContainer;
import a8k.app.type.a8k.opt.A8kOptPeak; import a8k.app.type.a8k.opt.A8kOptPeak;
import a8k.app.type.a8k.opt.PeakName; import a8k.app.type.a8k.opt.PeakName;
import a8k.app.type.a8k.opt.PeakQuotient; import a8k.app.type.a8k.opt.PeakQuotient;
import a8k.app.utils.ZJsonHelper; import a8k.app.utils.ZJsonHelper;
import cn.hutool.core.util.ArrayUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import java.util.List;
@Slf4j @Slf4j
@ -28,70 +34,37 @@ public class A8kOptCurveAnalyzer {
OptCfg optCfg; OptCfg optCfg;
Integer projId; Integer projId;
public static void assignPeakName(OptCfg optCfg, A8kOptPeakContainer peaks) {
Assert.notNull(optCfg, "OptCfg cannot be null");
Assert.notNull(peaks, "A8kOptPeakContainer cannot be null");
Assert.isTrue(optCfg.peakConfig != null && !optCfg.peakConfig.isEmpty(), "Peak configuration cannot be null or empty");
Assert.isTrue(optCfg.peakConfig.size() == 5, "Peak configuration must contain exactly 5 peaks");
public A8kOptPeakInfo analysCurve(Integer projId, OptCfg optCfg, Integer[] data) {
double[] doubleData = new double[data.length];
for (int i = 0; i < data.length; i++) {
doubleData[i] = data[i];
}
this.optCfg = optCfg;
this.rawData = data;
this.projId = projId;
return analysCurve(doubleData);
List<PeakName> peakNames = optCfg.peakConfig;
for (int i = 0; i < peakNames.size(); i++) {
peaks.getPeak(i).peakName = peakNames.get(i);
} }
public static void assignPeakName(Integer projId, Integer peakNameRefNum, A8kOptPeakContainer peaks) {
}
//项目ID==1时峰的命名是特殊的
if (projId == 2) {
peaks.P080.peakName = PeakName.C;
peaks.P120.peakName = PeakName.T;
} else {
//
switch (peakNameRefNum) {
case 2 -> {
peaks.P080.peakName = PeakName.T;
peaks.P120.peakName = PeakName.C;
}
case 3 -> {
peaks.P040.peakName = PeakName.H;
peaks.P080.peakName = PeakName.T;
peaks.P120.peakName = PeakName.C;
}
case 4 -> {
peaks.P040.peakName = PeakName.R;
peaks.P080.peakName = PeakName.H;
peaks.P120.peakName = PeakName.T;
peaks.P160.peakName = PeakName.C;
}
case 5 -> {
peaks.P040.peakName = PeakName.T4;
peaks.P080.peakName = PeakName.R;
peaks.P120.peakName = PeakName.H;
peaks.P160.peakName = PeakName.T;
peaks.P200.peakName = PeakName.C;
}
default -> {
}
}
}
public A8kOptPeakInfo analysisCurve(Integer projId, OptCfg optCfg, Integer[] data) {
Assert.isTrue(optCfg.peakConfig != null && !optCfg.peakConfig.isEmpty(), "Peak configuration cannot be null or empty");
Assert.isTrue(optCfg.peakConfig.size() == 5, "Peak configuration must contain exactly 5 peaks");
for (PeakName peakName : optCfg.peakConfig) {
Assert.notNull(peakName, "Peak name cannot be null");
} }
public static void main(String[] args) {
A8kOptPeakContainer peaks = new A8kOptPeakContainer();
assignPeakName(1, 2, peaks);
log.info("{}",
ZJsonHelper.objToPrettyJson(peaks));
double[] doubleData = new double[data.length];
for (int i = 0; i < data.length; i++) {
doubleData[i] = data[i];
} }
private A8kOptPeakInfo analysCurve(double[] data) {
this.optCfg = optCfg;
this.rawData = data;
this.projId = projId;
//过采样 //过采样
afSupperVal = SupperSampling.process(data, 5);
afSupperVal = SupperSampling.process(doubleData, 5);
afSubSampling = SubSampling.process(afSupperVal, 24); afSubSampling = SubSampling.process(afSupperVal, 24);
avg = afSubSampling; avg = afSubSampling;
@ -99,22 +72,62 @@ public class A8kOptCurveAnalyzer {
diffx2 = LeastSquare.calculatedSlopeCurve(diff, 5); diffx2 = LeastSquare.calculatedSlopeCurve(diff, 5);
avgLine = findAvgLine(afSubSampling); avgLine = findAvgLine(afSubSampling);
List<PeakName> peakNames = optCfg.peakConfig;
A8kOptPeakContainer peaks = new A8kOptPeakContainer(); A8kOptPeakContainer peaks = new A8kOptPeakContainer();
findpeak(avg, 20, 60, peaks.P040);
findpeak(avg, 60, 100, peaks.P080);
findpeak(avg, 100, 140, peaks.P120);
findpeak(avg, 140, 180, peaks.P160);
findpeak(avg, 180, 220, peaks.P200);
// assign peak names to peaks
for (int i = 0; i < peakNames.size(); i++) {
peaks.getPeak(i).peakName = peakNames.get(i);
}
for (int i = 0; i < peakNames.size(); i++) {
tryFindPeak(afSubSampling, peaks, i);
}
//计算峰的面积
for (int i = 0; i < 5; i++) {
A8kOptPeak peak = peaks.getPeak(i);
if (peak.isFindPeak()) {
peak.area = computePeakArea(afSubSampling, peak.peakStartPos, peak.peakEndPos);
}
}
return buildA8kOptPeakInfo(afSubSampling, peaks);
}
assignPeakName(projId, optCfg.peakNameRefNum, peaks);
void tryFindPeak(double[] data, A8kOptPeakContainer peaks, Integer off) {
A8kOptPeak curPeak = peaks.getPeak(off);
A8kOptPeak prePeak = off == 0 ? null : peaks.getPeak(off - 1);
if (!curPeak.peakName.equals(PeakName.N)) {
findPeak(avg, (off + 1) * 40, 40, curPeak);
return buildA8kOptPeakInfo(afSubSampling, peaks);
if (curPeak.isFindPeak()) {
if (prePeak == null || prePeak.peakName.equals(PeakName.N) || !prePeak.isFindPeak()) {
curPeak.peakStartPos = curPeak.peakPos - 20; // 20个点的中间位置
} else {
curPeak.peakStartPos = (prePeak.peakPos + curPeak.peakPos) / 2;
//修正上一个Peak的结束位置
prePeak.peakEndPos = curPeak.peakStartPos;
}
//Peak的结束位先设置为当前Peak的开始位 + 20个点
curPeak.peakEndPos = curPeak.peakPos + 20; // 20个点的中间位置
}
}
} }
double findAvgLine(double[] inputRaw) { double findAvgLine(double[] inputRaw) {
/*
* 下面代码是为了计算平均线
* 条件cnt < (range - 15 * inputRaw.length / 250) 是为了过滤掉曲线中的峰值.
*
* 逻辑如下
* 计算所有小于 base_min 的点的平均值
* 如果小于base_min的点数过少, 说明base_min过低, 提高base_min, 重新计算.
* 直到小于base_min的点数满足条件, 计算出平均值.
*
*/
double base_min = 500; double base_min = 500;
double fsum = 0; double fsum = 0;
int cnt = 0; int cnt = 0;
@ -131,83 +144,111 @@ public class A8kOptCurveAnalyzer {
} }
base_min = base_min + 50; base_min = base_min + 50;
} while (cnt < range - 15 * inputRaw.length / 250);
} while (cnt < (range - 15 * inputRaw.length / 250));
return fsum / cnt; return fsum / cnt;
} }
void findpeak(double[] data, int search_start, int search_end, A8kOptPeak retpeak) {
// find peak
int findpeakTargetPos = search_start + (search_end - search_start) / 2;
log.info("find peak {}", findpeakTargetPos);
void findPeak(double[] data, int search_center, int range, A8kOptPeak retpeak) {
/*
*
* ....
* / \
* | |
* -------| |--------
*
*
*/
int search_start = search_center - range / 2;
int search_end = search_center + range / 2;
boolean checkPeakPassed = true;
retpeak.state = A8kOptPeak.State.NOT_FIND_PEAK; retpeak.state = A8kOptPeak.State.NOT_FIND_PEAK;
retpeak.area = 0.0; retpeak.area = 0.0;
retpeak.peakPos = 0; retpeak.peakPos = 0;
retpeak.peakStartPos = 0; retpeak.peakStartPos = 0;
retpeak.peakEndPos = 0; retpeak.peakEndPos = 0;
double max = 0;
int peakpos = 0;
int midpos = search_start + (search_end - search_start) / 2;
for (int i = search_start; i < search_end; i++) {
if (data[i] > max) {
max = data[i];
peakpos = i;
double max = -1;
double peakposval = 0;
int peakpos = -1;
log.info("------> find peak-{}", search_center);
for (int i = search_start; i < search_end - 20; i++) {
double sum = computeSum(data, i, 20);
if (sum > max) {
max = sum;
peakpos = i + 10; // 20个点的中间位置
peakposval = data[i + 10]; // 20个点的中间位置的值
} }
} }
if (max < avgLine) {
log.warn("find peak-{} fail, max:{} < m_cxt.agvline:{}", findpeakTargetPos, max, avgLine);
return;
} else if (peakpos > midpos + 15) {
log.warn("find peak-{} fail, peakpos > midpos + 15:{}", findpeakTargetPos, midpos + 15);
return;
} else if (peakpos < midpos - 15) {
log.warn("find peak-{} fail, peakpos < midpos - 15:{}", findpeakTargetPos, midpos - 15);
log.info("- peakPos :{}", peakpos);
log.info("- peakDiffPo :{}", peakpos - search_center);
log.info("- peakValue :{}", peakposval);
log.info("-");
log.info("------> check it");
log.info("- check peakPosVal");
if (peakposval < avgLine) {
log.warn("- check peakPosVal < avgLine fail, peakPosVal={} avgLine={}", peakposval, avgLine);
checkPeakPassed = false;
} else {
log.info("- check peakPosVal < avgLine pass, peakPosVal={} avgLine={}", peakposval, avgLine);
}
log.info("-");
log.info("- check linear regression");
double[] result = LinearRegressionCalculator.calculateRegression(ArrayUtil.sub(data, search_start, search_end));
if (result[2] > 0.8) {
/*
* 这里对曲线做线性回归,如果线性回归通过,说明当前曲线是一条直线,说明当前曲线没有峰值
*/
log.warn("- check linear regression fail, rVal = {}", result[2]);
checkPeakPassed = false;
} else {
log.info("- check linear regression pass, rVal = {}", result[2]);
}
log.info("-");
if (!checkPeakPassed) {
log.error("find peak-{} fail", search_center);
log.info("-");
retpeak.state = A8kOptPeak.State.NOT_FIND_PEAK;
return; return;
} }
// find_peak_start
// 从pos向前找20个点从低于均值线的坐标开始找找到diff2的最大值
retpeak.peakPos = peakpos; retpeak.peakPos = peakpos;
retpeak.peakStartPos = findPeakStartTurnPoint(data, peakpos - 20, peakpos) - 4; //-4 是经验数值
retpeak.peakEndPos = findPeakEndTurnPoint(data, peakpos, peakpos + 20) + 4; //+4 是经验数值
retpeak.area = computePeakArea(data, retpeak.peakStartPos, retpeak.peakEndPos);
retpeak.state = A8kOptPeak.State.FIND_PEAK; retpeak.state = A8kOptPeak.State.FIND_PEAK;
log.info("find peak:{} startpos:{} endpos:{} area:{}", retpeak.peakPos, retpeak.peakStartPos, retpeak.peakEndPos, retpeak.area);
// find_peak_end
// 从pos向后找20个点找到diff2的最大值
log.info("- find peak-{} success, peakPos:{}", search_center, peakpos);
log.info("-");
} }
int findPeakStartTurnPoint(double[] data, int search_start, int suggest_search_end) {
int peakTurnPos = search_start;
double maxdiff2 = diffx2[search_start];
for (int i = search_start; i < suggest_search_end; i++) {
if (diffx2[i] > maxdiff2) {
maxdiff2 = diffx2[i];
peakTurnPos = i;
}
}
return peakTurnPos;
double computePeakArea(double[] data, int start, int end) {
double area = 0;
for (int i = start; i < end; i++) {
area += data[i];
} }
int findPeakEndTurnPoint(double[] data, int search_start, int suggest_search_end) {
return findPeakStartTurnPoint(new double[0], search_start, suggest_search_end);
double baselinearea = (data[start] + data[end]) * abs(end - start) / 2;
area = area - baselinearea;
if (area < 0) {
area = 0;
}
return area;
} }
double abs(double a) { double abs(double a) {
return a > 0 ? a : -a; return a > 0 ? a : -a;
} }
double computePeakArea(double[] data, int start, int end) {
double area = 0;
for (int i = start; i < end; i++) {
area += data[i];
}
double baselinearea = (data[start] + data[end]) * abs(end - start) / 2;
return abs(area - baselinearea);
double computeSum(double[] data, int start, int windowSize) {
double sum = 0.0;
for (int i = start; i < start + windowSize && i < data.length; i++) {
sum += data[i];
}
return sum;
} }
static private Double computePeakDivision(PeakQuotient pdtype, A8kOptPeakContainer peaks) { static private Double computePeakDivision(PeakQuotient pdtype, A8kOptPeakContainer peaks) {

49
src/main/java/a8k/app/optalgo/A8kPeakAnalyzer.java

@ -3,6 +3,7 @@ package a8k.app.optalgo;
import a8k.app.dao.type.db.OptCfg; import a8k.app.dao.type.db.OptCfg;
import a8k.app.dao.type.db.ProjExtInfoCard; import a8k.app.dao.type.db.ProjExtInfoCard;
import a8k.app.dao.type.db.ReactionReport; import a8k.app.dao.type.db.ReactionReport;
import a8k.app.factory.A8kOptProcessExceptionFactory;
import a8k.app.optalgo.type.A8kOptPeakInfo; import a8k.app.optalgo.type.A8kOptPeakInfo;
import a8k.app.optalgo.type.A8kOptProcessException; import a8k.app.optalgo.type.A8kOptProcessException;
import a8k.app.type.a8k.opt.PeakQuotient; import a8k.app.type.a8k.opt.PeakQuotient;
@ -64,25 +65,25 @@ public class A8kPeakAnalyzer {
case T4R -> ReactionResultStatus.ERROR_COMPUTE_T4R_FAIL; case T4R -> ReactionResultStatus.ERROR_COMPUTE_T4R_FAIL;
case T4T3R -> ReactionResultStatus.ERROR_COMPUTE_T4T3R_FAIL; case T4T3R -> ReactionResultStatus.ERROR_COMPUTE_T4T3R_FAIL;
}; };
throw new A8kOptProcessException(estatus, estatus.name());
throw A8kOptProcessExceptionFactory.create(estatus);
} }
public static void checkX(Double x, Double lowLimit, Double upLimit) throws A8kOptProcessException { public static void checkX(Double x, Double lowLimit, Double upLimit) throws A8kOptProcessException {
if (x > upLimit) { if (x > upLimit) {
throw new A8kOptProcessException(ReactionResultStatus.ERROR_X_EXCEED_THE_UPPER_LIMIT, "ERROR_X_OUT_OF_RANGE");
throw A8kOptProcessExceptionFactory.create(ReactionResultStatus.ERROR_X_EXCEED_THE_UPPER_LIMIT);
} }
if (x < lowLimit) { if (x < lowLimit) {
throw new A8kOptProcessException(ReactionResultStatus.ERROR_X_BELOW_THE_LOWER_LIMIT, "ERROR_X_OUT_OF_RANGE");
throw A8kOptProcessExceptionFactory.create(ReactionResultStatus.ERROR_X_BELOW_THE_LOWER_LIMIT);
} }
} }
static public void checkResult1(Double result1, Double lowLimit, Double upLimit) throws A8kOptProcessException { static public void checkResult1(Double result1, Double lowLimit, Double upLimit) throws A8kOptProcessException {
if (result1 > upLimit) { if (result1 > upLimit) {
throw new A8kOptProcessException(ReactionResultStatus.ERROR_RESULT_EXCEED_THE_UPPER_LIMIT, "ERROR_BEYOND_THE_UPPER_LIMIT");
throw A8kOptProcessExceptionFactory.create(ReactionResultStatus.ERROR_RESULT_EXCEED_THE_UPPER_LIMIT);
} }
if (result1 < lowLimit) { if (result1 < lowLimit) {
throw new A8kOptProcessException(ReactionResultStatus.ERROR_RESULT_BELOW_THE_LOWER_LIMIT, "ERROR_BEYOND_THE_LOWER_LIMIT");
throw A8kOptProcessExceptionFactory.create(ReactionResultStatus.ERROR_RESULT_BELOW_THE_LOWER_LIMIT);
} }
} }
@ -114,7 +115,10 @@ public class A8kPeakAnalyzer {
case T4R -> addPD(cxt, PeakQuotient.T4R); case T4R -> addPD(cxt, PeakQuotient.T4R);
case T4T3R -> addPD(cxt, PeakQuotient.T4T3R); case T4T3R -> addPD(cxt, PeakQuotient.T4T3R);
case RFR_ADD_T4T3R -> addPD(cxt, PeakQuotient.RFR, PeakQuotient.T4T3R); case RFR_ADD_T4T3R -> addPD(cxt, PeakQuotient.RFR, PeakQuotient.T4T3R);
default -> throw new A8kOptProcessException(ReactionResultStatus.ERROR_UNKOWN_X, "ERROR_UNKOWN_X");
case UNSUPPORTED -> {
zloggerRecorder.error("Unsupported X type: %s", xType);
throw A8kOptProcessExceptionFactory.create(ReactionResultStatus.ERROR_UNKNOWN_X);
}
}; };
} }
@ -290,10 +294,43 @@ public class A8kPeakAnalyzer {
return reactionResult; return reactionResult;
} }
static void checkPeakIsAllFind(OptCfg optCfg, A8kOptPeakInfo a8kOptPeakInfo) throws A8kOptProcessException {
for (var peak : optCfg.peakConfig) {
switch (peak) {
case T4 -> {
if (!a8kOptPeakInfo.T4.isFindPeak())
throw A8kOptProcessExceptionFactory.create(ReactionResultStatus.ERROR_LOST_PEAK_T4);
}
case R -> {
if (!a8kOptPeakInfo.R.isFindPeak())
throw A8kOptProcessExceptionFactory.create(ReactionResultStatus.ERROR_LOST_PEAK_R);
}
case H -> {
if (!a8kOptPeakInfo.H.isFindPeak())
throw A8kOptProcessExceptionFactory.create(ReactionResultStatus.ERROR_LOST_PEAK_H);
}
case T -> {
if (!a8kOptPeakInfo.T.isFindPeak())
throw A8kOptProcessExceptionFactory.create(ReactionResultStatus.ERROR_LOST_PEAK_T);
}
case C -> {
if (!a8kOptPeakInfo.C.isFindPeak())
throw A8kOptProcessExceptionFactory.create(ReactionResultStatus.ERROR_LOST_PEAK_C);
}
case N -> {
continue;
}
}
}
}
static private ReactionReport.ReactionResult analyzeResult(OptAnalyzeContext optcxt, A8kOptPeakInfo a8kOptPeakInfo) throws A8kOptProcessException { static private ReactionReport.ReactionResult analyzeResult(OptAnalyzeContext optcxt, A8kOptPeakInfo a8kOptPeakInfo) throws A8kOptProcessException {
// A8kOptFnFormula fnFormual = optcxt.projInfoCxt.getA8kOptFnFormula(optcxt.subProjIndex); // A8kOptFnFormula fnFormual = optcxt.projInfoCxt.getA8kOptFnFormula(optcxt.subProjIndex);
ProjExtInfoCard.A8kOptFnGroup fnFormual = ProjInfoUtils.getA8kOptFnFormula(optcxt.projInfoCxt.ext, optcxt.subProjIndex); ProjExtInfoCard.A8kOptFnGroup fnFormual = ProjInfoUtils.getA8kOptFnFormula(optcxt.projInfoCxt.ext, optcxt.subProjIndex);
Assert.isTrue(fnFormual != null, "fnFormual must not be null"); Assert.isTrue(fnFormual != null, "fnFormual must not be null");
checkPeakIsAllFind(optcxt.getProjOptInfo(), a8kOptPeakInfo);
Double result1 = null; Double result1 = null;
// //
//粪便类型的样本使用,血清和血浆的参数 //粪便类型的样本使用,血清和血浆的参数

106
src/main/java/a8k/app/optalgo/algo/LinearRegressionCalculator.java

@ -0,0 +1,106 @@
package a8k.app.optalgo.algo;
import lombok.Data;
import java.util.List;
public class LinearRegressionCalculator {
@Data
public static class Point2D {
public double x;
public double y;
public Point2D(double x, double y) {
this.x = x;
this.y = y;
}
}
public static double[] calculateRegression(double[] points) {
List<Point2D> pointList = new java.util.ArrayList<>();
for (int i = 0; i < points.length; i += 1) {
pointList.add(new Point2D(i, points[i]));
}
return calculateRegression(pointList);
}
/**
* 计算线性回归和相关系数
* @param points 数据点集合
* @return 包含斜率截距和相关系数的数组 [slope, intercept, r]
*/
public static double[] calculateRegression(List<Point2D> points) {
int n = points.size();
if (n < 2) {
throw new IllegalArgumentException("至少需要两个点进行计算");
}
// 计算x和y的总和
double sumX = 0, sumY = 0;
for (Point2D p : points) {
sumX += p.getX();
sumY += p.getY();
}
// 计算x和y的平均值
double meanX = sumX / n;
double meanY = sumY / n;
// 计算所需的和
double sumXY = 0, sumXSq = 0, sumYSq = 0;
for (Point2D p : points) {
double xDev = p.getX() - meanX;
double yDev = p.getY() - meanY;
sumXY += xDev * yDev;
sumXSq += xDev * xDev;
sumYSq += yDev * yDev;
}
// 计算斜率 (m) 和截距 (b)
double slope = sumXY / sumXSq;
double intercept = meanY - slope * meanX;
// 计算相关系数 r
double r = sumXY / Math.sqrt(sumXSq * sumYSq);
return new double[]{slope, intercept, r};
}
public static void main(String[] args) {
// 示例数据点
List<Point2D> points = List.of(
new Point2D(1, 2),
new Point2D(2, 3),
new Point2D(3, 5),
new Point2D(4, 4),
new Point2D(5, 6)
);
// 计算线性回归和相关系数
double[] result = calculateRegression(points);
double slope = result[0];
double intercept = result[1];
double r = result[2];
// 输出结果
System.out.println("回归方程: y = " + String.format("%.4f", slope) + "x + " + String.format("%.4f", intercept));
System.out.println("相关系数 r = " + String.format("%.4f", r));
System.out.println("决定系数 R² = " + String.format("%.4f", r * r));
// 解释相关系数
String interpretation;
if (Math.abs(r) >= 0.8) {
interpretation = "强相关";
} else if (Math.abs(r) >= 0.5) {
interpretation = "中度相关";
} else {
interpretation = "弱相关";
}
System.out.println("相关性: " + interpretation);
}
}

1
src/main/java/a8k/app/optalgo/type/A8kOptProcessException.java

@ -7,5 +7,6 @@ public class A8kOptProcessException extends Exception {
public A8kOptProcessException(ReactionResultStatus status, String errorMsg) { public A8kOptProcessException(ReactionResultStatus status, String errorMsg) {
super(status.toString()); super(status.toString());
this.status = status; this.status = status;
this.errorMsg = errorMsg;
} }
} }

12
src/main/java/a8k/app/optalgo/type/PeakPresetPosConfig.java

@ -0,0 +1,12 @@
package a8k.app.optalgo.type;
import a8k.app.type.a8k.opt.PeakName;
import java.util.List;
public class PeakPresetPosConfig {
public PeakName P040 = null;
public PeakName P080 = null;
public PeakName P120 = null;
public PeakName P160 = null;
public PeakName P200 = null;
}

2
src/main/java/a8k/app/optalgo/type/ReactionResultStatus.java

@ -48,5 +48,5 @@ public enum ReactionResultStatus {
ERROR_COMPUTE_T4T3R_FAIL, // ERROR_COMPUTE_T4T3R_FAIL, //
ERROR_UNKOWN_X,
ERROR_UNKNOWN_X,
} }

8
src/main/java/a8k/app/service/data/ProjIdCardInfoMgrService.java

@ -163,7 +163,7 @@ public class ProjIdCardInfoMgrService {
ecode.add(A8kEcode.PROJ_CARD_ERROR_WRONG_FUNCTION_X_TYPE); ecode.add(A8kEcode.PROJ_CARD_ERROR_WRONG_FUNCTION_X_TYPE);
} }
if (formula.x != null && formula.x.equals(A8kOptX.USUPPORT)) {
if (formula.x != null && formula.x.equals(A8kOptX.UNSUPPORTED)) {
ecode.add(A8kEcode.PROJ_CARD_ERROR_WRONG_FUNCTION_X_TYPE); ecode.add(A8kEcode.PROJ_CARD_ERROR_WRONG_FUNCTION_X_TYPE);
} }
@ -178,15 +178,15 @@ public class ProjIdCardInfoMgrService {
private void verifyA8kPiecewiseFn(A8kPiecewiseFn formula, List<A8kEcode> ecode) { private void verifyA8kPiecewiseFn(A8kPiecewiseFn formula, List<A8kEcode> ecode) {
if (formula.judeX == null || formula.judeX.equals(A8kOptX.USUPPORT)) {
if (formula.judeX == null || formula.judeX.equals(A8kOptX.UNSUPPORTED)) {
ecode.add(A8kEcode.PROJ_CARD_ERROR_WRONG_FUNCTION_JUDGE_X_TYPE); ecode.add(A8kEcode.PROJ_CARD_ERROR_WRONG_FUNCTION_JUDGE_X_TYPE);
} }
if (formula.lX == null || formula.lX.equals(A8kOptX.USUPPORT)) {
if (formula.lX == null || formula.lX.equals(A8kOptX.UNSUPPORTED)) {
ecode.add(A8kEcode.PROJ_CARD_ERROR_WRONG_FUNCTION_L_X_TYPE); ecode.add(A8kEcode.PROJ_CARD_ERROR_WRONG_FUNCTION_L_X_TYPE);
} }
if (formula.hX == null || formula.hX.equals(A8kOptX.USUPPORT)) {
if (formula.hX == null || formula.hX.equals(A8kOptX.UNSUPPORTED)) {
ecode.add(A8kEcode.PROJ_CARD_ERROR_WRONG_FUNCTION_H_X_TYPE); ecode.add(A8kEcode.PROJ_CARD_ERROR_WRONG_FUNCTION_H_X_TYPE);
} }

3
src/main/java/a8k/app/service/data/ReactionRecordMgrService.java

@ -36,7 +36,6 @@ public class ReactionRecordMgrService {
} }
public void addRecord(SampleInfo sampleInfo, ProjInfo projInfo, List<OptScanResult> optScanResults, List<ReactionReport.ReactionResult> reactionResults) { public void addRecord(SampleInfo sampleInfo, ProjInfo projInfo, List<OptScanResult> optScanResults, List<ReactionReport.ReactionResult> reactionResults) {
if (reactionResults == null || reactionResults.isEmpty()) { if (reactionResults == null || reactionResults.isEmpty()) {
return; return;
@ -49,9 +48,11 @@ public class ReactionRecordMgrService {
record.sampleBarcode = sampleInfo.sampleBarcode; record.sampleBarcode = sampleInfo.sampleBarcode;
record.sampleUserid = sampleInfo.userid; record.sampleUserid = sampleInfo.userid;
record.sampleId = sampleInfo.sampleId; record.sampleId = sampleInfo.sampleId;
// record.sampleIsEmergency = sampleInfo.isEmergency;
record.projName = projInfo.buildIn.projName; record.projName = projInfo.buildIn.projName;
record.lotId = projInfo.ext.lotId; record.lotId = projInfo.ext.lotId;
record.projId = projInfo.ext.projId; record.projId = projInfo.ext.projId;
// record.projExtInfoCard = projInfo.ext;//TODO:添加配置 支持保存和不保存ID卡信息
record.setExpiryDate(projInfo.ext.expiryDate); record.setExpiryDate(projInfo.ext.expiryDate);
record.operator = operator; record.operator = operator;
record.projShortName = projInfo.ext.projShortName; record.projShortName = projInfo.ext.projShortName;

47
src/main/java/a8k/app/service/lowerctrl/OptScanModuleLowerCtrlService.java

@ -37,9 +37,11 @@ public class OptScanModuleLowerCtrlService {
public Boolean scanAgain; public Boolean scanAgain;
public Integer maxval; public Integer maxval;
public void process(Integer[] data, double nowgain, int expectResult, int tolerate) { public void process(Integer[] data, double nowgain, int expectResult, int tolerate) {
Assert.isTrue(data.length == 1200, "data.length must be 1200"); Assert.isTrue(data.length == 1200, "data.length must be 1200");
maxval = data[0]; maxval = data[0];
for (int i = 1; i < data.length; i++) { for (int i = 1; i < data.length; i++) {
if (maxval < data[i]) { if (maxval < data[i]) {
@ -47,12 +49,20 @@ public class OptScanModuleLowerCtrlService {
} }
} }
if (maxval > expectResult + tolerate) {
log.info("maxval: {}", maxval);
if (maxval > 3900) {
scanAgain = true; scanAgain = true;
suggestGain = nowgain / 2; suggestGain = nowgain / 2;
return; return;
} }
if (maxval > expectResult + tolerate) {
scanAgain = true;
suggestGain = nowgain * ((double) expectResult / (double) maxval);
return;
}
if (maxval > expectResult) { if (maxval > expectResult) {
scanAgain = false; scanAgain = false;
suggestGain = nowgain; suggestGain = nowgain;
@ -132,6 +142,26 @@ public class OptScanModuleLowerCtrlService {
} }
public Integer getMaxVal(Integer[] vals) {
Integer maxVal = vals[0];
for (int i = 1; i < vals.length; i++) {
if (maxVal < vals[i]) {
maxVal = vals[i];
}
}
return maxVal;
}
public Integer[] softZoomIntegerVal(Integer[] vals, Integer maxTargetVal) {
Assert.notNull(vals, "vals must not be null");
Integer[] result = new Integer[vals.length];
double magnification = (double) maxTargetVal / getMaxVal(vals);
for (int i = 0; i < vals.length; i++) {
result[i] = (int) Math.round(vals[i] * magnification);
}
return result;
}
public OptRawScanData doOptScan(A8kOptType optType, Boolean autoAmpl) throws AppException { public OptRawScanData doOptScan(A8kOptType optType, Boolean autoAmpl) throws AppException {
log.info("doOptScan ............"); log.info("doOptScan ............");
log.info(" optType : {}", optType); log.info(" optType : {}", optType);
@ -142,10 +172,13 @@ public class OptScanModuleLowerCtrlService {
double scanRealGain = 0; double scanRealGain = 0;
int scanRawGain = 0; int scanRawGain = 0;
int lasterGain = optModuleExtParamsMgr.getOptLasterRawGain(optType); int lasterGain = optModuleExtParamsMgr.getOptLasterRawGain(optType);
int adjustCount = 2;
int adjustCount = 3;
Integer[] result = null; Integer[] result = null;
for (int i = 0; i < adjustCount; i++) {
for (int i = 0; ; i++) {
if (i >= adjustCount) {
break;
}
if (i == 0) { if (i == 0) {
scanRealGain = optModuleExtParamsMgr.getScanGain(optType); scanRealGain = optModuleExtParamsMgr.getScanGain(optType);
@ -200,7 +233,13 @@ public class OptScanModuleLowerCtrlService {
rawData.optType = optType; rawData.optType = optType;
rawData.scanGain = scanRawGain; rawData.scanGain = scanRawGain;
rawData.lasterGain = lasterGain; rawData.lasterGain = lasterGain;
rawData.overflow = getMaxVal(result) >= OptConstant.OPT_F_OVERFLOW_VAL;
rawData.rawData = result; rawData.rawData = result;
// rawData.rawData = softZoomIntegerVal(result, optType == A8kOptType.TOPT ? OptConstant.OPT_T_TARGET_VAL : OptConstant.OPT_F_FINAL_TARGET_VAL);
if (rawData.overflow) {
log.error("光学扫描结果溢出,建议调整光学增益");
}
return rawData; return rawData;
} }
@ -213,7 +252,7 @@ public class OptScanModuleLowerCtrlService {
result.scanDate = new Date(); result.scanDate = new Date();
OptRawScanData rawData = doOptScan(projBuildinInfo.optcfg.get(subProjIndex).optType, autoAmpl); OptRawScanData rawData = doOptScan(projBuildinInfo.optcfg.get(subProjIndex).optType, autoAmpl);
A8kOptPeakInfo analysResult = a8KOptCurveAnalyzer.analysCurve(projBuildinInfo.projId, projBuildinInfo.optcfg.get(subProjIndex), rawData.rawData);
A8kOptPeakInfo analysResult = a8KOptCurveAnalyzer.analysisCurve(projBuildinInfo.projId, projBuildinInfo.optcfg.get(subProjIndex), rawData.rawData);
result.rawData = rawData; result.rawData = rawData;
result.analysResult = analysResult; result.analysResult = analysResult;

2
src/main/java/a8k/app/service/module/OptScanCtrlModule.java

@ -191,8 +191,6 @@ public class OptScanCtrlModule {
List<ReactionReport.ReactionResult> reactionResults = FakeReactionResultFactory.build(projInfo); List<ReactionReport.ReactionResult> reactionResults = FakeReactionResultFactory.build(projInfo);
List<OptScanResult> optScanResults = new ArrayList<>(); List<OptScanResult> optScanResults = new ArrayList<>();
reactionRecordMgrService.addRecord(sampleInfo, projInfo, optScanResults, reactionResults); reactionRecordMgrService.addRecord(sampleInfo, projInfo, optScanResults, reactionResults);
} }

8
src/main/java/a8k/app/service/module/SamplePreProcessModule.java

@ -151,7 +151,7 @@ public class SamplePreProcessModule {
synchronized (this) { synchronized (this) {
try { try {
docmd("丢TIP", tipOperationCtrlModule::dropTip); docmd("丢TIP", tipOperationCtrlModule::dropTip);
docmd("HBOT复位", hbotMoveCtrlService::moveToZero);
docmd("HBOT复位", hbotMoveExCtrlService::moveQuickToZero);
} catch (AppException e) { } catch (AppException e) {
log.error("Error during cleanup", e); log.error("Error during cleanup", e);
state.setFatalAppError(AppErrorFactory.exceptionToAppError(e)); state.setFatalAppError(AppErrorFactory.exceptionToAppError(e));
@ -180,7 +180,7 @@ public class SamplePreProcessModule {
projBuildInInfo.reactionPlateIncubationTimeMin * 60 projBuildInInfo.reactionPlateIncubationTimeMin * 60
); );
docmd("丢TIP", tipOperationCtrlModule::dropTip); docmd("丢TIP", tipOperationCtrlModule::dropTip);
docmd("HBOT复位", hbotMoveCtrlService::moveToZero);
docmd("HBOT复位", hbotMoveExCtrlService::moveQuickToZero);
} }
@ -217,11 +217,15 @@ public class SamplePreProcessModule {
startTask(); startTask();
actionTaskPool.waitAllDone(); actionTaskPool.waitAllDone();
if (actionTaskPool.isHasError()) {
docmd("摇匀模组复位", tubePreProcessCtrlService::resteModule); docmd("摇匀模组复位", tubePreProcessCtrlService::resteModule);
docmd("HBOT复位", () -> { docmd("HBOT复位", () -> {
tipOperationCtrlModule.dropTip(); tipOperationCtrlModule.dropTip();
hbotMoveCtrlService.moveToZero(); hbotMoveCtrlService.moveToZero();
}); });
} else {
docmd("HBOT复位", hbotMoveExCtrlService::moveQuickToZero);
}
AppError fatalError = actionTaskPool.getFatalError(); AppError fatalError = actionTaskPool.getFatalError();

11
src/main/java/a8k/app/type/a8k/container/A8kOptPeakContainer.java

@ -15,6 +15,17 @@ public class A8kOptPeakContainer {
public A8kOptPeak P160 = new A8kOptPeak(); public A8kOptPeak P160 = new A8kOptPeak();
public A8kOptPeak P200 = new A8kOptPeak(); public A8kOptPeak P200 = new A8kOptPeak();
public A8kOptPeak getPeak(int off) {
return switch (off) {
case 0 -> P040;
case 1 -> P080;
case 2 -> P120;
case 3 -> P160;
case 4 -> P200;
default -> throw new IllegalArgumentException("Invalid offset: " + off);
};
}
public void trySetPeakArea(PeakName peakName, Double area) { public void trySetPeakArea(PeakName peakName, Double area) {
Assert.notNull(peakName, "peakName must not be null"); Assert.notNull(peakName, "peakName must not be null");
if (peakName.equals(P040.peakName)) { if (peakName.equals(P040.peakName)) {

4
src/main/java/a8k/app/type/a8k/opt/A8kOptPeak.java

@ -32,6 +32,10 @@ public class A8kOptPeak {
return val; return val;
} }
public Boolean isFindPeak() {
return state == State.FIND_PEAK;
}
public A8kOptPeak() { public A8kOptPeak() {
} }

1
src/main/java/a8k/app/type/a8k/opt/PeakName.java

@ -6,4 +6,5 @@ public enum PeakName {
H, H,
T, T,
C, C,
N,//
} }

4
src/main/java/a8k/app/type/a8k/optfn/A8kOptX.java

@ -1,7 +1,7 @@
package a8k.app.type.a8k.optfn; package a8k.app.type.a8k.optfn;
public enum A8kOptX { public enum A8kOptX {
USUPPORT,
UNSUPPORTED,
R, //1 R, //1
AR, //2 AR, //2
ATR, //3 ATR, //3
@ -25,7 +25,7 @@ public enum A8kOptX {
case 6 -> T4R; case 6 -> T4R;
case 7 -> T4T3R; case 7 -> T4T3R;
case 8 -> RFR_ADD_T4T3R; case 8 -> RFR_ADD_T4T3R;
default -> USUPPORT;
default -> UNSUPPORTED;
}; };
} }
} }

13
src/main/java/a8k/app/type/a8k/state/SampleInfo.java

@ -16,4 +16,17 @@ public class SampleInfo implements Serializable {
return String.format("sampleId:%s, isEmergency:%s, bloodType:%s, sampleBarcode:%s, userid:%s", return String.format("sampleId:%s, isEmergency:%s, bloodType:%s, sampleBarcode:%s, userid:%s",
sampleId, isEmergency, bloodType, sampleBarcode, userid); sampleId, isEmergency, bloodType, sampleBarcode, userid);
} }
public SampleInfo() {
//默认构造函数
}
public SampleInfo(String sampleId, Boolean isHighTube, Boolean isEmergency, BloodType bloodType, String sampleBarcode, String userid) {
this.sampleId = sampleId;
this.isHighTube = isHighTube;
this.isEmergency = isEmergency;
this.bloodType = bloodType;
this.sampleBarcode = sampleBarcode;
this.userid = userid;
}
} }

4
src/main/java/a8k/extui/mgr/ExtApiPageGroupCfgMgr.java

@ -13,6 +13,7 @@ import a8k.extui.page.extsetting.db.*;
import a8k.extui.page.init.DeviceInitPage; import a8k.extui.page.init.DeviceInitPage;
import a8k.extui.page.measurement.*; import a8k.extui.page.measurement.*;
import a8k.extui.page.optalgotest.OptAlgoTestPage;
import a8k.extui.page.test.codetest.OptFormulaTestPage; import a8k.extui.page.test.codetest.OptFormulaTestPage;
import a8k.extui.page.test.codetest.OptFormulaTestPageV2; import a8k.extui.page.test.codetest.OptFormulaTestPageV2;
import a8k.extui.page.extapp.debug_assistant.FakeReactionRecordGeneratorPage; import a8k.extui.page.extapp.debug_assistant.FakeReactionRecordGeneratorPage;
@ -150,7 +151,8 @@ public class ExtApiPageGroupCfgMgr {
new Menu("光学标定与验证", ZList.of( new Menu("光学标定与验证", ZList.of(
new Menu(A8kOptVerification.class, "光学模组验证"), new Menu(A8kOptVerification.class, "光学模组验证"),
new Menu(OptModuleParamCalibration.class, "光学模块参数校准")
new Menu(OptModuleParamCalibration.class, "光学模块参数校准"),
new Menu(OptAlgoTestPage.class, "光学算法测试")
)), )),
new Menu("过检专用", ZList.of( new Menu("过检专用", ZList.of(
new Menu(P01PipetteGunVerification.class, "移液枪验证"), new Menu(P01PipetteGunVerification.class, "移液枪验证"),

70
src/main/java/a8k/extui/page/optalgotest/OptAlgoTestPage.java

@ -0,0 +1,70 @@
package a8k.extui.page.optalgotest;
import a8k.app.dao.type.db.ProjExtInfoCard;
import a8k.app.dao.type.db.ReactionReport;
import a8k.app.hardware.type.A8kEcode;
import a8k.app.optalgo.A8kOptCurveAnalyzer;
import a8k.app.optalgo.A8kPeakAnalyzer;
import a8k.app.optalgo.type.A8kOptPeakInfo;
import a8k.app.service.data.ProjInfoMgrService;
import a8k.app.service.data.ReactionRecordMgrService;
import a8k.app.type.a8k.proj.ProjInfo;
import a8k.app.type.exception.AppException;
import a8k.extui.mgr.ExtApiPageMgr;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class OptAlgoTestPage {
private final ExtApiPageMgr extApiPageMgr;
private final ReactionRecordMgrService reactionRecordMgrService;
private final ProjInfoMgrService projInfoMgrService;
public ReactionReport.ReactionResult testAlgo(Integer recordId, Integer subProjIndex) throws AppException {
ReactionReport record = reactionRecordMgrService.getRecordById(recordId);
if (record == null) {
log.error("Record with ID {} not found", recordId);
throw AppException.of(A8kEcode.CODEERROR, "RECORD NOT FOUND");
}
var buildInRecordInfo = projInfoMgrService.getProjBuildInInfo(record.projId);
A8kOptCurveAnalyzer a8KOptCurveAnalyzer = new A8kOptCurveAnalyzer();
A8kOptPeakInfo analysResult = a8KOptCurveAnalyzer.analysisCurve(
record.projId,
buildInRecordInfo.optcfg.get(subProjIndex),
record.detailOptData.get(subProjIndex).rawData.rawData
);
// ProjExtInfoCard projExtInfoCard = record.projExtInfoCard;
ProjExtInfoCard projExtInfoCard = null;
if (projExtInfoCard == null) {
projExtInfoCard = projInfoMgrService.getProjExtInfoCard(record.lotId);
}
if (projExtInfoCard.lotId == null || projExtInfoCard.lotId.isEmpty()) {
log.error("Project Ext Info Card lotId is empty for record ID {}", recordId);
throw AppException.of(A8kEcode.CODEERROR, "PROJECT EXT INFO CARD LOT ID IS EMPTY");
}
ProjInfo projInfo = new ProjInfo(projInfoMgrService.getProjBuildInInfo(record.projId), projExtInfoCard);
return A8kPeakAnalyzer.analysisPeakInfo(record.getSampleInfo(), projInfo, subProjIndex, analysResult);
}
@PostConstruct
public void init() {
var page = extApiPageMgr.newPage(this);
page.addFunction("testAlgo", this::testAlgo)
.setParamVal("recordId", () -> 0)
.setParamVal("subProjIndex", () -> 0);
extApiPageMgr.addPage(page);
}
}

3
src/main/java/a8k/extui/page/test/codetest/OptFormulaTestPageV2.java

@ -82,7 +82,6 @@ public class OptFormulaTestPageV2 {
} }
A8kOptPeakInfo optAlgoAnalysResult; A8kOptPeakInfo optAlgoAnalysResult;
@ -128,7 +127,7 @@ public class OptFormulaTestPageV2 {
ProjInfo projInfoContext = getProjInfo(); ProjInfo projInfoContext = getProjInfo();
A8kOptPeakContainer peaks = new A8kOptPeakContainer(); A8kOptPeakContainer peaks = new A8kOptPeakContainer();
A8kOptCurveAnalyzer.assignPeakName(projInfoContext.buildIn.projId, projInfoContext.buildIn.optcfg.get(optIndex).peakNameRefNum, peaks);
A8kOptCurveAnalyzer.assignPeakName(projInfoContext.buildIn.optcfg.get(optIndex), peaks);
peaks.trySetPeakArea(PeakName.T4, T4Area); peaks.trySetPeakArea(PeakName.T4, T4Area);
peaks.trySetPeakArea(PeakName.R, RArea); peaks.trySetPeakArea(PeakName.R, RArea);

76
src/main/resources/db/zapp_a8k_project_opt_config.csv

@ -1,38 +1,38 @@
id,projId,projName,subProjIndex,subProjName,subProjShortName,optType,optScanRange,optScanPeakNum,peakNameRefNum
0,1,hsCRP,0,hsCRP,CA,FOPT,150,3,3
0,2,PCT,0,PCT,PC,TOPT,150,2,2
0,3,TSH,0,TSH,TS,FOPT,150,2,2
0,4,PRL,0,PRL,PL,FOPT,150,2,2
0,5,T3,0,T3,T3,FOPT,150,2,2
0,6,T4,0,T4,T4,FOPT,150,2,2
0,7,Total β hCG,0,Total β hCG,HC,FOPT,150,3,3
0,8,LH,0,LH,LH,FOPT,150,2,2
0,9,FSH,0,FSH,FS,FOPT,150,2,2
0,10,Progesterone,0,Progesterone,PG,FOPT,150,2,2
0,12,Tn-I plus,0,Tn-I plus,TG,TOPT,150,3,3
0,13,NT-proBNP,0,NT-proBNP,NB,TOPT,150,3,3
0,14,CK-MB,0,CK-MB ,CK,FOPT,150,2,2
0,15,Myoglobin,0,Myoglobin,MY,FOPT,150,2,2
0,16,D-Dimer,0,D-Dimer,DD,FOPT,150,2,2
0,17,HbAlC,0,HbAlC,HB,FOPT,150,2,2
0,18,PCT plus,0,PCT plus,PP,FOPT,150,3,3
0,20,Tn-I/CK-MB/Myoglobin,0,CK-MB,CK-MB,FOPT,200,3,4
0,20,Tn-I/CK-MB/Myoglobin,1,Myoglobin,Myoglobin,FOPT,200,3,4
0,20,Tn-I/CK-MB/Myoglobin,2,Tn-I,Tn-I,TOPT,200,4,4
0,22,PCT/hsCRP,0,PCT,PCT,TOPT,200,3,4
0,22,PCT/hsCRP,1,hsCRP,hsCRP,FOPT,200,3,4
0,24,SAA,0,SAA,SA,FOPT,150,3,3
0,25,AMH,0,AMH,AM,TOPT,150,3,3
0,26,SAA/CRP,0,SAA,SAA,FOPT,250,5,5
0,26,SAA/CRP,1,CRP,CRP,FOPT,250,5,5
0,27,Vitamin D,0,Vitamin D,VD,FOPT,150,2,2
0,33,ST2,0,ST2,ST,FOPT,150,2,2
0,36,MxA,0,MxA,MX,TOPT,150,2,2
0,48,IL-6,0,IL-6,IL,TOPT,150,3,3
0,49,Gastrin 17,0,Gastrin 17,GA,TOPT,150,2,2
0,50,Pepsinogen I/II,0,Pepsinogen I,PG-I,TOPT,150,3,3
0,50,Pepsinogen I/II,1,Pepsinogen II,PG-II,TOPT,150,3,3
0,52,NT-proBNP/ST2,0,ST2,ST2,FOPT,200,2,4
0,52,NT-proBNP/ST2,1,NT-proBNP,NT-proBNP,TOPT,200,3,4
0,54,Troponin T,0,Troponin T,TT,TOPT,150,3,3
0,55,BNP,0,BNP,BP,TOPT,150,3,3
id,projId,projName,subProjIndex,subProjName,subProjShortName,optType,optScanRange,optScanPeakNum,peakNameRefNum,peakConfig
0,1,hsCRP,0,hsCRP,CA,FOPT,150,3,3,"[H,T,C,N,N]"
0,2,PCT,0,PCT,PC,TOPT,150,2,2,"[N,C,T,N,N]"
0,3,TSH,0,TSH,TS,FOPT,150,2,2,"[N,T,C,N,N]"
0,4,PRL,0,PRL,PL,FOPT,150,2,2,"[N,T,C,N,N]"
0,5,T3,0,T3,T3,FOPT,150,2,2,"[N,T,C,N,N]"
0,6,T4,0,T4,T4,FOPT,150,2,2,"[N,T,C,N,N]"
0,7,Total β hCG,0,Total β hCG,HC,FOPT,150,3,3,"[H,T,C,N,N]"
0,8,LH,0,LH,LH,FOPT,150,2,2,"[N,T,C,N,N]"
0,9,FSH,0,FSH,FS,FOPT,150,2,2,"[N,T,C,N,N]"
0,10,Progesterone,0,Progesterone,PG,FOPT,150,2,2,"[N,T,C,N,N]"
0,12,Tn-I plus,0,Tn-I plus,TG,TOPT,150,3,3,"[H,T,C,N,N]"
0,13,NT-proBNP,0,NT-proBNP,NB,TOPT,150,3,3,"[H,T,C,N,N]"
0,14,CK-MB,0,CK-MB ,CK,FOPT,150,2,2,"[N,T,C,N,N]"
0,15,Myoglobin,0,Myoglobin,MY,FOPT,150,2,2,"[N,T,C,N,N]"
0,16,D-Dimer,0,D-Dimer,DD,FOPT,150,2,2,"[N,T,C,N,N]"
0,17,HbAlC,0,HbAlC,HB,FOPT,150,2,2,"[N,T,C,N,N]"
0,18,PCT plus,0,PCT plus,PP,FOPT,150,3,3,"[H,T,C,N,N]"
0,20,Tn-I/CK-MB/Myoglobin,0,CK-MB,CK-MB,FOPT,200,3,4,"[N,H,T,C,N]"
0,20,Tn-I/CK-MB/Myoglobin,1,Myoglobin,Myoglobin,FOPT,200,3,4,"[N,H,T,C,N]"
0,20,Tn-I/CK-MB/Myoglobin,2,Tn-I,Tn-I,TOPT,200,4,4,"[R,H,T,C,N]"
0,22,PCT/hsCRP,0,PCT,PCT,TOPT,200,3,4,"[N,H,T,C,N]"
0,22,PCT/hsCRP,1,hsCRP,hsCRP,FOPT,200,3,4,"[R,N,T,C,N]"
0,24,SAA,0,SAA,SA,FOPT,150,3,3,"[H,T,C,N,N]"
0,25,AMH,0,AMH,AM,TOPT,150,3,3,"[H,T,C,N,N]"
0,26,SAA/CRP,0,SAA,SAA,FOPT,250,5,5,"[T4,R,H,T,C]"
0,26,SAA/CRP,1,CRP,CRP,FOPT,250,5,5,"[T4,R,H,T,C]"
0,27,Vitamin D,0,Vitamin D,VD,FOPT,150,2,2,"[N,T,C,N,N]"
0,33,ST2,0,ST2,ST,FOPT,150,2,2,"[N,T,C,N,N]"
0,36,MxA,0,MxA,MX,TOPT,150,2,2,"[N,T,C,N,N]"
0,48,IL-6,0,IL-6,IL,TOPT,150,3,3,"[H,T,C,N,N]"
0,49,Gastrin 17,0,Gastrin 17,GA,TOPT,150,2,2,"[N,T,C,N,N]"
0,50,Pepsinogen I/II,0,Pepsinogen I,PG-I,TOPT,150,3,3,"[H,T,C,N,N]"
0,50,Pepsinogen I/II,1,Pepsinogen II,PG-II,TOPT,150,3,3,"[H,T,C,N,N]"
0,52,NT-proBNP/ST2,0,ST2,ST2,FOPT,200,2,4,"[N,N,T,C,N]"
0,52,NT-proBNP/ST2,1,NT-proBNP,NT-proBNP,TOPT,200,3,4,"[R,H,N,C,N]"
0,54,Troponin T,0,Troponin T,TT,TOPT,150,3,3,"[H,T,C,N,N]"
0,55,BNP,0,BNP,BP,TOPT,150,3,3,"[H,T,C,N,N]"

BIN
tools/20250427-F-光学光学报告.xlsx

BIN
tools/20250427-T-光学光学报告-2.xlsx

Loading…
Cancel
Save