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
13 changes: 8 additions & 5 deletions backend/app/api/alerts.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
from fastapi import APIRouter, HTTPException, Query
from fastapi import APIRouter, Query

from app.core.errors import NOT_FOUND, equipment_not_found
from app.schemas.alert import AlertOut
from app.services import alert_service, equipment_service

router = APIRouter(prefix="/alerts", tags=["Алерты"])


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


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

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

router = APIRouter(prefix="/equipment", tags=["Оборудование"])


def _or_404(value):
def _found(value, equipment_id: int):
if value is None:
raise HTTPException(status_code=404, detail="Оборудование не найдено")
raise equipment_not_found(equipment_id)
return value


Expand All @@ -17,25 +18,30 @@ def list_equipment():
return equipment_service.list_equipment()


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


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


@router.get("/{equipment_id}/history", response_model=list[ReadingOut],
summary="История показаний датчиков")
@router.get("/{equipment_id}/history", response_model=list[ReadingOut], responses=NOT_FOUND,
summary="История показаний датчиков за период")
def get_history(equipment_id: int,
from_cycle: int | None = Query(None, ge=0),
to_cycle: int | None = Query(None, ge=0),
limit: int = Query(500, ge=1, le=5000)):
return _or_404(equipment_service.get_history(equipment_id, from_cycle, to_cycle, limit))
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)


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

from app.core.errors import NOT_FOUND, equipment_not_found
from app.schemas.equipment import StatusOut
from app.schemas.predict import PredictIn
from app.services import predict_service

router = APIRouter(prefix="/predict", tags=["Прогнозирование"])


@router.post("", response_model=StatusOut, summary="Ручной запрос инференса")
@router.post("", response_model=StatusOut, responses=NOT_FOUND, summary="Ручной запрос инференса")
def predict(body: PredictIn):
result = predict_service.run_prediction(body.equipment_id)
if result is None:
raise HTTPException(status_code=404, detail="Оборудование не найдено")
raise equipment_not_found(body.equipment_id)
return result
8 changes: 8 additions & 0 deletions backend/app/core/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from fastapi import HTTPException


def equipment_not_found(equipment_id: int) -> HTTPException:
return HTTPException(status_code=404, detail=f"Оборудование с id={equipment_id} не найдено")


NOT_FOUND = {404: {"description": "Оборудование не найдено"}}
24 changes: 12 additions & 12 deletions backend/app/schemas/equipment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Literal

from pydantic import BaseModel
from pydantic import BaseModel, Field

Status = Literal["normal", "warning", "critical"]

Expand All @@ -12,33 +12,33 @@ class EquipmentOut(BaseModel):
unit_number: int
current_cycle: int
status: Status
predicted_rul: float
predicted_rul: float = Field(description="Прогноз остаточного ресурса, циклов")


class StatusOut(BaseModel):
equipment_id: int
status: Status
status: Status = Field(description="normal: RUL > 75, warning: 31–75, critical: ≤ 30 циклов")
current_cycle: int
predicted_rul: float
defect_probability: float
residual: float
predicted_rul: float = Field(description="Прогноз остаточного ресурса, циклов")
defect_probability: float = Field(ge=0, le=1, description="Вероятность дефекта, 0–1")
residual: float = Field(description="Невязка: отклонение от физически ожидаемого поведения")


class ReadingOut(BaseModel):
cycle: int
sensors: dict[str, float]
sensors: dict[str, float] = Field(description="Показания 14 рабочих датчиков: код → значение")


class ForecastPoint(BaseModel):
cycle: int
rul: float
rul_low: float
rul_high: float
rul: float = Field(description="Ожидаемый остаточный ресурс на этом цикле")
rul_low: float = Field(description="Нижняя граница доверительного интервала")
rul_high: float = Field(description="Верхняя граница доверительного интервала")


class ForecastOut(BaseModel):
equipment_id: int
current_cycle: int
predicted_rul: float
predicted_failure_cycle: int
points: list[ForecastPoint]
predicted_failure_cycle: int = Field(description="Цикл, на котором ожидается отказ")
points: list[ForecastPoint] = Field(description="От текущего цикла до ожидаемого отказа, шаг — 1 цикл")
4 changes: 2 additions & 2 deletions backend/app/services/equipment_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ def get_forecast(equipment_id: int) -> dict | None:
if e is None:
return None
rul, cycle = e["predicted_rul"], e["current_cycle"]
horizon = round(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
2 changes: 1 addition & 1 deletion backend/app/services/mock_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def _build() -> None:
now = datetime.now(UTC)
for i in range(1, 13):
life = _rnd.randint(130, 300)
cut = int(life * _rnd.uniform(0.35, 0.97))
cut = int(life * _rnd.uniform(0.35, 0.97))
READINGS[i] = [
{"cycle": c, "sensors": {
code: round(base + trend * std * 3 * (c / life) ** 2 + _rnd.gauss(0, std * 0.4), 4)
Expand Down
Loading