feat: v3.5 - Multi-language acoustic algorithm scheduler
- Core: Python/C++/MATLAB tri-protocol bridge architecture - Validation: 5-type real-time field, type, dimension, completeness, and compatibility checking - UI: Premium Deep Sea theme with visual pipeline preview - Modules: Sea Trial Exclusive + Exception Hub + Algorithm Management - Tests: Full coverage with 5 automated test cases
这个提交包含在:
+286
@@ -0,0 +1,286 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from src.models import WaterSoundData
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from typing import Callable, Dict, List, Any, Optional
|
||||
|
||||
class AlgorithmBridge(ABC):
|
||||
"""桥接插件层抽象基类"""
|
||||
def __init__(self, name: str, is_core: bool = False):
|
||||
self.name = name
|
||||
self.is_core = is_core # 核心算法标志,用于容错策略
|
||||
|
||||
@abstractmethod
|
||||
def call_algo(self, water_data: WaterSoundData) -> WaterSoundData:
|
||||
pass
|
||||
|
||||
class PythonBridge(AlgorithmBridge):
|
||||
"""Python 桥接插件"""
|
||||
def __init__(self, name: str, algo_func: Callable,
|
||||
input_mapping: Dict[str, str],
|
||||
output_mapping: Dict[str, str],
|
||||
preprocessing: Optional[List[Dict[str, Any]]] = None,
|
||||
is_core: bool = False,
|
||||
requirements: Optional[Dict[str, Any]] = None,
|
||||
output_specs: Optional[Dict[str, Any]] = None,
|
||||
algo_type: str = "python"):
|
||||
super().__init__(name, is_core)
|
||||
self.algo_func = algo_func # Python算法核心函数
|
||||
self.input_mapping = input_mapping # 输入映射 {algo_param: water_data_attr}
|
||||
self.output_mapping = output_mapping# 输出映射 {algo_return_index: water_data_attr}
|
||||
self.preprocessing = preprocessing or [] # 预处理算子 [{ "op": "norm", "params": {...} }]
|
||||
self.requirements = requirements or {}
|
||||
self.output_specs = output_specs or {}
|
||||
self.algo_type = algo_type
|
||||
|
||||
def _apply_preprocessing(self, data: Any, ops: List[Dict[str, Any]]) -> Any:
|
||||
"""应用轻量级预处理算子"""
|
||||
import numpy as np
|
||||
for op_config in ops:
|
||||
op = op_config.get("op")
|
||||
params = op_config.get("params", {})
|
||||
if op == "normalize" and isinstance(data, np.ndarray):
|
||||
# 简单归一化示例
|
||||
if data.size == 0:
|
||||
continue
|
||||
min_val = params.get("min", 0)
|
||||
max_val = params.get("max", 1)
|
||||
data_min = float(data.min())
|
||||
data_max = float(data.max())
|
||||
denom = data_max - data_min
|
||||
if denom == 0:
|
||||
data = np.full_like(data, float(min_val), dtype=float)
|
||||
else:
|
||||
data = (data.astype(float) - data_min) / denom * (float(max_val) - float(min_val)) + float(min_val)
|
||||
elif op == "scale" and isinstance(data, (int, float, np.ndarray)):
|
||||
data = data * params.get("factor", 1.0)
|
||||
return data
|
||||
|
||||
def call_algo(self, water_data: WaterSoundData) -> WaterSoundData:
|
||||
try:
|
||||
water_data.add_log(self.name, "Start Execution")
|
||||
|
||||
# 1. 提取并映射输入,同时应用预处理
|
||||
algo_inputs = {}
|
||||
for param, attr in self.input_mapping.items():
|
||||
if hasattr(water_data, attr):
|
||||
val = getattr(water_data, attr)
|
||||
# 查找是否有针对该字段的预处理
|
||||
relevant_ops = [op for op in self.preprocessing if op.get("field") == attr]
|
||||
if relevant_ops:
|
||||
val = self._apply_preprocessing(val, relevant_ops)
|
||||
algo_inputs[param] = val
|
||||
else:
|
||||
raise AttributeError(f"Field {attr} not found in WaterSoundData")
|
||||
|
||||
# 2. 调用算法
|
||||
results = self.algo_func(**algo_inputs)
|
||||
|
||||
# 3. 映射结果回 standardization 数据
|
||||
if not isinstance(results, (list, tuple)):
|
||||
results = [results]
|
||||
|
||||
for key, attr in self.output_mapping.items():
|
||||
# key can be an index (int) or a key if results is dict
|
||||
if isinstance(results, dict):
|
||||
val = results[key]
|
||||
else:
|
||||
idx = int(key) if isinstance(key, str) and key.isdigit() else key
|
||||
val = results[idx]
|
||||
setattr(water_data, attr, val)
|
||||
|
||||
water_data.add_log(self.name, "Finished Execution")
|
||||
|
||||
except Exception as e:
|
||||
water_data.set_error(f"Algorithm {self.name} failed: {str(e)}")
|
||||
|
||||
return water_data
|
||||
|
||||
import ctypes
|
||||
import numpy as np
|
||||
|
||||
class CppBridge(AlgorithmBridge):
|
||||
"""C++ 桥接插件 (ctypes 实现)"""
|
||||
def __init__(self, name: str, lib_path: str, function_name: str,
|
||||
input_mapping: Dict[str, str],
|
||||
output_mapping: Dict[str, str],
|
||||
preprocessing: Optional[List[Dict[str, Any]]] = None,
|
||||
requirements: Optional[Dict[str, Any]] = None,
|
||||
output_specs: Optional[Dict[str, Any]] = None,
|
||||
is_core: bool = False):
|
||||
super().__init__(name, is_core)
|
||||
self.lib_path = lib_path
|
||||
self.function_name = function_name
|
||||
self.input_mapping = input_mapping
|
||||
self.output_mapping = output_mapping
|
||||
self.preprocessing = preprocessing or []
|
||||
self.requirements = requirements or {}
|
||||
self.output_specs = output_specs or {}
|
||||
self.algo_type = "cpp"
|
||||
self._lib = None
|
||||
self._func = None
|
||||
|
||||
def _load_lib(self):
|
||||
"""延迟加载动态库"""
|
||||
if self._lib is None:
|
||||
if not os.path.exists(self.lib_path):
|
||||
raise FileNotFoundError(f"C++ library not found: {self.lib_path}")
|
||||
self._lib = ctypes.CDLL(self.lib_path)
|
||||
self._func = getattr(self._lib, self.function_name)
|
||||
# 注意:在 C 侧我们需要定义统一签名,例如:
|
||||
# void func(double* in, int in_len, double* out, int out_len)
|
||||
|
||||
def call_algo(self, water_data: WaterSoundData) -> WaterSoundData:
|
||||
try:
|
||||
water_data.add_log(self.name, "Start C++ Execution")
|
||||
self._load_lib()
|
||||
|
||||
# 1. 准备输入
|
||||
# 为了简化 Phase 3.3,我们假设目前只支持 1进1出 (double array)
|
||||
# 实际生产中会根据 input_mapping 构造更复杂的结构
|
||||
in_attr = list(self.input_mapping.values())[0]
|
||||
in_data = getattr(water_data, in_attr)
|
||||
|
||||
if not isinstance(in_data, np.ndarray):
|
||||
raise TypeError(f"C++ bridge expected numpy array for {in_attr}")
|
||||
|
||||
# 确保数据连续且为 float64 (double)
|
||||
in_data = np.ascontiguousarray(in_data, dtype=np.float64)
|
||||
in_ptr = in_data.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
|
||||
in_len = ctypes.c_int(in_data.size)
|
||||
|
||||
# 2. 准备输出空间
|
||||
# 同样简化为与输入等长
|
||||
out_data = np.zeros_like(in_data)
|
||||
out_ptr = out_data.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
|
||||
out_len = ctypes.c_int(out_data.size)
|
||||
|
||||
# 3. 调用 C++ 函数
|
||||
# 默认签名: void (double*, int, double*, int)
|
||||
self._func(in_ptr, in_len, out_ptr, out_len)
|
||||
|
||||
# 4. 映射结果回 standardization 数据
|
||||
out_attr = list(self.output_mapping.values())[0]
|
||||
setattr(water_data, out_attr, out_data)
|
||||
|
||||
water_data.add_log(self.name, "Finished C++ Execution")
|
||||
|
||||
except Exception as e:
|
||||
water_data.set_error(f"C++ Algorithm {self.name} failed: {str(e)}")
|
||||
|
||||
return water_data
|
||||
|
||||
|
||||
class MatlabBridge(AlgorithmBridge):
|
||||
"""MATLAB/Octave 桥接插件 (支持 Engine/Oct2py/File 降级)"""
|
||||
def __init__(self, name: str, m_path: str, function_name: str,
|
||||
input_mapping: Dict[str, str],
|
||||
output_mapping: Dict[str, str],
|
||||
preprocessing: Optional[List[Dict[str, Any]]] = None,
|
||||
requirements: Optional[Dict[str, Any]] = None,
|
||||
output_specs: Optional[Dict[str, Any]] = None,
|
||||
is_core: bool = False):
|
||||
super().__init__(name, is_core)
|
||||
self.m_path = m_path # .m 文件所在目录
|
||||
self.function_name = function_name
|
||||
self.input_mapping = input_mapping
|
||||
self.output_mapping = output_mapping
|
||||
self.preprocessing = preprocessing or []
|
||||
self.requirements = requirements or {}
|
||||
self.output_specs = output_specs or {}
|
||||
self.algo_type = "matlab"
|
||||
self._engine = None
|
||||
|
||||
def _call_via_engine(self, inputs: Dict[str, Any]) -> Any:
|
||||
"""方式 1: 使用官方 matlab.engine"""
|
||||
import matlab.engine
|
||||
if self._engine is None:
|
||||
self._engine = matlab.engine.start_matlab()
|
||||
self._engine.addpath(self.m_path)
|
||||
|
||||
func = getattr(self._engine, self.function_name)
|
||||
# 将 numpy 数组转为 matlab 类型 (这里简化处理)
|
||||
m_inputs = [matlab.double(v.tolist()) if isinstance(v, np.ndarray) else v for v in inputs.values()]
|
||||
return func(*m_inputs, nargout=len(self.output_mapping))
|
||||
|
||||
def _call_via_oct2py(self, inputs: Dict[str, Any]) -> Any:
|
||||
"""方式 2: 使用开源 oct2py (Octave)"""
|
||||
from oct2py import octave
|
||||
octave.addpath(self.m_path)
|
||||
func = getattr(octave, self.function_name)
|
||||
return func(*inputs.values())
|
||||
|
||||
def _call_via_file(self, inputs: Dict[str, Any]) -> Any:
|
||||
"""方式 3: 文件兜底 (使用 .mat 文件交换)"""
|
||||
from scipy.io import savemat, loadmat
|
||||
import subprocess
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="matlab_bridge_") as tmp_dir:
|
||||
in_file = os.path.join(tmp_dir, "tmp_in.mat")
|
||||
out_file = os.path.join(tmp_dir, "tmp_out.mat")
|
||||
savemat(in_file, inputs)
|
||||
|
||||
# 构造执行脚本:加入 m_path,加载输入,调用函数,保存输出
|
||||
m_path = os.path.abspath(self.m_path).replace("'", "''")
|
||||
in_file_escaped = in_file.replace("'", "''")
|
||||
out_file_escaped = out_file.replace("'", "''")
|
||||
arglist = ",".join(inputs.keys())
|
||||
script = (
|
||||
f"addpath('{m_path}'); "
|
||||
f"load('{in_file_escaped}'); "
|
||||
f"results = {self.function_name}({arglist}); "
|
||||
f"save('-mat', '{out_file_escaped}', 'results'); "
|
||||
"exit;"
|
||||
)
|
||||
|
||||
octave = shutil.which("octave") or shutil.which("octave-cli")
|
||||
matlab = shutil.which("matlab")
|
||||
if octave:
|
||||
cmd = [octave, "--quiet", "--eval", script]
|
||||
elif matlab:
|
||||
cmd = [matlab, "-batch", script]
|
||||
else:
|
||||
raise RuntimeError("No MATLAB/Octave CLI found for file-based execution.")
|
||||
|
||||
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||
|
||||
res = loadmat(out_file)
|
||||
return res.get("results")
|
||||
|
||||
def call_algo(self, water_data: WaterSoundData) -> WaterSoundData:
|
||||
try:
|
||||
water_data.add_log(self.name, "Start MATLAB Execution")
|
||||
|
||||
# 准备输入
|
||||
algo_inputs = {}
|
||||
for param, attr in self.input_mapping.items():
|
||||
algo_inputs[param] = getattr(water_data, attr)
|
||||
|
||||
# 自动寻优/降级逻辑
|
||||
from src.env_detect import collect_env_info
|
||||
env = collect_env_info()
|
||||
|
||||
if env["matlab"]["engine_available"]:
|
||||
results = self._call_via_engine(algo_inputs)
|
||||
elif env["matlab"]["oct2py_available"]:
|
||||
results = self._call_via_oct2py(algo_inputs)
|
||||
elif env["matlab"]["octave_cli"]:
|
||||
results = self._call_via_file(algo_inputs)
|
||||
else:
|
||||
raise RuntimeError("No MATLAB/Octave environment found for execution.")
|
||||
|
||||
# 结果回写 (支持单出/多出)
|
||||
if not isinstance(results, (list, tuple)):
|
||||
results = [results]
|
||||
|
||||
for key, attr in self.output_mapping.items():
|
||||
idx = int(key) if key.isdigit() else 0
|
||||
setattr(water_data, attr, results[idx])
|
||||
|
||||
water_data.add_log(self.name, "Finished MATLAB Execution")
|
||||
|
||||
except Exception as e:
|
||||
water_data.set_error(f"MATLAB Algorithm {self.name} failed: {str(e)}")
|
||||
|
||||
return water_data
|
||||
在新工单中引用
屏蔽一个用户