Язык (backend): Python Язык (frontend): TypeScript ML/DL: PyTorch, XGBoost, pandas Вычисления: NumPy, SciPy Backend: FastAPI Frontend: React База данных: PostgreSQL Инструменты автоматизации: Ollama
Промышленные предприятия вынуждены проводить плановое техническое обслуживание оборудования по жёсткому графику, даже когда оборудование находится в исправном состоянии. Это приводит к избыточным затратам на человеческие ресурсы и простоям производства. При этом отказ от планового обслуживания в пользу обслуживания «по факту поломки» несёт риск внезапных аварий. В результате предприятия вынуждены выбирать между переплатой за избыточное обслуживание и риском пропустить развивающийся дефект.
-
Как инженер по эксплуатации, я хочу видеть текущий прогноз остаточного ресурса каждой единицы оборудования, чтобы планировать техобслуживание по факту приближающегося дефекта, а не по жёсткому графику.
-
Как ответственный за производство, я хочу получать уведомление, когда состояние оборудования становится критическим, чтобы оперативно среагировать до внеплановой остановки или аварии.
-
Как инженер по эксплуатации, я хочу видеть историю показаний датчиков и отклонений от физически ожидаемого поведения оборудования, чтобы понимать, как развивался дефект, и точнее диагностировать его причину.
Обучающие данные — NASA C-MAPSS (симуляция деградации турбовентиляторных двигателей), файл train_FD001.txt: 100 двигателей, 20 631 строка, жизнь двигателя от 128 до 362 циклов (в среднем 206), один режим работы, пропусков нет. Каждая строка — один цикл двигателя: unit, cycle, 3 операционные настройки, 21 датчик.
RUL (остаточный ресурс, в циклах) размечается как rul = последний_цикл_двигателя − текущий_цикл; в последнем цикле rul = 0 (отказ). Для обучения дополнительно используется rul_clipped — RUL с потолком 125 циклов (на раннем этапе жизни износ по датчикам не виден). Разметка: ml/prepare_rul.py.
Датчики. В работе остаются 14 из 21: s2, s3, s4, s7, s8, s9, s11, s12, s13, s14, s15, s17, s20, s21. Исключены 7 датчиков без информации об износе: s1, s5, s10, s16, s18, s19 (константы) и s6 (почти константа). Операционные настройки op1–op3 в FD001 — шум/константа, в модель не идут. Для FD002–FD004 (6 режимов работы) датчики по константности не удаляются — требуется нормализация по режимам.
| Датчик | Что измеряет | Ед. | Связь с RUL (Спирмен) |
|---|---|---|---|
| s2 | T24 — температура на выходе КНД | °R | −0,63 |
| s3 | T30 — температура на выходе КВД | °R | −0,61 |
| s4 | T50 — температура на выходе ТНД | °R | −0,70 |
| s7 | P30 — давление на выходе КВД | psia | +0,68 |
| s8 | Nf — физическая скорость вентилятора | об/мин | −0,57 |
| s9 | Nc — физическая скорость ядра | об/мин | −0,32 |
| s11 | Ps30 — статическое давление на выходе КВД | psia | −0,72 |
| s12 | phi — расход топлива / Ps30 | pps/psia | +0,69 |
| s13 | NRf — приведённая скорость вентилятора | об/мин | −0,57 |
| s14 | NRc — приведённая скорость ядра | об/мин | −0,20 |
| s15 | BPR — степень двухконтурности | — | −0,67 |
| s17 | htBleed — энтальпия отбора | — | −0,63 |
| s20 | W31 — охлаждающий отбор ТВД | lbm/s | +0,65 |
| s21 | W32 — охлаждающий отбор ТНД | lbm/s | +0,66 |
Подготовка признаков: нормализация (параметры сохраняются для инференса), сглаживание (скользящее среднее/EWMA), скользящие признаки (среднее, std, наклон за 5–30 циклов) для XGBoost, окна по 30 циклов для нейросети на PyTorch. Разбиение train/val — по unit, не по строкам, чтобы не было утечки.
Статусы оборудования по прогнозу RUL: normal — более 75 циклов, warning — от 31 до 75, critical — 30 циклов и меньше.
Физическая модель (ml/physics.py) считается воркером вместе с RUL. Фактическая деградация — проекция нормированных показаний 14 датчиков на первую главную компоненту, отмасштабированная так, что 0 — исправный двигатель, 1 — отказ (по двигателям 1–80), со сглаживанием EWMA. Ожидаемая деградация — экспоненциальный закон накопления повреждений D(τ) = a·(e^(b·τ) − 1), которым NASA моделировала износ в C-MAPSS (Saxena et al., 2008). Эквивалентная наработка τ за цикл растёт на коэффициент ускорения по Аррениусу от температуры воздуха из Open-Meteo: при 15 °C он равен 1, при 0 °C — 0,83, при 30 °C — 1,18. Координаты площадки — WEATHER_LATITUDE и WEATHER_LONGITUDE (по умолчанию Москва). Ожидаемая деградация пишется в physics_predictions, невязка «фактическая минус ожидаемая» — в ml_predictions.residual. Проверка модели: python physics_check.py ../data/train_FD001.txt из ml/.
Целевая аудитория системы - инженеры по эксплуатации и ответственные за производство на промышленных предприятиях. При оценке масштаба в 10 000 уникальных пользователей в сутки предполагается, что система обслуживает порядка N предприятий с M единицами оборудования на каждом, где каждый пользователь в среднем совершает K запросов в день (просмотр дашборда, проверка истории, получение алертов). Основная нагрузка - read-запросы (просмотр данных), запись происходит асинхронно от симулятора/датчиков оборудования, а не от действий пользователей напрямую.
При недоступности Telegram Bot API система применяет паттерн Circuit Breaker: после 3 неудачных попыток отправки подряд прекращает попытки на 60 секунд, чтобы не тратить ресурсы на заведомо неудачные запросы. Алерт при этом всё равно сохраняется в базу данных и отображается в интерфейсе.
При недоступности Open-Meteo API физическая модель использует последнее закэшированное значение погодных условий (кэш на 1 час). Если кэш пуст (например, при первом запуске) - модель работает без погодного фактора.
Исходные допущения:
| Параметр | Значение | Почему |
|---|---|---|
| Пользователей в сутки | 10 000 | — |
| Единиц оборудования в системе | 1 000 | условно |
| Частота показаний датчика | 1 раз / 30 сек (1 цикл) | типично для промышленного мониторинга |
| Частота инференса модели | 1 раз / 5 мин на единицу | нет смысла считать прогноз чаще, чем меняются данные |
| Запросов пользователя за сессию | ~5-10 (дашборд + история + алерты) | из прошлой оценки RPS |
WRITE в БД идет не от пользователей, а от фонового simulator + ml / physics predictions: 1 000 / 30 + 1 000 / 300 + 1 000 / 300 ~40 записей в сек READ 10 000 * 8 запросов ~ 80 000, правило 80%/20%, 20% от 24 часов = ~5, 80% от 80 000 = 64 000, 64 000 / (5 * 3600) ~ 3.5 RPS в пик
Средний размер JSON-ответа: ~10 КБ Запросов в сутки: 10 000 польз. × 8 запросов ≈ 80 000 запросов/сутки Трафик: 80 000 × 10 КБ ≈ 800 МБ/сутки ≈ ~24 ГБ/месяц
Показания датчиков (таблица sensor_readings, широкая строка: 14 датчиков × 4 байта (REAL) = 56 Б + id 8 Б + equipment_id 4 Б + cycle 4 Б + rul 4 Б + created_at 8 Б + служебный заголовок строки PostgreSQL ~24 Б ≈ 110 байт):
33 записи/сек × 86 400 сек = ~2.88 млн строк/сутки
2.88 млн × 110 байт ≈ 320 МБ/сутки → ~115 ГБ/год → ~580 ГБ за 5 лет (без индексов; индекс (equipment_id, cycle) добавляет порядка 30–40%).
Предсказания модели: 6.6 записи/сек × 86 400 = ~570 тыс. строк/сутки. 570 тыс. × 50 байт ≈ 28.5 МБ/сутки → ~10 ГБ/год → ~52 ГБ за 5 лет.
api/v1 (эндпоинты, валидация) → services (бизнес-логика) → repositories (доступ к данным)
└→ predictor (модель RUL: заглушка → XGBoost/PyTorch)
Репозиторий — интерфейс EquipmentRepository. Сейчас работает MockRepository (данные в памяти, похожие на FD001), позже — PostgresRepository. Переключение: OFFTECH_USE_MOCK_REPOSITORY. Эндпоинты и фронтенд при этом не меняются.
Ниже указаны пути backend. Frontend обращается к ним через прокси /api (например, /api/auth/login). Документация OpenAPI backend: /docs.
Публичной регистрации нет. Аккаунты выдаёт администратор; он вручную задаёт логин и постоянный пароль. Почтовая отправка не используется.
employee: оборудование, история, графики, алерты и прогнозирование.admin: все возможности сотрудника, плюс список аккаунтов, создание сотрудников, сброс их паролей и блокировка/разблокировка.- Логин: 3–64 символа, латинские буквы, цифры,
.,_,-. Пробелы по краям убираются, регистр приводится к нижнему. Уникальность обеспечивает PostgreSQL. - Новый пароль: от 12 символов, не более 72 байт UTF-8 (лимит bcrypt). Пробелы, регистр и спецсимволы сохраняются. В базе хранится только bcrypt-хеш с индивидуальной солью.
- JWT выдаётся только при входе и действует
JWT_EXPIRE_MINUTESминут (по умолчанию 1440). Для запуска обязателен постоянныйJWT_SECRETдлиной минимум 32 байта. Он не является паролем пользователя. - На каждом защищённом запросе backend сверяет пользователя, его состояние и версию токена с базой; административные операции дополнительно проверяют роль. Данные роли из браузера не принимаются.
- Сброс пароля и изменение блокировки увеличивают
token_version. Старые токены больше не работают, в том числе после разблокировки. Сброс пароля не разблокирует аккаунт. - Через панель можно изменять только сотрудников. Администратора нельзя заблокировать или изменить его пароль через эти endpoints.
- Лимит входа: 10 попыток на логин и 60 на адрес за 15 минут, включая успешные попытки. Счётчики атомарные, общие для процессов backend в PostgreSQL. Ответ
429содержитRetry-After. Учитывается адрес подключения, не пользовательскийX-Forwarded-For; за обратным прокси лимит адреса может быть общим для клиентов этого прокси.
| Метод | Путь | Доступ и действие |
|---|---|---|
| POST | /auth/login |
Публичный вход: { "login": "ivan.petrov", "password": "…" } |
| GET | /auth/me |
Текущий активный пользователь |
| GET | /admin/users?limit=50&offset=0 |
Администратор: список аккаунтов без паролей и хешей |
| POST | /admin/users |
Администратор: { "login": "ivan.petrov", "password": "…" }, создаёт только employee |
| PUT | /admin/users/{id}/password |
Администратор: { "password": "…" }, сброс пароля сотрудника и отзыв токенов |
| PUT | /admin/users/{id}/status |
Администратор: { "is_active": false } для блокировки, true для разблокировки |
Остальные рабочие endpoints тоже требуют Authorization: Bearer <токен>. Без действующего токена — 401; сотруднику на административном endpoint — 403; занятый логин — 409. /auth/register удалён, отвечает 404.
- Скопировать
.env.exampleв.envи заполнить настройки. Секрет подписи сгенерировать командойpython3 -c "import secrets; print(secrets.token_urlsafe(32))"и записать вJWT_SECRET. Файл.envне коммитить. - Запустить
docker compose up -d --build db backend frontend. Backend применит миграции автоматически. Для данных симулятора и расчётов отдельно запустить нужные сервисы проекта. - Выполнить
docker compose exec backend python -m app.create_admin. Вручную ввести логин и дважды пароль (ввод пароля скрыт). Повторение занятого логина не изменяет существующий аккаунт. Команда доступна только имеющему доступ к серверу; не принимает пароль аргументом командной строки. - Открыть
http://localhost:5173(либо портFRONTEND_PORT), войти администратором. В разделе «Пользователи» создать сотрудника и лично передать ему введённые данные.
При локальном запуске без Docker сначала выполнить alembic upgrade head из backend, затем python -m app.create_admin в том же окружении с настроенным DATABASE_URL и JWT_SECRET.
Миграция b48a001 следует за миграцией PR #48 1395cb8114bd. Она рассчитана на согласованное отсутствие существующих аккаунтов: если users не пуста, миграция остановится без удаления данных. Не применять её к базе прежней локальной реализации с другой веткой миграций. Для параллельной проверки использовать отдельную базу/Compose-проект. Порты задаются POSTGRES_PORT, BACKEND_PORT, FRONTEND_PORT.
Для размещения вне локальной разработки требуется HTTPS. В этой версии сохранён способ хранения токена из #48 — localStorage; выход удаляет его из браузера, но сам по себе не отзывает скопированную копию токена. Серверный отзыв выполняется при сбросе пароля или блокировке. Администратор знает вручную заданный пароль сотрудника — это осознанное упрощение учебного проекта, обязательной смены пароля нет.
Backend-тесты очищают таблицы. Указать отдельную тестовую базу:
cd backend
DATABASE_URL=postgresql+psycopg://pdm:pdm@localhost:5432/offtech_test alembic upgrade head
TEST_DATABASE_URL=postgresql+psycopg://pdm:pdm@localhost:5432/offtech_test pytest -qТесты задают тестовый ключ JWT самостоятельно. Проверяют обе роли, административные операции, прямой обход UI, блокировку, отзыв токенов, параллельные изменения, валидацию и лимиты входа. Проверка миграций: alembic check. Проверка frontend: cd frontend && npm ci && npm run build. Эти проверки также добавлены в CI.
| Метод | Эндпоинт | Описание | Ожидаемый latency |
|---|---|---|---|
| GET | /equipment |
Список всех единиц оборудования (с текущим циклом, статусом и прогнозом RUL) | < 200 мс |
| GET | /equipment/{id} |
Информация об одной единице | < 100 мс |
| GET | /equipment/{id}/status |
Текущее состояние: статус, RUL, вероятность дефекта, невязка | < 150 мс |
| GET | /equipment/{id}/history |
История показаний датчиков (from_cycle, to_cycle, limit) |
< 500 мс |
| GET | /equipment/{id}/forecast |
Прогноз деградации: RUL по будущим циклам с доверительным интервалом | < 500 мс |
| GET | /equipment/{id}/physics |
Физическая модель против фактических показаний: ожидаемая и фактическая деградация, невязка (limit) |
< 500 мс |
| Метод | Эндпоинт | Описание | Ожидаемый latency |
|---|---|---|---|
| GET | /alerts |
Список последних алертов | < 200 мс |
| GET | /alerts/{equipment_id} |
Алерты по конкретному оборудованию | < 200 мс |
| Метод | Эндпоинт | Описание | Ожидаемый latency |
|---|---|---|---|
| POST | /predict |
Ручной запрос инференса модели для конкретного оборудования | < 2000 мс |
| Метод | Эндпоинт | Описание | Ожидаемый latency |
|---|---|---|---|
| GET | /sensors |
Каталог датчиков (21, с признаком is_used) |
< 100 мс |
| GET | /health |
Проверка работоспособности (без префикса) | < 50 мс |
Таблицы привязаны к структуре датасета C-MAPSS: одна единица оборудования = один двигатель (unit), одна запись показаний = один цикл (cycle). Обучающая выборка (train_FD00X.txt с разметкой RUL) хранится файлами в data/, а не в БД.
erDiagram
equipment ||--o{ sensor_readings : "показания"
equipment ||--o{ physics_predictions : "физ. прогнозы"
equipment ||--o{ ml_predictions : "ML прогнозы"
equipment ||--o{ alerts : "алерты"
ml_predictions ||--o{ alerts : "порождают"
equipment {
int id PK
varchar name
varchar type
varchar dataset "FD001..FD004"
int unit_number "unit из датасета"
date install_date
date last_maintenance_date
}
sensor_catalog {
varchar code PK "s1..s21"
varchar name
varchar unit
boolean is_used "false для s1 s5 s6 s10 s16 s18 s19"
}
sensor_readings {
bigint id PK
int equipment_id FK
int cycle
real s2
real s3
real s4
real s7
real s8
real s9
real s11
real s12
real s13
real s14
real s15
real s17
real s20
real s21
int rul
timestamp created_at
}
physics_predictions {
bigint id PK
int equipment_id FK
int cycle
float predicted_degradation
timestamp created_at
}
ml_predictions {
bigint id PK
int equipment_id FK
int cycle
float predicted_rul
float defect_probability
float residual
varchar model_version
timestamp created_at
}
alerts {
bigint id PK
int equipment_id FK
bigint ml_prediction_id FK
int cycle
timestamp triggered_at
varchar severity "info warning critical"
text message
}
users {
int id PK
text email "уникальный, в нижнем регистре"
text password_hash "bcrypt"
timestamp created_at
}
Ограничения и индексы: UNIQUE (dataset, unit_number) в equipment; UNIQUE (equipment_id, cycle) в sensor_readings, physics_predictions, ml_predictions; индекс alerts (equipment_id, triggered_at DESC); UNIQUE (email) в users. Показания хранятся широкой таблицей: набор из 14 датчиков фиксирован, так запросы истории не требуют PIVOT. Для других наборов (FD002–FD004) добавляются колонки миграцией; полный список датчиков и признак is_used лежат в sensor_catalog.
При записи (40 записей/сек) все вставки идут в разные таблицы от фоновых процессов. 40 INSERT/сек — незначительная нагрузка для PostgreSQL. При чтении (~10-20 RPS) PostgreSQL будет находить строки за O(log n). Проблем не возникнет.
При 100 000 — запускаем несколько экземпляров за балансировщиком нагрузки.
Основная нагрузка — чтение (дашборд, история, алерты). Добавляем 1-2 read-реплики, куда направляем все GET-запросы. Запись идёт только в primary
Часто запрашиваемые данные кэшируем в Redis, чтобы не ходить в Postgres на каждый запрос
Open-Meteo API: Погодные данные меняются медленно - нет смысла запрашивать чаще раза в час. Кэшируем ответ в Redis.
Telegram Bot API: Алертов немного, rate limit маловероятен.
- Запускаем несколько воркеров ML-engine параллельно,
каждый обрабатывает свой диапазон
equipment_id - Если CPU не хватает - батчинг: вместо инференса по одной единице за раз, модель обрабатывает пачку, что сильно эффективнее.

