Compare commits

..

No commits in common. "main" and "prod-hardening" have entirely different histories.

7 changed files with 21 additions and 42 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,
- 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 krótkie uzasadnienie diagnozy,
- priorytet kontroli, trzy najbardziej anomalne pasma oraz rekomendowany następny krok,
- 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.

24
app.py
View File

@ -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 wyjaśnienie oparte na widmie.</p>
<p>Diagnoza cylindra, nasilenie i następny krok.</p>
</div>
""",
unsafe_allow_html=True,
@ -192,21 +192,12 @@ 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["Odchylenie [mV]"] = ranking["anomaly_score_mean_abs_mv"].round(1)
ranking["Priorytet"] = ranking["priority_display"]
st.dataframe(
ranking[
[
"Kolejność",
"Cylinder",
"Diagnoza",
"Nasilenie",
"Odchylenie [mV]",
]
],
ranking[["Cylinder", "Diagnoza", "Nasilenie", "Priorytet"]],
hide_index=True,
width="stretch",
height=360,
@ -262,8 +253,13 @@ def _render_cylinder_detail(
config=PLOTLY_CONFIG,
)
st.markdown("#### Uzasadnienie diagnozy")
st.write(explanation.reason)
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)
def _store_selected_cylinder(state_key: str, widget_key: str) -> None:

View File

@ -2,7 +2,7 @@
## 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 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.”
„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ń.”
## 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.
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 krótkie uzasadnienie diagnozy.
5. Wskaż priorytet, zaznaczone pasma, nasilenie i rekomendowany następny krok.
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: skuteczną klasyfikację, weryfikowalne uzasadnienie i jasną kolejność kontroli cylindrów. Działa lokalnie na CPU i jest gotowy na podłączenie do warsztatowego procesu.”
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.”
## 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?
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.
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.
## Co robi aplikacja przy błędnym pliku?

View File

@ -28,7 +28,7 @@ def _base_layout(
fig.update_layout(
template=None,
height=height,
margin=dict(l=72, r=24, t=margin_top, b=margin_bottom),
margin=dict(l=24, 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, automargin=True, title_standoff=12)
fig.update_yaxes(**axis_style, automargin=True, title_standoff=18)
fig.update_xaxes(**axis_style)
fig.update_yaxes(**axis_style)
return fig

View File

@ -65,19 +65,12 @@ 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 "Kolejność" in table.value.columns
table for table in app.dataframe if "Priorytet" in table.value.columns
)
self.assertEqual(
list(overview_table.value.columns),
[
"Kolejność",
"Cylinder",
"Diagnoza",
"Nasilenie",
"Odchylenie [mV]",
],
["Cylinder", "Diagnoza", "Nasilenie", "Priorytet"],
)
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()
@ -118,7 +111,5 @@ 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)

View File

@ -74,15 +74,7 @@ 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")
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).data), 2)
self.assertEqual(
len(cylinder_spectrum(self.analysis, cylinder, compared).data),
5,