Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions backend/app/api/alerts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import APIRouter, Query

from app.core.db import DB
from app.core.errors import NOT_FOUND, equipment_not_found
from app.schemas.alert import AlertOut
from app.services import alert_service, equipment_service
Expand All @@ -8,14 +9,14 @@


@router.get("", response_model=list[AlertOut], summary="Список последних алертов")
def list_alerts(limit: int = Query(50, ge=1, le=500, description="Максимум записей")):
return alert_service.list_alerts(limit=limit)
def list_alerts(db: DB, limit: int = Query(50, ge=1, le=500, description="Максимум записей")):
return alert_service.list_alerts(db, limit=limit)


@router.get("/{equipment_id}", response_model=list[AlertOut], responses=NOT_FOUND,
summary="Алерты по конкретному оборудованию")
def alerts_by_equipment(equipment_id: int,
def alerts_by_equipment(equipment_id: int, db: DB,
limit: int = Query(50, ge=1, le=500, description="Максимум записей")):
if equipment_service.get_equipment(equipment_id) is None:
if not equipment_service.equipment_exists(db, equipment_id):
raise equipment_not_found(equipment_id)
return alert_service.list_alerts(equipment_id, limit)
return alert_service.list_alerts(db, equipment_id, limit)
21 changes: 11 additions & 10 deletions backend/app/api/equipment.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import APIRouter, HTTPException, Query

from app.core.db import DB
from app.core.errors import NOT_FOUND, equipment_not_found
from app.schemas.equipment import EquipmentOut, ForecastOut, ReadingOut, StatusOut
from app.services import equipment_service
Expand All @@ -14,34 +15,34 @@ def _found(value, equipment_id: int):


@router.get("", response_model=list[EquipmentOut], summary="Список всех единиц оборудования")
def list_equipment():
return equipment_service.list_equipment()
def list_equipment(db: DB):
return equipment_service.list_equipment(db)


@router.get("/{equipment_id}", response_model=EquipmentOut, responses=NOT_FOUND,
summary="Информация об одной единице")
def get_equipment(equipment_id: int):
return _found(equipment_service.get_equipment(equipment_id), equipment_id)
def get_equipment(equipment_id: int, db: DB):
return _found(equipment_service.get_equipment(db, equipment_id), equipment_id)


@router.get("/{equipment_id}/status", response_model=StatusOut, responses=NOT_FOUND,
summary="Текущее состояние: статус, RUL, вероятность дефекта, невязка")
def get_status(equipment_id: int):
return _found(equipment_service.get_status(equipment_id), equipment_id)
def get_status(equipment_id: int, db: DB):
return _found(equipment_service.get_status(db, equipment_id), equipment_id)


@router.get("/{equipment_id}/history", response_model=list[ReadingOut], responses=NOT_FOUND,
summary="История показаний датчиков за период")
def get_history(equipment_id: int,
def get_history(equipment_id: int, db: DB,
from_cycle: int | None = Query(None, ge=0, description="С какого цикла (включительно)"),
to_cycle: int | None = Query(None, ge=0, description="По какой цикл (включительно)"),
limit: int = Query(500, ge=1, le=5000, description="Максимум записей (берутся последние)")):
if from_cycle is not None and to_cycle is not None and from_cycle > to_cycle:
raise HTTPException(status_code=400, detail="from_cycle не может быть больше to_cycle")
return _found(equipment_service.get_history(equipment_id, from_cycle, to_cycle, limit), equipment_id)
return _found(equipment_service.get_history(db, equipment_id, from_cycle, to_cycle, limit), equipment_id)


@router.get("/{equipment_id}/forecast", response_model=ForecastOut, responses=NOT_FOUND,
summary="Прогноз деградации: RUL по будущим циклам с доверительным интервалом")
def get_forecast(equipment_id: int):
return _found(equipment_service.get_forecast(equipment_id), equipment_id)
def get_forecast(equipment_id: int, db: DB):
return _found(equipment_service.get_forecast(db, equipment_id), equipment_id)
5 changes: 3 additions & 2 deletions backend/app/api/predict.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import APIRouter

from app.core.db import DB
from app.core.errors import NOT_FOUND, equipment_not_found
from app.schemas.equipment import StatusOut
from app.schemas.predict import PredictIn
Expand All @@ -9,8 +10,8 @@


@router.post("", response_model=StatusOut, responses=NOT_FOUND, summary="Ручной запрос инференса")
def predict(body: PredictIn):
result = predict_service.run_prediction(body.equipment_id)
def predict(body: PredictIn, db: DB):
result = predict_service.run_prediction(db, body.equipment_id)
if result is None:
raise equipment_not_found(body.equipment_id)
return result
16 changes: 16 additions & 0 deletions backend/app/core/db.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import os
from typing import Annotated

from dotenv import load_dotenv
from fastapi import Depends
from sqlalchemy import Connection, create_engine
from sqlalchemy.orm import DeclarativeBase

load_dotenv()

DATABASE_URL = os.getenv("DATABASE_URL")

engine = create_engine(DATABASE_URL)

class Base(DeclarativeBase):
pass

def get_db():
with engine.connect() as connection:
yield connection

DB = Annotated[Connection, Depends(get_db)]
14 changes: 10 additions & 4 deletions backend/app/services/alert_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from app.services.mock_data import ALERTS
from sqlalchemy import Connection, select

from app.models.alerts import Alert

def list_alerts(equipment_id: int | None = None, limit: int = 50) -> list[dict]:
items = [a for a in ALERTS if equipment_id is None or a["equipment_id"] == equipment_id]
return items[:limit]

def list_alerts(db: Connection, equipment_id: int | None = None, limit: int = 50) -> list[dict]:
query = select(Alert)
if equipment_id is not None:
query = query.where(Alert.equipment_id == equipment_id)
query = query.order_by(Alert.triggered_at.desc()).limit(limit)

return [dict(row) for row in db.execute(query).mappings()]
129 changes: 105 additions & 24 deletions backend/app/services/equipment_service.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,128 @@
import math
from sqlalchemy import Connection, and_, func, select

from app.services.mock_data import EQUIPMENT, READINGS
from app.core.config import RUL_CLIP, RUL_CRITICAL, RUL_WARNING
from app.models.equipment import Equipment
from app.models.ml_predictions import MlPrediction
from app.models.sensor_readings import SensorReading

SENSOR_CODES = ["s2", "s3", "s4", "s7", "s8", "s9", "s11", "s12", "s13", "s14", "s15", "s17", "s20", "s21"]

def list_equipment() -> list[dict]:
return list(EQUIPMENT.values())

def status_from_rul(rul: float) -> str:
if rul <= RUL_CRITICAL:
return "critical"
if rul <= RUL_WARNING:
return "warning"
return "normal"

def get_equipment(equipment_id: int) -> dict | None:
return EQUIPMENT.get(equipment_id)

def equipment_exists(db: Connection, equipment_id: int) -> bool:
query = select(Equipment.id).where(Equipment.id == equipment_id)
return db.execute(query).first() is not None

def get_status(equipment_id: int) -> dict | None:
e = EQUIPMENT.get(equipment_id)
if e is None:

def _latest_predictions(db: Connection, equipment_id: int | None = None) -> list[dict]:
"""Оборудование + его последний прогноз (по максимальному циклу)."""
last = (
select(MlPrediction.equipment_id, func.max(MlPrediction.cycle).label("cycle"))
.group_by(MlPrediction.equipment_id)
.subquery()
)

query = (
select(
Equipment.id,
Equipment.name,
Equipment.type,
Equipment.unit_number,
MlPrediction.cycle.label("current_cycle"),
MlPrediction.predicted_rul,
MlPrediction.defect_probability,
MlPrediction.residual,
)
.outerjoin(last, last.c.equipment_id == Equipment.id)
.outerjoin(
MlPrediction,
and_(MlPrediction.equipment_id == last.c.equipment_id, MlPrediction.cycle == last.c.cycle),
)
.order_by(Equipment.id)
)
if equipment_id is not None:
query = query.where(Equipment.id == equipment_id)

items = []
for row in db.execute(query).mappings():
item = dict(row)
if item["predicted_rul"] is None:
item["current_cycle"] = 0
item["predicted_rul"] = float(RUL_CLIP)
item["defect_probability"] = 0.0
item["residual"] = 0.0
item["status"] = status_from_rul(item["predicted_rul"])
items.append(item)
return items


def _to_equipment_out(item: dict) -> dict:
keys = ("id", "name", "type", "unit_number", "current_cycle", "status", "predicted_rul")
return {k: item[k] for k in keys}


def list_equipment(db: Connection) -> list[dict]:
return [_to_equipment_out(item) for item in _latest_predictions(db)]


def get_equipment(db: Connection, equipment_id: int) -> dict | None:
items = _latest_predictions(db, equipment_id)
return _to_equipment_out(items[0]) if items else None


def get_status(db: Connection, equipment_id: int) -> dict | None:
items = _latest_predictions(db, equipment_id)
if not items:
return None
rul = e["predicted_rul"]
e = items[0]
return {
"equipment_id": equipment_id, "status": e["status"], "current_cycle": e["current_cycle"],
"predicted_rul": rul, "residual": e["residual"],
"defect_probability": round(1 / (1 + math.exp((rul - 50) / 12)), 3),
"equipment_id": e["id"],
"status": e["status"],
"current_cycle": e["current_cycle"],
"predicted_rul": e["predicted_rul"],
"defect_probability": e["defect_probability"],
"residual": e["residual"],
}


def get_history(equipment_id: int, from_cycle: int | None, to_cycle: int | None,
def get_history(db: Connection, equipment_id: int, from_cycle: int | None, to_cycle: int | None,
limit: int) -> list[dict] | None:
if equipment_id not in EQUIPMENT:
if not equipment_exists(db, equipment_id):
return None
rows = [r for r in READINGS[equipment_id]
if (from_cycle is None or r["cycle"] >= from_cycle)
and (to_cycle is None or r["cycle"] <= to_cycle)]
return rows[-limit:]

columns = [getattr(SensorReading, code) for code in SENSOR_CODES]
query = select(SensorReading.cycle, *columns).where(SensorReading.equipment_id == equipment_id)
if from_cycle is not None:
query = query.where(SensorReading.cycle >= from_cycle)
if to_cycle is not None:
query = query.where(SensorReading.cycle <= to_cycle)
query = query.order_by(SensorReading.cycle.desc()).limit(limit)

rows = []
for row in db.execute(query).mappings():
sensors = {code: row[code] for code in SENSOR_CODES if row[code] is not None}
rows.append({"cycle": row["cycle"], "sensors": sensors})
rows.reverse()
return rows


def get_forecast(equipment_id: int) -> dict | None:
e = EQUIPMENT.get(equipment_id)
if e is None:
def get_forecast(db: Connection, equipment_id: int) -> dict | None:
items = _latest_predictions(db, equipment_id)
if not items:
return None
e = items[0]
rul, cycle = e["predicted_rul"], e["current_cycle"]
horizon = int(rul)
horizon = int(rul)
points = []
for k in range(horizon + 1):
value, spread = rul - k, 2 + 0.12 * k
value, spread = rul - k, 2 + 0.12 * k
points.append({"cycle": cycle + k, "rul": round(value, 1),
"rul_low": round(max(0.0, value - spread), 1), "rul_high": round(value + spread, 1)})
return {"equipment_id": equipment_id, "current_cycle": cycle, "predicted_rul": rul,
Expand Down
62 changes: 0 additions & 62 deletions backend/app/services/mock_data.py

This file was deleted.

12 changes: 4 additions & 8 deletions backend/app/services/predict_service.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from sqlalchemy import Connection

from app.services import equipment_service
from app.services.mock_data import EQUIPMENT, noisy_rul, status_from_rul


def run_prediction(equipment_id: int) -> dict | None:
e = EQUIPMENT.get(equipment_id)
if e is None:
return None
e["predicted_rul"] = noisy_rul(e["true_rul"])
e["status"] = status_from_rul(e["predicted_rul"])
return equipment_service.get_status(equipment_id)
def run_prediction(db: Connection, equipment_id: int) -> dict | None:
return equipment_service.get_status(db, equipment_id)
Loading