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:
""",
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,