|
|
@@ -0,0 +1,194 @@
|
|
|
+<template>
|
|
|
+ <div class="adaptive-optimize">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card shadow="never">
|
|
|
+ <template #header><span class="title">\u521b\u5efa\u4f18\u5316\u4efb\u52a1</span></template>
|
|
|
+ <el-form :model="form" label-width="100px" size="small">
|
|
|
+ <el-form-item label="\u4f18\u5316\u76ee\u6807">
|
|
|
+ <el-select v-model="form.objective_metric">
|
|
|
+ <el-option label="\u5e73\u5747\u8f6c\u77e9 (Nm)" value="tavg_nm" />
|
|
|
+ <el-option label="\u6548\u7387 (%)" value="efficiency_pct" />
|
|
|
+ <el-option label="\u603b\u635f\u8017 (W)" value="total_losses_w" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="\u4f18\u5316\u65b9\u5411">
|
|
|
+ <el-radio-group v-model="form.objective_direction">
|
|
|
+ <el-radio value="maximize">\u6700\u5927\u5316</el-radio>
|
|
|
+ <el-radio value="minimize">\u6700\u5c0f\u5316</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="\u603b\u9884\u7b97">
|
|
|
+ <el-input-number v-model="form.total_budget" :min="20" :max="500" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="\u6279\u6b21\u5927\u5c0f">
|
|
|
+ <el-input-number v-model="form.batch_size" :min="1" :max="16" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="\u521d\u59cb\u6837\u672c">
|
|
|
+ <el-input-number v-model="form.initial_samples" :min="4" :max="100" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-divider />\u626b\u63cf\u53d8\u91cf
|
|
|
+ <div v-for="(p, idx) in form.parameters" :key="idx" class="param-row">
|
|
|
+ <el-input v-model="p.name" placeholder="\u53d8\u91cf\u540d" style="width: 120px;" />
|
|
|
+ <el-input-number v-model="p.min_value" placeholder="min" style="width: 90px;" controls-position="right" />
|
|
|
+ <el-input-number v-model="p.max_value" placeholder="max" style="width: 90px;" controls-position="right" />
|
|
|
+ <el-input-number v-model="p.step" placeholder="step" style="width: 70px;" controls-position="right" />
|
|
|
+ <el-button type="danger" size="small" link @click="removeParam(idx)">\u5220</el-button>
|
|
|
+ </div>
|
|
|
+ <el-button size="small" @click="addParam" style="margin-top: 8px;">+ \u6dfb\u52a0\u53d8\u91cf</el-button>
|
|
|
+ <el-button type="primary" style="width: 100%; margin-top: 12px;" :loading="creating" @click="createSearch">
|
|
|
+ \u521b\u5efa\u5e76\u542f\u52a8
|
|
|
+ </el-button>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <el-col :span="16">
|
|
|
+ <el-card v-if="state" shadow="never" class="state-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>\u4f18\u5316\u72b6\u6001 - {{ state.run_id }}</span>
|
|
|
+ <el-tag :type="statusType">{{ state.convergence_status }}</el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-row :gutter="16" class="stats-row">
|
|
|
+ <el-col :span="6"><div class="stat-box"><div class="stat-val">{{ state.used_budget }}/{{ state.total_budget }}</div><div class="stat-label">\u5df2\u7528\u9884\u7b97</div></div></el-col>
|
|
|
+ <el-col :span="6"><div class="stat-box"><div class="stat-val">{{ state.feasible_points }}</div><div class="stat-label">\u53ef\u884c\u70b9</div></div></el-col>
|
|
|
+ <el-col :span="6"><div class="stat-box"><div class="stat-val">{{ state.best_objective_value?.toFixed(2) || '-' }}</div><div class="stat-label">\u6700\u4f18\u503c</div></div></el-col>
|
|
|
+ <el-col :span="6"><div class="stat-box"><div class="stat-val">{{ state.trust_region_radius?.toFixed(3) }}</div><div class="stat-label">\u4fe1\u4efb\u57df\u534a\u5f84</div></div></el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-progress :percentage="Math.round((state.used_budget / state.total_budget) * 100)" :status="state.convergence_status === 'converged' ? 'success' : ''" style="margin: 16px 0;" />
|
|
|
+ <el-descriptions :column="2" border size="small">
|
|
|
+ <el-descriptions-item label="\u5f53\u524d\u6279\u6b21">{{ state.current_batch }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="\u603b\u70b9\u6570">{{ state.total_points }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="\u5f85\u6267\u884c">{{ state.pending_points }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="\u5df2\u5b8c\u6210">{{ state.completed_points }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="\u4fe1\u4efb\u57df">{{ state.trust_region_active ? '\u5df2\u6fc0\u6d3b' : '\u672a\u6fc0\u6d3b' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="\u641c\u7d22\u65b9\u6cd5">{{ state.search_method }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ <div class="actions" style="margin-top: 16px;">
|
|
|
+ <el-button type="primary" :loading="loading" @click="getNextBatch" :disabled="state.remaining_budget <= 0">
|
|
|
+ \u83b7\u53d6\u4e0b\u4e00\u6279
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="refreshState">\u5237\u65b0\u72b6\u6001</el-button>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card v-if="currentBatch.length" shadow="never" style="margin-top: 20px;">
|
|
|
+ <template #header><span>\u5f53\u524d\u6279\u6b21 ({{ currentBatch.length }}\u70b9)</span></template>
|
|
|
+ <el-table :data="currentBatch" border size="small">
|
|
|
+ <el-table-column prop="id" label="ID" width="80" />
|
|
|
+ <el-table-column v-for="p in paramNames" :key="p" :prop="`params.${p}`" :label="p" />
|
|
|
+ <el-table-column prop="status" label="\u72b6\u6001" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.status === 'pending' ? 'warning' : 'success'" size="small">{{ row.status }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card v-if="bestPoint" shadow="never" style="margin-top: 20px;">
|
|
|
+ <template #header><span>\u6700\u4f18\u70b9\u53c2\u6570</span></template>
|
|
|
+ <el-descriptions :column="3" border size="small">
|
|
|
+ <el-descriptions-item v-for="(v, k) in bestPoint" :key="k" :label="k">{{ v }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive, computed } from 'vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import { searchApi } from '@/api/ai'
|
|
|
+
|
|
|
+const form = reactive({
|
|
|
+ objective_metric: 'tavg_nm',
|
|
|
+ objective_direction: 'maximize',
|
|
|
+ total_budget: 80,
|
|
|
+ batch_size: 4,
|
|
|
+ initial_samples: 16,
|
|
|
+ parameters: [
|
|
|
+ { name: 'airgap_mm', min_value: 0.5, max_value: 2.0, step: 0.1 },
|
|
|
+ { name: 'magnet_thickness_mm', min_value: 3.0, max_value: 8.0, step: 0.5 },
|
|
|
+ ],
|
|
|
+})
|
|
|
+
|
|
|
+const state = ref<any>(null)
|
|
|
+const currentBatch = ref<any[]>([])
|
|
|
+const creating = ref(false)
|
|
|
+const loading = ref(false)
|
|
|
+const runId = ref('')
|
|
|
+
|
|
|
+const paramNames = computed(() => form.parameters.map((p: any) => p.name))
|
|
|
+const bestPoint = computed(() => state.value?.best_point_params)
|
|
|
+const statusType = computed(() => {
|
|
|
+ const s = state.value?.convergence_status
|
|
|
+ if (s === 'converged') return 'success'
|
|
|
+ if (s === 'budget_exhausted') return 'danger'
|
|
|
+ return 'warning'
|
|
|
+})
|
|
|
+
|
|
|
+const addParam = () => {
|
|
|
+ form.parameters.push({ name: '', min_value: 0, max_value: 1, step: 0.1 })
|
|
|
+}
|
|
|
+const removeParam = (idx: number) => {
|
|
|
+ form.parameters.splice(idx, 1)
|
|
|
+}
|
|
|
+
|
|
|
+const createSearch = async () => {
|
|
|
+ if (!form.parameters.length) {
|
|
|
+ ElMessage.warning('\u8bf7\u6dfb\u52a0\u81f3\u5c11\u4e00\u4e2a\u626b\u63cf\u53d8\u91cf')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ creating.value = true
|
|
|
+ try {
|
|
|
+ const res: any = await searchApi.create({ ...form })
|
|
|
+ runId.value = res.run_id
|
|
|
+ state.value = res.state
|
|
|
+ currentBatch.value = res.initial_points || []
|
|
|
+ ElMessage.success('\u4f18\u5316\u4efb\u52a1\u5df2\u521b\u5efa')
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error('\u521b\u5efa\u5931\u8d25: ' + (e.message || e))
|
|
|
+ } finally {
|
|
|
+ creating.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getNextBatch = async () => {
|
|
|
+ if (!runId.value) return
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const res: any = await searchApi.nextBatch(runId.value)
|
|
|
+ currentBatch.value = res.points || []
|
|
|
+ state.value = res.state
|
|
|
+ ElMessage.success(`\u83b7\u53d6\u5230 ${currentBatch.value.length} \u4e2a\u65b0\u70b9`)
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error('\u83b7\u53d6\u5931\u8d25: ' + (e.message || e))
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const refreshState = async () => {
|
|
|
+ if (!runId.value) return
|
|
|
+ try {
|
|
|
+ state.value = await searchApi.getState(runId.value)
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error('\u5237\u65b0\u5931\u8d25: ' + (e.message || e))
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.adaptive-optimize { padding: 20px; }
|
|
|
+.title { font-weight: 600; }
|
|
|
+.card-header { display: flex; justify-content: space-between; align-items: center; }
|
|
|
+.param-row { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; }
|
|
|
+.stats-row { margin-bottom: 8px; }
|
|
|
+.stat-box { text-align: center; padding: 12px; background: #f5f7fa; border-radius: 8px; }
|
|
|
+.stat-val { font-size: 20px; font-weight: 700; color: #409eff; }
|
|
|
+.stat-label { font-size: 12px; color: #909399; margin-top: 4px; }
|
|
|
+.actions { display: flex; gap: 12px; }
|
|
|
+</style>
|