diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e683d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# Generated data directory by md2word.py +/data/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..93f789e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# 基础镜像 +FROM python:3.10 + +# 设置阿里云的Debian镜像源并安装系统依赖和中文字体 +RUN rm -f /etc/apt/sources.list /etc/apt/sources.list.d/* \ + && echo "deb https://mirrors.aliyun.com/debian/ bookworm main" > /etc/apt/sources.list \ + && echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main" >> /etc/apt/sources.list \ + && apt-get update && apt-get install -y --no-install-recommends \ + fonts-wqy-zenhei \ + fonts-wqy-microhei \ + ttf-wqy-zenhei \ + ttf-wqy-microhei \ + # 清理 apt 缓存 + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# 设置清华大学的pip镜像源 +RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple + +# 设置容器工作目录 +WORKDIR /app + +# 复制依赖文件 +COPY requirements.txt . + +# 安装依赖 +# taospy 可能会在 aarch64 架构的机器上(如M1/M2 Mac)安装失败,如果遇到问题可以尝试注释掉 +RUN pip3 install taospy && \ + pip3 install --no-cache-dir -r requirements.txt + +# 复制 td_analysis 源代码到工作目录 +COPY src/td_analysis /app/src/td_analysis + +# 暴露端口 +EXPOSE 5002 + +# 容器启动时运行Python脚本 +CMD ["python3", "-u", "src/td_analysis/td_analyzer.py"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9be0738 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +services: + tdengine: + image: tdengine/tdengine + container_name: tdengine + ports: + - "6041:6041" + environment: + - TAOS_FQDN=tdengine + restart: unless-stopped + + td_analysis_service: + build: . + container_name: td_analysis_service + ports: + - "5002:5002" + depends_on: + - tdengine + env_file: + - .env + restart: unless-stopped \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1acdbb2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +matplotlib==3.8.4 +requests==2.32.3 +taospy==2.7.21 +pandas>=1.0.0 +numpy>=1.18.0 +python-dotenv>=0.10.0 +fastapi>=0.95.0 +uvicorn>=0.21.0 +scipy>=1.10.0 +openpyxl \ No newline at end of file diff --git a/src/td_analysis/__init__.py b/src/td_analysis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/td_analysis/static/images/fft_a00001_1e7a722f.png b/src/td_analysis/static/images/fft_a00001_1e7a722f.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_1e7a722f.png differ diff --git a/src/td_analysis/static/images/fft_a00001_46c09b34.png b/src/td_analysis/static/images/fft_a00001_46c09b34.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_46c09b34.png differ diff --git a/src/td_analysis/static/images/fft_a00001_5015ba1d.png b/src/td_analysis/static/images/fft_a00001_5015ba1d.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_5015ba1d.png differ diff --git a/src/td_analysis/static/images/fft_a00001_56494130.png b/src/td_analysis/static/images/fft_a00001_56494130.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_56494130.png differ diff --git a/src/td_analysis/static/images/fft_a00001_60887291.png b/src/td_analysis/static/images/fft_a00001_60887291.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_60887291.png differ diff --git a/src/td_analysis/static/images/fft_a00001_65730bae.png b/src/td_analysis/static/images/fft_a00001_65730bae.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_65730bae.png differ diff --git a/src/td_analysis/static/images/fft_a00001_6a36564d.png b/src/td_analysis/static/images/fft_a00001_6a36564d.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_6a36564d.png differ diff --git a/src/td_analysis/static/images/fft_a00001_7a35581d.png b/src/td_analysis/static/images/fft_a00001_7a35581d.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_7a35581d.png differ diff --git a/src/td_analysis/static/images/fft_a00001_8b928932.png b/src/td_analysis/static/images/fft_a00001_8b928932.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_8b928932.png differ diff --git a/src/td_analysis/static/images/fft_a00001_c4685e6c.png b/src/td_analysis/static/images/fft_a00001_c4685e6c.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_c4685e6c.png differ diff --git a/src/td_analysis/static/images/fft_a00001_c46c3e2e.png b/src/td_analysis/static/images/fft_a00001_c46c3e2e.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_c46c3e2e.png differ diff --git a/src/td_analysis/static/images/fft_a00001_cca2b031.png b/src/td_analysis/static/images/fft_a00001_cca2b031.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_cca2b031.png differ diff --git a/src/td_analysis/static/images/fft_a00001_d2d0efc3.png b/src/td_analysis/static/images/fft_a00001_d2d0efc3.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_d2d0efc3.png differ diff --git a/src/td_analysis/static/images/fft_a00001_e9dca626.png b/src/td_analysis/static/images/fft_a00001_e9dca626.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_e9dca626.png differ diff --git a/src/td_analysis/static/images/fft_a00001_f3594127.png b/src/td_analysis/static/images/fft_a00001_f3594127.png new file mode 100644 index 0000000..cc0b4dc Binary files /dev/null and b/src/td_analysis/static/images/fft_a00001_f3594127.png differ diff --git a/src/td_analysis/static/images/ts_a00001_279d9369.png b/src/td_analysis/static/images/ts_a00001_279d9369.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_279d9369.png differ diff --git a/src/td_analysis/static/images/ts_a00001_284639e1.png b/src/td_analysis/static/images/ts_a00001_284639e1.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_284639e1.png differ diff --git a/src/td_analysis/static/images/ts_a00001_35a7ed87.png b/src/td_analysis/static/images/ts_a00001_35a7ed87.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_35a7ed87.png differ diff --git a/src/td_analysis/static/images/ts_a00001_545f7003.png b/src/td_analysis/static/images/ts_a00001_545f7003.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_545f7003.png differ diff --git a/src/td_analysis/static/images/ts_a00001_56ec99ca.png b/src/td_analysis/static/images/ts_a00001_56ec99ca.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_56ec99ca.png differ diff --git a/src/td_analysis/static/images/ts_a00001_58aab216.png b/src/td_analysis/static/images/ts_a00001_58aab216.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_58aab216.png differ diff --git a/src/td_analysis/static/images/ts_a00001_5c942c18.png b/src/td_analysis/static/images/ts_a00001_5c942c18.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_5c942c18.png differ diff --git a/src/td_analysis/static/images/ts_a00001_683eb938.png b/src/td_analysis/static/images/ts_a00001_683eb938.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_683eb938.png differ diff --git a/src/td_analysis/static/images/ts_a00001_88caaa26.png b/src/td_analysis/static/images/ts_a00001_88caaa26.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_88caaa26.png differ diff --git a/src/td_analysis/static/images/ts_a00001_946e00f0.png b/src/td_analysis/static/images/ts_a00001_946e00f0.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_946e00f0.png differ diff --git a/src/td_analysis/static/images/ts_a00001_b70d97db.png b/src/td_analysis/static/images/ts_a00001_b70d97db.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_b70d97db.png differ diff --git a/src/td_analysis/static/images/ts_a00001_b8391856.png b/src/td_analysis/static/images/ts_a00001_b8391856.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_b8391856.png differ diff --git a/src/td_analysis/static/images/ts_a00001_bc53851c.png b/src/td_analysis/static/images/ts_a00001_bc53851c.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_bc53851c.png differ diff --git a/src/td_analysis/static/images/ts_a00001_df2db032.png b/src/td_analysis/static/images/ts_a00001_df2db032.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_df2db032.png differ diff --git a/src/td_analysis/static/images/ts_a00001_ee21aee0.png b/src/td_analysis/static/images/ts_a00001_ee21aee0.png new file mode 100644 index 0000000..d3b1579 Binary files /dev/null and b/src/td_analysis/static/images/ts_a00001_ee21aee0.png differ diff --git a/src/td_analysis/td_analyzer.py b/src/td_analysis/td_analyzer.py new file mode 100644 index 0000000..4f18ccf --- /dev/null +++ b/src/td_analysis/td_analyzer.py @@ -0,0 +1,1125 @@ +import os +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +import uuid +import logging +import json +import traceback +from datetime import datetime, timedelta +from scipy import stats +from scipy.stats import entropy +from scipy.signal import welch, find_peaks +from fastapi import FastAPI, HTTPException, Body +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import JSONResponse +from pydantic import BaseModel +from typing import List, Dict, Any, Optional +from td_connector import connect_to_tdengine, execute_query +from td_config import APP_CONFIG, API_CONFIG, get_image_url, validate_config + +import matplotlib +matplotlib.use('Agg') +plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签 +plt.rcParams['axes.unicode_minus'] = False + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 验证配置 +validate_config() + +# 使用配置的图片目录 +IMAGES_DIR = APP_CONFIG['images_dir'] +os.makedirs(IMAGES_DIR, exist_ok=True) + +app = FastAPI( + title=APP_CONFIG['title'], + description=APP_CONFIG['description'], + version=APP_CONFIG['version'] +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# app.mount("/static", StaticFiles(directory="static"), name="static") +app.mount("/static", StaticFiles(directory="src/td_analysis/static"), name="static") + + +class PrefixRequest(BaseModel): + prefix: str + +class TimestampAnalysisRequest(BaseModel): + table_name: str + timestamp: str + +class SidebandRequest(BaseModel): + table_name: str + +# 添加MutationAnalysis模型类 +class MutationAnalysis(BaseModel): + first_sample_concentration: float + first_sample_time: datetime + second_sample_concentration: float + second_sample_time: datetime + +class AnalysisResponse(BaseModel): + metrics: Dict[str, float] + scores: Dict[str, float] + total_score: float + status: str + time_series_plot_url: str + fft_plot_url: str + statistical_metrics: Dict[str, Any] + +class SidebandEnergyResponse(BaseModel): + sideband_energy_plot_url: str + sideband_energy_data: List[Dict[str, Any]] + warning_threshold: float + alert_threshold: float + warning_count: int + alert_count: int + +def get_tables_by_prefix(prefix): + """ + 获取以特定前缀开头的所有表名 + + """ + try: + conn = connect_to_tdengine() + tables_df = execute_query(conn, "SHOW TABLES") + # 不需要关闭 REST API 连接 + + if not tables_df.empty: + # 打印列名以便调试 + logger.info(f"SHOW TABLES 返回的列名: {tables_df.columns.tolist()}") + + # 尝试找到表名列 - 可能是 'name', 'table_name' 或其他 + table_name_col = None + for possible_col in ['table_name', 'name', 'tablename', 'tbname']: + if possible_col in tables_df.columns: + table_name_col = possible_col + logger.info(f"找到表名列: {table_name_col}") + break + + if table_name_col: + matching_tables = [table for table in tables_df[table_name_col].tolist() + if table.startswith(prefix)] + return matching_tables + else: + logger.error(f"在返回的数据中未找到表名列。列名: {tables_df.columns.tolist()}") + # 如果找不到表名列,尝试使用第一列作为表名 + if len(tables_df.columns) > 0: + first_col = tables_df.columns[0] + logger.info(f"使用第一列 '{first_col}' 作为表名列") + matching_tables = [table for table in tables_df[first_col].tolist() + if table.startswith(prefix)] + return matching_tables + return [] + return [] + except Exception as e: + logger.error(f"获取表列表时出错: {str(e)}") + return [] + +def get_timestamps_from_table(table_name): + """ + 获取指定表中的时间戳列表,只取整秒的时间戳 + + """ + try: + conn = connect_to_tdengine() + + query = f"SELECT ts FROM {table_name} WHERE (CAST(ts AS BIGINT) % 1000000000) = 0" + logger.info(f"执行查询: {query}") + df = execute_query(conn, query) + print(df) + timestamps = [] + + # 打印列名以便调试 + logger.info(f"时间戳查询返回的列名: {df.columns.tolist()}") + + if not df.empty: + # 确定时间戳列名 + ts_col = None + if 'ts' in df.columns: + ts_col = 'ts' + elif 0 in df.columns: + ts_col = 0 + elif '0' in df.columns: + ts_col = '0' + elif len(df.columns) > 0: # 如果没有找到预期的列名,使用第一列 + ts_col = df.columns[0] + + logger.info(f"使用列 '{ts_col}' 作为时间戳列") + + if ts_col is not None: + for ts in df[ts_col].tolist(): + if hasattr(ts, 'isoformat'): + formatted_ts = ts.strftime('%Y-%m-%d %H:%M:%S') + timestamps.append(formatted_ts) + elif isinstance(ts, (int, float)) or (isinstance(ts, str) and ts.isdigit()): + ts_value = int(ts) + ts_seconds = ts_value / 1_000_000_000 + dt = datetime.fromtimestamp(ts_seconds) + formatted_ts = dt.strftime('%Y-%m-%d %H:%M:%S') + timestamps.append(formatted_ts) + else: + try: + timestamps.append(str(ts)) + except: + timestamps.append(str(ts)) + + logger.info(f"从表 {table_name} 中提取了 {len(timestamps)} 个时间戳") + + # 不需要关闭 REST API 连接 + return timestamps + except Exception as e: + logger.error(f"获取表 {table_name} 的时间戳列表时出错: {str(e)}") + return [] + +def get_data_by_timestamp(table_name, timestamp): + """ + 获取指定表在特定时间戳的数据 + + Args: + table_name: 表名 + timestamp: 时间戳 (ISO 8601格式) + + Returns: + DataFrame: 包含vals数据的DataFrame + """ + try: + conn = connect_to_tdengine() + + # 将时间戳转换为datetime对象 + try: + dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) + # 转换为ISO 8601格式,包含时区信息 + start_ts = dt.strftime('%Y-%m-%dT%H:%M:%S+08:00') + + # 计算下一秒的时间戳 + end_dt = dt + timedelta(seconds=1) + end_ts = end_dt.strftime('%Y-%m-%dT%H:%M:%S+00:00') + except ValueError: + logger.warning(f"无法解析时间戳 {timestamp},尝试直接使用") + start_ts = timestamp + try: + # 尝试解析时间戳并添加一秒 + dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) + end_dt = dt + timedelta(seconds=1) + end_ts = end_dt.strftime('%Y-%m-%dT%H:%M:%S+00:00') + except: + logger.error(f"无法计算下一秒时间戳,使用近似查询") + end_ts = None + + # 构建查询 + if end_ts: + # 使用精确时间范围查询 + query = f"SELECT vals FROM {table_name} WHERE ts >= '{start_ts}' AND ts < '{end_ts}'" + else: + # 如果无法计算结束时间,使用简单查询 + query = f"SELECT vals FROM {table_name} WHERE ts >= '{start_ts}'" + + logger.info(f"执行查询: {query}") + df = execute_query(conn, query) + + # 如果没有找到数据,尝试更宽松的匹配 + if df.empty: + logger.info(f"没有找到基于时间戳 {start_ts} 的数据,尝试查询第一条数据") + query = f"SELECT vals FROM {table_name} LIMIT 1" + logger.info(f"执行查询: {query}") + df = execute_query(conn, query) + + # 检查DataFrame中是否有vals列或数字列 + if 'vals' not in df.columns: + # 检查是否有数字列(可能是0) + numeric_cols = [col for col in df.columns if isinstance(col, (int, float)) or str(col).isdigit()] + if numeric_cols: + # 重命名第一个数字列为vals + df = df.rename(columns={numeric_cols[0]: 'vals'}) + logger.info(f"将列 {numeric_cols[0]} 重命名为vals") + else: + logger.warning(f"DataFrame中没有vals列或数字列") + logger.warning(f"可用的列: {df.columns.tolist()}") + return pd.DataFrame() + + return df + + except Exception as e: + logger.error(f"获取数据时出错: {str(e)}") + raise + +def extract_vals_data(df): + """ + 从DataFrame中提取vals列的数据 + """ + try: + if df.empty: + logger.warning("DataFrame为空,无法提取vals数据") + return np.array([]) + + # 检查是否有vals列或数字列 + if 'vals' not in df.columns: + numeric_cols = [col for col in df.columns if isinstance(col, (int, float)) or str(col).isdigit()] + if numeric_cols: + # 使用第一个数字列 + vals_data = df[numeric_cols[0]].values + else: + logger.warning("DataFrame中没有vals列或数字列") + return np.array([]) + else: + vals_data = df['vals'].values + + # 确保数据是数值类型 + vals_data = np.array(vals_data, dtype=float) + + logger.info(f"成功从DataFrame中提取了 {len(vals_data)} 个数据点") + return vals_data + except Exception as e: + logger.error(f"提取vals数据时出错: {str(e)}") + return np.array([]) + +def save_time_series_plot(data_values, table_name): + """创建时序数据图并保存为文件,返回访问URL""" + plt.figure(figsize=(12, 6)) + + # 创建时间轴(假设等时间间隔) + x = np.arange(len(data_values)) + + plt.plot(x, data_values) + plt.title(f"Time Series Data - Table: {table_name}") + plt.xlabel("Sample Index") + plt.ylabel("Amplitude") + plt.grid(True) + + # 生成唯一文件名并保存 + file_name = f"ts_{table_name}_{uuid.uuid4().hex[:8]}.png" + file_path = os.path.join(IMAGES_DIR, file_name) + plt.savefig(file_path, format='png') + plt.close() + + # 返回文件URL (使用配置的基础URL) + return get_image_url(file_name) + +def save_fft_plot(data_values, table_name): + """创建FFT频谱图并保存为文件,返回访问URL""" + plt.figure(figsize=(12, 6)) + + # 执行FFT + fft_values = np.fft.fft(data_values) + n = len(data_values) + frequency = np.fft.fftfreq(n)[:n//2] + amplitude = np.abs(fft_values)[:n//2] + + plt.plot(frequency, amplitude) + plt.title(f"FFT Spectrum - Table: {table_name}") + plt.xlabel("Frequency") + plt.ylabel("Amplitude") + plt.grid(True) + + file_name = f"fft_{table_name}_{uuid.uuid4().hex[:8]}.png" + file_path = os.path.join(IMAGES_DIR, file_name) + plt.savefig(file_path, format='png') + plt.close() + + return get_image_url(file_name) + +def calculate_statistical_metrics(data_values): + """计算时序数据的统计指标""" + data_array = np.array(data_values) + + metrics = { + "mean": float(np.mean(data_array)), + "median": float(np.median(data_array)), + "std_dev": float(np.std(data_array)), + "min": float(np.min(data_array)), + "max": float(np.max(data_array)), + "samples": len(data_array), + } + + + fs = 1.0 + f, Pxx = welch(data_array, fs, nperseg=min(256, len(data_array))) + + dominant_indices = np.argsort(Pxx)[-5:][::-1] + dominant_freqs = [{ + "frequency": float(f[idx]), + "magnitude": float(Pxx[idx]) + } for idx in dominant_indices] + + metrics["dominant_frequencies"] = dominant_freqs + + return metrics + +def calculate_vibration_power_entropy(data_values): + """ + 计算振动功率熵 (0-2.8) + """ + try: + # 计算FFT + fft_values = np.fft.fft(data_values) + # 取绝对值得到幅度谱 + amplitude_spectrum = np.abs(fft_values) + + # 计算功率谱 + power_spectrum = amplitude_spectrum ** 2 + + # 归一化功率谱(使总和为1) + total_power = np.sum(power_spectrum) + if total_power > 0: + normalized_power = power_spectrum / total_power + else: + return 0.0 + + # 计算Shannon熵 + vibration_entropy = -np.sum(normalized_power * np.log2(normalized_power + 1e-10)) + + return min(max(vibration_entropy, 0.0), 2.8) # 限制在0-2.8范围内 + except Exception as e: + logger.error(f"计算振动功率熵时出错: {str(e)}") + return 0.0 + +def calculate_vibration_determinacy(data_values): + """ + 计算振动确定度 (0.9-0) + """ + try: + n = len(data_values) + if n < 2: + return 0.9 + + # 计算自相关函数 + autocorr = np.correlate(data_values, data_values, mode='full') + if autocorr[n-1] == 0: + return 0.0 + + autocorr = autocorr[n-1:] / autocorr[n-1] # 标准化 + + # 计算自相关函数的衰减率 + decay_rate = 0.0 + for i in range(1, min(20, len(autocorr))): + decay_rate += abs(autocorr[i]) + + decay_rate = decay_rate / min(19, len(autocorr) - 1) + + # 转换为确定度(衰减率越大,确定度越低) + determinacy = 0.9 * (1 - decay_rate) + + return min(max(determinacy, 0.0), 0.9) # 限制在0-0.9范围内 + except Exception as e: + logger.error(f"计算振动确定度时出错: {str(e)}") + return 0.0 + +def calculate_vibration_fluctuation_rate(data_values): + """ + 计算振动波动率 (0%-20%) + """ + try: + if len(data_values) < 2: + return 0.0 + + # 计算相邻样本之间的差异 + diffs = np.abs(np.diff(data_values)) + + # 计算均值 + mean_value = np.mean(np.abs(data_values)) + + # 计算波动率 + if mean_value > 0: + fluctuation_rate = 100 * np.mean(diffs) / mean_value + else: + fluctuation_rate = 0.0 + + return min(max(fluctuation_rate, 0.0), 20.0) # 限制在0-20%范围内 + except Exception as e: + logger.error(f"计算振动波动率时出错: {str(e)}") + return 0.0 + +def calculate_high_frequency_vibration_ratio(data_values): + """ + 计算高频振动占比 (0%-35%) + """ + try: + # 计算FFT + fft_values = np.fft.fft(data_values) + amplitude = np.abs(fft_values) + + # 计算总能量 + total_energy = np.sum(amplitude ** 2) + + if total_energy == 0: + return 0.0 + + # 定义高频部分(频率大于采样频率的1/3) + high_freq_idx = len(amplitude) // 3 + high_freq_energy = np.sum(amplitude[high_freq_idx:] ** 2) + + # 计算高频占比 + high_freq_ratio = 100 * high_freq_energy / total_energy + + return min(max(high_freq_ratio, 0.0), 35.0) # 限制在0-35%范围内 + except Exception as e: + logger.error(f"计算高频振动占比时出错: {str(e)}") + return 0.0 + +def map_to_score(value, min_val, max_val, distribution_type, reverse=False): + """ + 根据指定的分布类型将指标值映射为分数 + + """ + # 标准化到0-1范围 + normalized_value = (value - min_val) / (max_val - min_val) + normalized_value = max(0, min(normalized_value, 1)) + + if reverse: + normalized_value = 1 - normalized_value + + # 根据分布类型映射到分数 + if distribution_type == 'normal': + # 正态分布 (均值0.5, 标准差0.2) + score = 100 * stats.norm.cdf(normalized_value, loc=0.5, scale=0.2) + + elif distribution_type == 'right_skewed': + # 右偏正态分布 (使用Beta分布) + score = 100 * stats.beta.cdf(normalized_value, a=2, b=5) + + elif distribution_type == 'left_skewed': + # 左偏正态分布 (使用Beta分布) + score = 100 * stats.beta.cdf(normalized_value, a=5, b=2) + + elif distribution_type == 'poisson': + # 泊松分布近似 (使用Gamma分布) + score = 100 * stats.gamma.cdf(normalized_value, a=2) + + else: + # 默认线性映射 + score = 100 * normalized_value + + return score + +def calculate_vibration_metrics(data_values): + """计算所有振动指标""" + metrics = { + "vibration_power_entropy": calculate_vibration_power_entropy(data_values), + "vibration_determinacy": calculate_vibration_determinacy(data_values), + "vibration_fluctuation_rate": calculate_vibration_fluctuation_rate(data_values), + "high_frequency_vibration_ratio": calculate_high_frequency_vibration_ratio(data_values) + } + + # 计算各指标的分数 + scores = { + "vibration_power_entropy": map_to_score( + metrics["vibration_power_entropy"], 0, 2.8, 'normal', reverse=False + ), + "vibration_determinacy": map_to_score( + metrics["vibration_determinacy"], 0, 0.9, 'right_skewed', reverse=True + ), + "vibration_fluctuation_rate": map_to_score( + metrics["vibration_fluctuation_rate"], 0, 20, 'left_skewed', reverse=False + ), + "high_frequency_vibration_ratio": map_to_score( + metrics["high_frequency_vibration_ratio"], 0, 35, 'poisson', reverse=False + ) + } + + # 计算总分(四个指标的平均值) + total_score = sum(scores.values()) / len(scores) + + # 确定状态 + if total_score >= 81: + status = "异常状态" + elif total_score >= 69: + status = "注意状态" + else: + status = "正常状态" + + return metrics, scores, total_score, status + +def analyze_table_data_by_timestamp(table_name, timestamp): + """ + 分析特定表在指定时间戳的数据 + + """ + try: + logger.info(f"开始分析表 '{table_name}' 在时间戳 {timestamp} 的数据") + + # 获取指定时间戳的数据 + data_df = get_data_by_timestamp(table_name, timestamp) + + if data_df.empty: + logger.warning(f"表 '{table_name}' 在时间戳 {timestamp} 没有数据") + return {"error": f"表 '{table_name}' 在时间戳 {timestamp} 没有数据"} + + # 提取vals数据 + data_values = extract_vals_data(data_df) + + if len(data_values) == 0: + logger.warning(f"表 '{table_name}' 在时间戳 {timestamp} 没有可用的vals数据") + return {"error": f"表 '{table_name}' 在时间戳 {timestamp} 没有可用的vals数据"} + + # 生成图表 + time_series_plot_url = save_time_series_plot(data_values, table_name) + fft_plot_url = save_fft_plot(data_values, table_name) + + # 计算统计指标 + statistical_metrics = calculate_statistical_metrics(data_values) + + # 计算振动指标 + metrics, scores, total_score, status = calculate_vibration_metrics(data_values) + + # 记录分析结果 + actual_timestamp = data_df.iloc[0]['ts'].isoformat() if 'ts' in data_df.columns else timestamp + + return { + "table_name": table_name, + "timestamp": actual_timestamp, + "metrics": metrics, + "scores": scores, + "total_score": total_score, + "status": status, + "time_series_plot_url": time_series_plot_url, + "fft_plot_url": fft_plot_url, + "statistical_metrics": statistical_metrics + } + except Exception as e: + logger.error(f"分析表 '{table_name}' 在时间戳 {timestamp} 的数据时出错: {str(e)}") + return {"error": str(e)} + +# API路由 +@app.get("/") +def read_root(): + """API根路径""" + return { + "message": "变压器振动分析API", + "version": "1.0.0", + "endpoints": { + "/tables/prefix": "POST - 获取指定前缀的表列表", + "/analyze/timestamp": "POST - 分析指定表在特定时间戳的数据", + "/analyze/sideband-energy": "POST - 分析指定表中所有时间段的边频能量比", + "/mutationAnalysis": "POST - 基于溶解气体分析突变分析" + } + } + +@app.post("/tables/prefix") +def get_tables_by_prefix_api(request: PrefixRequest): + """ + 获取指定前缀的表列表和特定表的时间戳 + + """ + try: + tables = get_tables_by_prefix(request.prefix) + + if not tables: + return { + "status": "success", + "message": f"没有找到以 '{request.prefix}' 开头的表", + "tables": [], + "timestamps": [] + } + + # 尝试找到a00001表或使用第一个表 + timestamp_table = None + if "a00001" in tables: + timestamp_table = "a00001" + else: + timestamp_table = tables[0] + + # 获取选定表的时间戳 + timestamps = get_timestamps_from_table(timestamp_table) + + return { + "status": "success", + "message": f"找到 {len(tables)} 个以 '{request.prefix}' 开头的表", + "tables": tables, + "timestamps": timestamps + } + except Exception as e: + logger.error(f"获取表前缀时出错: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/analyze/timestamp", response_model=AnalysisResponse) +def analyze_by_timestamp_api(request: TimestampAnalysisRequest): + """ + 分析指定表在特定时间戳的数据 + + """ + try: + result = analyze_table_data_by_timestamp(request.table_name, request.timestamp) + + if "error" in result: + raise HTTPException(status_code=404, detail=result["error"]) + + return result + except HTTPException: + raise + except Exception as e: + logger.error(f"分析时间戳数据时出错: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +def get_all_timeseries_data(table_name): + """ + 获取指定表的所有时序数据,并按秒分组 + """ + try: + conn = connect_to_tdengine() + + # 首先直接获取所有数据,然后在Python中进行分组处理 + # 这样可以避免复杂的TDengine时间戳语法问题 + data_query = f"SELECT ts, vals FROM {table_name} ORDER BY ts" + logger.info(f"执行查询: {data_query}") + + all_data_df = execute_query(conn, data_query) + if all_data_df.empty: + logger.warning(f"表 {table_name} 中没有找到数据") + return [] + + # 确定列名 + ts_col = 'ts' if 'ts' in all_data_df.columns else all_data_df.columns[0] + vals_col = 'vals' if 'vals' in all_data_df.columns else all_data_df.columns[1] + + logger.info(f"获取到 {len(all_data_df)} 条数据记录,列名: {all_data_df.columns.tolist()}") + logger.info(f"数据示例: {all_data_df.iloc[0].to_dict()}") + + # 确保pandas日期时间格式正确 + try: + # 尝试将时间戳列转换为pandas datetime + all_data_df[ts_col] = pd.to_datetime(all_data_df[ts_col]) + logger.info(f"成功将时间戳转换为标准日期时间格式") + except Exception as e: + logger.warning(f"时间戳转换失败,将继续使用原始格式: {str(e)}") + + # 创建一个新列,只包含秒级时间戳 + try: + all_data_df['second_ts'] = all_data_df[ts_col].dt.strftime('%Y-%m-%d %H:%M:%S') + except Exception as e: + logger.warning(f"创建秒级时间戳失败: {str(e)}") + # 备选方案:手动处理时间戳 + all_data_df['second_ts'] = all_data_df[ts_col].apply(lambda x: + x.strftime('%Y-%m-%d %H:%M:%S') if hasattr(x, 'strftime') else str(x).split('.')[0]) + + # 按秒级时间戳分组 + time_segments = [] + grouped = all_data_df.groupby('second_ts') + + for second_ts, group in grouped: + # 检查数据点数量 + if len(group) < 100: # 设置一个最小点数阈值 + logger.warning(f"时间段 {second_ts} 的数据点数量过少 ({len(group)}), 跳过") + continue + + # 将数据添加到结果列表 + time_segments.append({ + 'timestamp': second_ts, + 'values': group[vals_col].values, + 'raw_data': group + }) + logger.info(f"时间段 {second_ts} 包含 {len(group)} 个数据点") + + logger.info(f"在表 {table_name} 中找到 {len(time_segments)} 个有效时间段") + return time_segments + + except Exception as e: + logger.error(f"获取表 {table_name} 的时序数据时出错: {str(e)}") + raise + +def calculate_sideband_energy_ratio(signal, fs=8192, base_freq=50, fb=15, freq_range=(0, 1000), bandwidth=2): + """ + 计算振动信号的边频能量比 + + 参数: + signal: 振动信号数据 + fs: 采样频率 + base_freq: 基频(Hz) + fb: 边频间隔(Hz) + freq_range: 频率范围,格式为(min_freq, max_freq) + bandwidth: 频带宽度(Hz) + + 返回: + 边频能量比(%) + """ + try: + # 确保数据是numpy数组 + signal = np.array(signal, dtype=float) + + # 去除直流分量 + signal = signal - np.mean(signal) + + # 计算频谱 + N = len(signal) + freq = np.fft.fftfreq(N, 1/fs) + freq_positive = freq[:N//2] + fft_signal = np.fft.fft(signal) + magnitude = 2 * np.abs(fft_signal) / N + magnitude_positive = magnitude[:N//2] + + # 限制频率范围 + mask = (freq_positive >= freq_range[0]) & (freq_positive <= freq_range[1]) + freq_limited = freq_positive[mask] + magnitude_limited = magnitude_positive[mask] + + # 设置振幅阈值(用于峰值检测) + min_amplitude = 0.001 * np.max(magnitude_limited) + + # 找出所有主要峰值 + peaks, _ = find_peaks(magnitude_limited, height=min_amplitude) + peak_freqs = freq_limited[peaks] + peak_amplitudes = magnitude_limited[peaks] + + # 识别基频及其谐波 + center_freqs = [] + tolerance = 2 # 频率匹配容差(Hz) + + for multiple in range(1, int(freq_range[1]/base_freq) + 1): + theoretical_freq = multiple * base_freq + + # 跳过超出范围的频率 + if theoretical_freq < freq_range[0] or theoretical_freq > freq_range[1]: + continue + + # 在检测到的峰值中寻找最接近理论频率的峰值 + for i, fc in enumerate(peak_freqs): + if abs(fc - theoretical_freq) <= tolerance: + center_freqs.append({ + 'freq': fc, + 'amplitude': peak_amplitudes[i], + 'multiple': multiple + }) + break + + # 收集所有边频 + sidebands = [] + for cf_info in center_freqs: + cf = cf_info['freq'] + + # 检查左右边频是否存在 + left_sb_freq = cf - fb + right_sb_freq = cf + fb + + # 跳过超出频率范围的边频 + if left_sb_freq < freq_range[0] or right_sb_freq > freq_range[1]: + continue + + # 查找左边频 + left_sb_found = False + for i, p_freq in enumerate(peak_freqs): + if abs(p_freq - left_sb_freq) <= bandwidth/2: + left_sb_found = True + left_sb_amp = peak_amplitudes[i] + left_sb_freq = p_freq # 使用实际峰值频率 + break + + # 查找右边频 + right_sb_found = False + for i, p_freq in enumerate(peak_freqs): + if abs(p_freq - right_sb_freq) <= bandwidth/2: + right_sb_found = True + right_sb_amp = peak_amplitudes[i] + right_sb_freq = p_freq # 使用实际峰值频率 + break + + # 只有当左右边频都存在时才添加到结果 + if left_sb_found and right_sb_found: + sidebands.append({ + 'center_freq': cf, + 'left_freq': left_sb_freq, + 'right_freq': right_sb_freq, + 'left_amp': left_sb_amp, + 'right_amp': right_sb_amp + }) + + # 如果没有发现边频对,返回0 + if len(sidebands) == 0: + return 0.0 + + # 计算边频能量 + sideband_energy = 0 + for sb in sidebands: + # 查找左边频能量 + left_idx = np.abs(freq_limited - sb['left_freq']).argmin() + left_start = max(0, left_idx - int(bandwidth/2 * N/fs)) + left_end = min(len(freq_limited), left_idx + int(bandwidth/2 * N/fs)) + sideband_energy += np.sum(magnitude_limited[left_start:left_end]) + + # 查找右边频能量 + right_idx = np.abs(freq_limited - sb['right_freq']).argmin() + right_start = max(0, right_idx - int(bandwidth/2 * N/fs)) + right_end = min(len(freq_limited), right_idx + int(bandwidth/2 * N/fs)) + sideband_energy += np.sum(magnitude_limited[right_start:right_end]) + + # 计算总能量(不包括基频及其谐波) + total_energy = np.sum(magnitude_limited) + + # 计算基频及其谐波的能量 + base_freq_energy = 0 + for cf_info in center_freqs: + cf = cf_info['freq'] + cf_idx = np.abs(freq_limited - cf).argmin() + cf_start = max(0, cf_idx - int(bandwidth/2 * N/fs)) + cf_end = min(len(freq_limited), cf_idx + int(bandwidth/2 * N/fs)) + base_freq_energy += np.sum(magnitude_limited[cf_start:cf_end]) + + # 调整总能量(去除基频能量) + adjusted_total_energy = total_energy - base_freq_energy + + # 计算边频能量比 + sideband_energy_ratio = 0.0 + if adjusted_total_energy > 0: + sideband_energy_ratio = 100.0 * sideband_energy / adjusted_total_energy + + return sideband_energy_ratio + + except Exception as e: + logger.error(f"计算边频能量比时出错: {str(e)}") + return 0.0 + +def save_sideband_energy_plot(timestamps, energy_ratios, table_name): + """ + 创建边频能量比随时间变化的图表,并标记超过阈值的点 + """ + # 设置警告和告警阈值 + warning_threshold = 1.0 # 警告阈值 (%) + alert_threshold = 2.5 # 告警阈值 (%) + + plt.figure(figsize=(15, 6)) + + # 计算超过阈值的点 + warning_points = [i for i, ratio in enumerate(energy_ratios) if ratio >= warning_threshold and ratio < alert_threshold] + alert_points = [i for i, ratio in enumerate(energy_ratios) if ratio >= alert_threshold] + + # 绘制能量比曲线 + plt.plot(timestamps, energy_ratios, 'k-', linewidth=1.5) + + # 添加阈值线 + plt.axhline(y=warning_threshold, color='r', linestyle='--', label=f'警告阈值 ({warning_threshold}%)') + plt.axhline(y=alert_threshold, color='b', linestyle='--', label=f'告警阈值 ({alert_threshold}%)') + + # 在图右侧显示阈值标签 + plt.text(len(timestamps)-1, warning_threshold, f"{warning_threshold}%", + color='red', ha='right', va='bottom') + plt.text(len(timestamps)-1, alert_threshold, f"{alert_threshold}%", + color='blue', ha='right', va='bottom') + + # 标记超过阈值的点 + if warning_points: + plt.plot([timestamps[i] for i in warning_points], + [energy_ratios[i] for i in warning_points], + 'ro', markersize=6, label=f'警告点 ({len(warning_points)}个)') + + if alert_points: + plt.plot([timestamps[i] for i in alert_points], + [energy_ratios[i] for i in alert_points], + 'bo', markersize=6, label=f'告警点 ({len(alert_points)}个)') + + # 设置图表属性 + plt.title(f"边频能量比随时间变化 - {table_name}", fontsize=14) + plt.xlabel("时间", fontsize=12) + plt.ylabel("边频能量比 (%)", fontsize=12) + plt.grid(True, linestyle='--', alpha=0.7) + plt.xticks(rotation=45, ha='right') + + # 设置y轴范围 + max_ratio = max(max(energy_ratios), alert_threshold) * 1.1 + plt.ylim(0, max_ratio) + + # 添加图例 + plt.legend(loc='upper right') + + plt.tight_layout() + + # 生成唯一文件名并保存 + file_name = f"sideband_energy_{table_name}_{uuid.uuid4().hex[:8]}.png" + file_path = os.path.join(IMAGES_DIR, file_name) + plt.savefig(file_path, format='png', dpi=150) + plt.close() + + # 返回文件URL和统计信息 + return { + 'plot_url': get_image_url(file_name), + 'warning_count': len(warning_points), + 'alert_count': len(alert_points), + 'warning_threshold': warning_threshold, + 'alert_threshold': alert_threshold + } + +def analyze_sideband_energy_ratio(table_name): + """ + 分析表中所有时间段的边频能量比 + + 参数: + table_name: 表名 + + 返回: + 包含图表URL和边频能量比数据的字典 + """ + try: + logger.info(f"开始分析表 {table_name} 的边频能量比") + + # 获取表中所有时间段的数据 + time_segments = get_all_timeseries_data(table_name) + + if not time_segments: + logger.warning(f"表 {table_name} 中没有找到有效的时间段数据") + return { + "error": f"表 {table_name} 中没有找到有效的时间段数据" + } + + # 计算每个时间段的边频能量比 + timestamps = [] + energy_ratios = [] + detailed_data = [] + + for segment in time_segments: + # 计算边频能量比 + ser = calculate_sideband_energy_ratio( + segment['values'], + fs=8192, + base_freq=50, + fb=7.5, + freq_range=(0, 1000), + bandwidth=2 + ) + + # 记录结果 + timestamps.append(segment['timestamp']) + energy_ratios.append(ser) + + detailed_data.append({ + 'timestamp': segment['timestamp'], + 'sideband_energy_ratio': ser, + 'data_points': len(segment['values']) + }) + + logger.info(f"时间段 {segment['timestamp']} 的边频能量比: {ser:.4f}%") + + # 如果没有有效数据,返回错误 + if not timestamps: + logger.warning(f"表 {table_name} 中没有生成有效的边频能量比数据") + return { + "error": f"表 {table_name} 中没有生成有效的边频能量比数据" + } + + # 保存图表 + plot_info = save_sideband_energy_plot(timestamps, energy_ratios, table_name) + + # 准备返回结果 + result = { + "table_name": table_name, + "sideband_energy_plot_url": plot_info['plot_url'], + "sideband_energy_data": detailed_data, + "warning_threshold": plot_info['warning_threshold'], + "alert_threshold": plot_info['alert_threshold'], + "warning_count": plot_info['warning_count'], + "alert_count": plot_info['alert_count'], + "data_segments_count": len(time_segments) + } + + logger.info(f"完成分析表 {table_name} 的边频能量比,共 {len(time_segments)} 个时间段") + return result + + except Exception as e: + logger.error(f"分析表 {table_name} 的边频能量比时出错: {str(e)}") + return {"error": str(e)} + +@app.post("/analyze/sideband-energy", response_model=SidebandEnergyResponse) +def analyze_sideband_energy_api(request: SidebandRequest): + """ + 分析指定表中所有时间段的边频能量比,生成随时间变化的图表 + + 参数: + - table_name: 表名 + + 返回: + - sideband_energy_plot_url: 边频能量比图表URL + - sideband_energy_data: 详细的边频能量比数据 + - warning_threshold: 警告阈值 + - alert_threshold: 告警阈值 + - warning_count: 警告点数量 + - alert_count: 告警点数量 + """ + try: + logger.info(f"收到分析边频能量比请求,表名: {request.table_name}") + result = analyze_sideband_energy_ratio(request.table_name) + + if "error" in result: + raise HTTPException(status_code=404, detail=result["error"]) + + return result + except HTTPException: + raise + except Exception as e: + logger.error(f"分析边频能量比时出错: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/mutationAnalysis", summary="突变分析", description="基于溶解气体分析突变分析", response_description="返回分析结果") +async def mutation_analysis_api(mutationAnalysis: MutationAnalysis): + """气体浓度突变分析 + + 参数: + mutationAnalysis: 包含两次取样数据的对象 + - first_sample_concentration: 第一次取样浓度 + - first_sample_time: 第一次取样时间 + - second_sample_concentration: 第二次取样浓度 + - second_sample_time: 第二次取样时间 + + 返回: + 分析结果 + """ + try: + logger.info(f"收到突变分析请求: {mutationAnalysis}") + + # 计算时间间隔(天数) + time_diff = (mutationAnalysis.second_sample_time - mutationAnalysis.first_sample_time).total_seconds() / 86400 + + # 计算浓度差值 + change_rate = mutationAnalysis.second_sample_concentration - mutationAnalysis.first_sample_concentration + + # 计算突变 + result = change_rate / time_diff + + logger.info(f"突变分析结果: {result}") + return success(data={"result": result}) + + except Exception as e: + error_detail = traceback.format_exc() + logger.error(f"突变分析失败: {error_detail}") + return fail(msg=f"突变分析失败: {str(e)}") + +# 添加API响应帮助函数 +def success(data=None, msg="操作成功"): + """返回成功响应""" + return { + "code": 200, + "msg": msg, + "data": data + } + +def fail(code=400, msg="操作失败", data=None): + """返回失败响应""" + return { + "code": code, + "msg": msg, + "data": data + } + +def main(): + import uvicorn + uvicorn.run( + "td_analyzer:app", + host=API_CONFIG['host'], + port=API_CONFIG['port'], + reload=True + ) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/td_analysis/td_config.py b/src/td_analysis/td_config.py new file mode 100644 index 0000000..c871750 --- /dev/null +++ b/src/td_analysis/td_config.py @@ -0,0 +1,68 @@ +import os +from dotenv import load_dotenv +import logging + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 加载.env文件中的环境变量 +load_dotenv() + +# TDengine数据库配置 +TDENGINE_CONFIG = { + 'url': os.getenv("TDENGINE_URL", "http://tdengine:6041"), + # 'url': "http://tdengine:6041", + 'user': os.getenv("TDENGINE_USER", "root"), + 'password': os.getenv("TDENGINE_PASSWORD", "taosdata"), + # 'password': "taosdata", + 'database': os.getenv("TDENGINE_DATABASE", "ddllm"), + 'timeout': int(os.getenv("TDENGINE_TIMEOUT", "30")) +} + +# API服务器配置 +API_CONFIG = { + 'host': os.getenv("API_HOST", "0.0.0.0"), + # 'host': "0.0.0.0", + 'port': int(os.getenv("API_PORT", "5002")), + # 'port': 5002, + # 'base_url': os.getenv("API_BASE_URL", f"http://127.0.0.1:9100") + 'base_url': os.getenv("API_BASE_URL", "http://localhost:5002") # 外部访问地址 + # 'base_url': "http://localhost:5002" # 外部访问地址 +} + +# 应用配置 +APP_CONFIG = { + 'title': os.getenv("APP_TITLE", "变压器振动分析API"), + 'description': os.getenv("APP_DESCRIPTION", "基于TDengine的变压器振动数据分析API"), + 'version': os.getenv("APP_VERSION", "1.0.0"), + 'images_dir': os.getenv("IMAGES_DIR", os.path.join(os.path.dirname(__file__), "static", "images")) +} + +# 验证配置是否正确加载 +def validate_config(): + """验证配置是否正确加载并打印关键配置信息""" + logger.info("加载配置...") + logger.info(f"TDengine URL: {TDENGINE_CONFIG['url']}") + logger.info(f"TDengine 数据库: {TDENGINE_CONFIG['database']}") + logger.info(f"API服务器地址: {API_CONFIG['base_url']}") + logger.info(f"API服务器端口: {API_CONFIG['port']}") + logger.info(f"图片目录: {APP_CONFIG['images_dir']}") + + # 确保图片目录存在 + os.makedirs(APP_CONFIG['images_dir'], exist_ok=True) + + return True + +# 获取图片URL +def get_image_url(file_name): + """返回图片的完整URL""" + print(f"{API_CONFIG['base_url']}/static/images/{file_name}") + return f"{API_CONFIG['base_url']}/static/images/{file_name}" + +if __name__ == "__main__": + validate_config() + print("配置加载完成!") \ No newline at end of file diff --git a/src/td_analysis/td_connector.py b/src/td_analysis/td_connector.py new file mode 100644 index 0000000..727cd8b --- /dev/null +++ b/src/td_analysis/td_connector.py @@ -0,0 +1,124 @@ +import taosrest +import pandas as pd +import logging +from td_config import TDENGINE_CONFIG + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def connect_to_tdengine(): + """ + 连接到TDengine数据库 + """ + try: + client = taosrest.RestClient( + url=TDENGINE_CONFIG['url'], + user=TDENGINE_CONFIG['user'], + password=TDENGINE_CONFIG['password'], + database=TDENGINE_CONFIG['database'], + timeout=TDENGINE_CONFIG['timeout'] + ) + logger.info(f"成功连接到TDengine服务器 {TDENGINE_CONFIG['url']}") + return client + except Exception as e: + logger.error(f"连接到TDengine时出错: {str(e)}") + raise + +def execute_query(conn, sql): + """ + 执行SQL查询并返回结果 + """ + try: + logger.info(f"执行查询: {sql}") + # 使用sql方法执行查询 + result = conn.sql(sql) + + # 将结果转换为DataFrame + if result and isinstance(result, dict) and 'data' in result: + data = result['data'] + if data: + df = pd.DataFrame(data) + logger.info(f"查询成功,返回{len(df)}行数据") + return df + + logger.info("查询成功,但未返回数据") + return pd.DataFrame() + + except Exception as e: + logger.error(f"执行查询时出错: {str(e)}") + raise + +def get_database_info(conn): + """ + 获取数据库信息 + """ + try: + super_tables_df = execute_query(conn, "SHOW STABLES") + tables_df = execute_query(conn, "SHOW TABLES") + db_info = execute_query(conn, f"SHOW {TDENGINE_CONFIG['database']}") + + return { + "super_tables": super_tables_df, + "tables": tables_df, + "database_info": db_info + } + except Exception as e: + logger.error(f"获取数据库信息时出错: {str(e)}") + raise + +def get_latest_timeseries_data(conn, table_name, limit=1): + """ + 获取指定表的最新时序数据 + """ + try: + sql = f"SELECT * FROM {table_name} ORDER BY ts DESC LIMIT {limit}" + return execute_query(conn, sql) + except Exception as e: + logger.error(f"获取最新时序数据时出错: {str(e)}") + raise + +def test_connection(): + """测试TDengine连接并执行基本查询""" + try: + conn = connect_to_tdengine() + + # 测试服务器版本 + version_df = execute_query(conn, "SELECT SERVER_VERSION()") + version = version_df.iloc[0, 0] if not version_df.empty else "未知" + logger.info(f"TDengine服务器版本: {version}") + + # # 测试数据库列表 + # status_df = execute_query(conn, "SHOW DATABASES") + # logger.info(f"找到 {len(status_df)} 个数据库:") + # for idx, row in status_df.iterrows(): + # logger.info(f" - {row['name']}") + + # # 测试当前数据库的表 + # if TDENGINE_CONFIG['database']: + # tables_df = execute_query(conn, "SHOW TABLES") + # logger.info(f"在数据库 '{TDENGINE_CONFIG['database']}' 中找到 {len(tables_df)} 个表") + # if not tables_df.empty: + # for idx, row in tables_df.iterrows(): + # logger.info(f" - {row['table_name']} (创建于 {row['created_time']})") + + # logger.info("连接测试完成") + return True + except Exception as e: + logger.error(f"连接测试失败: {str(e)}") + return False + +if __name__ == "__main__": + print("=" * 60) + print(f"TDengine连接测试 - {TDENGINE_CONFIG['url']}") + print("=" * 60) + + if test_connection(): + print("\n✓ 连接测试成功!") + else: + print("\n✗ 连接测试失败,请检查连接参数和错误日志。") + + print("\n提示: 要使用此模块,请先安装TDengine REST客户端:") + print("pip install taosrest") \ No newline at end of file