From ca8cf02aa47cb93c2cf6589e8c2300763d2be5d3 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Tue, 9 Jun 2026 17:25:32 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=89=B9=E5=BE=81?=
=?UTF-8?q?=E5=88=86=E6=9E=90=E9=A1=B5=E9=9D=A2=E5=9B=BE=E8=A1=A8=20+=20?=
=?UTF-8?q?=E8=A1=A5=E5=85=85=E6=B4=BE=E7=94=9F=E7=89=B9=E5=BE=81=E8=AE=A1?=
=?UTF-8?q?=E7=AE=97=E8=84=9A=E6=9C=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- src/routes.py: 补充火箭炮分析API缺失的8个字段(射速/射程/发动机/机动参数)
- AnalysisPage.vue: 修复火力/机动性能图表data映射,机动图表改用双Y轴avoid ECharts渲染失败
- AlgorithmDemoPage.vue: 移除算法卡片中的平均绝对误差和均方根误差显示
- scripts/calculate_features.py: 新增派生特征计算脚本(min-max归一化评分、巡飞弹缺失字段估算)
---
frontend/src/views/AlgorithmDemoPage.vue | 5 +-
frontend/src/views/AnalysisPage.vue | 33 +-
scripts/calculate_features.py | 383 +++++++++++++++++++++++
src/routes.py | 10 +-
4 files changed, 418 insertions(+), 13 deletions(-)
create mode 100644 scripts/calculate_features.py
diff --git a/frontend/src/views/AlgorithmDemoPage.vue b/frontend/src/views/AlgorithmDemoPage.vue
index b95724a..4a7e843 100644
--- a/frontend/src/views/AlgorithmDemoPage.vue
+++ b/frontend/src/views/AlgorithmDemoPage.vue
@@ -86,10 +86,7 @@
最佳
{{ formatScore(row.r2) }}
-
- 平均绝对误差 {{ formatMoney(row.mae) }}
- 均方根误差 {{ formatMoney(row.rmse) }}
-
+
diff --git a/frontend/src/views/AnalysisPage.vue b/frontend/src/views/AnalysisPage.vue
index c501f8b..252c0d0 100644
--- a/frontend/src/views/AnalysisPage.vue
+++ b/frontend/src/views/AnalysisPage.vue
@@ -735,7 +735,13 @@ const renderCharts = () => {
mobilityScore: analysisResult.value.mobility_score || [],
deploymentScore: analysisResult.value.deployment_score || [],
terrainScore: analysisResult.value.terrain_adaptability_score || [],
- combatReadinessScore: analysisResult.value.combat_readiness_score || []
+ combatReadinessScore: analysisResult.value.combat_readiness_score || [],
+ rocketWeight: analysisResult.value.rocket_weight_kg || [],
+ rocketDiameter: analysisResult.value.rocket_diameter_mm || [],
+ rocketLength: analysisResult.value.rocket_length_m || [],
+ speedKmh: analysisResult.value.speed_kmh || [],
+ powerHp: analysisResult.value.power_hp || [],
+ travelRangeKm: analysisResult.value.travel_range_km || []
}
// 火力性能分析图表配置
@@ -849,7 +855,7 @@ const renderCharts = () => {
top: 30,
data: [
'机动性评分', '部署评分', '地形适应性评分', '战备状态评分',
- '行驶速度', '功率', '行程' // 新增参数
+ '行驶速度', '功率', '行程'
]
},
grid: {
@@ -861,12 +867,20 @@ const renderCharts = () => {
type: 'category',
data: chartData.names
},
- yAxis: {
- type: 'value',
- name: '评分',
- min: 0,
- max: 10
- },
+ yAxis: [
+ {
+ type: 'value',
+ name: '评分',
+ position: 'left',
+ min: 0,
+ max: 10
+ },
+ {
+ type: 'value',
+ name: '速度/功率/行程',
+ position: 'right'
+ }
+ ],
series: [
{
name: '机动性评分',
@@ -891,16 +905,19 @@ const renderCharts = () => {
{
name: '行驶速度',
type: 'line',
+ yAxisIndex: 1,
data: chartData.speedKmh
},
{
name: '功率',
type: 'line',
+ yAxisIndex: 1,
data: chartData.powerHp
},
{
name: '行程',
type: 'line',
+ yAxisIndex: 1,
data: chartData.travelRangeKm
}
]
diff --git a/scripts/calculate_features.py b/scripts/calculate_features.py
new file mode 100644
index 0000000..7e78619
--- /dev/null
+++ b/scripts/calculate_features.py
@@ -0,0 +1,383 @@
+"""
+计算并填充数据库中的派生/计算特征字段,包括:
+1. 火箭炮:火力密度、射程比、各项评分
+2. 巡飞弹:长宽比、重量射程比等 + 估算缺失的原始字段
+ (engine_power_kw, engine_thrust_n, min/max_altitude_m,
+ guidance_accuracy_m, datalink_range_km)
+
+用法:
+ python -m scripts.calculate_features
+"""
+
+import sys
+import os
+import math
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from src.database.db_connection import get_db_connection, init_db
+from src.logger import setup_logger
+
+logger = setup_logger(__name__)
+
+
+def clamp(value, low=1, high=10):
+ return max(low, min(high, value))
+
+
+def safe_float(val, default=0.0):
+ if val is None:
+ return default
+ return float(val)
+
+
+# ────────────────────────────────────────────────
+# 火箭炮
+# ────────────────────────────────────────────────
+
+def calculate_rocket_features(conn):
+ cursor = conn.cursor()
+
+ cursor.execute("""
+ SELECT rap.id, rap.equipment_id,
+ rap.rate_of_fire, rap.rocket_weight_kg, rap.rocket_length_m,
+ rap.rocket_diameter_mm, rap.min_range_km, rap.max_range_km,
+ rap.speed_kmh, rap.power_hp, rap.combat_weight_kg,
+ rap.travel_range_km, rap.firing_angle_horizontal,
+ rap.firing_angle_vertical,
+ cp.length_m, cp.width_m, cp.weight_kg
+ FROM rocket_artillery_params rap
+ LEFT JOIN common_params cp ON rap.equipment_id = cp.equipment_id
+ """)
+
+ rows = cursor.fetchall()
+
+ # ── 第一遍:收集原始值,计算 min/max(用于归一化)──
+ all_vals = {
+ 'spd': [], 'power_ratio': [], 'tr': [],
+ 'angle_cov': [], 'deploy_factor': [], 'rof': [],
+ 'cw': []
+ }
+ for row in rows:
+ spd = safe_float(row['speed_kmh'])
+ pwr = safe_float(row['power_hp'])
+ cw = safe_float(row['combat_weight_kg'])
+ tr = safe_float(row['travel_range_km'])
+ ah = safe_float(row['firing_angle_horizontal'])
+ av = safe_float(row['firing_angle_vertical'])
+ rof = safe_float(row['rate_of_fire'])
+
+ all_vals['spd'].append(spd)
+ all_vals['power_ratio'].append(pwr / max(1, cw))
+ all_vals['tr'].append(tr)
+ all_vals['angle_cov'].append((ah / 360) * (av / 90))
+ all_vals['deploy_factor'].append(1.0 / max(1, cw)) # 越小越难部署
+ all_vals['rof'].append(rof)
+ all_vals['cw'].append(cw)
+
+ def norm10(v, vals):
+ """Min-max 归一化到 1-10"""
+ mn, mx = min(vals), max(vals)
+ span = mx - mn
+ if span == 0:
+ return 5.0
+ return 1.0 + (v - mn) / span * 9.0
+
+ # ── 第二遍:逐条计算并更新 ──
+ updates = []
+
+ for row in rows:
+ rof = safe_float(row['rate_of_fire'])
+ rw = safe_float(row['rocket_weight_kg'])
+ rl = safe_float(row['rocket_length_m'])
+ rd = safe_float(row['rocket_diameter_mm'])
+ min_r = safe_float(row['min_range_km'])
+ max_r = safe_float(row['max_range_km'])
+ spd = safe_float(row['speed_kmh'])
+ pwr = safe_float(row['power_hp'])
+ cw = safe_float(row['combat_weight_kg'])
+ tr = safe_float(row['travel_range_km'])
+ ah = safe_float(row['firing_angle_horizontal'])
+ av = safe_float(row['firing_angle_vertical'])
+
+ # 火力密度 (kg/min)
+ fire_density = round(rw * rof, 2)
+
+ # 射程比
+ range_ratio = round(max_r / max(1, min_r), 4)
+
+ # 火箭弹功重比
+ if rd > 0 and rl > 0:
+ rocket_power_ratio = round((rw * rl * rof) / (rd * 10), 4)
+ else:
+ rocket_power_ratio = 0.0
+
+ # 平台效率 (m/kg)
+ platform_efficiency = round(max_r / max(1, cw) * 1000, 4)
+
+ power_ratio = pwr / max(1, cw)
+ angle_cov = (ah / 360) * (av / 90)
+ deploy_factor = 1.0 / max(1, cw)
+
+ # 机动性评分 (1-10):综合速度、功重比、行程,min-max 归一化
+ s_speed = norm10(spd, all_vals['spd'])
+ s_power = norm10(power_ratio, all_vals['power_ratio'])
+ s_range = norm10(tr, all_vals['tr'])
+ mobility_score = int(round((s_speed + s_power + s_range) / 3))
+
+ # 战备状态评分 (1-10):射界覆盖、部署便捷度(轻量化)、射速
+ s_angle = norm10(angle_cov, all_vals['angle_cov'])
+ s_deploy = norm10(deploy_factor, all_vals['deploy_factor'])
+ s_rof = norm10(rof, all_vals['rof'])
+ combat_readiness_score = int(round((s_angle + s_deploy + s_rof) / 3))
+
+ # 部署评分 (1-10):轻量化 + 速度
+ s_weight = 11.0 - norm10(cw, all_vals['cw']) # 越轻越高
+ s_speed2 = norm10(spd, all_vals['spd'])
+ deployment_score = int(round((s_weight + s_speed2) / 2))
+
+ # 地形适应性评分 (1-10):功重比 + 速度
+ terrain_score = int(round((s_power + s_speed2) / 2))
+
+ updates.append((
+ fire_density, range_ratio, mobility_score,
+ combat_readiness_score, deployment_score, terrain_score,
+ rocket_power_ratio, platform_efficiency,
+ row['id']
+ ))
+
+ if updates:
+ cursor.executemany("""
+ UPDATE rocket_artillery_params
+ SET fire_density = ?, range_ratio = ?, mobility_score = ?,
+ combat_readiness_score = ?, deployment_score = ?,
+ terrain_adaptability_score = ?, rocket_power_ratio = ?,
+ platform_efficiency = ?
+ WHERE id = ?
+ """, updates)
+ logger.info(f"火箭炮:更新了 {len(updates)} 条记录")
+
+ return len(updates)
+
+
+# ────────────────────────────────────────────────
+# 巡飞弹
+# ────────────────────────────────────────────────
+
+def estimate_guidance_accuracy(guidance_system):
+ """根据制导系统文字估算制导精度 (m)"""
+ gs = (guidance_system or '').lower()
+ # 统计制导模式数量作为精度指标
+ modes = [m.strip() for m in guidance_system.split('/') if m.strip()] if guidance_system else []
+ mode_count = len(modes)
+
+ # 基础精度:模式越多越精确
+ base_accuracy = {
+ 1: 50, 2: 30, 3: 20, 4: 15, 5: 10, 6: 5, 7: 3, 8: 2, 9: 1.5, 10: 1,
+ }.get(mode_count, max(1, 60 - mode_count * 6))
+
+ # 根据制导类型微调
+ if any(k in gs for k in ['卫星', 'satellite', 'gps']):
+ base_accuracy = max(3, base_accuracy * 0.7)
+ if any(k in gs for k in ['激光', 'laser']):
+ base_accuracy = max(1, base_accuracy * 0.4)
+ if any(k in gs for k in ['红外', 'infrared', 'ir', '光电']):
+ base_accuracy = max(2, base_accuracy * 0.6)
+ if any(k in gs for k in ['雷达', 'radar']):
+ base_accuracy = max(5, base_accuracy * 0.8)
+ if any(k in gs for k in ['ai', '辅助']):
+ base_accuracy = base_accuracy * 0.8
+
+ return round(base_accuracy, 1)
+
+
+def calculate_guidance_score(guidance_system, estimated_accuracy):
+ """计算制导系统评分 (1-10)"""
+ gs = (guidance_system or '').lower()
+ modes = [m.strip() for m in guidance_system.split('/') if m.strip()] if guidance_system else []
+
+ # 制导模式数量 (1-7 映射到 1-6 分)
+ mode_score = clamp(len(modes) / 2, 0.5, 6)
+
+ # 精度分 (精度越高分越高)
+ if estimated_accuracy <= 3:
+ accuracy_score = 5
+ elif estimated_accuracy <= 5:
+ accuracy_score = 4.5
+ elif estimated_accuracy <= 10:
+ accuracy_score = 4
+ elif estimated_accuracy <= 20:
+ accuracy_score = 3
+ elif estimated_accuracy <= 30:
+ accuracy_score = 2
+ elif estimated_accuracy <= 50:
+ accuracy_score = 1
+ else:
+ accuracy_score = 0.5
+
+ # 高端制导方式加成
+ high_end_bonus = 0
+ if any(k in gs for k in ['卫星', 'satellite']):
+ high_end_bonus += 0.5
+
+ return int(round(clamp(mode_score + accuracy_score + high_end_bonus, 1, 10)))
+
+
+def calculate_warhead_score(warhead_type, warhead_weight_kg):
+ """计算战斗部威力评分 (1-10)"""
+ wt_lower = (warhead_type or '').lower()
+
+ # 重量得分: 5kg→2, 10kg→4, 20kg→6, 30kg→8, 50kg→10
+ weight_score = clamp(warhead_weight_kg * 0.2, 1, 10)
+
+ # 类型加成
+ type_kw_scores = {
+ '模块化': 3, 'modular': 3,
+ '高爆': 2, 'explosive': 2, 'he': 2,
+ '云爆': 3, 'thermobaric': 3,
+ '破片杀伤': 1.5, 'fragmentation': 1.5,
+ '穿甲': 2.5, 'armor': 2.5, 'penetrat': 2.5,
+ '动能': 2, 'kinetic': 2,
+ '双用': 2, 'dual': 2,
+ '破甲': 2, 'heat': 2,
+ '子母': 2, 'cluster': 2, 'submunition': 2
+ }
+ type_bonus = 0
+ for kw, val in type_kw_scores.items():
+ if kw in wt_lower:
+ type_bonus = max(type_bonus, val)
+
+ return int(round(clamp(weight_score + type_bonus, 1, 10)))
+
+
+def calculate_loitering_features(conn):
+ """计算巡飞弹的派生特征 + 估算缺失的原始字段"""
+ cursor = conn.cursor()
+
+ cursor.execute("""
+ SELECT lmp.id, lmp.equipment_id,
+ lmp.wingspan_m, lmp.warhead_weight_kg, lmp.max_speed_ms,
+ lmp.max_range_km, lmp.guidance_accuracy_m, lmp.datalink_range_km,
+ lmp.engine_power_kw, lmp.engine_thrust_n,
+ lmp.min_altitude_m, lmp.max_altitude_m,
+ lmp.guidance_system, lmp.warhead_type,
+ lmp.cruise_speed_kmh, lmp.endurance_min,
+ lmp.ceiling_altitude_m, lmp.max_payload_kg,
+ lmp.combat_radius_km,
+ cp.length_m, cp.width_m, cp.weight_kg
+ FROM loitering_munition_params lmp
+ LEFT JOIN common_params cp ON lmp.equipment_id = cp.equipment_id
+ """)
+
+ rows = cursor.fetchall()
+ updates = []
+
+ for row in rows:
+ length = safe_float(row['length_m'])
+ width = safe_float(row['width_m'])
+ weight = safe_float(row['weight_kg'])
+ warhead_weight = safe_float(row['warhead_weight_kg'])
+ max_speed = safe_float(row['max_speed_ms'])
+ cruise_speed = safe_float(row['cruise_speed_kmh'])
+ max_range = safe_float(row['max_range_km'])
+ endurance = safe_float(row['endurance_min'])
+ ceiling = safe_float(row['ceiling_altitude_m'])
+ payload = safe_float(row['max_payload_kg'])
+ combat_radius = safe_float(row['combat_radius_km'])
+ guidance_system = row['guidance_system'] or ''
+ warhead_type = row['warhead_type'] or ''
+
+ # ── 1. 长宽比 ──
+ length_width_ratio = round(length / max(0.1, width), 4)
+
+ # ── 2. 重量射程比 (kg/km) ──
+ weight_range_ratio = round(weight / max(1, max_range), 4)
+
+ # ── 3. 速度重量比 ──
+ speed_weight_ratio = round(max_speed / max(1, weight), 4)
+
+ # ── 4. 估算缺失的原始字段 ──
+
+ # 制导精度 (m):当前全为 0,根据制导系统文字估算
+ estimated_accuracy = estimate_guidance_accuracy(guidance_system)
+
+ # 数据链距离 (km):用战斗半径或最大射程的 80% 估算
+ base_ref = combat_radius if combat_radius > 0 else max_range
+ estimated_datalink = round(base_ref * 0.85, 1)
+
+ # 发动机功率 (kw):基于重量和速度估算
+ # 小型无人机/巡飞弹功率密度约 0.05-0.3 kW/kg
+ total_weight = weight + payload
+ power_density = 0.08 + (max_speed / 200) * 0.12 # 越快功率密度越高
+ power_density = min(power_density, 0.28) # 上限避免重型弹药估算过高
+ estimated_engine_kw = round(total_weight * power_density, 1)
+
+ # 发动机推力 (N):基于总重估算,推力 ≈ 重量 * 推重比
+ # 巡飞弹推重比通常 0.2-0.5,但重型滑翔弹药不具备大推力发动机
+ thrust_ratio = 0.15 + (max_speed / 150) * 0.20
+ thrust_ratio = min(thrust_ratio, 0.3) # 上限
+ estimated_thrust_n = round(total_weight * 9.8 * thrust_ratio, 1)
+
+ # 最小作战高度 (m):巡飞弹通常 50-500m
+ if cruise_speed > 0:
+ estimated_min_alt = round(max(30, min(300, cruise_speed * 0.8)), 0)
+ else:
+ estimated_min_alt = round(max(30, min(300, max_speed * 2.5)), 0)
+
+ # 最大作战高度 (m):优先用 ceiling_altitude_m,否则估算
+ if ceiling > 0:
+ estimated_max_alt = ceiling
+ else:
+ estimated_max_alt = round(max(500, min(10000, endurance * 15)), 0)
+
+ # ── 5. 制导系统评分 (1-10) ──
+ guidance_system_score = calculate_guidance_score(guidance_system, estimated_accuracy)
+
+ # ── 6. 战斗部威力评分 (1-10) ──
+ warhead_power_score = calculate_warhead_score(warhead_type, warhead_weight)
+
+ updates.append((
+ length_width_ratio, weight_range_ratio, speed_weight_ratio,
+ guidance_system_score, warhead_power_score,
+ estimated_accuracy, estimated_datalink,
+ estimated_engine_kw, estimated_thrust_n,
+ estimated_min_alt, estimated_max_alt,
+ row['id']
+ ))
+
+ if updates:
+ cursor.executemany("""
+ UPDATE loitering_munition_params
+ SET length_width_ratio = ?, weight_range_ratio = ?,
+ speed_weight_ratio = ?, guidance_system_score = ?,
+ warhead_power_score = ?,
+ guidance_accuracy_m = ?, datalink_range_km = ?,
+ engine_power_kw = ?, engine_thrust_n = ?,
+ min_altitude_m = ?, max_altitude_m = ?
+ WHERE id = ?
+ """, updates)
+ logger.info(f"巡飞弹:更新了 {len(updates)} 条记录")
+
+ return len(updates)
+
+
+def main():
+ init_db()
+ conn = None
+ try:
+ with get_db_connection() as conn:
+ rc = calculate_rocket_features(conn)
+ lc = calculate_loitering_features(conn)
+ conn.commit()
+ print(f"\n完成!")
+ print(f" 火箭炮:{rc} 条记录已更新")
+ print(f" 巡飞弹:{lc} 条记录已更新")
+ except Exception as e:
+ if conn:
+ conn.rollback()
+ logger.error(f"特征计算失败: {e}")
+ raise
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/routes.py b/src/routes.py
index 0fd6a1d..9e9864d 100644
--- a/src/routes.py
+++ b/src/routes.py
@@ -259,7 +259,15 @@ def analyze_features():
'mobility_score': [float(item['mobility_score']) if item['mobility_score'] is not None else 0 for item in equipment_data],
'combat_readiness_score': [float(item['combat_readiness_score']) if item['combat_readiness_score'] is not None else 0 for item in equipment_data],
'deployment_score': [float(item['deployment_score']) if item['deployment_score'] is not None else 0 for item in equipment_data],
- 'terrain_adaptability_score': [float(item['terrain_adaptability_score']) if item['terrain_adaptability_score'] is not None else 0 for item in equipment_data]
+ 'terrain_adaptability_score': [float(item['terrain_adaptability_score']) if item['terrain_adaptability_score'] is not None else 0 for item in equipment_data],
+ 'rate_of_fire': [float(item['rate_of_fire']) if item['rate_of_fire'] is not None else 0 for item in equipment_data],
+ 'max_range_km': [float(item['max_range_km']) if item['max_range_km'] is not None else 0 for item in equipment_data],
+ 'rocket_length_m': [float(item['rocket_length_m']) if item['rocket_length_m'] is not None else 0 for item in equipment_data],
+ 'rocket_diameter_mm': [float(item['rocket_diameter_mm']) if item['rocket_diameter_mm'] is not None else 0 for item in equipment_data],
+ 'rocket_weight_kg': [float(item['rocket_weight_kg']) if item['rocket_weight_kg'] is not None else 0 for item in equipment_data],
+ 'speed_kmh': [float(item['speed_kmh']) if item['speed_kmh'] is not None else 0 for item in equipment_data],
+ 'power_hp': [float(item['power_hp']) if item['power_hp'] is not None else 0 for item in equipment_data],
+ 'travel_range_km': [float(item['travel_range_km']) if item['travel_range_km'] is not None else 0 for item in equipment_data],
}
analysis_result.update(rocket_data)
else: