diff --git a/src/api/alert/model/assessReport.ts b/src/api/alert/model/assessReport.ts new file mode 100644 index 0000000..d840ad9 --- /dev/null +++ b/src/api/alert/model/assessReport.ts @@ -0,0 +1,58 @@ +import type { + AssessCleanSummary, + AssessEvaluateRequest, + AssessEvaluateResult, + AssessInitQuery, + AssessInitResponse, + AssessReportDetail, + AssessReportSavePayload, +} from './model/assessReportModel' +import { defHttp } from '@/utils/http/axios' + +enum Api { + INIT = '/alert/assess-report/init', + REPORT = '/alert/assess-report/report', + EVALUATE = '/alert/assess-report/evaluate', + SAVE = '/alert/assess-report', + CLEAN_SUMMARY = '/alert/assess-report/clean/summary', + CLEAN_DEAD = '/alert/assess-report/clean/dead', + CLEAN_CONDITION_RATE = '/alert/assess-report/clean/condition-rate', + CLEAN_ENVELOPE_RATE = '/alert/assess-report/clean/envelope-rate', + CONDITION = '/alert/assess-report/condition', +} + +export function fetchAssessInit(params: AssessInitQuery) { + return defHttp.get({ url: Api.INIT, params }) +} + +export function fetchReportDetail(id: number | string) { + return defHttp.get({ url: `${Api.REPORT}/${id}` }) +} + +export function evaluateAssess(data: AssessEvaluateRequest) { + return defHttp.post({ url: Api.EVALUATE, data }) +} + +export function saveAssessReport(data: AssessReportSavePayload) { + return defHttp.post({ url: Api.SAVE, data }) +} + +export function fetchCleanSummary(params: { modelId: number | string; time: string; version: string }) { + return defHttp.get({ url: Api.CLEAN_SUMMARY, params }) +} + +export function fetchDeadCleanDetail(params: { pointIds: string; time: string }) { + return defHttp.get<{ rate: number[]; result: string[] }>({ url: Api.CLEAN_DEAD, params }) +} + +export function fetchConditionRate(params: { condition: string; st: string; et: string }) { + return defHttp.get({ url: Api.CLEAN_CONDITION_RATE, params }) +} + +export function fetchEnvelopeRate(params: { condition: string; st: string; et: string }) { + return defHttp.get({ url: Api.CLEAN_ENVELOPE_RATE, params }) +} + +export function fetchAssessCondition(algorithm: string) { + return defHttp.get<{ modelScore?: number; condition?: string }>({ url: Api.CONDITION, params: { algorithm } }) +} diff --git a/src/api/alert/model/model/assessReportModel.ts b/src/api/alert/model/model/assessReportModel.ts new file mode 100644 index 0000000..91e0da2 --- /dev/null +++ b/src/api/alert/model/model/assessReportModel.ts @@ -0,0 +1,123 @@ +export interface AssessInitQuery { + modelId: number | string + version?: string + reportId?: number | string + sampleRange?: string[] +} + +export interface ModeRow { + name: string + content: string | number +} + +export interface AssessPointRow { + description: string + pointId: string + alarm?: boolean + lock?: boolean + dead?: boolean + limit?: boolean + lower?: number + upper?: number + unit?: string + tMin?: number + tMax?: number + amplitude?: number | string + index?: number +} + +export interface AssessRow extends AssessPointRow { + fdr?: string + far?: string + recon?: string +} + +export interface AssessResultRow { + name: string + content: string | number +} + +export interface AssessInitResponse { + modelName: string + modelInfo: Record + modeRows: ModeRow[] + pointRows: AssessPointRow[] + assessRows: AssessRow[] + assessResult?: AssessResultRow[] + bottomScore?: number + coverage?: number + identifier?: string + timestamp?: string +} + +export interface AssessEvaluateRowResult { + pointId: string + index?: number + fdrForward: number + fdrReverse: number + farForward: number + farReverse: number + reconForward?: number + reconReverse?: number +} + +export interface AssessEvaluateRequest { + modelId: number | string + version: string + assessRows: AssessRow[] + pointRows: AssessPointRow[] + timeRange?: string[] + assessMode?: 'full' | 'single' + alg?: string + modelInfo?: string + rawJson?: string +} + +export interface AssessEvaluateResult { + points: AssessEvaluateRowResult[] + durationSeconds?: number + coverage?: number + cleanDurationSeconds?: number +} + +export interface AssessReportSavePayload { + modelId: number | string + version: string + report: Record + score: number + coverage: number + valid: boolean + identifier: string + verifier?: string + timestamp: string + conditionName?: string +} + +export interface AssessReportDetail { + id: number + modelName: string + report: { + pointRows: AssessPointRow[] + modeRows: ModeRow[] + assessRows: AssessRow[] + assessResult: AssessResultRow[] + identifier?: string + time?: string + } +} + +export interface AssessCleanSummaryItem { + type: string + orgNum?: number + sampleNum?: number + sampleRate?: number + message?: string +} + +export interface AssessCleanSummary { + summary: AssessCleanSummaryItem[] + deadPoints?: AssessPointRow[] + conditionClauses?: string[] + envelopeClauses?: string[] + timeRange?: string[] +} diff --git a/src/router/routes/index.ts b/src/router/routes/index.ts index f0b7378..44163a4 100644 --- a/src/router/routes/index.ts +++ b/src/router/routes/index.ts @@ -118,6 +118,28 @@ export const basicRoutes = [ RootRoute, ProfileRoute, CodegenRoute, + { + path: '/model/assess-report/:id?', + name: 'AssessReportBasic', + component: LAYOUT, + meta: { + title: '评估报告', + hideMenu: true, + hideBreadcrumb: true, + }, + children: [ + { + path: '', + name: 'AssessReport', + component: () => import('@/views/model/AssessReport.vue'), + meta: { + title: '评估报告', + hideMenu: true, + currentActiveMenu: '/model', + }, + }, + ], + }, REDIRECT_ROUTE, PAGE_NOT_FOUND_ROUTE, ] diff --git a/src/router/routes/modules/model-assess.ts b/src/router/routes/modules/model-assess.ts new file mode 100644 index 0000000..d450052 --- /dev/null +++ b/src/router/routes/modules/model-assess.ts @@ -0,0 +1,29 @@ +import type { AppRouteModule } from '@/router/types' +import { LAYOUT } from '@/router/constant' + +const assessReport: AppRouteModule = { + path: '/model', + name: 'ModelAssess', + component: LAYOUT, + redirect: '/model/assess-report', + meta: { + orderNo: 25, + icon: 'ant-design:file-protect-outlined', + title: '模型评估', + }, + children: [ + { + path: 'assess-report/:id?', + name: 'AssessReport', + component: () => import('@/views/model/AssessReport.vue'), + meta: { + title: '评估报告', + currentActiveMenu: '/model', + hideMenu: true, + hideBreadcrumb: true, + }, + }, + ], +} + +export default assessReport diff --git a/src/views/model/AssessReport.vue b/src/views/model/AssessReport.vue new file mode 100644 index 0000000..c1581ac --- /dev/null +++ b/src/views/model/AssessReport.vue @@ -0,0 +1,846 @@ + + + + + diff --git a/src/views/model/train/data.tsx b/src/views/model/train/data.tsx index 15f3e73..ee488fa 100644 --- a/src/views/model/train/data.tsx +++ b/src/views/model/train/data.tsx @@ -103,6 +103,58 @@ export const pointTableSchema: BasicColumn[] = [ dataIndex: 'Lower', edit: true, }, + { + title: '残差上限', + width: 150, + dataIndex: 'upperBound', + edit: true, + }, + { + title: '残差下限', + width: 150, + dataIndex: 'lowerBound', + edit: true, + }, + { + title: '训练样本最大值', + width: 150, + dataIndex: 'TMax', + }, + { + title: '训练样本最小值', + width: 150, + dataIndex: 'TMin', + }, + { + title: '训练样本平均值', + width: 150, + dataIndex: 'TAvg', + }, + { + title: '训练最大偏差', + width: 150, + dataIndex: 'DMax', + }, + { + title: '训练最小偏差', + width: 150, + dataIndex: 'DMin', + }, + { + title: '训练平均偏差', + width: 150, + dataIndex: 'DAvg', + }, + { + title: '死区清洗', + width: 120, + dataIndex: 'dead', + }, + { + title: '限值清洗', + width: 120, + dataIndex: 'limit', + }, { title: '编辑', width: 100, diff --git a/src/views/model/train/index.vue b/src/views/model/train/index.vue index 6c09b35..1e5a48a 100644 --- a/src/views/model/train/index.vue +++ b/src/views/model/train/index.vue @@ -39,6 +39,7 @@ import { getExaHistorys } from '@/api/alert/exa/index' import { useECharts } from '@/hooks/web/useECharts' import { useMessage } from '@/hooks/web/useMessage' import { pointListApi } from '@/api/alert/model/select' +import { useGo } from '@/hooks/web/usePage' export default defineComponent({ components: { @@ -69,6 +70,7 @@ export default defineComponent({ setup() { const { createMessage } = useMessage() const route = useRoute() + const go = useGo() const id = route.params.id const model = ref(null) const brushActivated = ref>(new Set()) @@ -76,12 +78,31 @@ export default defineComponent({ let trainTimeCopy = '' const fetchModelInfo = async () => { const modelInfo = await modelInfoApi(id) - model.value = modelInfo + model.value = normalizeTrainTime(modelInfo) trainTimeCopy = JSON.stringify(model.value.trainTime) getHistory() } - const pointData = computed(() => model.value?.pointInfo || []) + const pointData = computed(() => { + const list = model.value?.pointInfo || [] + return list.map((p: any) => ({ + description: p.description ?? p.Description, + pointId: p.pointId ?? p.PointId, + unit: p.unit ?? p.Unit, + Upper: p.Upper ?? p.upper, + Lower: p.Lower ?? p.lower, + dead: p.dead === true ? '是' : '否', + limit: p.limit === true ? '是' : '否', + upperBound: p.upperBound ?? p.upperbound ?? p.upperBound, + lowerBound: p.lowerBound ?? p.lowerbound ?? p.lowerBound, + TMax: p.TMax ?? p.tMax, + TMin: p.TMin ?? p.tMin, + TAvg: p.TAvg ?? p.tAvg, + DMax: p.DMax ?? p.dMax, + DMin: p.DMin ?? p.dMin, + DAvg: p.DAvg ?? p.dAvg, + })) + }) const [pointTable] = useTable({ columns: pointTableSchema, pagination: true, @@ -181,6 +202,26 @@ export default defineComponent({ return Array.from({ length: count }, (_, i) => t1.add(i * intervalMs, 'millisecond').valueOf()) } + function normalizeTrainTime(modelInfo: any) { + if (!modelInfo?.trainTime || !Array.isArray(modelInfo.trainTime)) + return modelInfo + const samplingMs = (modelInfo.sampling || 0) * 1000 + modelInfo.trainTime = modelInfo.trainTime.map((item: any) => { + const durationSec = item.duration || Math.max(0, (dayjs(item.et).valueOf() - dayjs(item.st).valueOf()) / 1000) + const number = item.number !== undefined && item.number !== '' ? item.number : (samplingMs > 0 ? Math.floor((durationSec * 1000) / samplingMs) : '') + const filter = item.filter !== undefined && item.filter !== '' ? item.filter : 0 + const mode = item.mode !== undefined && item.mode !== '' ? item.mode : (number !== '' ? Math.max(0, number - filter) : '') + return { + ...item, + duration: durationSec, + number, + filter, + mode, + } + }) + return modelInfo + } + function getOption(item: any) { const name = ['测量值', '模型值', ''] const color = ['blue', '#ff6f00', 'red'] @@ -308,8 +349,8 @@ export default defineComponent({ console.log('Selected train time:', trainTime) if (trainTimeCopy === JSON.stringify(trainTime)) return - model.value.trainTime = trainTime - trainTimeCopy = JSON.stringify(trainTime) + model.value = normalizeTrainTime({ ...model.value, trainTime }) + trainTimeCopy = JSON.stringify(model.value.trainTime) echartsRefs.value.forEach((chart, index) => { if (chart) { chart.dispatchAction({ @@ -435,7 +476,60 @@ export default defineComponent({ try { const response = await trainModelApi(params) model.value.para = response - model.value.principal = response.Model_info.K + + const modelInfoDetail = response.Model_info || response.modelInfo + if (modelInfoDetail) { + model.value.principal = modelInfoDetail.K ?? modelInfoDetail.k ?? model.value.principal + model.value.precision = modelInfoDetail.R ? `${(modelInfoDetail.R * 100).toFixed(2)}%` : model.value.precision + + const pxMax = modelInfoDetail.Train_X_max || modelInfoDetail.trainXMax || [] + const pxMin = modelInfoDetail.Train_X_min || modelInfoDetail.trainXMin || [] + const pxMean = modelInfoDetail.Train_X_mean || modelInfoDetail.trainXMean || [] + const pbMax = (modelInfoDetail.Train_X_bais_max || modelInfoDetail.trainXBaisMax || [])[0] || [] + const pbMin = (modelInfoDetail.Train_X_bais_min || modelInfoDetail.trainXBaisMin || [])[0] || [] + const pbMean = (modelInfoDetail.Train_X_bais_mean || modelInfoDetail.trainXBaisMean || [])[0] || [] + const qcul99Line = modelInfoDetail.QCUL_99_line || modelInfoDetail.qcul99Line || [] + + model.value.pointInfo = (model.value.pointInfo || []).map((p: any, idx: number) => { + const q99 = qcul99Line[idx] + const upperB = parseFloat(p.upperBound) + const lowerB = parseFloat(p.lowerBound) + return { + ...p, + TMax: pxMax[idx] !== undefined ? Number(pxMax[idx]).toFixed(2) : p.TMax, + TMin: pxMin[idx] !== undefined ? Number(pxMin[idx]).toFixed(2) : p.TMin, + TAvg: pxMean[idx] !== undefined ? Number(pxMean[idx]).toFixed(2) : p.TAvg, + DMax: pbMax[idx] !== undefined ? Number(pbMax[idx]).toFixed(2) : p.DMax, + DMin: pbMin[idx] !== undefined ? Number(pbMin[idx]).toFixed(2) : p.DMin, + DAvg: pbMean[idx] !== undefined ? Number(pbMean[idx]).toFixed(2) : p.DAvg, + upperBound: + Number.isNaN(upperB) || upperB === undefined + ? (q99 !== undefined ? Number(q99).toFixed(2) : p.upperBound) + : p.upperBound, + lowerBound: + Number.isNaN(lowerB) || lowerB === undefined + ? (q99 !== undefined ? (-Number(q99)).toFixed(2) : p.lowerBound) + : p.lowerBound, + } + }) + } + + // 填充训练区间的采样/清洗/有效样本数,如果接口有总数则均摊,否则保留原值 + if (Array.isArray(model.value.trainTime) && model.value.trainTime.length) { + const beforeTotal = response.BeforeCleanSamNum ?? response.beforeCleanSamNum + const afterTotal = response.AfterCleanSamNum ?? response.afterCleanSamNum + if (beforeTotal !== undefined && afterTotal !== undefined) { + const avgBefore = Math.floor(beforeTotal / model.value.trainTime.length) + const avgAfter = Math.floor(afterTotal / model.value.trainTime.length) + model.value.trainTime = model.value.trainTime.map((t: any) => ({ + ...t, + number: t.number || avgBefore, + filter: t.filter || Math.max(0, avgBefore - avgAfter), + mode: t.mode || avgAfter, + })) + } + } + updateModelInfoDebounced() getTestData() createMessage.success('模型训练成功') @@ -618,6 +712,12 @@ export default defineComponent({ } } + function goAssessReport() { + const version = model.value?.Cur_Version || 'v-test' + const algorithm = model.value?.algorithm || 'PCA' + go(`/model/assess-report/${id}?version=${encodeURIComponent(version)}&algorithm=${algorithm}`) + } + return { pointTable, model, @@ -653,6 +753,7 @@ export default defineComponent({ handleEditModelOk, handleEditModelCancel, bottomModel, + goAssessReport, } }, }) @@ -673,7 +774,9 @@ export default defineComponent({ {{ model?.Cur_Version || "v-test" }} - 暂无 + + 新增/查看 + {{ model?.founder }} @@ -785,6 +888,13 @@ export default defineComponent({ > 清除训练结果 + + 评估报告 + 修改模型