Rugnux live page: serial pre-pass and GPU analysis

Where the run is a serial one analysed on the GPU, the page shows its
pre-pass (state, images held, duration), the beam centre and distance
stated and decided with the shift, the masked shadow and bad pixels, and
the indexed fraction and analysis rate, in place of the rotation headline
and result. Rotation runs render as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqXYdJKPUFLoeAchjzv3x4
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-11-02 20:22:34 +01:00
1 parent 4a54e0c6f5
commit c78e6b99be
2 files changed
+107 -5

No files matched your search

+8 -5
View File
@@ -8,6 +8,7 @@ import EmptyState from "./EmptyState";
import RugnuxTimeline from "./RugnuxTimeline";
import RugnuxSettings from "./RugnuxSettings";
import RugnuxHero from "./RugnuxHero";
import RugnuxStills from "./RugnuxStills";
import {CutAlert, lastImageTime, ModelScores, ProbeAlert, RunMessage, StateChip, StepTable, VerdictAlert} from "./RugnuxParts";
import BoltIcon from "@mui/icons-material/Bolt";
import {lazy, Suspense} from "react";
@@ -152,25 +153,27 @@ function RugnuxPage({measuring}: { measuring: boolean }) {
text="Dataset token required (key button in the top bar)"/></Paper>;
else if (isError || s === undefined || s.state === 'Idle')
live = <Paper><EmptyState icon={<BoltIcon fontSize="inherit"/>}
text="No rugnux run yet - start a rotation collection with live processing enabled"/></Paper>;
text="No rugnux run yet - start a collection with live processing enabled"/></Paper>;
else
live = (
<Stack spacing={2}>
<Header s={s}/>
<RugnuxHero s={s} modelLoaded={settings?.model_loaded === true}/>
{s.stills
? <Section title="Serial · pre-pass and GPU analysis"><RugnuxStills st={s.stills}/></Section>
: <RugnuxHero s={s} modelLoaded={settings?.model_loaded === true}/>}
<CutAlert s={s}/>
<ProbeAlert s={s} large/>
<Section title="Timeline">
<RugnuxTimeline s={s} deadline_s={settings?.deadline_s ?? 0}/>
</Section>
{s.model && s.state !== 'Running' && <ModelSection s={s}/>}
<Box sx={{display: "grid", gridTemplateColumns: {xs: "1fr", lg: "minmax(400px, 1fr) 2fr"}, gap: 2,
<Box sx={{display: "grid", gridTemplateColumns: {xs: "1fr", lg: s.stills ? "1fr" : "minmax(400px, 1fr) 2fr"}, gap: 2,
alignItems: "start"}}>
<Section title="Processing">
<CurrentStep s={s}/>
<StepTable steps={s.steps}/>
</Section>
<Section title="Result">
{!s.stills && <Section title="Result">
{s.verdict
? <VerdictAlert s={s}/>
: <Typography color="text.secondary">
@@ -180,7 +183,7 @@ function RugnuxPage({measuring}: { measuring: boolean }) {
<Stack spacing={1} sx={{mt: s.warnings.length > 0 ? 2 : 0}}>
{s.warnings.map((w, i) => <Alert key={i} severity="warning">{w}</Alert>)}
</Stack>
</Section>
</Section>}
</Box>
</Stack>
);
+99
View File
@@ -0,0 +1,99 @@
import {Box, Chip, LinearProgress, Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography} from "@mui/material";
import {grey, indigo} from "@mui/material/colors";
import {rugnux_live_stills} from "../client";
// Serial frames converted by the FPGA and analysed on the GPU: the pre-pass on the first images - the beam
// centre, distance and mask the whole collection is analysed with - and the analysis since.
const PREPASS_COLOUR = {Holding: "default", Running: "primary", Done: "success", Failed: "error"} as const;
function Tile({label, value, sub}: { label: string, value: string, sub?: string }) {
return (
<Box sx={{p: 1.5, borderRadius: 2, bgcolor: indigo[50], border: "1px solid", borderColor: indigo[100]}}>
<Typography sx={{fontSize: "0.75rem", fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase",
color: "primary.main"}}>
{label}
</Typography>
<Typography sx={{fontSize: "1.2rem", fontWeight: 700, color: "primary.dark"}}>{value}</Typography>
{sub && <Typography sx={{fontSize: "0.85rem", color: "text.secondary"}}>{sub}</Typography>}
</Box>
);
}
function signed(x: number, digits: number) {
return (x >= 0 ? "+" : "") + x.toFixed(digits);
}
function RugnuxStills({st}: { st: rugnux_live_stills }) {
const decided = st.beam_x_pxl !== undefined && st.beam_y_pxl !== undefined;
const shift = decided ? Math.hypot(st.beam_x_pxl! - st.file_beam_x_pxl, st.beam_y_pxl! - st.file_beam_y_pxl) : undefined;
const indexed = st.images_analysed > 0 ? 100 * st.images_indexed / st.images_analysed : undefined;
const fmt = (n?: number) => n === undefined ? "—" : n.toLocaleString();
return (
<Stack spacing={2}>
<Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap" useFlexGap>
<Typography sx={{fontWeight: 700, fontSize: "1.25rem"}}>Pre-pass</Typography>
<Chip label={st.prepass} color={PREPASS_COLOUR[st.prepass]}/>
<Typography color="text.secondary">
{st.prepass === "Holding" && `holding ${st.images_held} / ${st.images_needed} images`}
{st.prepass === "Running" && `on the first ${st.images_held} images`}
{st.prepass_s !== undefined && `on ${st.images_held} images in ${st.prepass_s.toFixed(1)} s`}
{st.prepass === "Failed" && " · the stated beam centre and mask are used"}
</Typography>
</Stack>
{st.prepass === "Holding" &&
<LinearProgress variant="determinate" value={100 * st.images_held / Math.max(1, st.images_needed)}
sx={{height: 10, borderRadius: 5}}/>}
{st.prepass === "Running" && <LinearProgress sx={{height: 10, borderRadius: 5}}/>}
<Table size="small">
<TableHead>
<TableRow>
<TableCell/>
<TableCell align="right">Stated</TableCell>
<TableCell align="right">Pre-pass</TableCell>
<TableCell align="right">Shift</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell component="th" scope="row">Beam centre (px)</TableCell>
<TableCell align="right">{st.file_beam_x_pxl.toFixed(2)}, {st.file_beam_y_pxl.toFixed(2)}</TableCell>
<TableCell align="right" sx={{fontWeight: 700}}>
{decided ? `${st.beam_x_pxl!.toFixed(2)}, ${st.beam_y_pxl!.toFixed(2)}` : "—"}
</TableCell>
<TableCell align="right">
{decided ? `${signed(st.beam_x_pxl! - st.file_beam_x_pxl, 2)}, ${signed(st.beam_y_pxl! - st.file_beam_y_pxl, 2)} (${shift!.toFixed(2)} px)` : "—"}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">Detector distance (mm)</TableCell>
<TableCell align="right">{st.file_distance_mm.toFixed(2)}</TableCell>
<TableCell align="right" sx={{fontWeight: 700}}>
{st.distance_mm !== undefined ? st.distance_mm.toFixed(2) : "—"}
</TableCell>
<TableCell align="right">
{st.distance_mm !== undefined ? `${signed(st.distance_mm - st.file_distance_mm, 2)} mm` : "—"}
</TableCell>
</TableRow>
</TableBody>
</Table>
<Box sx={{display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 1.5}}>
<Tile label="Beam-stop shadow" value={`${fmt(st.shadow_pixels)} px`}/>
<Tile label="Bad pixels" value={`${fmt(st.bad_pixels)} px`} sub="hot, dead, error value"/>
<Tile label="Indexed" value={indexed !== undefined ? `${indexed.toFixed(1)} %` : "—"}
sub={`${st.images_indexed.toLocaleString()} of ${st.images_analysed.toLocaleString()} images`}/>
<Tile label="GPU analysis" value={st.analysis_rate_hz !== undefined ? `${st.analysis_rate_hz.toFixed(0)} Hz` : "—"}
sub="since the pre-pass"/>
</Box>
<Typography sx={{fontSize: "0.85rem", color: grey[600]}}>
Every image is analysed with the pre-pass's beam centre, distance and mask; images arriving
during the pre-pass are held and analysed after it.
</Typography>
</Stack>
);
}
export default RugnuxStills;