"""ENGIN industrial diagnostic console built on the tested application core."""
from __future__ import annotations
import html
import logging
from dataclasses import dataclass
from pathlib import Path
import pandas as pd
import streamlit as st
from engin.charts import cylinder_spectrum, deviation_chart, engine_heatmap
from engin.config import (
LABEL_COLORS,
LABEL_DISPLAY,
SEVERITY_DISPLAY,
AppConfig,
)
from engin.errors import UserFacingError
from engin.explainability import (
analyze_engine,
explain_cylinder,
rank_cylinders,
summarize_engine,
)
from engin.io import PandasCsvReader
from engin.model import PrecomputedPredictionModel, SklearnPredictionModel
from engin.service import DiagnosisResult, DiagnosticService
from engin.validation import SpectrumFrameValidator
LOGGER = logging.getLogger("engin.app")
BASE_DIR = Path(__file__).resolve().parent
MODEL_VERSION = "engin-2026.08.25-1"
MODEL_ARTIFACT_DIR = BASE_DIR / "artifacts" / MODEL_VERSION
VIEW_OVERVIEW = "Przegląd"
VIEW_DETAIL = "Szczegóły cylindra"
NO_COMPARISON = 0
PLOTLY_CONFIG = {"displayModeBar": False, "displaylogo": False}
DIAGNOSTIC_COLUMN_DISPLAY = {
"engine_id": "Silnik",
"cylinder": "Cylinder",
"label": "Diagnoza",
"severity": "Nasilenie",
"raw_model_label": "Surowa diagnoza modelu",
"decision_source": "Źródło decyzji",
"label_confidence": "Wynik diagnozy",
"raw_model_confidence": "Surowy wynik diagnozy",
"label_margin": "Margines decyzji",
"severity_confidence": "Wynik oceny nasilenia",
"anomaly_score_mean_abs_mv": "Średnie odchylenie bezwzględne [mV]",
"anomaly_ratio_to_engine_median": "Odchylenie względem mediany silnika",
"ood_absolute_threshold_mv": "Bezwzględny próg anomalii [mV]",
"ood_ratio_threshold": "Względny próg anomalii",
"top_anomalous_frequencies_khz": "Anomalne częstotliwości [kHz]",
"missing_spectral_cells": "Brakujące pomiary widma",
"model_version": "Wersja modelu",
"model_artifact_sha256": "Suma SHA-256 artefaktu",
}
@dataclass(frozen=True)
class AppDependencies:
service: DiagnosticService
demo_payload: bytes
live_inference: bool = True
startup_warning: str | None = None
model_version: str = "unknown"
def _reader() -> PandasCsvReader:
return PandasCsvReader(AppConfig().max_upload_bytes)
@st.cache_resource(show_spinner="Ładowanie modelu diagnostycznego…")
def build_dependencies() -> AppDependencies:
reader = _reader()
validator = SpectrumFrameValidator()
demo_path = BASE_DIR / "test.csv"
demo_payload = demo_path.read_bytes()
try:
model = SklearnPredictionModel.from_artifact(
MODEL_ARTIFACT_DIR,
expected_model_version=MODEL_VERSION,
)
return AppDependencies(
service=DiagnosticService(reader=reader, validator=validator, model=model),
demo_payload=demo_payload,
model_version=MODEL_VERSION,
)
except Exception:
LOGGER.exception("Live model initialization failed; enabling demo fallback")
submission = pd.read_csv(BASE_DIR / "predictions.csv")
diagnostics = pd.read_csv(BASE_DIR / "prediction_diagnostics.csv")
fallback = PrecomputedPredictionModel(submission, diagnostics)
return AppDependencies(
service=DiagnosticService(reader=reader, validator=validator, model=fallback),
demo_payload=demo_payload,
live_inference=False,
startup_warning=(
"Artefakt modelu nie został załadowany. Aplikacja działa w bezpiecznym "
"trybie demonstracyjnym na zapisanych predykcjach."
),
model_version="tryb-demonstracyjny",
)
@st.cache_data(show_spinner="Analiza widm i klasyfikacja cylindrów…")
def diagnose_payload(_service: DiagnosticService, payload: bytes) -> DiagnosisResult:
return _service.diagnose_bytes(payload)
def _load_css() -> None:
css_path = BASE_DIR / "assets" / "app.css"
st.markdown(f"", unsafe_allow_html=True)
def _render_error(error: UserFacingError) -> None:
st.error(f"**{error.title}**\n\n{error.message}")
if error.hint:
st.info(error.hint)
st.caption(f"Kod błędu: `{error.code}`")
def _diagnostics_for_display(frame: pd.DataFrame) -> pd.DataFrame:
display = frame.copy()
for column in ("label", "raw_model_label"):
if column in display:
display[column] = display[column].map(LABEL_DISPLAY).fillna(display[column])
if "severity" in display:
display["severity"] = (
display["severity"].map(SEVERITY_DISPLAY).fillna(display["severity"])
)
if "decision_source" in display:
display["decision_source"] = display["decision_source"].replace(
{
"classifier": "Klasyfikator spektralny",
"ood_override": "Reguła anomalii",
}
)
if "model_version" in display:
display["model_version"] = display["model_version"].replace(
{"demo-precomputed": "tryb demonstracyjny"}
)
return display.rename(columns=DIAGNOSTIC_COLUMN_DISPLAY)
def _render_header() -> None:
st.markdown(
"""
""",
unsafe_allow_html=True,
)
def _render_summary(summary) -> None:
st.markdown(
f"""
STATUS SILNIKA{html.escape(summary.status)}
NAJWYŻSZE NASILENIE{html.escape(summary.highest_severity_display)}
WYMAGA UWAGI{summary.attention} / {summary.cylinders}
PIERWSZY DO KONTROLIC{summary.top_cylinder:02d}
""",
unsafe_allow_html=True,
)
def _render_engine_overview(analysis) -> None:
left, right = st.columns(2, gap="large", vertical_alignment="top")
with left:
st.markdown(
'Mapa odchyleń
',
unsafe_allow_html=True,
)
st.plotly_chart(
engine_heatmap(analysis),
width="stretch",
key=f"heatmap_{analysis.engine_id}",
config=PLOTLY_CONFIG,
)
with right:
st.markdown(
'Priorytet kontroli
',
unsafe_allow_html=True,
)
ranking = rank_cylinders(analysis).head(6).copy()
ranking["Cylinder"] = ranking["cylinder"].map(lambda value: f"C{int(value):02d}")
ranking["Diagnoza"] = ranking["label_display"]
ranking["Nasilenie"] = ranking["severity_display"]
ranking["Priorytet"] = ranking["priority_display"]
st.dataframe(
ranking[["Cylinder", "Diagnoza", "Nasilenie", "Priorytet"]],
hide_index=True,
width="stretch",
height=360,
)
def _render_cylinder_detail(
analysis,
cylinder: int,
comparison_cylinders: list[int],
) -> None:
explanation = explain_cylinder(analysis, cylinder)
color = LABEL_COLORS[explanation.label]
priority = rank_cylinders(analysis).loc[
lambda frame: frame["cylinder"].eq(cylinder), "priority_display"
].iloc[0]
top_bands = ", ".join(f"{value} kHz" for value in explanation.top_frequencies)
st.markdown(
f"""
CYLINDER {explanation.cylinder:02d}
{html.escape(explanation.label_display)}
{html.escape(explanation.severity_display)}
Priorytet{html.escape(priority)}
Średnie odchylenie{explanation.anomaly_score:.1f} mV
Główne pasma{html.escape(top_bands)}
""",
unsafe_allow_html=True,
)
st.markdown("#### Widma porównawcze")
st.plotly_chart(
cylinder_spectrum(analysis, cylinder, comparison_cylinders),
width="stretch",
key=(
f"spectrum_{analysis.engine_id}_"
+ "_".join(str(value) for value in comparison_cylinders)
),
config=PLOTLY_CONFIG,
)
st.markdown("#### Odchylenie cylindra głównego")
st.plotly_chart(
deviation_chart(analysis, cylinder),
width="stretch",
key=f"deviation_{analysis.engine_id}_{cylinder}",
config=PLOTLY_CONFIG,
)
why, next_step = st.columns(2, gap="large")
with why:
st.markdown("#### Uzasadnienie")
st.write(explanation.reason)
with next_step:
st.markdown("#### Następny krok")
st.write(explanation.recommendation)
def _store_selected_cylinder(state_key: str, widget_key: str) -> None:
st.session_state[state_key] = int(st.session_state[widget_key])
def _comparison_label(value: int) -> str:
if value == NO_COMPARISON:
return "Bez porównania"
return f"Cylinder {value:02d}"
def _render_cylinder_selectors(
analysis,
available: list[int],
selected_state_key: str,
) -> tuple[int, list[int]]:
selector_key = f"cylinder_selector_{analysis.engine_id}"
selected = int(st.session_state[selected_state_key])
if selector_key not in st.session_state or st.session_state[selector_key] not in available:
st.session_state[selector_key] = selected
with st.container(border=True):
st.markdown("### Wybór cylindra")
columns = st.columns([1.35, 1.0, 1.0, 1.0], gap="medium")
with columns[0]:
primary = int(
st.selectbox(
"Cylinder do analizy",
available,
format_func=lambda value: f"Cylinder {value:02d}",
key=selector_key,
on_change=_store_selected_cylinder,
args=(selected_state_key, selector_key),
)
)
comparisons: list[int] = []
for slot, column in enumerate(columns[1:], start=1):
comparison_key = f"comparison_slot_{analysis.engine_id}_{slot}"
options = [
NO_COMPARISON,
*(
cylinder
for cylinder in available
if cylinder != primary and cylinder not in comparisons
),
]
if (
comparison_key not in st.session_state
or st.session_state[comparison_key] not in options
):
st.session_state[comparison_key] = NO_COMPARISON
with column:
comparison = int(
st.selectbox(
f"Porównanie {slot}",
options,
format_func=_comparison_label,
key=comparison_key,
)
)
if comparison != NO_COMPARISON:
comparisons.append(comparison)
st.session_state[selected_state_key] = primary
return primary, comparisons
def _render_technical(result: DiagnosisResult) -> None:
st.markdown("### Walidacja i model")
c1, c2, c3, c4 = st.columns(4)
c1.metric("Makro F1 (grupowe)", "0.981")
c2.metric("Trafność nasilenia", "0.930")
c3.metric("Punkty walidacyjne", "33.65 / 40")
c4.metric("Testy", "38 / 38")
st.caption(
"Walidacja grupowa według silników · średnia z 5 uruchomień · "
"makro F1 przy 5% braków: 0.983 · wnioskowanie lokalne na CPU."
)
st.markdown("#### Dane diagnostyczne")
st.dataframe(
_diagnostics_for_display(result.diagnostics),
width="stretch",
hide_index=True,
)
def render_app(dependencies: AppDependencies | None = None) -> None:
st.set_page_config(
page_title="Konsola diagnostyczna ENGIN",
page_icon="⚙️",
layout="wide",
initial_sidebar_state="expanded",
)
_load_css()
deps = dependencies or build_dependencies()
_render_header()
with st.sidebar:
st.markdown("## Centrum diagnostyczne")
source = st.radio(
"Źródło danych",
["Dane demonstracyjne", "Wgraj plik CSV"],
captions=["50 silników testowych", "Własne kompletne silniki 8/12/16"],
)
payload: bytes | None
if source == "Dane demonstracyjne":
payload = deps.demo_payload
st.success("Dane demonstracyjne załadowane")
else:
uploaded = st.file_uploader("Plik pomiarowy CSV", type=["csv"])
payload = uploaded.getvalue() if uploaded is not None else None
if payload is None:
st.info("Wgraj CSV, aby rozpocząć diagnozę.")
st.caption("Wymagane: kompletne silniki oraz mV_0...mV_20.")
if deps.startup_warning:
st.warning(deps.startup_warning)
if payload is None:
st.markdown("### Oczekiwanie na dane")
st.write("Po wgraniu pliku aplikacja zweryfikuje strukturę przed uruchomieniem modelu.")
return
try:
result = diagnose_payload(deps.service, payload)
except UserFacingError as exc:
_render_error(exc)
return
except Exception:
LOGGER.exception("Unexpected application failure")
st.error("**Nieoczekiwany błąd aplikacji**\n\nDiagnoza została bezpiecznie przerwana; dane nie zostały zmodyfikowane.")
st.info("Uruchom aplikację ponownie lub użyj zestawu demonstracyjnego.")
return
for warning in result.warnings:
st.warning(warning)
with st.sidebar:
engine_id = st.selectbox("Aktywny silnik", result.engine_ids)
st.download_button(
"Pobierz wyniki (CSV)",
data=result.submission.to_csv(index=False).encode("utf-8"),
file_name="predictions.csv",
mime="text/csv",
width="stretch",
)
st.download_button(
"Pobierz dane diagnostyczne (CSV)",
data=result.diagnostics.to_csv(index=False).encode("utf-8"),
file_name="prediction_diagnostics.csv",
mime="text/csv",
width="stretch",
)
analysis = analyze_engine(result, str(engine_id))
summary = summarize_engine(analysis)
_render_summary(summary)
session_key = f"active_cylinder_{analysis.engine_id}"
available = analysis.measurements["cylinder"].astype(int).tolist()
if session_key not in st.session_state or st.session_state[session_key] not in available:
st.session_state[session_key] = summary.top_cylinder
view_key = f"active_view_{analysis.engine_id}"
if view_key not in st.session_state:
st.session_state[view_key] = VIEW_OVERVIEW
view = st.segmented_control(
"Widok",
[VIEW_OVERVIEW, VIEW_DETAIL],
required=True,
key=view_key,
label_visibility="collapsed",
width="stretch",
)
if view == VIEW_OVERVIEW:
_render_engine_overview(analysis)
elif view == VIEW_DETAIL:
selected_from_box, additional_cylinders = _render_cylinder_selectors(
analysis,
available,
session_key,
)
comparison_cylinders = [
int(selected_from_box),
*(int(value) for value in additional_cylinders),
]
_render_cylinder_detail(
analysis,
int(selected_from_box),
comparison_cylinders,
)
with st.expander("Informacje techniczne", expanded=False):
_render_technical(result)
if __name__ == "__main__":
render_app()