Skip to content

biwt.gui

The Qt layer. create_biwt_widget is the host entry point; everything else here is internal.

Note

Importing this module requires PyQt5 (pip install "biwt[gui]").

The factory

biwt.gui.walkthrough.create_biwt_widget

create_biwt_widget(
    biwt_input: BiwtInputSource,
    on_complete: Optional[
        Callable[[BiwtResult], None]
    ] = None,
    *,
    cell_template_paths=(),
    host_name: str = "Host",
    name_matches: Optional[
        Callable[[str, str], bool]
    ] = None,
    name_match_cutoff: float = 0.85
) -> BioinformaticsWalkthrough

Create and return a BIWT walkthrough widget, suitable for embedding or use as a popup.

The widget does not close itself on completion — the host is responsible for closing or resetting it if desired.

Parameters:

Name Type Description Default
biwt_input BiwtInputSource

A BiwtInput, or a zero-argument callable returning one. Pass the callable when the host outlives one run — BIWT calls it at the start of each run, so a domain or cell-type list the user changed after the widget was built is picked up without the host having to push an update. The resolved value is snapshotted for the run: nothing the host does mid-run can move the domain the cells are being placed into.

required
on_complete Optional[Callable[[BiwtResult], None]]

Callback called with the BiwtResult when the user finishes the workflow. Not called if the user never finishes.

None
cell_template_paths

Paths to .toml files of cell templates, each mapping a template name to an opaque content string (for a PhysiCell host, an XML <phenotype> block). They seed the library listed on the landing screen, which the user then owns: files they add stay for every run, and files they remove — yours included — stay gone. That is why this is an argument here rather than a field of BiwtInput: re-reading it per run would put a removed file back every time.

BIWT ships no templates and never parses the contents; the files are read at the cell-templates step and nowhere else, so an unreadable one costs a warning there rather than anything at startup. str or os.PathLike; anything else is dropped with a warning.

()
host_name str

Your application's name, shown in BIWT's UI — the domain editor's "Use Domain" button, and the tag on your own cell types at the cell-templates step. Blank is replaced with "Host".

'Host'
name_matches Optional[Callable[[str, str], bool]]

Predicate deciding whether two strings name the same cell type, used for rename suggestions and template pre-selection. Supplying it replaces BIWT's default and name_match_cutoff. None means use biwt.core.cell_types.default_name_matches.

Must be deterministic and free of side effects: it is called once per (cell type, candidate) pair whenever matches are resolved, from inside widget construction and Qt signal handlers, and the total number of calls is not part of the contract.

None
name_match_cutoff float

Similarity threshold for that default only; ignored when name_matches is supplied.

0.85
Example

::

from biwt import BiwtInput, DomainSpec
from biwt.gui import create_biwt_widget

widget = create_biwt_widget(
    BiwtInput(
        preferred_domain=DomainSpec(xmin=-500, xmax=500,
                                    ymin=-500, ymax=500),
        host_cell_type_names=["default", "tumor", "immune"],
    ),
    on_complete=lambda result: print(result.coordinates.head()),
    host_name="My App",
    cell_template_paths=["/path/to/my_templates.toml"],
)
widget.show()

An embedded, long-lived host passes a callable instead, and BIWT reads it at the start of every run::

def host_input():
    return BiwtInput(preferred_domain=my_app.current_domain(),
                     host_cell_type_names=my_app.cell_type_names())

widget = create_biwt_widget(host_input, on_complete=save,
                            host_name="My App")

BIWT never writes to disk. To persist the result, do it in on_complete — e.g. result.to_csv("cells.csv").

Source code in src/biwt/gui/walkthrough.py
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
def create_biwt_widget(
    biwt_input: BiwtInputSource,
    on_complete: Optional[Callable[[BiwtResult], None]] = None,
    *,
    cell_template_paths=(),
    host_name: str = "Host",
    name_matches: Optional[Callable[[str, str], bool]] = None,
    name_match_cutoff: float = 0.85,
) -> BioinformaticsWalkthrough:
    """Create and return a BIWT walkthrough widget, suitable for embedding or use as a popup.

    The widget does not close itself on completion — the host is responsible
    for closing or resetting it if desired.

    Parameters
    ----------
    biwt_input:
        A ``BiwtInput``, or a zero-argument callable returning one.  Pass the
        callable when the host outlives one run — BIWT calls it at the start of
        each run, so a domain or cell-type list the user changed after the widget
        was built is picked up without the host having to push an update.  The
        resolved value is snapshotted for the run: nothing the host does mid-run
        can move the domain the cells are being placed into.
    on_complete:
        Callback called with the ``BiwtResult`` when the user finishes the
        workflow.  Not called if the user never finishes.
    cell_template_paths:
        Paths to ``.toml`` files of cell templates, each mapping a
        template name to an opaque content string (for a PhysiCell host, an XML
        ``<phenotype>`` block).  They seed the library listed on the landing
        screen, which the user then owns: files they add stay for every run, and
        files they remove — yours included — stay gone.  That is why this is an
        argument here rather than a field of ``BiwtInput``: re-reading it per run
        would put a removed file back every time.

        BIWT ships no templates and never parses the contents; the files are read
        at the cell-templates step and nowhere else, so an unreadable one costs a
        warning there rather than anything at startup.  ``str`` or ``os.PathLike``;
        anything else is dropped with a warning.
    host_name:
        Your application's name, shown in BIWT's UI — the domain editor's
        "Use <host_name> Domain" button, and the tag on your own cell types at
        the cell-templates step.  Blank is replaced with ``"Host"``.
    name_matches:
        Predicate deciding whether two strings name the same cell type, used for
        rename suggestions and template pre-selection.  Supplying it replaces
        BIWT's default **and** ``name_match_cutoff``.  ``None`` means use
        ``biwt.core.cell_types.default_name_matches``.

        Must be deterministic and free of side effects: it is called once per
        (cell type, candidate) pair whenever matches are resolved, from inside
        widget construction and Qt signal handlers, and the total number of calls
        is not part of the contract.
    name_match_cutoff:
        Similarity threshold for that default only; ignored when ``name_matches``
        is supplied.

    Example
    -------
    ::

        from biwt import BiwtInput, DomainSpec
        from biwt.gui import create_biwt_widget

        widget = create_biwt_widget(
            BiwtInput(
                preferred_domain=DomainSpec(xmin=-500, xmax=500,
                                            ymin=-500, ymax=500),
                host_cell_type_names=["default", "tumor", "immune"],
            ),
            on_complete=lambda result: print(result.coordinates.head()),
            host_name="My App",
            cell_template_paths=["/path/to/my_templates.toml"],
        )
        widget.show()

    An embedded, long-lived host passes a callable instead, and BIWT reads it
    at the start of every run::

        def host_input():
            return BiwtInput(preferred_domain=my_app.current_domain(),
                             host_cell_type_names=my_app.cell_type_names())

        widget = create_biwt_widget(host_input, on_complete=save,
                                    host_name="My App")

    BIWT never writes to disk.  To persist the result, do it in
    ``on_complete`` — e.g. ``result.to_csv("cells.csv")``.
    """
    return BioinformaticsWalkthrough(
        biwt_input=biwt_input,
        on_complete=on_complete,
        cell_template_paths=cell_template_paths,
        host_name=host_name,
        name_matches=name_matches,
        name_match_cutoff=name_match_cutoff,
    )

Session state

WalkthroughSession is the pure-Python state machine behind the widget — no Qt dependency. All the answers you give during the walkthrough accumulate here, and each step window reads and writes it. Internal.

biwt.gui.walkthrough.WalkthroughSession dataclass

WalkthroughSession(
    biwt_input: BiwtInput,
    data: Optional[BiwtData] = None,
    user_domain: Optional[DomainSpec] = None,
    data_domain: Optional[DomainSpec] = None,
    domain_accepted: bool = False,
    scale_factor: Optional[float] = None,
    apply_scale: bool = True,
    spatial_data: Optional[ndarray] = None,
    spatial_data_final: Optional[ndarray] = None,
    spatial_query_answer: Optional[bool] = None,
    spot_deconv_asked: bool = False,
    perform_spot_deconvolution: bool = False,
    cell_types_max: Optional[list] = None,
    cell_prob_feature_dicts: Optional[list] = None,
    cell_prob_feature_dicts_final: Optional[list] = None,
    current_column: Optional[str] = None,
    cell_types_original: Optional[list] = None,
    cell_types_list_original: Optional[list] = None,
    cell_type_dict_on_edit: Optional[dict] = None,
    intermediate_types: Optional[list] = None,
    intermediate_type_pre_image: Optional[dict] = None,
    cell_types_list_final: Optional[list] = None,
    cell_type_dict_on_rename: Optional[dict] = None,
    cell_types_final: Optional[list] = None,
    cell_counts: Optional[dict] = None,
    cell_counts_confirmed: bool = False,
    cell_volume: Optional[dict] = None,
    coords_by_type: dict = dict(),
    plotted_cell_types_per_spot: list = list(),
    positions_set: bool = False,
    template_library_paths: Optional[list] = None,
    cell_templates: dict = dict(),
    templates_assigned: bool = False,
)

Accumulates all data decisions made during the BIWT workflow.

This object is the single source of truth shared between the walkthrough controller and all step windows. No Qt objects are stored here.

Fields are populated progressively as the user advances through steps. None means "not yet determined".

biwt_input is the host's state for this run. The widget's own setup — its host name, its template library, its name matcher — is not here: it does not change between runs, and copying it into every session would invite the two to disagree.

biwt_input instance-attribute

biwt_input: BiwtInput

data class-attribute instance-attribute

data: Optional[BiwtData] = None

user_domain class-attribute instance-attribute

user_domain: Optional[DomainSpec] = None

data_domain class-attribute instance-attribute

data_domain: Optional[DomainSpec] = None

domain_accepted class-attribute instance-attribute

domain_accepted: bool = False

scale_factor class-attribute instance-attribute

scale_factor: Optional[float] = None

apply_scale class-attribute instance-attribute

apply_scale: bool = True

spatial_data class-attribute instance-attribute

spatial_data: Optional[ndarray] = None

spatial_data_final class-attribute instance-attribute

spatial_data_final: Optional[ndarray] = None

spatial_query_answer class-attribute instance-attribute

spatial_query_answer: Optional[bool] = None

spot_deconv_asked class-attribute instance-attribute

spot_deconv_asked: bool = False

perform_spot_deconvolution class-attribute instance-attribute

perform_spot_deconvolution: bool = False

cell_types_max class-attribute instance-attribute

cell_types_max: Optional[list] = None

cell_prob_feature_dicts class-attribute instance-attribute

cell_prob_feature_dicts: Optional[list] = None

cell_prob_feature_dicts_final class-attribute instance-attribute

cell_prob_feature_dicts_final: Optional[list] = None

current_column class-attribute instance-attribute

current_column: Optional[str] = None

cell_types_original class-attribute instance-attribute

cell_types_original: Optional[list] = None

cell_types_list_original class-attribute instance-attribute

cell_types_list_original: Optional[list] = None

cell_type_dict_on_edit class-attribute instance-attribute

cell_type_dict_on_edit: Optional[dict] = None

intermediate_types class-attribute instance-attribute

intermediate_types: Optional[list] = None

intermediate_type_pre_image class-attribute instance-attribute

intermediate_type_pre_image: Optional[dict] = None

cell_types_list_final class-attribute instance-attribute

cell_types_list_final: Optional[list] = None

cell_type_dict_on_rename class-attribute instance-attribute

cell_type_dict_on_rename: Optional[dict] = None

cell_types_final class-attribute instance-attribute

cell_types_final: Optional[list] = None

cell_counts class-attribute instance-attribute

cell_counts: Optional[dict] = None

cell_counts_confirmed class-attribute instance-attribute

cell_counts_confirmed: bool = False

cell_volume class-attribute instance-attribute

cell_volume: Optional[dict] = None

coords_by_type class-attribute instance-attribute

coords_by_type: dict = field(default_factory=dict)

plotted_cell_types_per_spot class-attribute instance-attribute

plotted_cell_types_per_spot: list = field(
    default_factory=list
)

positions_set class-attribute instance-attribute

positions_set: bool = False

template_library_paths class-attribute instance-attribute

template_library_paths: Optional[list] = None

cell_templates class-attribute instance-attribute

cell_templates: dict = field(default_factory=dict)

templates_assigned class-attribute instance-attribute

templates_assigned: bool = False

preferred_domain property

preferred_domain: DomainSpec

effective_domain property

effective_domain: DomainSpec

Domain to use for coordinate placement.

The host's domain until the user accepts an edited one in BIWT's own editor. There is deliberately no third value between them: a second latched copy of the host's domain is what let one dialog compute its mismatch warning against one box and resolve "Use Domain" against another.

data_has_z property

data_has_z: bool

True when the imported file supplied a third coordinate axis.

Decides whether the scale factor applies to z: a z the file measured converts like x and y, a synthesized slab has nothing to convert.

use_spatial_data property

use_spatial_data: bool

Whether cells are placed at their measured coordinates.

Derived, never stored, so it is always a real bool:

  • spot deconvolution is on → True (deconvolution is spatial)
  • the data has no coordinates → False (nothing to ask about)
  • otherwise → the user's SpatialQuery answer, False until they answer

The last case is safe because the SpatialQuery step runs before any consumer of this property is reachable.

effective_scale

effective_scale() -> float

Uniform factor applied to place cells (1.0 = no conversion).

The stored scale_factor (host-units per data unit) is applied only when apply_scale is on and a positive factor exists; otherwise cells are placed at their raw extent, centered.

Source code in src/biwt/gui/walkthrough.py
889
890
891
892
893
894
895
896
897
898
def effective_scale(self) -> float:
    """Uniform factor applied to place cells (``1.0`` = no conversion).

    The stored ``scale_factor`` (host-units per data unit) is applied only
    when ``apply_scale`` is on and a positive factor exists; otherwise cells
    are placed at their raw extent, centered.
    """
    if self.apply_scale and self.scale_factor and self.scale_factor > 0:
        return self.scale_factor
    return 1.0

collect_cell_type_data

collect_cell_type_data() -> None

Extract unique cell types from the selected obs column.

Source code in src/biwt/gui/walkthrough.py
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
def collect_cell_type_data(self) -> None:
    """Extract unique cell types from the selected obs column."""
    if self.current_column is None:
        raise ValueError(
            "collect_cell_type_data() needs current_column to be set by the "
            "cluster-column step; the spot-deconvolution path derives cell "
            "types from probability columns instead (see "
            "setup_spot_deconvolution_data)."
        )
    col_data = self.data.obs[self.current_column]
    # Stringified per cell as well as in the unique list: the rename mapping
    # is keyed by the unique (string) labels, so a numeric column — integer
    # Leiden/Louvain cluster ids, say — would otherwise match nothing and
    # silently drop every cell in apply_rename.
    self.cell_types_original = [str(ct) for ct in col_data.tolist()]
    self.cell_types_list_original = sorted(set(self.cell_types_original), key=alpha_key)

setup_spot_deconvolution_data

setup_spot_deconvolution_data() -> None

Build per-spot probability dicts from probability columns.

Source code in src/biwt/gui/walkthrough.py
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
def setup_spot_deconvolution_data(self) -> None:
    """Build per-spot probability dicts from probability columns."""
    prob_cols = self.data.probability_columns
    self.cell_types_list_original = sorted((
        {c.replace("_probability", "") for c in prob_cols}
    ), key=alpha_key)
    # Clamped before use, not just when the columns were selected: a raw NaN
    # wins argmax outright, so one bad spot picked its own cell type as the
    # spot's maximum and carried the NaN into the per-spot weights.
    prob_matrix = np.column_stack(
        [data_loader.clamp_probabilities(self.data.obs[c]) for c in prob_cols]
    )
    max_indices = prob_matrix.argmax(axis=1)
    cell_types = [c.replace("_probability", "") for c in prob_cols]
    self.cell_types_max = [cell_types[i] for i in max_indices]
    self.cell_prob_feature_dicts = [
        dict(zip(cell_types, prob_matrix[i]))
        for i in range(len(self.data.obs))
    ]

setup_spatial_data

setup_spatial_data() -> None

Extract raw spatial coordinates into self.spatial_data.

Source code in src/biwt/gui/walkthrough.py
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
def setup_spatial_data(self) -> None:
    """Extract raw spatial coordinates into self.spatial_data."""
    from biwt.core.domain import (
        _find_spatial_key, resolve_obs_coord_cols, build_obs_coords,
    )
    if self.data.obsm:
        key = _find_spatial_key(self.data.obsm)
        if key:
            arr = np.asarray(self.data.obsm[key])
            if arr.ndim == 2 and arr.shape[1] == 2:
                arr = np.column_stack([arr, np.zeros(len(arr))])
            self.spatial_data = arr
            return
    cols = list(self.data.obs.columns)
    x_col, y_col, z_col, is_image_coords = resolve_obs_coord_cols(cols)
    if x_col and y_col:
        xy = build_obs_coords(self.data.obs, x_col, y_col, z_col, is_image_coords)
        if xy.shape[1] == 2:
            xy = np.column_stack([xy, np.zeros(len(xy))])
        self.spatial_data = xy

reseed_derived_state

reseed_derived_state() -> None

Re-derive the bulk data that follows from the user's answers.

_STEP_FIELDS lists the fields whose values a step's user chose; anything derived from those choices belongs here instead, so that a downstream invalidation can wipe the choices without destroying what follows from the ones still standing. Called right after that invalidation and again before every step-predicate evaluation, so it must be idempotent, and it must never overwrite a user decision.

Not called from go_back_to_prev_window: that path can reuse cached windows, and repairing the session behind a live window would leave the two disagreeing.

Spot deconvolution dominates the body because it is the only answer in the walkthrough from which bulk data follows — three arrays over every spot. Every other step's answer is its own state. Implications that are merely logical, such as "deconvolution means spatial coordinates are in use", need no derivation at all: see the use_spatial_data property.

Source code in src/biwt/gui/walkthrough.py
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
def reseed_derived_state(self) -> None:
    """Re-derive the bulk data that follows from the user's answers.

    ``_STEP_FIELDS`` lists the fields whose values a step's user *chose*;
    anything **derived** from those choices belongs here instead, so that a
    downstream invalidation can wipe the choices without destroying what
    follows from the ones still standing.  Called right after that
    invalidation and again before every step-predicate evaluation, so it
    must be idempotent, and it must never overwrite a user decision.

    Not called from ``go_back_to_prev_window``: that path can reuse cached
    windows, and repairing the session behind a live window would leave the
    two disagreeing.

    Spot deconvolution dominates the body because it is the only answer in
    the walkthrough from which *bulk* data follows — three arrays over every
    spot.  Every other step's answer is its own state.  Implications that are
    merely logical, such as "deconvolution means spatial coordinates are in
    use", need no derivation at all: see the ``use_spatial_data`` property.
    """
    if self.data is None:
        return

    if self.perform_spot_deconvolution:
        # One function produces all three, so a single missing one means
        # the whole set has to be rebuilt.
        if (
            self.cell_types_list_original is None
            or self.cell_types_max is None
            or self.cell_prob_feature_dicts is None
        ):
            self.setup_spot_deconvolution_data()
    else:
        # Declining deconvolution after having accepted it must not leave
        # its per-spot artifacts behind.
        self.cell_types_max = None
        self.cell_prob_feature_dicts = None

    if self.data.has_spatial and self.spatial_data is None:
        self.setup_spatial_data()
        if self.spatial_data is None:
            log.warning(
                "Data reports spatial information in %s, but no coordinates "
                "could be extracted from it.", self.data.spatial_location,
            )

resolved_cell_type_map

resolved_cell_type_map() -> dict

Every original data label → its final name, or None if deleted.

Reads the two dicts the walkthrough actually fills: an original absent from cell_type_dict_on_rename was deleted at the edit step, since only surviving types have a pre-image there. Merged originals all resolve to the one name their group was given.

Source code in src/biwt/gui/walkthrough.py
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
def resolved_cell_type_map(self) -> dict:
    """Every original data label → its final name, or ``None`` if deleted.

    Reads the two dicts the walkthrough actually fills: an original absent
    from ``cell_type_dict_on_rename`` was deleted at the edit step, since only
    surviving types have a pre-image there.  Merged originals all resolve to
    the one name their group was given.
    """
    renamed = self.cell_type_dict_on_rename or {}
    return {orig: renamed.get(orig) for orig in (self.cell_types_list_original or [])}

compute_intermediate_types

compute_intermediate_types() -> None

Derive intermediate_types from cell_type_dict_on_edit.

Source code in src/biwt/gui/walkthrough.py
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
def compute_intermediate_types(self) -> None:
    """Derive intermediate_types from cell_type_dict_on_edit."""
    self.intermediate_types = []
    self.intermediate_type_pre_image = {}
    for orig in sorted(self.cell_type_dict_on_edit, key=alpha_key):
        intermed = self.cell_type_dict_on_edit[orig]
        if intermed is None:
            continue
        if intermed not in self.intermediate_types:
            self.intermediate_types.append(intermed)
            self.intermediate_type_pre_image[intermed] = [orig]
        else:
            self.intermediate_type_pre_image[intermed].append(orig)

apply_rename

apply_rename() -> None

Build cell_types_final, spatial_data_final, counts, and volumes.

Source code in src/biwt/gui/walkthrough.py
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
def apply_rename(self) -> None:
    """Build cell_types_final, spatial_data_final, counts, and volumes."""
    mapping = self.cell_type_dict_on_rename
    if self.perform_spot_deconvolution:
        updated_dicts, spatial_rows = [], []
        final_set = set()
        for prob_dict, sp in zip(self.cell_prob_feature_dicts, self.spatial_data):
            new_dict = {}
            for orig, prob in prob_dict.items():
                if orig not in mapping:
                    continue
                renamed = mapping[orig]
                new_dict[renamed] = new_dict.get(renamed, 0.0) + prob
                final_set.add(renamed)
            if sum(new_dict.values()) > 0:
                updated_dicts.append(new_dict)
                spatial_rows.append(sp)
        # Written to a separate field: this method re-runs on every Continue
        # from the rename step, and consuming its own output would rename
        # already-renamed keys and re-zip filtered dicts against the full
        # coordinate array.
        self.cell_prob_feature_dicts_final = updated_dicts
        self.spatial_data_final = np.vstack(spatial_rows) if spatial_rows else np.empty((0, 3))
        self.cell_types_final = sorted(final_set, key=alpha_key)
    else:
        pairs = [
            (mapping[ct], pos)
            for ct, pos in zip(self.cell_types_original,
                               self.spatial_data if self.use_spatial_data else [None] * len(self.cell_types_original))
            if ct in mapping
        ]
        if self.use_spatial_data:
            self.cell_types_final = [p[0] for p in pairs]
            # Every type deleted leaves nothing to stack, and np.vstack([])
            # raises — from a Qt slot, which aborts the host process.
            self.spatial_data_final = (
                np.vstack([p[1] for p in pairs]) if pairs
                else np.empty((0, self.spatial_data.shape[1]))
            )
        else:
            self.cell_types_final = [mapping[ct] for ct in self.cell_types_original if ct in mapping]

    self._count_final_cell_types()
    self._compute_cell_volumes()

_count_final_cell_types

_count_final_cell_types() -> None
Source code in src/biwt/gui/walkthrough.py
1097
1098
1099
1100
1101
def _count_final_cell_types(self) -> None:
    self.cell_counts = {ct: 0 for ct in self.cell_types_list_final}
    for ct in self.cell_types_final:
        if ct in self.cell_counts:
            self.cell_counts[ct] += 1

_compute_cell_volumes

_compute_cell_volumes() -> None

Default volume 2494 µm³ (PhysiCell default). TODO: accept host cell volumes via BiwtInput.

Source code in src/biwt/gui/walkthrough.py
1103
1104
1105
1106
def _compute_cell_volumes(self) -> None:
    """Default volume 2494 µm³ (PhysiCell default).
    TODO: accept host cell volumes via BiwtInput."""
    self.cell_volume = {ct: 2494.0 for ct in self.cell_types_list_final}

Step ordering

biwt.gui.walkthrough._step_predicates

_step_predicates(s: 'WalkthroughSession') -> list

Return [(predicate, label), ...] in walkthrough order.

Each predicate is a zero-arg callable returning bool. Each label is a stable string identifier for the step.

This function is the single source of truth for step-selection logic. BioinformaticsWalkthrough._build_next_window maps each label to its factory; tests import this function directly so they never duplicate the predicate logic.

Predicates read derived state, so s.reseed_derived_state() must run first — _build_next_window does that for every real evaluation.

Source code in src/biwt/gui/walkthrough.py
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
def _step_predicates(s: "WalkthroughSession") -> list:
    """Return ``[(predicate, label), ...]`` in walkthrough order.

    Each *predicate* is a zero-arg callable returning ``bool``.
    Each *label* is a stable string identifier for the step.

    This function is the single source of truth for step-selection logic.
    ``BioinformaticsWalkthrough._build_next_window`` maps each label to its
    factory; tests import this function directly so they never duplicate the
    predicate logic.

    Predicates read derived state, so ``s.reseed_derived_state()`` must run
    first — ``_build_next_window`` does that for every real evaluation.
    """
    return [
        (
            lambda: not s.spot_deconv_asked
                    and bool(s.data and s.data.probability_columns)
                    and bool(s.data and s.data.has_spatial),
            "SpotDeconvQuery",
        ),
        (
            lambda: s.current_column is None and not s.perform_spot_deconvolution,
            "ClusterColumn",
        ),
        (
            # Deconvolution implies spatial, so there is nothing left to ask.
            lambda: s.spatial_query_answer is None
                    and not s.perform_spot_deconvolution
                    and s.data is not None and s.data.has_spatial,
            "SpatialQuery",
        ),
        (
            lambda: s.cell_type_dict_on_edit is None,
            "EditCellTypes",
        ),
        (
            lambda: s.cell_types_list_final is None,
            "RenameCellTypes",
        ),
        (
            lambda: not s.use_spatial_data and not s.cell_counts_confirmed,
            "CellCounts",
        ),
        (
            lambda: not s.positions_set,
            "Positions",
        ),
        (
            lambda: not s.templates_assigned,
            "LoadCellTemplates",
        ),
    ]

The widget

biwt.gui.walkthrough.BioinformaticsWalkthrough

BioinformaticsWalkthrough(
    biwt_input: BiwtInputSource,
    on_complete: Optional[
        Callable[[BiwtResult], None]
    ] = None,
    *,
    cell_template_paths=(),
    host_name: str = "Host",
    name_matches: Optional[
        Callable[[str, str], bool]
    ] = None,
    name_match_cutoff: float = 0.85
)

Bases: QWidget

Top-level BIWT popup controller.

Parameters:

Name Type Description Default
biwt_input BiwtInputSource

The host's state, re-read at the start of every run (domain, cell-type names, etc.)

required
on_complete Optional[Callable[[BiwtResult], None]]

Callback receiving a BiwtResult when the user finishes. Called once, when the user finishes. There is no cancel callback: closing the widget is the host's own event to handle.

None
cell_template_paths

How this widget is set up, as opposed to what the host currently is — see :func:create_biwt_widget, which documents them.

()
host_name

How this widget is set up, as opposed to what the host currently is — see :func:create_biwt_widget, which documents them.

()
name_matches

How this widget is set up, as opposed to what the host currently is — see :func:create_biwt_widget, which documents them.

()
name_match_cutoff

How this widget is set up, as opposed to what the host currently is — see :func:create_biwt_widget, which documents them.

()
Source code in src/biwt/gui/walkthrough.py
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
def __init__(
    self,
    biwt_input: BiwtInputSource,
    on_complete: Optional[Callable[[BiwtResult], None]] = None,
    *,
    cell_template_paths=(),
    host_name: str = "Host",
    name_matches: Optional[Callable[[str, str], bool]] = None,
    name_match_cutoff: float = 0.85,
):
    super().__init__()
    self.setWindowTitle(f"BioInformatics WalkThrough (BIWT) v{__version__}")
    self.setWindowIcon(biwt_icon())
    self.setWindowFlags(Qt.Window)
    self.setAcceptDrops(True)

    if not isinstance(biwt_input, BiwtInput) and not callable(biwt_input):
        raise TypeError(
            "biwt_input must be a BiwtInput or a callable returning one, "
            f"not {type(biwt_input).__name__}"
        )
    self._host_input_source = biwt_input
    self._host_input_error = ""

    self.on_complete = on_complete or (lambda result: None)
    # Reaches the screen — the domain editor's "Use <host_name> Domain", and
    # the tag on the host's own cell types — so it cannot be blank.
    self.host_name = (host_name or "").strip() or "Host"
    # Resolved once: the host cannot change it, and every match this widget
    # ever resolves has to be scored the same way.
    self.name_matcher = resolve_name_matcher(name_matches, name_match_cutoff)
    # This first resolution only seeds the home screen and stands in until
    # the first import; nothing derived, placed, or handed back to the host
    # comes out of it.
    self.session = WalkthroughSession(
        biwt_input=self._resolve_host_input() or BiwtInput()
    )

    # Window stack management.
    # Two-list model mirrors the original biwt_tab.py design:
    #
    #   window_history — windows already visited (most-recent last).
    #                    Going back pops from the end of this list.
    #   window_future  — windows that were visited but are still valid
    #                    (not stale).  Going forward reuses these instead
    #                    of rebuilding, so a back→forward without any
    #                    user change preserves window state.
    #   stale_futures  — set to True by any widget that changes session
    #                    state.  When True, going forward discards
    #                    window_future and builds fresh windows instead.
    self.window_history: list[QWidget] = []
    self.window_future: list[QWidget] = []
    self.stale_futures: bool = False
    self.current_window_idx: int = -1
    self.window: Optional[QWidget] = None

    # The template library the next run starts from.  The host seeds it here,
    # once, and it is the user's from then on: the files they add stay, and the
    # ones they remove — the host's included — stay gone.  That is why it is
    # not part of BiwtInput, which is re-read at every run and would put a
    # removed file back each time.  Contents are nobody's business here: the
    # landing screen names files, and the step is where they are read.
    self._library_paths = core_templates.normalize_template_paths(
        cell_template_paths
    )

    self._build_home_ui()

advance

advance() -> None

Move forward one step.

If stale_futures is True (user changed something on the current step), reset all downstream session fields and build a fresh window. If stale_futures is False and cached future windows exist, reuse the next one so that back→forward without changes preserves state.

Source code in src/biwt/gui/walkthrough.py
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
def advance(self) -> None:
    """Move forward one step.

    If ``stale_futures`` is True (user changed something on the current
    step), reset all downstream session fields and build a fresh window.
    If ``stale_futures`` is False and cached future windows exist, reuse
    the next one so that back→forward without changes preserves state.
    """
    if self.window is not None:
        self.window_history.append(self.window)
        self.window.hide()

    if self.stale_futures or not self.window_future:
        if self.stale_futures:
            label = getattr(self.window, "_step_label", None)
            if label:
                self._invalidate_downstream_of(label)
            self.window_future.clear()
        next_win = self._build_next_window()
        if next_win is None:
            self._finish()
            return
    else:
        # Reuse cached future — user went back without changing anything
        next_win = self.window_future.pop(0)

    self.stale_futures = False
    self.current_window_idx += 1
    self.window = next_win
    self.window.show()

go_back_to_prev_window

go_back_to_prev_window() -> None

Return to the previous step.

If the current window has been marked stale (user changed something), discard all future windows — they must be rebuilt when the user advances again. Otherwise, save the current window to the front of window_future so it can be reused on the next forward step.

Source code in src/biwt/gui/walkthrough.py
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
def go_back_to_prev_window(self) -> None:
    """Return to the previous step.

    If the current window has been marked stale (user changed something),
    discard all future windows — they must be rebuilt when the user
    advances again.  Otherwise, save the current window to the front of
    ``window_future`` so it can be reused on the next forward step.
    """
    if not self.window_history:
        return

    if self.window is not None:
        self.window.hide()
        if self.stale_futures:
            # Invalidate here, against the step being returned to, rather than
            # leaving the flag set for the next advance(): carried upstream it
            # would fire again from an earlier step and wipe the committed
            # answers of the ones in between.
            self.window_future.clear()
            label = getattr(self.window_history[-1], "_step_label", None)
            if label:
                self._invalidate_downstream_of(label)
            self.stale_futures = False
        else:
            # Current window is still valid — preserve as next future
            self.window_future.insert(0, self.window)

    self.current_window_idx -= 1
    self.window = self.window_history.pop()
    self.window.show()