diff --git a/README.md b/README.md index c1ead6c..96575a8 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ rollbacku znajduje się w `docs/PRODUCTION_RUNBOOK.md`. - status silnika, najwyższe rzeczywiste nasilenie i kolejka cylindrów do kontroli, - mapa odchyleń całego silnika ze stałą, porównywalną skalą, - porównanie widma cylindra z medianą pozostałych cylindrów tej samej jednostki, -- priorytet kontroli, trzy najbardziej anomalne pasma oraz rekomendowany następny krok, +- priorytet kontroli, trzy najbardziej anomalne pasma oraz krótkie uzasadnienie diagnozy, - pobieranie `predictions.csv` i rozszerzonej diagnostyki, - ścisła walidacja CSV i bezpieczne komunikaty błędów, - lokalne działanie na CPU, bez wysyłania danych do zewnętrznych usług. diff --git a/app.py b/app.py index 5f974d3..f0a0519 100644 --- a/app.py +++ b/app.py @@ -151,7 +151,7 @@ def _render_header() -> None:
AESTEEL · DIAGNOSTYKA WTRYSKU DIESEL

Konsola diagnostyczna ENGIN

-

Diagnoza cylindra, nasilenie i następny krok.

+

Diagnoza cylindra, nasilenie i wyjaśnienie oparte na widmie.

""", unsafe_allow_html=True, @@ -192,12 +192,21 @@ def _render_engine_overview(analysis) -> None: unsafe_allow_html=True, ) ranking = rank_cylinders(analysis).head(6).copy() + ranking["Kolejność"] = range(1, len(ranking) + 1) 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"] + ranking["Odchylenie [mV]"] = ranking["anomaly_score_mean_abs_mv"].round(1) st.dataframe( - ranking[["Cylinder", "Diagnoza", "Nasilenie", "Priorytet"]], + ranking[ + [ + "Kolejność", + "Cylinder", + "Diagnoza", + "Nasilenie", + "Odchylenie [mV]", + ] + ], hide_index=True, width="stretch", height=360, @@ -253,13 +262,8 @@ def _render_cylinder_detail( 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) + st.markdown("#### Uzasadnienie diagnozy") + st.write(explanation.reason) def _store_selected_cylinder(state_key: str, widget_key: str) -> None: diff --git a/docs/DEMO_SCENARIO.md b/docs/DEMO_SCENARIO.md index eaf70db..55325f8 100644 --- a/docs/DEMO_SCENARIO.md +++ b/docs/DEMO_SCENARIO.md @@ -2,7 +2,7 @@ ## 0:00–0:35 — problem -„Jeden przemysłowy silnik ma 8, 12 lub 16 cylindrów. Mechanik dostaje 21 punktów widma dla każdego cylindra, ale potrzebuje decyzji: który cylinder sprawdzić, jakiej usterki szukać i jak pilna jest interwencja. ENGIN zamienia te pomiary w kolejkę konkretnych działań.” +„Jeden przemysłowy silnik ma 8, 12 lub 16 cylindrów. Mechanik dostaje 21 punktów widma dla każdego cylindra, ale potrzebuje szybkiej odpowiedzi: który cylinder odstaje, jaki typ usterki wskazuje sygnał i jak duże jest nasilenie. ENGIN porządkuje cylindry do kontroli i pokazuje dowód w widmie.” ## 0:35–1:15 — ekran silnika @@ -18,7 +18,7 @@ Komunikat: „Nie zmuszamy mechanika do czytania 21 wykresów. Najpierw widzi na 2. Wybierz najwyżej sklasyfikowany cylinder. 3. Opcjonalnie dodaj drugi cylinder do porównania. 4. Porównaj widmo z medianą pozostałych cylindrów tego samego silnika. -5. Wskaż priorytet, zaznaczone pasma, nasilenie i rekomendowany następny krok. +5. Wskaż priorytet, zaznaczone pasma, nasilenie i krótkie uzasadnienie diagnozy. Komunikat: „Referencją jest konkretny silnik, nie abstrakcyjna średnia całej floty. Dzięki temu wynik jest łatwiejszy do zweryfikowania przez człowieka.” @@ -35,7 +35,7 @@ Komunikat: „Wynik nie powstał przez przeciek między cylindrami tego samego s 1. Pokaż opcję „Wgraj plik CSV”, ale nie przełączaj jej, jeśli czas jest napięty. 2. Pokaż przyciski pobrania wyników oraz pełnej diagnostyki. -Zamknięcie: „ENGIN daje trzy rzeczy naraz: konkursowo skuteczny model, weryfikowalne uzasadnienie i prostą decyzję serwisową. Działa lokalnie na CPU i jest gotowy na podłączenie do warsztatowego procesu.” +Zamknięcie: „ENGIN daje trzy rzeczy naraz: skuteczną klasyfikację, weryfikowalne uzasadnienie i jasną kolejność kontroli cylindrów. Działa lokalnie na CPU i jest gotowy na podłączenie do warsztatowego procesu.” ## Plan awaryjny diff --git a/docs/JURY_QA.md b/docs/JURY_QA.md index cef61c2..130f649 100644 --- a/docs/JURY_QA.md +++ b/docs/JURY_QA.md @@ -28,7 +28,7 @@ Nie. Status jest deterministyczną regułą biznesową opartą bezpośrednio na ## Czy score modelu jest prawdopodobieństwem awarii? -Nie. To niekalibrowany wynik `predict_proba`, używany wyłącznie do diagnostyki technicznej. Główny widok mechanika go nie pokazuje; zamiast tego prezentuje diagnozę, nasilenie, priorytet i następny krok. Dla `unknown` utworzonego przez regułę OOD wartość pozostaje pusta. +Nie. To niekalibrowany wynik `predict_proba`, używany wyłącznie do diagnostyki technicznej. Główny widok mechanika go nie pokazuje; zamiast tego prezentuje diagnozę, nasilenie, priorytet oraz dowód w widmie i porównanie z pozostałymi cylindrami. Dla `unknown` utworzonego przez regułę OOD wartość pozostaje pusta. ## Co robi aplikacja przy błędnym pliku? diff --git a/engin/charts.py b/engin/charts.py index da35912..beb0048 100644 --- a/engin/charts.py +++ b/engin/charts.py @@ -28,7 +28,7 @@ def _base_layout( fig.update_layout( template=None, height=height, - margin=dict(l=24, r=24, t=margin_top, b=margin_bottom), + margin=dict(l=72, r=24, t=margin_top, b=margin_bottom), paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor=PLOT_BG, font=dict(color=TEXT, family="Inter, system-ui, sans-serif"), @@ -47,8 +47,8 @@ def _base_layout( title_font=dict(color=TEXT), zeroline=False, ) - fig.update_xaxes(**axis_style) - fig.update_yaxes(**axis_style) + fig.update_xaxes(**axis_style, automargin=True, title_standoff=12) + fig.update_yaxes(**axis_style, automargin=True, title_standoff=18) return fig diff --git a/tests/test_app_smoke.py b/tests/test_app_smoke.py index 6e16e95..534b8df 100644 --- a/tests/test_app_smoke.py +++ b/tests/test_app_smoke.py @@ -65,12 +65,19 @@ class StreamlitSmokeTests(unittest.TestCase): self.assertIn("Źródło decyzji", diagnostic_columns) self.assertNotIn("decision_source", diagnostic_columns) overview_table = next( - table for table in app.dataframe if "Priorytet" in table.value.columns + table for table in app.dataframe if "Kolejność" in table.value.columns ) self.assertEqual( list(overview_table.value.columns), - ["Cylinder", "Diagnoza", "Nasilenie", "Priorytet"], + [ + "Kolejność", + "Cylinder", + "Diagnoza", + "Nasilenie", + "Odchylenie [mV]", + ], ) + self.assertNotIn("Priorytet", overview_table.value.columns) def test_upload_mode_has_safe_empty_state(self) -> None: app = AppTest.from_file(str(ROOT / "app.py"), default_timeout=30).run() @@ -111,5 +118,7 @@ class StreamlitSmokeTests(unittest.TestCase): rendered = "\n".join(markdown.value for markdown in app.markdown) self.assertIn("CYLINDER 03", rendered) self.assertIn("### Wybór cylindra", rendered) + self.assertIn("#### Uzasadnienie diagnozy", rendered) + self.assertNotIn("#### Następny krok", rendered) self.assertNotIn("Select all", rendered) self.assertNotIn("You can select up to", rendered) diff --git a/tests/test_explainability.py b/tests/test_explainability.py index 7c7e7bd..264a76b 100644 --- a/tests/test_explainability.py +++ b/tests/test_explainability.py @@ -74,7 +74,15 @@ class ExplainabilityTests(unittest.TestCase): self.assertEqual(heatmap.layout.plot_bgcolor, "#101820") self.assertEqual(heatmap.data[0].colorbar.bgcolor, "rgba(0,0,0,0)") self.assertEqual(heatmap.data[0].colorbar.tickfont.color, "#dce8ee") - self.assertEqual(len(cylinder_spectrum(self.analysis, cylinder).data), 2) + spectrum = cylinder_spectrum(self.analysis, cylinder) + self.assertEqual(spectrum.layout.margin.l, 72) + self.assertTrue(spectrum.layout.yaxis.automargin) + self.assertEqual(spectrum.layout.yaxis.title.standoff, 18) + deviation = deviation_chart(self.analysis, cylinder) + self.assertEqual(deviation.layout.margin.l, 72) + self.assertTrue(deviation.layout.yaxis.automargin) + self.assertEqual(deviation.layout.yaxis.title.standoff, 18) + self.assertEqual(len(spectrum.data), 2) self.assertEqual( len(cylinder_spectrum(self.analysis, cylinder, compared).data), 5,