Compare commits
1 Commits
prod-harde
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5251e9629f |
@ -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.
|
||||
|
||||
22
app.py
22
app.py
@ -151,7 +151,7 @@ def _render_header() -> None:
|
||||
<div class="product-header">
|
||||
<div class="eyebrow">AESTEEL · DIAGNOSTYKA WTRYSKU DIESEL</div>
|
||||
<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>
|
||||
""",
|
||||
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.markdown("#### Uzasadnienie diagnozy")
|
||||
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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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?
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user