ui: simplify diagnostics before demo
This commit is contained in:
parent
2eb1e293c8
commit
58653e38d4
@ -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.
|
||||||
|
|||||||
22
app.py
22
app.py
@ -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.markdown("#### Uzasadnienie")
|
|
||||||
st.write(explanation.reason)
|
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:
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 0:00–0:35 — problem
|
## 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
|
## 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.
|
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
|
||||||
|
|
||||||
|
|||||||
@ -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?
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user