"""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( """
AESTEEL · DIAGNOSTYKA WTRYSKU DIESEL

Konsola diagnostyczna ENGIN

Diagnoza cylindra, nasilenie i następny krok.

""", 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}", theme=None, 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) ), theme=None, 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}", theme=None, 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()