Skip to content

prediction_setup_screen

anomaly_match_ui.screens.prediction_setup_screen

Prediction setup screen — choose model, search folder, and output folder.

PredictionSetupScreen(app)

Bases: BaseScreen

Setup screen for configuring a prediction run.

Provides file choosers for the model checkpoint, search folder, and output folder. Validates selections and counts source images in a background thread before enabling the start button.

Parameters:

Name Type Description Default
app AnomalyMatchApp

The parent application instance used for navigation.

required
Source code in anomaly_match_ui/screens/prediction_setup_screen.py
def __init__(self, app: AnomalyMatchApp) -> None:
    super().__init__(app)
    self._cfg = BackendInterface.get_config()
    self._model_ok = False
    self._search_ok = False
    self._output_ok = False
    self._source_count: int | None = None
    self._search_type: DataSourceType | None = None
    self._counting = False
    self._counting_status: str = "Counting sources..."
    self._resume_count: int | None = None
    self._resume_compatible: bool | None = None
    self._resume_message: str = ""
    self._image_files: list[str] = []
    self._cutana_preview_error: str = ""
    # Normalisation summary read from the selected model checkpoint.  The
    # model is the source of truth — prediction overrides its decode
    # pipeline from the checkpoint — so the screen shows this read-only
    # rather than letting the user pick settings that could desync the run.
    self._model_norm: dict | None = None
    # Set when reading the selected model's metadata raised, so the overview
    # can say "model unreadable" rather than "select a model".
    self._model_norm_error: str | None = None
    self._log_sink = ScreenLogSink("PredictionSetupScreen")

build()

Build and return the prediction setup layout.

Returns:

Type Description
Widget

The root VBox widget for the setup screen.

Source code in anomaly_match_ui/screens/prediction_setup_screen.py
def build(self) -> widgets.Widget:
    """Build and return the prediction setup layout.

    Returns:
        The root VBox widget for the setup screen.
    """
    self._out = widgets.Output(
        layout=widgets.Layout(
            border="1px solid white",
            height="120px",
            background_color=BG_COLOR,
            overflow="auto",
        ),
    )
    header = HeaderWidget(self.app, self._out, show_back_button=True)

    # ── Model chooser ────────────────────────────────────────
    model_dir, model_name = initial_file(
        self._cfg.model_path,
        purpose="model_chooser",
        is_shipped_default=BackendInterface.is_shipped_default_path(
            "model_path", self._cfg.model_path
        ),
    )
    self._model_chooser = AMFileChooser(
        path=model_dir,
        filename=model_name,
        select_default=bool(model_name),
        filter_pattern="*.safetensors",
        title="Model checkpoint",
        purpose="model_chooser",
    )
    self._model_chooser.register_callback(self._on_model_change)

    # ── Search folder chooser ────────────────────────────────
    # Resolve once so auto-validation counts the same dir the
    # chooser displays — see TrainingSetupScreen.build for the
    # rationale (#429 follow-up).
    self._search_initial_path = initial_dir(
        self._cfg.prediction_search_dir, purpose="search_chooser"
    )
    search_has_default = bool(
        self._search_initial_path and os.path.isdir(self._search_initial_path)
    )
    self._search_chooser = AMFileChooser(
        path=self._search_initial_path,
        show_only_dirs=True,
        select_default=search_has_default,
        title="Search folder (images to scan)",
        purpose="search_chooser",
    )
    self._search_chooser.register_callback(self._on_search_change)

    # ── Output folder chooser ────────────────────────────────
    # Decide on the same path the chooser displays, as the search chooser
    # above does: deriving the flag from cfg while showing the resolved dir
    # leaves Start disabled next to a perfectly valid folder.  Only a real
    # resolution may be pre-selected: the cwd fallback is merely where the
    # chooser opens, and selecting it would make ``_on_start`` write the
    # cwd over the configured ``cfg.output_dir``.
    self._output_resolved_path = resolved_dir(self._cfg.output_dir, purpose="output_chooser")
    self._output_chooser = AMFileChooser(
        path=self._output_resolved_path or os.getcwd(),
        show_only_dirs=True,
        select_default=self._output_resolved_path is not None,
        title="Output folder (results destination)",
        purpose="output_chooser",
    )
    self._output_chooser.register_callback(self._on_output_change)

    # ── Preview grid ──────────────────────────────────────────
    self._preview_grid = PreviewGrid()

    # ── Normalisation overview (read-only) ────────────────────
    # Prediction must reuse the model's training normalisation (the
    # subprocess overrides its decode pipeline from the checkpoint), so
    # this is a read-only summary, not an editable control.  Populated
    # when a model is selected.
    self._norm_overview = widgets.HTML(value=self._render_norm_overview())

    # ── Validation panel + start button ──────────────────────
    self._validation_html = widgets.HTML(
        value=self._render_validation(),
        layout=widgets.Layout(padding="8px 0"),
    )

    self._start_btn = widgets.Button(
        description="Start Prediction",
        button_style="success",
        disabled=True,
        layout=widgets.Layout(width="200px", height="40px"),
        style={"font_size": "14px"},
    )
    self._start_btn.on_click(lambda _: self._on_start())

    # ── Layout ───────────────────────────────────────────────
    # Left: file choosers (constrained so preview gets space)
    left_panel = VBox(
        [
            self._model_chooser,
            self._search_chooser,
            self._output_chooser,
        ],
        layout=widgets.Layout(
            background_color=BG_COLOR,
            padding="12px",
            gap="8px",
            flex="0 1 auto",
            max_width="420px",
        ),
    )

    # Centre: image preview (flex so it grows with window width)
    centre_panel = VBox(
        [self._preview_grid.widget],
        layout=widgets.Layout(
            background_color=BG_COLOR,
            padding="8px",
            flex="1 1 auto",
            min_width="140px",
            border_left=f"1px solid {ESA_BLUE_DEEP}",
            border_right=f"1px solid {ESA_BLUE_DEEP}",
        ),
    )

    # Right: normalisation overview + validation + start
    right_panel = VBox(
        [
            self._norm_overview,
            self._validation_html,
            self._start_btn,
        ],
        layout=widgets.Layout(
            background_color=BG_COLOR,
            padding="8px 12px",
            gap="8px",
            width="380px",
            flex="0 0 auto",
        ),
    )

    form = HBox(
        [left_panel, centre_panel, right_panel],
        layout=widgets.Layout(background_color=BG_COLOR),
    )

    return VBox(
        [
            header.widget,
            form,
            self._out,
        ],
        layout=widgets.Layout(
            background_color=BG_COLOR,
            border=f"1px solid {ESA_BLUE_DEEP}",
            width="100%",
        ),
    )

on_enter()

Set up terminal output and auto-validate from config.

Source code in anomaly_match_ui/screens/prediction_setup_screen.py
def on_enter(self) -> None:
    """Set up terminal output and auto-validate from config."""
    BackendInterface.set_terminal_output(self._out)
    self._log_sink.attach(self._out)
    self._auto_validate_from_config()

on_leave()

Remove the widget sink when navigating away.

Source code in anomaly_match_ui/screens/prediction_setup_screen.py
def on_leave(self) -> None:
    """Remove the widget sink when navigating away."""
    self._log_sink.detach()