diff --git a/src/main/java/a8k/app/constant/OptConstant.java b/src/main/java/a8k/app/constant/OptConstant.java index 135f87e..2702f3f 100644 --- a/src/main/java/a8k/app/constant/OptConstant.java +++ b/src/main/java/a8k/app/constant/OptConstant.java @@ -1,8 +1,12 @@ package a8k.app.constant; 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_TARGET_VAL = 2300; - static public final Integer OPT_F_TARGET_VAL = 3600; + static public final Integer OPT_T_RESULT_TOLERATE = 700; + 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_RESULT_TOLERATE = 300; + static public final Integer OPT_F_FINAL_TARGET_VAL = 3600; + static public final Integer OPT_F_OVERFLOW_VAL = 3900; } diff --git a/src/main/java/a8k/app/dao/AppStatePersistenceDao.java b/src/main/java/a8k/app/dao/AppStatePersistenceDao.java new file mode 100644 index 0000000..25e8320 --- /dev/null +++ b/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 { + @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; + } + } + +} diff --git a/src/main/java/a8k/app/dao/type/db/OptCfg.java b/src/main/java/a8k/app/dao/type/db/OptCfg.java index 878dbae..8f745b6 100644 --- a/src/main/java/a8k/app/dao/type/db/OptCfg.java +++ b/src/main/java/a8k/app/dao/type/db/OptCfg.java @@ -1,8 +1,10 @@ package a8k.app.dao.type.db; import a8k.app.type.a8k.opt.A8kOptType; +import a8k.app.type.a8k.opt.PeakName; import java.io.Serializable; +import java.util.List; public class OptCfg implements Serializable { public int id = 0; @@ -17,6 +19,7 @@ public class OptCfg implements Serializable { public A8kOptType optType; //子项目 光学类型 necessity public Integer optScanRange; //子项目 扫描范围 扫描范围 - public Integer optScanPeakNum; //子项目 实际扫描到的峰的数量 - public Integer peakNameRefNum; //子项目 峰的数量 + public Integer optScanPeakNum; //子项目 实际扫描到的峰的数量 + public Integer peakNameRefNum; //子项目 峰的数量 + public List peakConfig; } diff --git a/src/main/java/a8k/app/dao/type/db/OptRawScanData.java b/src/main/java/a8k/app/dao/type/db/OptRawScanData.java index 83ddfed..a84c37b 100644 --- a/src/main/java/a8k/app/dao/type/db/OptRawScanData.java +++ b/src/main/java/a8k/app/dao/type/db/OptRawScanData.java @@ -8,6 +8,7 @@ public class OptRawScanData { public Integer lasterGain; public Integer scanGain; public Integer[] rawData; + public Boolean overflow; public String toString() { return ZJsonHelper.objectToJson(this); diff --git a/src/main/java/a8k/app/dao/type/db/ReactionReport.java b/src/main/java/a8k/app/dao/type/db/ReactionReport.java index d284c7d..ab755c9 100644 --- a/src/main/java/a8k/app/dao/type/db/ReactionReport.java +++ b/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.opt.A8kOptType; import a8k.app.type.a8k.proj.ProjInfo; +import a8k.app.type.a8k.state.SampleInfo; import a8k.app.utils.ZJsonHelper; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; @@ -81,13 +82,15 @@ public class ReactionReport implements Serializable { //记录单个反应板的 public Date creatDate = new Date(); @Schema(description = "血液类型") - public BloodType sampleBloodType = BloodType.WHOLE_BLOOD; //血液类型 + public BloodType sampleBloodType = BloodType.WHOLE_BLOOD; //血液类型 @Schema(description = "样本条码") - public String sampleBarcode = ""; //用于请求用户信息的条码ID + public String sampleBarcode = ""; //用于请求用户信息的条码ID @Schema(description = "样本USRID(不做逻辑,只做展示)") - public String sampleUserid = ""; //用户输入的样本ID,不做逻辑,只做展示 + public String sampleUserid = ""; //用户输入的样本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") @@ -130,6 +133,8 @@ public class ReactionReport implements Serializable { //记录单个反应板的 @JsonIgnore public List detailOptData = new ArrayList<>();// 扫描上下文数据(包含原始结果,扫描参数,扫描原始数据) +// public ProjExtInfoCard projExtInfoCard; + public OptScanResult getOptData(int subProjIndex) { A8kOptType subProjOptType = getSubProjOptType(subProjIndex); @@ -150,5 +155,17 @@ public class ReactionReport implements Serializable { //记录单个反应板的 return projInfo.buildIn.optcfg.get(subProjIndex).subProjName; } + @JsonIgnore + public SampleInfo getSampleInfo() { + return new SampleInfo( + sampleId, + false, + false,//在最终结果中添加急诊标识位 + sampleBloodType, + sampleUserid, + sampleId + ); + } + } diff --git a/src/main/java/a8k/app/factory/A8kOptProcessExceptionFactory.java b/src/main/java/a8k/app/factory/A8kOptProcessExceptionFactory.java new file mode 100644 index 0000000..7a7647b --- /dev/null +++ b/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)); + } +} diff --git a/src/main/java/a8k/app/i18n/Internationalization.java b/src/main/java/a8k/app/i18n/Internationalization.java index ed5d821..ddc048c 100644 --- a/src/main/java/a8k/app/i18n/Internationalization.java +++ b/src/main/java/a8k/app/i18n/Internationalization.java @@ -1,5 +1,6 @@ package a8k.app.i18n; +import a8k.app.optalgo.type.ReactionResultStatus; import a8k.app.type.a8k.BloodType; import a8k.app.type.error.ConsumablesScanReportErrorType; import a8k.app.hardware.type.A8kEcode; @@ -120,4 +121,33 @@ public class Internationalization { 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"; + }; + } + } diff --git a/src/main/java/a8k/app/optalgo/A8kOptCurveAnalyzer.java b/src/main/java/a8k/app/optalgo/A8kOptCurveAnalyzer.java index ad3fbfa..1b2620a 100644 --- a/src/main/java/a8k/app/optalgo/A8kOptCurveAnalyzer.java +++ b/src/main/java/a8k/app/optalgo/A8kOptCurveAnalyzer.java @@ -1,17 +1,23 @@ package a8k.app.optalgo; +import a8k.app.dao.type.db.OptCfg; import a8k.app.optalgo.algo.LeastSquare; +import a8k.app.optalgo.algo.LinearRegressionCalculator; import a8k.app.optalgo.algo.SubSampling; 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.type.a8k.container.A8kOptPeakContainer; import a8k.app.type.a8k.opt.A8kOptPeak; import a8k.app.type.a8k.opt.PeakName; import a8k.app.type.a8k.opt.PeakQuotient; import a8k.app.utils.ZJsonHelper; +import cn.hutool.core.util.ArrayUtil; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; + +import java.util.List; @Slf4j @@ -28,8 +34,27 @@ public class A8kOptCurveAnalyzer { OptCfg optCfg; 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"); + + List peakNames = optCfg.peakConfig; + for (int i = 0; i < peakNames.size(); i++) { + peaks.getPeak(i).peakName = peakNames.get(i); + } + + } + + + 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 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]; @@ -37,61 +62,9 @@ public class A8kOptCurveAnalyzer { this.optCfg = optCfg; this.rawData = data; this.projId = projId; - return analysCurve(doubleData); - } - - 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 static void main(String[] args) { - A8kOptPeakContainer peaks = new A8kOptPeakContainer(); - assignPeakName(1, 2, peaks); - log.info("{}", - ZJsonHelper.objToPrettyJson(peaks)); - } - - - private A8kOptPeakInfo analysCurve(double[] data) { - //过采样 - afSupperVal = SupperSampling.process(data, 5); + afSupperVal = SupperSampling.process(doubleData, 5); afSubSampling = SubSampling.process(afSupperVal, 24); avg = afSubSampling; @@ -99,22 +72,62 @@ public class A8kOptCurveAnalyzer { diffx2 = LeastSquare.calculatedSlopeCurve(diff, 5); avgLine = findAvgLine(afSubSampling); + List peakNames = optCfg.peakConfig; + A8kOptPeakContainer peaks = new A8kOptPeakContainer(); - A8kOptPeakContainer peaks = new A8kOptPeakContainer(); + // 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); - 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); + } - 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) { + + /* + * 下面代码是为了计算平均线 + * 条件cnt < (range - 15 * inputRaw.length / 250) 是为了过滤掉曲线中的峰值. + * + * 逻辑如下 + * 计算所有小于 base_min 的点的平均值 + * 如果小于base_min的点数过少, 说明base_min过低, 提高base_min, 重新计算. + * 直到小于base_min的点数满足条件, 计算出平均值. + * + */ + double base_min = 500; double fsum = 0; int cnt = 0; @@ -131,74 +144,85 @@ public class A8kOptCurveAnalyzer { } base_min = base_min + 50; - } while (cnt < range - 15 * inputRaw.length / 250); + } while (cnt < (range - 15 * inputRaw.length / 250)); 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.area = 0.0; retpeak.peakPos = 0; retpeak.peakStartPos = 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); - return; + 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("-"); - // find_peak_start - // 从pos向前找20个点,从低于均值线的坐标开始找,找到diff2的最大值 - 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; - log.info("find peak:{} startpos:{} endpos:{} area:{}", retpeak.peakPos, retpeak.peakStartPos, retpeak.peakEndPos, retpeak.area); - - // find_peak_end - // 从pos向后找20个点,找到diff2的最大值 - } - - 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; - } + if (!checkPeakPassed) { + log.error("find peak-{} fail", search_center); + log.info("-"); + retpeak.state = A8kOptPeak.State.NOT_FIND_PEAK; + return; } - return peakTurnPos; - } - int findPeakEndTurnPoint(double[] data, int search_start, int suggest_search_end) { - return findPeakStartTurnPoint(new double[0], search_start, suggest_search_end); + retpeak.peakPos = peakpos; + retpeak.state = A8kOptPeak.State.FIND_PEAK; + log.info("- find peak-{} success, peakPos:{}", search_center, peakpos); + log.info("-"); } - double abs(double a) { - return a > 0 ? a : -a; - } double computePeakArea(double[] data, int start, int end) { double area = 0; @@ -207,7 +231,24 @@ public class A8kOptCurveAnalyzer { } double baselinearea = (data[start] + data[end]) * abs(end - start) / 2; - return abs(area - baselinearea); + area = area - baselinearea; + if (area < 0) { + area = 0; + } + return area; + } + + double abs(double a) { + return a > 0 ? a : -a; + } + + + 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) { diff --git a/src/main/java/a8k/app/optalgo/A8kPeakAnalyzer.java b/src/main/java/a8k/app/optalgo/A8kPeakAnalyzer.java index d5964a1..6fda628 100644 --- a/src/main/java/a8k/app/optalgo/A8kPeakAnalyzer.java +++ b/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.ProjExtInfoCard; import a8k.app.dao.type.db.ReactionReport; +import a8k.app.factory.A8kOptProcessExceptionFactory; import a8k.app.optalgo.type.A8kOptPeakInfo; import a8k.app.optalgo.type.A8kOptProcessException; import a8k.app.type.a8k.opt.PeakQuotient; @@ -64,25 +65,25 @@ public class A8kPeakAnalyzer { case T4R -> ReactionResultStatus.ERROR_COMPUTE_T4R_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 { 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) { - 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 { 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) { - 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 T4T3R -> addPD(cxt, 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; } + + 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 { // A8kOptFnFormula fnFormual = optcxt.projInfoCxt.getA8kOptFnFormula(optcxt.subProjIndex); ProjExtInfoCard.A8kOptFnGroup fnFormual = ProjInfoUtils.getA8kOptFnFormula(optcxt.projInfoCxt.ext, optcxt.subProjIndex); Assert.isTrue(fnFormual != null, "fnFormual must not be null"); + checkPeakIsAllFind(optcxt.getProjOptInfo(), a8kOptPeakInfo); Double result1 = null; // //粪便类型的样本使用,血清和血浆的参数 diff --git a/src/main/java/a8k/app/optalgo/algo/LinearRegressionCalculator.java b/src/main/java/a8k/app/optalgo/algo/LinearRegressionCalculator.java new file mode 100644 index 0000000..a39623c --- /dev/null +++ b/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 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 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 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); + } +} diff --git a/src/main/java/a8k/app/optalgo/type/A8kOptProcessException.java b/src/main/java/a8k/app/optalgo/type/A8kOptProcessException.java index 6fddf32..e515fdd 100644 --- a/src/main/java/a8k/app/optalgo/type/A8kOptProcessException.java +++ b/src/main/java/a8k/app/optalgo/type/A8kOptProcessException.java @@ -7,5 +7,6 @@ public class A8kOptProcessException extends Exception { public A8kOptProcessException(ReactionResultStatus status, String errorMsg) { super(status.toString()); this.status = status; + this.errorMsg = errorMsg; } } diff --git a/src/main/java/a8k/app/optalgo/type/PeakPresetPosConfig.java b/src/main/java/a8k/app/optalgo/type/PeakPresetPosConfig.java new file mode 100644 index 0000000..3fe3b2b --- /dev/null +++ b/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; +} diff --git a/src/main/java/a8k/app/optalgo/type/ReactionResultStatus.java b/src/main/java/a8k/app/optalgo/type/ReactionResultStatus.java index 1d772aa..e73d60c 100644 --- a/src/main/java/a8k/app/optalgo/type/ReactionResultStatus.java +++ b/src/main/java/a8k/app/optalgo/type/ReactionResultStatus.java @@ -48,5 +48,5 @@ public enum ReactionResultStatus { ERROR_COMPUTE_T4T3R_FAIL, // - ERROR_UNKOWN_X, + ERROR_UNKNOWN_X, } diff --git a/src/main/java/a8k/app/service/data/ProjIdCardInfoMgrService.java b/src/main/java/a8k/app/service/data/ProjIdCardInfoMgrService.java index 444e6c8..71bee9c 100644 --- a/src/main/java/a8k/app/service/data/ProjIdCardInfoMgrService.java +++ b/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); } - 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); } @@ -178,15 +178,15 @@ public class ProjIdCardInfoMgrService { private void verifyA8kPiecewiseFn(A8kPiecewiseFn formula, List 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); } - 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); } - 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); } diff --git a/src/main/java/a8k/app/service/data/ReactionRecordMgrService.java b/src/main/java/a8k/app/service/data/ReactionRecordMgrService.java index 4ccf624..630c49d 100644 --- a/src/main/java/a8k/app/service/data/ReactionRecordMgrService.java +++ b/src/main/java/a8k/app/service/data/ReactionRecordMgrService.java @@ -36,7 +36,6 @@ public class ReactionRecordMgrService { } - public void addRecord(SampleInfo sampleInfo, ProjInfo projInfo, List optScanResults, List reactionResults) { if (reactionResults == null || reactionResults.isEmpty()) { return; @@ -45,13 +44,15 @@ public class ReactionRecordMgrService { String operator = appUserMgrService.getLoginUsr() != null ? appUserMgrService.getLoginUsr().account : "UNLOGIN"; - record.sampleBloodType = sampleInfo.bloodType; - record.sampleBarcode = sampleInfo.sampleBarcode; - record.sampleUserid = sampleInfo.userid; - record.sampleId = sampleInfo.sampleId; - record.projName = projInfo.buildIn.projName; - record.lotId = projInfo.ext.lotId; - record.projId = projInfo.ext.projId; + record.sampleBloodType = sampleInfo.bloodType; + record.sampleBarcode = sampleInfo.sampleBarcode; + record.sampleUserid = sampleInfo.userid; + record.sampleId = sampleInfo.sampleId; +// record.sampleIsEmergency = sampleInfo.isEmergency; + record.projName = projInfo.buildIn.projName; + record.lotId = projInfo.ext.lotId; + record.projId = projInfo.ext.projId; +// record.projExtInfoCard = projInfo.ext;//TODO:添加配置 支持保存和不保存ID卡信息 record.setExpiryDate(projInfo.ext.expiryDate); record.operator = operator; record.projShortName = projInfo.ext.projShortName; diff --git a/src/main/java/a8k/app/service/lowerctrl/OptScanModuleLowerCtrlService.java b/src/main/java/a8k/app/service/lowerctrl/OptScanModuleLowerCtrlService.java index abfa9da..955b7fa 100644 --- a/src/main/java/a8k/app/service/lowerctrl/OptScanModuleLowerCtrlService.java +++ b/src/main/java/a8k/app/service/lowerctrl/OptScanModuleLowerCtrlService.java @@ -37,9 +37,11 @@ public class OptScanModuleLowerCtrlService { public Boolean scanAgain; public Integer maxval; + public void process(Integer[] data, double nowgain, int expectResult, int tolerate) { Assert.isTrue(data.length == 1200, "data.length must be 1200"); + maxval = data[0]; for (int i = 1; i < data.length; 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; suggestGain = nowgain / 2; return; } + if (maxval > expectResult + tolerate) { + scanAgain = true; + suggestGain = nowgain * ((double) expectResult / (double) maxval); + return; + } + if (maxval > expectResult) { scanAgain = false; 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 { log.info("doOptScan ............"); log.info(" optType : {}", optType); @@ -142,10 +172,13 @@ public class OptScanModuleLowerCtrlService { double scanRealGain = 0; int scanRawGain = 0; int lasterGain = optModuleExtParamsMgr.getOptLasterRawGain(optType); - int adjustCount = 2; + int adjustCount = 3; Integer[] result = null; - for (int i = 0; i < adjustCount; i++) { + for (int i = 0; ; i++) { + if (i >= adjustCount) { + break; + } if (i == 0) { scanRealGain = optModuleExtParamsMgr.getScanGain(optType); @@ -200,7 +233,13 @@ public class OptScanModuleLowerCtrlService { rawData.optType = optType; rawData.scanGain = scanRawGain; rawData.lasterGain = lasterGain; + rawData.overflow = getMaxVal(result) >= OptConstant.OPT_F_OVERFLOW_VAL; 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; } @@ -213,7 +252,7 @@ public class OptScanModuleLowerCtrlService { result.scanDate = new Date(); 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.analysResult = analysResult; diff --git a/src/main/java/a8k/app/service/module/OptScanCtrlModule.java b/src/main/java/a8k/app/service/module/OptScanCtrlModule.java index d83bfe3..fda6bb0 100644 --- a/src/main/java/a8k/app/service/module/OptScanCtrlModule.java +++ b/src/main/java/a8k/app/service/module/OptScanCtrlModule.java @@ -191,8 +191,6 @@ public class OptScanCtrlModule { List reactionResults = FakeReactionResultFactory.build(projInfo); List optScanResults = new ArrayList<>(); reactionRecordMgrService.addRecord(sampleInfo, projInfo, optScanResults, reactionResults); - - } diff --git a/src/main/java/a8k/app/service/module/SamplePreProcessModule.java b/src/main/java/a8k/app/service/module/SamplePreProcessModule.java index d266cfb..c698928 100644 --- a/src/main/java/a8k/app/service/module/SamplePreProcessModule.java +++ b/src/main/java/a8k/app/service/module/SamplePreProcessModule.java @@ -151,7 +151,7 @@ public class SamplePreProcessModule { synchronized (this) { try { docmd("丢TIP", tipOperationCtrlModule::dropTip); - docmd("HBOT复位", hbotMoveCtrlService::moveToZero); + docmd("HBOT复位", hbotMoveExCtrlService::moveQuickToZero); } catch (AppException e) { log.error("Error during cleanup", e); state.setFatalAppError(AppErrorFactory.exceptionToAppError(e)); @@ -180,7 +180,7 @@ public class SamplePreProcessModule { projBuildInInfo.reactionPlateIncubationTimeMin * 60 ); docmd("丢TIP", tipOperationCtrlModule::dropTip); - docmd("HBOT复位", hbotMoveCtrlService::moveToZero); + docmd("HBOT复位", hbotMoveExCtrlService::moveQuickToZero); } @@ -217,11 +217,15 @@ public class SamplePreProcessModule { startTask(); actionTaskPool.waitAllDone(); - docmd("摇匀模组复位", tubePreProcessCtrlService::resteModule); - docmd("HBOT复位", () -> { - tipOperationCtrlModule.dropTip(); - hbotMoveCtrlService.moveToZero(); - }); + if (actionTaskPool.isHasError()) { + docmd("摇匀模组复位", tubePreProcessCtrlService::resteModule); + docmd("HBOT复位", () -> { + tipOperationCtrlModule.dropTip(); + hbotMoveCtrlService.moveToZero(); + }); + } else { + docmd("HBOT复位", hbotMoveExCtrlService::moveQuickToZero); + } AppError fatalError = actionTaskPool.getFatalError(); diff --git a/src/main/java/a8k/app/type/a8k/container/A8kOptPeakContainer.java b/src/main/java/a8k/app/type/a8k/container/A8kOptPeakContainer.java index 18b96d2..f622fe0 100644 --- a/src/main/java/a8k/app/type/a8k/container/A8kOptPeakContainer.java +++ b/src/main/java/a8k/app/type/a8k/container/A8kOptPeakContainer.java @@ -15,6 +15,17 @@ public class A8kOptPeakContainer { public A8kOptPeak P160 = 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) { Assert.notNull(peakName, "peakName must not be null"); if (peakName.equals(P040.peakName)) { diff --git a/src/main/java/a8k/app/type/a8k/opt/A8kOptPeak.java b/src/main/java/a8k/app/type/a8k/opt/A8kOptPeak.java index 4cc76dd..d5174ba 100644 --- a/src/main/java/a8k/app/type/a8k/opt/A8kOptPeak.java +++ b/src/main/java/a8k/app/type/a8k/opt/A8kOptPeak.java @@ -32,6 +32,10 @@ public class A8kOptPeak { return val; } + public Boolean isFindPeak() { + return state == State.FIND_PEAK; + } + public A8kOptPeak() { } diff --git a/src/main/java/a8k/app/type/a8k/opt/PeakName.java b/src/main/java/a8k/app/type/a8k/opt/PeakName.java index 96cdeb3..6e82e36 100644 --- a/src/main/java/a8k/app/type/a8k/opt/PeakName.java +++ b/src/main/java/a8k/app/type/a8k/opt/PeakName.java @@ -6,4 +6,5 @@ public enum PeakName { H, T, C, + N,//空 } diff --git a/src/main/java/a8k/app/type/a8k/optfn/A8kOptX.java b/src/main/java/a8k/app/type/a8k/optfn/A8kOptX.java index 5db9a41..2e00335 100644 --- a/src/main/java/a8k/app/type/a8k/optfn/A8kOptX.java +++ b/src/main/java/a8k/app/type/a8k/optfn/A8kOptX.java @@ -1,7 +1,7 @@ package a8k.app.type.a8k.optfn; public enum A8kOptX { - USUPPORT, + UNSUPPORTED, R, //1 AR, //2 ATR, //3 @@ -25,7 +25,7 @@ public enum A8kOptX { case 6 -> T4R; case 7 -> T4T3R; case 8 -> RFR_ADD_T4T3R; - default -> USUPPORT; + default -> UNSUPPORTED; }; } } diff --git a/src/main/java/a8k/app/type/a8k/state/SampleInfo.java b/src/main/java/a8k/app/type/a8k/state/SampleInfo.java index b25a1a6..5cf364d 100644 --- a/src/main/java/a8k/app/type/a8k/state/SampleInfo.java +++ b/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", 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; + } } diff --git a/src/main/java/a8k/extui/mgr/ExtApiPageGroupCfgMgr.java b/src/main/java/a8k/extui/mgr/ExtApiPageGroupCfgMgr.java index 54b4779..8262595 100644 --- a/src/main/java/a8k/extui/mgr/ExtApiPageGroupCfgMgr.java +++ b/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.measurement.*; +import a8k.extui.page.optalgotest.OptAlgoTestPage; import a8k.extui.page.test.codetest.OptFormulaTestPage; import a8k.extui.page.test.codetest.OptFormulaTestPageV2; import a8k.extui.page.extapp.debug_assistant.FakeReactionRecordGeneratorPage; @@ -89,7 +90,7 @@ public class ExtApiPageGroupCfgMgr { new Menu(P12TueStateDebugPage.class, "试管"), new Menu(P13OptModuleStateDebugPage.class, "光学模块"), new Menu(PreReactionPosStateMgrDebugPage.class, "预反应区状态") - )), + )), new Menu("数据库", ZList.of( new Menu(ProjInfoMgrPage.class, "项目信息管理"), new Menu(ReactionRecordMgrDebugPage.class, "反应记录") @@ -150,7 +151,8 @@ public class ExtApiPageGroupCfgMgr { new Menu("光学标定与验证", ZList.of( new Menu(A8kOptVerification.class, "光学模组验证"), - new Menu(OptModuleParamCalibration.class, "光学模块参数校准") + new Menu(OptModuleParamCalibration.class, "光学模块参数校准"), + new Menu(OptAlgoTestPage.class, "光学算法测试") )), new Menu("过检专用", ZList.of( new Menu(P01PipetteGunVerification.class, "移液枪验证"), diff --git a/src/main/java/a8k/extui/page/optalgotest/OptAlgoTestPage.java b/src/main/java/a8k/extui/page/optalgotest/OptAlgoTestPage.java new file mode 100644 index 0000000..ad47fa4 --- /dev/null +++ b/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); + } +} diff --git a/src/main/java/a8k/extui/page/test/codetest/OptFormulaTestPageV2.java b/src/main/java/a8k/extui/page/test/codetest/OptFormulaTestPageV2.java index ac444aa..8595866 100644 --- a/src/main/java/a8k/extui/page/test/codetest/OptFormulaTestPageV2.java +++ b/src/main/java/a8k/extui/page/test/codetest/OptFormulaTestPageV2.java @@ -82,7 +82,6 @@ public class OptFormulaTestPageV2 { } - A8kOptPeakInfo optAlgoAnalysResult; @@ -128,7 +127,7 @@ public class OptFormulaTestPageV2 { ProjInfo projInfoContext = getProjInfo(); 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.R, RArea); diff --git a/src/main/resources/db/zapp_a8k_project_opt_config.csv b/src/main/resources/db/zapp_a8k_project_opt_config.csv index ad54979..63ec2b2 100644 --- a/src/main/resources/db/zapp_a8k_project_opt_config.csv +++ b/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]" diff --git a/tools/20250427-F-光学光学报告.xlsx b/tools/20250427-F-光学光学报告.xlsx index f08366c..cc4c5d9 100644 Binary files a/tools/20250427-F-光学光学报告.xlsx and b/tools/20250427-F-光学光学报告.xlsx differ diff --git a/tools/20250427-T-光学光学报告-2.xlsx b/tools/20250427-T-光学光学报告-2.xlsx index 1bde0b2..6f9a38c 100644 Binary files a/tools/20250427-T-光学光学报告-2.xlsx and b/tools/20250427-T-光学光学报告-2.xlsx differ