CostPrediction/src/routes.py

1333 lines
54 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from flask import Blueprint, request, jsonify, send_file
from .cost_prediction import CostPredictor
from .feature_analysis import FeatureAnalysis
import pandas as pd
from datetime import datetime
import numpy as np
from sklearn.metrics import mean_absolute_error
from .create_template import create_excel_template
import json
import os
from .data_preparation import DataPreparation
from .model_trainer import ModelTrainer
from .database import get_db_connection
from .demo_service import DemoModelService
from .logger import setup_logger
# PyTorch 为可选导入
try:
import torch
_HAS_TORCH = True
except ImportError:
_HAS_TORCH = False
# 创建蓝图
api_bp = Blueprint('api', __name__)
# 获取logger
logger = setup_logger(__name__)
@api_bp.route('/', methods=['GET'])
def index():
"""
API根路由
返回API版本信息和可用端点列表
"""
return jsonify({
'name': '装备成本估算系统 API',
'version': '1.0.0',
'endpoints': {
'predict': {
'url': '/api/predict',
'method': 'POST',
'description': '成本预测'
},
'analyze-features': {
'url': '/api/analyze-features',
'method': 'POST',
'description': '特征分析'
},
'train': {
'url': '/api/train',
'method': 'POST',
'description': '模型训练'
},
'evaluate': {
'url': '/api/evaluate',
'method': 'POST',
'description': '模型评估'
},
'analyze-manufacturers': {
'url': '/api/analyze-manufacturers',
'method': 'POST',
'description': '供应商分析'
}
}
})
@api_bp.route('/demo/algorithms', methods=['GET'])
def demo_algorithms():
"""Return algorithms supported by the file-based demo."""
try:
service = DemoModelService()
return jsonify({'algorithms': service.get_algorithms()})
except Exception as e:
logger.error(f"Error getting demo algorithms: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/demo/dataset', methods=['GET'])
def demo_dataset():
"""Return the local demo dataset summary without using a database."""
try:
service = DemoModelService()
return jsonify(service.get_dataset_summary())
except Exception as e:
logger.error(f"Error getting demo dataset: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/demo/run', methods=['POST'])
def demo_run():
"""Run selected demo algorithms against the local data file."""
try:
data = request.get_json(silent=True) or {}
service = DemoModelService()
return jsonify(service.run_demo(data.get('algorithms')))
except Exception as e:
logger.error(f"Error running demo algorithms: {str(e)}")
logger.error("Detailed traceback:", exc_info=True)
return jsonify({'error': str(e)}), 500
@api_bp.route('/predict', methods=['POST'])
def predict():
"""使用最优机器学习模型进行预测"""
try:
data = request.get_json()
equipment_type = data.get('type')
logger.info(f"Received prediction request for equipment type: {equipment_type}")
# 获取最新的激活模型非PLS模型
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM trained_models
WHERE equipment_type = ?
AND model_type != 'pls'
AND is_active = TRUE
ORDER BY training_date DESC LIMIT 1
""", (equipment_type,))
model = cursor.fetchone()
if not model:
return jsonify({'error': '未找到可用的模型'}), 404
# 使用普通预测方法
predictor = CostPredictor()
prediction = predictor.predict(data, model) # 使用普通predict方法
# 返回预测结果
return jsonify({
'model_info': {
'type': model['model_type'], # 使用数据库中的模型类型
'name': model['model_name']
},
'predicted_cost': prediction['predicted_cost'],
'confidence_interval': prediction['confidence_interval']
})
except Exception as e:
logger.error(f"Error in prediction: {str(e)}")
logger.error("Detailed traceback:", exc_info=True)
return jsonify({'error': str(e)}), 500
@api_bp.route('/analyze-features', methods=['POST'])
def analyze_features():
"""基于数据集进行特征分析"""
try:
data = request.get_json()
dataset_id = data.get('dataset_id')
logger.info(f"Starting feature analysis for dataset {dataset_id}")
if not dataset_id:
logger.warning("No dataset_id provided")
return jsonify({'error': '请选择数据集'}), 400
with get_db_connection() as conn:
cursor = conn.cursor()
# 首先获取数据集的装备类型
cursor.execute("""
SELECT DISTINCT e.type
FROM equipments e
JOIN dataset_equipments de ON e.id = de.equipment_id
WHERE de.dataset_id = ?
LIMIT 1
""", (dataset_id,))
equipment_type = cursor.fetchone()['type']
logger.info(f"Equipment type: {equipment_type}")
# 根据装备类型选择查询
if equipment_type == '火箭炮':
cursor.execute("""
SELECT e.id, e.name, e.type, e.manufacturer, e.manufacturer_id,
m.tech_level, m.scale_level, m.supply_chain_level, m.country,
cp.length_m, cp.width_m, cp.height_m, cp.weight_kg,
cd.actual_cost,
rap.max_range_km, rap.firing_angle_horizontal, rap.firing_angle_vertical,
rap.rocket_length_m, rap.rocket_diameter_mm, rap.rocket_weight_kg,
rap.rate_of_fire, rap.combat_weight_kg, rap.speed_kmh,
rap.min_range_km, rap.power_hp, rap.travel_range_km,
rap.fire_density, rap.range_ratio, rap.mobility_score,
rap.combat_readiness_score, rap.deployment_score, rap.terrain_adaptability_score,
rap.rocket_power_ratio, rap.platform_efficiency
FROM equipments e
JOIN dataset_equipments de ON e.id = de.equipment_id
LEFT JOIN manufacturers m ON e.manufacturer_id = m.id
LEFT JOIN common_params cp ON e.id = cp.equipment_id
LEFT JOIN rocket_artillery_params rap ON e.id = rap.equipment_id
LEFT JOIN cost_data cd ON e.id = cd.equipment_id
WHERE de.dataset_id = ?
AND cd.actual_cost IS NOT NULL
""", (dataset_id,))
else:
cursor.execute("""
SELECT e.id, e.name, e.type, e.manufacturer, e.manufacturer_id,
m.tech_level, m.scale_level, m.supply_chain_level, m.country,
cp.length_m, cp.width_m, cp.height_m, cp.weight_kg,
cd.actual_cost,
lmp.max_range_km, lmp.wingspan_m, lmp.warhead_weight_kg,
lmp.max_speed_ms, lmp.cruise_speed_kmh, lmp.endurance_min,
lmp.length_width_ratio, lmp.weight_range_ratio,
lmp.speed_weight_ratio, lmp.ceiling_altitude_m,
lmp.guidance_system_score, lmp.warhead_power_score,
lmp.engine_power_kw, lmp.engine_thrust_n,
lmp.min_altitude_m, lmp.max_altitude_m,
lmp.max_payload_kg, lmp.combat_radius_km,
lmp.datalink_range_km, lmp.guidance_accuracy_m
FROM equipments e
JOIN dataset_equipments de ON e.id = de.equipment_id
LEFT JOIN manufacturers m ON e.manufacturer_id = m.id
LEFT JOIN common_params cp ON e.id = cp.equipment_id
LEFT JOIN loitering_munition_params lmp ON e.id = lmp.equipment_id
LEFT JOIN cost_data cd ON e.id = cd.equipment_id
WHERE de.dataset_id = ?
AND cd.actual_cost IS NOT NULL
""", (dataset_id,))
equipment_data = cursor.fetchall()
# 提取特征和目标值
analyzer = FeatureAnalysis()
feature_names = analyzer.get_equipment_specific_features(equipment_data[0]['type'])
features = []
targets = []
for item in equipment_data:
# 计算生产商特征
manufacturer_features = analyzer.calculate_manufacturer_features({
'tech_level': item['tech_level'],
'scale_level': item['scale_level'],
'supply_chain_level': item['supply_chain_level'],
'country': item['country']
})
# 获取装备特征
feature_values = []
for name in feature_names:
if name in manufacturer_features:
value = manufacturer_features[name]
else:
value = item.get(name)
feature_values.append(float(value) if value is not None else 0.0)
features.append(feature_values)
targets.append(float(item['actual_cost']))
# 执行特征分析
analysis_result = analyzer.analyze_features(features, targets, feature_names)
# 添加装备名称列表
analysis_result['equipment_names'] = [item['name'] for item in equipment_data]
# 添加装备特有的分析数据
if equipment_data[0]['type'] == '火箭炮':
rocket_data = {
'fire_density': [float(item['fire_density']) if item['fire_density'] is not None else 0 for item in equipment_data],
'range_ratio': [float(item['range_ratio']) if item['range_ratio'] is not None else 0 for item in equipment_data],
'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],
'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:
missile_data = {
'length_width_ratio': [float(item['length_width_ratio']) if item['length_width_ratio'] is not None else 0 for item in equipment_data],
'weight_range_ratio': [float(item['weight_range_ratio']) if item['weight_range_ratio'] is not None else 0 for item in equipment_data],
'speed_weight_ratio': [float(item['speed_weight_ratio']) if item['speed_weight_ratio'] is not None else 0 for item in equipment_data],
'guidance_system_score': [float(item['guidance_system_score']) if item['guidance_system_score'] is not None else 0 for item in equipment_data],
'warhead_power_score': [float(item['warhead_power_score']) if item['warhead_power_score'] is not None else 0 for item in equipment_data],
'guidance_accuracy_m': [float(item['guidance_accuracy_m']) if item['guidance_accuracy_m'] is not None else 0 for item in equipment_data],
'datalink_range_km': [float(item['datalink_range_km']) if item['datalink_range_km'] is not None else 0 for item in equipment_data],
'max_altitude_m': [float(item['max_altitude_m']) if item['max_altitude_m'] is not None else 0 for item in equipment_data],
'min_altitude_m': [float(item['min_altitude_m']) if item['min_altitude_m'] is not None else 0 for item in equipment_data],
'engine_power_kw': [float(item['engine_power_kw']) if item['engine_power_kw'] is not None else 0 for item in equipment_data],
'engine_thrust_n': [float(item['engine_thrust_n']) if item['engine_thrust_n'] is not None else 0 for item in equipment_data]
}
analysis_result.update(missile_data)
return jsonify(analysis_result)
except Exception as e:
logger.error(f"Error analyzing features: {str(e)}")
logger.error("Detailed traceback:", exc_info=True)
return jsonify({'error': str(e)}), 500
@api_bp.route('/train', methods=['POST'])
def train_model():
"""训练模型"""
try:
data = request.get_json()
logger.info(f"Starting model training for {data.get('type')}")
equipment_type = data.get('type')
train_dataset_id = data.get('train_dataset_id')
validation_dataset_id = data.get('validation_dataset_id')
models = data.get('models', [])
auto_split = data.get('auto_split', False) # 自动划分训练/验证集
split_ratio = data.get('split_ratio', 0.2) # 验证集比例,默认 20%
logger.info(f"Training dataset: {train_dataset_id}")
logger.info(f"Validation dataset: {validation_dataset_id}")
logger.info(f"Selected models: {models}")
# 获取训练数据
with get_db_connection() as conn:
cursor = conn.cursor()
# 获取训练集数据(包含生产商信息)
if equipment_type == '火箭炮':
cursor.execute("""
SELECT e.*, cp.*, rap.*, cd.actual_cost,
m.tech_level, m.scale_level, m.supply_chain_level,
m.id as manufacturer_id
FROM equipments e
JOIN dataset_equipments de ON e.id = de.equipment_id
LEFT JOIN common_params cp ON e.id = cp.equipment_id
LEFT JOIN rocket_artillery_params rap ON e.id = rap.equipment_id
LEFT JOIN cost_data cd ON e.id = cd.equipment_id
LEFT JOIN manufacturers m ON e.manufacturer_id = m.id
WHERE de.dataset_id = ?
AND cd.actual_cost IS NOT NULL
""", (train_dataset_id,))
else:
cursor.execute("""
SELECT e.*, cp.*, lmp.*, cd.actual_cost,
m.tech_level, m.scale_level, m.supply_chain_level,
m.id as manufacturer_id
FROM equipments e
JOIN dataset_equipments de ON e.id = de.equipment_id
LEFT JOIN common_params cp ON e.id = cp.equipment_id
LEFT JOIN loitering_munition_params lmp ON e.id = lmp.equipment_id
LEFT JOIN cost_data cd ON e.id = cd.equipment_id
LEFT JOIN manufacturers m ON e.manufacturer_id = m.id
WHERE de.dataset_id = ?
AND cd.actual_cost IS NOT NULL
""", (train_dataset_id,))
train_data = cursor.fetchall()
# 获取验证集数据
if validation_dataset_id:
if equipment_type == '火箭炮':
cursor.execute("""
SELECT e.*, cp.*, rap.*, cd.actual_cost,
m.tech_level, m.scale_level, m.supply_chain_level,
m.id as manufacturer_id
FROM equipments e
JOIN dataset_equipments de ON e.id = de.equipment_id
LEFT JOIN common_params cp ON e.id = cp.equipment_id
LEFT JOIN rocket_artillery_params rap ON e.id = rap.equipment_id
LEFT JOIN cost_data cd ON e.id = cd.equipment_id
LEFT JOIN manufacturers m ON e.manufacturer_id = m.id
WHERE de.dataset_id = ?
AND cd.actual_cost IS NOT NULL
""", (validation_dataset_id,))
else:
cursor.execute("""
SELECT e.*, cp.*, lmp.*, cd.actual_cost,
m.tech_level, m.scale_level, m.supply_chain_level,
m.id as manufacturer_id
FROM equipments e
JOIN dataset_equipments de ON e.id = de.equipment_id
LEFT JOIN common_params cp ON e.id = cp.equipment_id
LEFT JOIN loitering_munition_params lmp ON e.id = lmp.equipment_id
LEFT JOIN cost_data cd ON e.id = cd.equipment_id
LEFT JOIN manufacturers m ON e.manufacturer_id = m.id
WHERE de.dataset_id = ?
AND cd.actual_cost IS NOT NULL
""", (validation_dataset_id,))
validation_data = cursor.fetchall()
if not train_data:
return jsonify({'error': '训练数据集为空'}), 400
# 1. 准备数据
data_processor = DataPreparation()
# 准备训练数据
if auto_split and not validation_dataset_id:
# 自动划分模式:从训练数据中随机拆分验证集
logger.info(f"Auto-splitting training data with validation ratio={split_ratio}")
train_prepared = data_processor.prepare_training_data(
train_data, equipment_type, validation_split=split_ratio
)
validation_prepared = {
'X': train_prepared.pop('X_val'),
'y': train_prepared.pop('y_val'),
}
else:
# 传统模式:单独查询验证数据集
train_prepared = data_processor.prepare_training_data(train_data, equipment_type)
validation_prepared = None
if validation_data:
validation_prepared = data_processor.prepare_validation_data(
validation_data,
equipment_type,
train_prepared['feature_names'],
{
'feature_scaler': train_prepared['feature_scaler'],
'target_scaler': train_prepared['target_scaler']
}
)
# 2. 训练模型
model_trainer = ModelTrainer()
model_trainer.feature_scaler = train_prepared['feature_scaler']
model_trainer.target_scaler = train_prepared['target_scaler']
# 执行训练传入equipment_type参数
training_result = model_trainer.fit_model(
train_prepared['X'],
train_prepared['y'],
models,
validation_prepared['X'] if validation_prepared else None,
validation_prepared['y'] if validation_prepared else None,
equipment_type=equipment_type # 添加这个参数
)
return jsonify(training_result)
except Exception as e:
logger.error(f"Error in model training: {str(e)}")
logger.error("Detailed traceback:", exc_info=True)
return jsonify({'error': str(e)}), 500
@api_bp.route('/evaluate', methods=['POST'])
def evaluate_model():
"""
模型评估接口
"""
try:
data = request.get_json()
logger.info("Received model evaluation request")
if 'test_data' not in data:
return jsonify({'error': 'Test data is required'}), 400
predictor = CostPredictor()
evaluation_result = predictor.evaluate(
data['test_data']['actual'],
data['test_data']['predicted']
)
logger.info("Model evaluation completed")
return jsonify(evaluation_result)
except Exception as e:
logger.error(f"Error in model evaluation: {str(e)}")
return jsonify({'error': str(e)}), 500
def get_required_params(equipment_type):
"""
根据装备类型获取必要参数
"""
common_params = [
'length_m',
'width_m',
'height_m',
'weight_kg',
'max_range_km'
]
if equipment_type == '火箭炮':
return common_params + [
'firing_angle_horizontal',
'firing_angle_vertical',
'rocket_length_m',
'rocket_diameter_mm',
'rocket_weight_kg'
]
elif equipment_type == '巡飞弹':
return common_params + [
'max_speed_kmh',
'cruise_speed_kmh',
'flight_time_min',
'folded_length_mm',
'folded_width_mm',
'folded_height_mm'
]
return common_params
@api_bp.errorhandler(404)
def not_found(error):
return jsonify({'error': 'Not found'}), 404
@api_bp.errorhandler(500)
def internal_error(error):
logger.error(f"Internal server error: {str(error)}")
return jsonify({'error': 'Internal server error'}), 500
@api_bp.route('/data', methods=['GET'])
def get_equipment_data():
"""获取装备数据列表"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
# 获取所有装备数据(使用equipment_id替代id)
cursor.execute("""
SELECT e.id as equipment_id, e.name, e.type, e.manufacturer,
cp.length_m, cp.width_m, cp.height_m, cp.weight_kg,
cd.actual_cost, cd.predicted_cost,
CASE
WHEN e.type = '火箭炮' THEN (
SELECT firing_angle_horizontal || ',' ||
firing_angle_vertical || ',' ||
rocket_length_m || ',' ||
rocket_diameter_mm || ',' ||
rocket_weight_kg || ',' ||
rate_of_fire || ',' ||
combat_weight_kg || ',' ||
speed_kmh || ',' ||
min_range_km || ',' ||
max_range_km || ',' ||
mobility_type || ',' ||
structure_layout || ',' ||
engine_model || ',' ||
engine_params || ',' ||
power_hp || ',' ||
travel_range_km
FROM rocket_artillery_params
WHERE equipment_id = e.id
)
WHEN e.type = '巡飞弹' THEN (
SELECT wingspan_m || ',' ||
warhead_weight_kg || ',' ||
max_speed_ms || ',' ||
cruise_speed_kmh || ',' ||
endurance_min || ',' ||
max_range_km || ',' ||
max_payload_kg || ',' ||
ceiling_altitude_m || ',' ||
combat_radius_km || ',' ||
warhead_type || ',' ||
launch_mode || ',' ||
power_system || ',' ||
guidance_system
FROM loitering_munition_params
WHERE equipment_id = e.id
)
END as specific_params
FROM equipments e
LEFT JOIN common_params cp ON e.id = cp.equipment_id
LEFT JOIN cost_data cd ON e.id = cd.equipment_id
ORDER BY e.id
""")
result = cursor.fetchall()
logger.info(f"Found {len(result)} equipment records")
logger.info(f"Sample data: {result[0] if result else None}")
return jsonify(result)
except Exception as e:
logger.error(f"Error getting equipment data: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/data/<int:id>', methods=['DELETE'])
def delete_equipment(id):
"""
删除装备数据
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
# 删除相关数据
cursor.execute("DELETE FROM cost_data WHERE equipment_id = ?", (id,))
cursor.execute("DELETE FROM rocket_artillery_params WHERE equipment_id = ?", (id,))
cursor.execute("DELETE FROM loitering_munition_params WHERE equipment_id = ?", (id,))
cursor.execute("DELETE FROM common_params WHERE equipment_id = ?", (id,))
cursor.execute("DELETE FROM equipments WHERE id = ?", (id,))
conn.commit()
return jsonify({'status': 'success'})
except Exception as e:
logger.error(f"Error deleting equipment: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/data/template', methods=['GET'])
def download_template():
"""
下载数据模板
"""
try:
# 创建模板文件
from .create_template import create_excel_template
template_path = create_excel_template()
# 检查文件是否存
if not os.path.exists(template_path):
raise FileNotFoundError("模板文件不存在")
# 返回文件
return send_file(
template_path,
as_attachment=True,
download_name='equipment_data_template.xlsx',
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
except Exception as e:
logger.error(f"Error creating template: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/pls/predict', methods=['POST'])
def pls_predict():
"""使用PLS模型进行预测"""
try:
data = request.get_json()
equipment_type = data.get('type')
# 获取最新的PLS模型
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM trained_models
WHERE equipment_type = ?
AND model_type = 'pls'
AND is_active = TRUE
ORDER BY training_date DESC LIMIT 1
""", (equipment_type,))
model = cursor.fetchone()
if not model:
return jsonify({'error': '未找到可用的PLS模型'}), 404
# 使用普通predict方法传入模型信息
predictor = CostPredictor()
prediction = predictor.predict(data, model) # 传入model参数
# 返回预测结果
return jsonify({
'model_info': {
'type': 'pls',
'name': model['model_name']
},
'predicted_cost': prediction['predicted_cost'],
'confidence_interval': prediction['confidence_interval']
})
except Exception as e:
logger.error(f"Error in PLS prediction: {str(e)}")
logger.error("Detailed traceback:", exc_info=True)
return jsonify({'error': str(e)}), 500
@api_bp.route('/data/import', methods=['POST'])
def import_data():
"""
导入数据接口
"""
try:
if 'file' not in request.files:
return jsonify({'error': '没有上传文件'}), 400
file = request.files['file']
if not file.filename.endswith(('.xls', '.xlsx')):
return jsonify({'error': '请上传Excel文件'}), 400
# 保存上的文件
upload_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data')
os.makedirs(upload_dir, exist_ok=True)
file_path = os.path.join(upload_dir, file.filename)
file.save(file_path)
# 导入数据
from .import_data import import_training_data
import_training_data(file_path)
return jsonify({
'success': True,
'message': '数据导入成功'
})
except Exception as e:
logger.error(f"Error importing data: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/data/<int:id>', methods=['PUT'])
def update_equipment(id):
"""更新装备数据"""
try:
data = request.get_json()
logger.info(f"Updating equipment with data: {data}")
with get_db_connection() as conn:
cursor = conn.cursor()
# 使用 equipment_id 而不是 id
equipment_id = data.get('equipment_id')
if not equipment_id:
raise ValueError("Missing equipment_id")
# 更新装备基本信息
cursor.execute("""
UPDATE equipments
SET name = ?, manufacturer = ?
WHERE id = ?
""", (data['name'], data['manufacturer'], equipment_id))
# 更新通用参数
cursor.execute("""
UPDATE common_params
SET length_m = ?, width_m = ?, height_m = ?, weight_kg = ?
WHERE equipment_id = ?
""", (data['length_m'], data['width_m'], data['height_m'], data['weight_kg'], equipment_id))
# 根据装备类型更新特有参数
if data['type'] == '火箭炮':
cursor.execute("""
UPDATE rocket_artillery_params
SET firing_angle_horizontal = ?,
firing_angle_vertical = ?,
rocket_length_m = ?,
rocket_diameter_mm = ?,
rocket_weight_kg = ?,
rate_of_fire = ?,
combat_weight_kg = ?,
speed_kmh = ?,
min_range_km = ?,
max_range_km = ?,
mobility_type = ?,
structure_layout = ?,
engine_model = ?,
engine_params = ?,
power_hp = ?,
travel_range_km = ?
WHERE equipment_id = ?
""", (
data['firing_angle_horizontal'],
data['firing_angle_vertical'],
data['rocket_length_m'],
data['rocket_diameter_mm'],
data['rocket_weight_kg'],
data['rate_of_fire'],
data['combat_weight_kg'],
data['speed_kmh'],
data['min_range_km'],
data['max_range_km'],
data['mobility_type'],
data['structure_layout'],
data['engine_model'],
data['engine_params'],
data['power_hp'],
data['travel_range_km'],
equipment_id
))
else:
cursor.execute("""
UPDATE loitering_munition_params
SET wingspan_m = ?,
warhead_weight_kg = ?,
max_speed_ms = ?,
cruise_speed_kmh = ?,
endurance_min = ?,
max_range_km = ?,
warhead_type = ?,
launch_mode = ?,
power_system = ?,
guidance_system = ?
WHERE equipment_id = ?
""", (
data['wingspan_m'],
data['warhead_weight_kg'],
data['max_speed_ms'],
data['cruise_speed_kmh'],
data['endurance_min'],
data['max_range_km'],
data['warhead_type'],
data['launch_mode'],
data['power_system'],
data['guidance_system'],
equipment_id
))
# 更新成本数据
if 'actual_cost' in data:
cursor.execute("""
UPDATE cost_data
SET actual_cost = ?
WHERE equipment_id = ?
""", (data['actual_cost'], equipment_id))
conn.commit()
return jsonify({'success': True})
except Exception as e:
logger.error(f"Error updating equipment: {str(e)}")
logger.error("Detailed traceback:", exc_info=True)
return jsonify({'error': str(e)}), 500
@api_bp.route('/data/details/<int:id>', methods=['GET'])
def get_equipment_details(id):
"""获取装备详情"""
try:
logger.info(f"Getting details for equipment ID: {id}")
with get_db_connection() as conn:
cursor = conn.cursor()
# 获取装备基本信息类型
cursor.execute("""
SELECT e.*, cp.*, cd.actual_cost, cd.predicted_cost
FROM equipments e
LEFT JOIN common_params cp ON e.id = cp.equipment_id
LEFT JOIN cost_data cd ON e.id = cd.equipment_id
WHERE e.id = ?
""", (id,))
result = cursor.fetchone()
if not result:
logger.warning(f"Equipment with ID {id} not found")
return jsonify({'error': '装备不在'}), 404
logger.info(f"Equipment type: {result['type']}")
logger.info(f"Found equipment details: {result['name']}")
# 根据装备类型获取特有参数
if result['type'] == '火箭炮':
cursor.execute("""
SELECT *
FROM rocket_artillery_params
WHERE equipment_id = ?
""", (id,))
custom_params = cursor.fetchone()
else:
cursor.execute("""
SELECT *
FROM loitering_munition_params
WHERE equipment_id = ?
""", (id,))
custom_params = cursor.fetchone()
# 合并特有参数到结果中
if custom_params:
result.update(custom_params)
logger.info(f"Custom params: {custom_params}")
return jsonify(result)
except Exception as e:
logger.error(f"Error getting equipment details: {str(e)}")
return jsonify({'error': str(e)}), 500
# 添加数据集相关的路由
@api_bp.route('/datasets', methods=['GET'])
def get_datasets():
"""
获取数据集列表
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT d.*,
COUNT(de.equipment_id) as equipment_count,
GROUP_CONCAT(e.name) as equipment_names
FROM datasets d
LEFT JOIN dataset_equipments de ON d.id = de.dataset_id
LEFT JOIN equipments e ON de.equipment_id = e.id
GROUP BY d.id
""")
datasets = cursor.fetchall()
# 整理装备名称列表
for dataset in datasets:
if dataset['equipment_names']:
dataset['equipment_names'] = dataset['equipment_names'].split(',')
else:
dataset['equipment_names'] = []
# SQLite 已存储日期为文本格式,无需转换
return jsonify(datasets)
except Exception as e:
logger.error(f"Error getting datasets: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/datasets/<int:id>', methods=['GET'])
def get_dataset(id):
"""
获取数据集详情
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
# 获取数据集基本信息
cursor.execute("""
SELECT d.*,
COUNT(de.equipment_id) as equipment_count
FROM datasets d
LEFT JOIN dataset_equipments de ON d.id = de.dataset_id
WHERE d.id = ?
GROUP BY d.id
""", (id,))
dataset = cursor.fetchone()
if not dataset:
return jsonify({'error': 'Dataset not found'}), 404
# 获取数据集中的装备 - 修改查询确保返回正确的ID
cursor.execute("""
SELECT e.id as equipment_id, e.name, e.type, e.manufacturer,
cd.actual_cost
FROM equipments e
JOIN dataset_equipments de ON e.id = de.equipment_id
LEFT JOIN cost_data cd ON e.id = cd.equipment_id
WHERE de.dataset_id = ?
""", (id,))
equipment = cursor.fetchall()
# 算统计信
if equipment:
total_cost = sum(item['actual_cost'] or 0 for item in equipment)
avg_cost = total_cost / len(equipment)
dataset['statistics'] = {
'equipment_count': len(equipment),
'total_cost': total_cost,
'average_cost': avg_cost
}
else:
dataset['statistics'] = {
'equipment_count': 0,
'total_cost': 0,
'average_cost': 0
}
dataset['equipment'] = equipment
return jsonify(dataset)
except Exception as e:
logger.error(f"Error getting dataset: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/datasets', methods=['POST'])
def create_dataset():
"""创建数据集"""
try:
data = request.get_json()
logger.info(f"Creating dataset with data: {data}")
with get_db_connection() as conn:
cursor = conn.cursor()
# 1. 验证装备ID是否存在
if 'equipment_ids' in data and data['equipment_ids']:
ids = data['equipment_ids']
placeholders = ','.join(['?'] * len(ids))
cursor.execute(f"""
SELECT DISTINCT id FROM equipments
WHERE id IN ({placeholders}) AND type = ?
""", (*ids, data['equipment_type']))
valid_ids = [row['id'] for row in cursor.fetchall()]
logger.info(f"Valid equipment IDs: {valid_ids}")
# 如果有无效的ID返回错误
invalid_ids = set(data['equipment_ids']) - set(valid_ids)
if invalid_ids:
logger.error(f"Invalid equipment IDs: {invalid_ids}")
return jsonify({'error': f'无效的装备ID: {invalid_ids}'}), 400
# 2. 创建数据集
cursor.execute("""
INSERT INTO datasets (name, description, equipment_type, purpose)
VALUES (?, ?, ?, ?)
""", (data['name'], data['description'], data['equipment_type'], data['purpose']))
dataset_id = cursor.lastrowid
logger.info(f"Created dataset with ID: {dataset_id}")
# 3. 添加装备关联
if 'equipment_ids' in data and data['equipment_ids']:
values = [(dataset_id, equipment_id) for equipment_id in valid_ids]
cursor.executemany("""
INSERT INTO dataset_equipments (dataset_id, equipment_id)
VALUES (?, ?)
""", values)
logger.info(f"Added {len(values)} equipment associations")
conn.commit()
return jsonify({'id': dataset_id, 'message': '数据集创建成功'})
except Exception as e:
logger.error(f"Error creating dataset: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/datasets/<int:id>', methods=['PUT'])
def update_dataset(id):
"""更新数据集"""
try:
data = request.get_json()
logger.info(f"Updating dataset {id} with data: {data}")
with get_db_connection() as conn:
cursor = conn.cursor()
# 1. 验证装备ID是否存在
if 'equipment_ids' in data and data['equipment_ids']:
ids = data['equipment_ids']
placeholders = ','.join(['?'] * len(ids))
cursor.execute(f"""
SELECT id FROM equipments
WHERE id IN ({placeholders}) AND type = ?
""", (*ids, data['equipment_type']))
valid_ids = [row['id'] for row in cursor.fetchall()]
logger.info(f"Valid equipment IDs: {valid_ids}")
# 如果有无效的ID返回错误
invalid_ids = set(data['equipment_ids']) - set(valid_ids)
if invalid_ids:
logger.error(f"Invalid equipment IDs: {invalid_ids}")
return jsonify({'error': f'无效的装备ID: {invalid_ids}'}), 400
# 2. 更新数据集基本信息
cursor.execute("""
UPDATE datasets
SET name = ?, description = ?, equipment_type = ?, purpose = ?
WHERE id = ?
""", (data['name'], data['description'], data['equipment_type'], data['purpose'], id))
# 3. 更新装备关联
if 'equipment_ids' in data:
# 先删除旧的关联
cursor.execute("DELETE FROM dataset_equipments WHERE dataset_id = ?", (id,))
# 添加新的关联
if valid_ids: # 确保有有效的ID才执行插入
values = [(id, equipment_id) for equipment_id in valid_ids]
cursor.executemany("""
INSERT INTO dataset_equipments (dataset_id, equipment_id)
VALUES (?, ?)
""", values)
logger.info(f"Updated {len(values)} equipment associations")
conn.commit()
return jsonify({'success': True})
except Exception as e:
logger.error(f"Error updating dataset: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/datasets/<int:id>', methods=['DELETE'])
def delete_dataset(id):
"""
删除数据集
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
# 删除装备关联
cursor.execute("DELETE FROM dataset_equipments WHERE dataset_id = ?", (id,))
# 删除数据集
cursor.execute("DELETE FROM datasets WHERE id = ?", (id,))
conn.commit()
return jsonify({'success': True})
except Exception as e:
logger.error(f"Error deleting dataset: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/models/<equipment_type>/latest', methods=['GET'])
def get_latest_model(equipment_type):
"""
获取最新训练的模型信息
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM trained_models
WHERE equipment_type = ? AND is_active = TRUE
ORDER BY training_date DESC LIMIT 1
""", (equipment_type,))
model = cursor.fetchone()
return jsonify(model)
except Exception as e:
logger.error(f"Error getting latest model: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/models', methods=['GET'])
def get_models():
"""获取模型列表"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM trained_models
ORDER BY training_date DESC
""")
models = cursor.fetchall()
# 格式化时间和数值字段
for model in models:
# SQLite 已存储日期为文本格式,无需转换
if model['r2_score'] is not None:
model['r2_score'] = float(model['r2_score'])
if model['mae'] is not None:
model['mae'] = float(model['mae'])
if model['rmse'] is not None:
model['rmse'] = float(model['rmse'])
# 解析特征重要性
if model['feature_importance']:
model['feature_importance'] = json.loads(model['feature_importance'])
return jsonify(models)
except Exception as e:
logger.error(f"Error getting models: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/models/<int:id>/activate', methods=['POST'])
def activate_model(id):
"""激活指定的模型"""
try:
with get_db_connection() as conn:
cursor = conn.cursor() # 使用字典游标
# 获取模型信息
cursor.execute("""
SELECT equipment_type, model_type FROM trained_models
WHERE id = ?
""", (id,))
model = cursor.fetchone()
if not model:
return jsonify({'error': 'Model not found'}), 404
# 将同类型的其他模型设置为非激活
cursor.execute("""
UPDATE trained_models
SET is_active = FALSE
WHERE equipment_type = ? AND model_type = ?
""", (model['equipment_type'], model['model_type']))
# 激活指定模型
cursor.execute("""
UPDATE trained_models
SET is_active = TRUE
WHERE id = ?
""", (id,))
conn.commit()
return jsonify({'success': True})
except Exception as e:
logger.error(f"Error activating model: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/models/<int:id>', methods=['DELETE'])
def delete_model(id):
"""
删除指定的模型
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
# 获取模型文件路径
cursor.execute("""
SELECT model_path, scaler_path
FROM trained_models
WHERE id = ?
""", (id,))
model = cursor.fetchone()
if not model:
return jsonify({'error': 'Model not found'}), 404
# 删除模型文件
if os.path.exists(model['model_path']):
os.remove(model['model_path'])
if os.path.exists(model['scaler_path']):
os.remove(model['scaler_path'])
# 删除数据库记录
cursor.execute("DELETE FROM trained_models WHERE id = ?", (id,))
conn.commit()
return jsonify({'success': True})
except Exception as e:
logger.error(f"Error deleting model: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/predict/all', methods=['POST'])
def predict_all():
"""
获取所有机学习模型的预测结果
"""
try:
data = request.get_json()
logger.info(f"Received prediction request for all models, equipment type: {data.get('type')}")
predictor = CostPredictor()
results = predictor.predict_all(data)
return jsonify(results)
except Exception as e:
logger.error(f"Error in prediction: {str(e)}")
return jsonify({'error': str(e)}), 500
@api_bp.route('/analyze-manufacturers', methods=['POST'])
def analyze_manufacturers():
"""分析生产商数据"""
try:
data = request.get_json()
dataset_id = data.get('dataset_id')
logger.info(f"Starting manufacturer analysis for dataset {dataset_id}")
if not dataset_id:
logger.warning("No dataset_id provided")
return jsonify({'error': '请选择数据集'}), 400
with get_db_connection() as conn:
cursor = conn.cursor()
# 获取数据集中的装备和生产商数据
cursor.execute("""
SELECT DISTINCT m.*, e.type as equipment_type
FROM manufacturers m
JOIN equipments e ON e.manufacturer_id = m.id
JOIN dataset_equipments de ON e.id = de.equipment_id
WHERE de.dataset_id = ?
""", (dataset_id,))
manufacturers = cursor.fetchall()
if not manufacturers:
logger.info("No manufacturer data in dataset, returning empty result")
return jsonify({
'manufacturer_names': [],
'manufacturer_tech_levels': [],
'manufacturer_scale_levels': [],
'manufacturer_supply_chain_levels': [],
'manufacturer_composite_scores': [],
'region_distribution': [],
'manufacturer_scores': []
})
# 准备分析数据
manufacturer_names = []
tech_levels = []
scale_levels = []
supply_chain_levels = []
composite_scores = []
region_count = {}
manufacturer_scores = []
for manu in manufacturers:
manufacturer_names.append(manu['name'])
tech_levels.append(manu['tech_level'])
scale_levels.append(manu['scale_level'])
supply_chain_levels.append(manu['supply_chain_level'])
# 计算综合得分
composite_score = (
manu['tech_level'] * 0.4 +
manu['scale_level'] * 0.3 +
manu['supply_chain_level'] * 0.3
)
composite_scores.append(composite_score)
# 统计地区分布
region_count[manu['country']] = region_count.get(manu['country'], 0) + 1
# 计算区域系数
region_factors = {
'美国': 1.2, '英国': 1.15, '德国': 1.15,
'法国': 1.15, '以色列': 1.1, '中国': 0.8,
'俄罗斯': 0.85, '韩国': 0.9, '日本': 1.1
}
region_factor = region_factors.get(manu['country'], 1.0)
# 添加雷达图数据
manufacturer_scores.append({
'name': manu['name'],
'value': [
manu['tech_level'],
manu['scale_level'],
manu['supply_chain_level'],
region_factor,
composite_score
]
})
# 准备地区分布数据
region_distribution = [
{'name': country, 'value': count}
for country, count in region_count.items()
]
# 返回分析结果
result = {
'manufacturer_names': manufacturer_names,
'manufacturer_tech_levels': tech_levels,
'manufacturer_scale_levels': scale_levels,
'manufacturer_supply_chain_levels': supply_chain_levels,
'manufacturer_composite_scores': composite_scores,
'region_distribution': region_distribution,
'manufacturer_scores': manufacturer_scores
}
return jsonify(result)
except Exception as e:
logger.error(f"Error analyzing manufacturers: {str(e)}")
logger.error("Detailed traceback:", exc_info=True)
return jsonify({'error': str(e)}), 500