ui: simplify diagnostics before demo
All checks were successful
ENGIN CI / Build, test and smoke (push) Successful in 1m1s

This commit is contained in:
Jakub Famulski 2 2026-08-25 16:38:18 +02:00
parent 2eb1e293c8
commit 5251e9629f
7 changed files with 42 additions and 21 deletions

View File

@ -45,7 +45,7 @@ rollbacku znajduje się w `docs/PRODUCTION_RUNBOOK.md`.
- status silnika, najwyższe rzeczywiste nasilenie i kolejka cylindrów do kontroli, - status silnika, najwyższe rzeczywiste nasilenie i kolejka cylindrów do kontroli,
- mapa odchyleń całego silnika ze stałą, porównywalną skalą, - mapa odchyleń całego silnika ze stałą, porównywalną skalą,
- porównanie widma cylindra z medianą pozostałych cylindrów tej samej jednostki, - 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, - pobieranie `predictions.csv` i rozszerzonej diagnostyki,
- ścisła walidacja CSV i bezpieczne komunikaty błędów, - ścisła walidacja CSV i bezpieczne komunikaty błędów,
- lokalne działanie na CPU, bez wysyłania danych do zewnętrznych usług. - lokalne działanie na CPU, bez wysyłania danych do zewnętrznych usług.

24
app.py
View File

@ -151,7 +151,7 @@ def _render_header() -> None:
<div class="product-header"> <div class="product-header">
<div class="eyebrow">AESTEEL · DIAGNOSTYKA WTRYSKU DIESEL</div> <div class="eyebrow">AESTEEL · DIAGNOSTYKA WTRYSKU DIESEL</div>
<h1>Konsola diagnostyczna ENGIN</h1> <h1>Konsola diagnostyczna ENGIN</h1>
<p>Diagnoza cylindra, nasilenie i następny krok.</p> <p>Diagnoza cylindra, nasilenie i wyjaśnienie oparte na widmie.</p>
</div> </div>
""", """,
unsafe_allow_html=True, unsafe_allow_html=True,
@ -192,12 +192,21 @@ def _render_engine_overview(analysis) -> None:
unsafe_allow_html=True, unsafe_allow_html=True,
) )
ranking = rank_cylinders(analysis).head(6).copy() 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["Cylinder"] = ranking["cylinder"].map(lambda value: f"C{int(value):02d}")
ranking["Diagnoza"] = ranking["label_display"] ranking["Diagnoza"] = ranking["label_display"]
ranking["Nasilenie"] = ranking["severity_display"] ranking["Nasilenie"] = ranking["severity_display"]
ranking["Priorytet"] = ranking["priority_display"] ranking["Odchylenie [mV]"] = ranking["anomaly_score_mean_abs_mv"].round(1)
st.dataframe( st.dataframe(
ranking[["Cylinder", "Diagnoza", "Nasilenie", "Priorytet"]], ranking[
[
"Kolejność",
"Cylinder",
"Diagnoza",
"Nasilenie",
"Odchylenie [mV]",
]
],
hide_index=True, hide_index=True,
width="stretch", width="stretch",
height=360, height=360,
@ -253,13 +262,8 @@ def _render_cylinder_detail(
config=PLOTLY_CONFIG, config=PLOTLY_CONFIG,
) )
why, next_step = st.columns(2, gap="large") st.markdown("#### Uzasadnienie diagnozy")
with why: st.write(explanation.reason)
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: def _store_selected_cylinder(state_key: str, widget_key: str) -> None:

View File

@ -2,7 +2,7 @@
## 0:000:35 — problem ## 0:000: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:351:15 — ekran silnika ## 0:351: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. 2. Wybierz najwyżej sklasyfikowany cylinder.
3. Opcjonalnie dodaj drugi cylinder do porównania. 3. Opcjonalnie dodaj drugi cylinder do porównania.
4. Porównaj widmo z medianą pozostałych cylindrów tego samego silnika. 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.” 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. 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. 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 ## Plan awaryjny

View File

@ -28,7 +28,7 @@ Nie. Status jest deterministyczną regułą biznesową opartą bezpośrednio na
## Czy score modelu jest prawdopodobieństwem awarii? ## 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? ## Co robi aplikacja przy błędnym pliku?

View File

@ -28,7 +28,7 @@ def _base_layout(
fig.update_layout( fig.update_layout(
template=None, template=None,
height=height, 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)", paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor=PLOT_BG, plot_bgcolor=PLOT_BG,
font=dict(color=TEXT, family="Inter, system-ui, sans-serif"), font=dict(color=TEXT, family="Inter, system-ui, sans-serif"),
@ -47,8 +47,8 @@ def _base_layout(
title_font=dict(color=TEXT), title_font=dict(color=TEXT),
zeroline=False, zeroline=False,
) )
fig.update_xaxes(**axis_style) fig.update_xaxes(**axis_style, automargin=True, title_standoff=12)
fig.update_yaxes(**axis_style) fig.update_yaxes(**axis_style, automargin=True, title_standoff=18)
return fig return fig

View File

@ -65,12 +65,19 @@ class StreamlitSmokeTests(unittest.TestCase):
self.assertIn("Źródło decyzji", diagnostic_columns) self.assertIn("Źródło decyzji", diagnostic_columns)
self.assertNotIn("decision_source", diagnostic_columns) self.assertNotIn("decision_source", diagnostic_columns)
overview_table = next( 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( self.assertEqual(
list(overview_table.value.columns), 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: def test_upload_mode_has_safe_empty_state(self) -> None:
app = AppTest.from_file(str(ROOT / "app.py"), default_timeout=30).run() 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) rendered = "\n".join(markdown.value for markdown in app.markdown)
self.assertIn("CYLINDER 03", rendered) self.assertIn("CYLINDER 03", rendered)
self.assertIn("### Wybór cylindra", 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("Select all", rendered)
self.assertNotIn("You can select up to", rendered) self.assertNotIn("You can select up to", rendered)

View File

@ -74,7 +74,15 @@ class ExplainabilityTests(unittest.TestCase):
self.assertEqual(heatmap.layout.plot_bgcolor, "#101820") 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.bgcolor, "rgba(0,0,0,0)")
self.assertEqual(heatmap.data[0].colorbar.tickfont.color, "#dce8ee") 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( self.assertEqual(
len(cylinder_spectrum(self.analysis, cylinder, compared).data), len(cylinder_spectrum(self.analysis, cylinder, compared).data),
5, 5,