Skip to content

The API contract

Three dataclasses in biwt.types define everything that crosses the host boundary.

For generated signatures see the API reference.

DomainSpec — the simulation box

DomainSpec(
    xmin=-500.0, xmax=500.0,
    ymin=-500.0, ymax=500.0,
    zmin=-10.0,  zmax=10.0,
    source="host",
    units="micron",
)

Passed in as BiwtInput.preferred_domain and returned out as BiwtResult.domain_used.

units

Defaults to "micron", the PhysiCell convention. BIWT uses this for two things: labelling the domain editor fields (micron/data unit), and flagging potential mismatches.

Non-micron hosts: a known gap

The Visium scale factor BIWT auto-detects is in µm per pixel. If your units is something else, that seeded value is not converted and will be wrong. Users can override it manually. Until this is fixed, tell your users to expect it if you work in other units.

source

Records how the spec was determined, so you can tell whether your domain survived:

biwt.types.DomainSource names the values, so you need not hand-type strings:

DomainSource Value Meaning
HOST "host" The domain you passed in
DATA "data" The data's own extent, however it was found
USER "user" Bounds the user typed in the domain editor
DEFAULT "default" No usable domain: none was passed, or the one passed was degenerate or non-finite and BIWT substituted its ±500 µm × ±10 µm box

Check result.domain_used.source in your handler. If it is not HOST, the domain changed during the walkthrough and your application's configured domain no longer matches the initial conditions you just received.

The domain editor derives this from the bounds rather than asserting it: HOST if they match the domain you passed, DATA if they are the data's own extent, USER only if they are neither. The dialog pre-fills the data extent whenever the data's coordinates are in use, so accepting it unchanged reports DATA.

Convenience members

width, height, depth are derived properties. is_2d is true when the z extent is at most one default PhysiCell voxel (20 µm). DomainSpec.default() builds the fallback.

BiwtInput — host to BIWT

BiwtInput(
    preferred_domain=domain,              # optional; defaults to ±500 × ±500 × ±10 µm
    host_cell_type_names=[],              # optional
)

Both fields have defaults, so BiwtInput() is valid.

Two questions, both about what your application currently holds: which cell types it defines, and what domain it wants. Everything else BIWT needs from a host is widget setup — set once, not re-asked per run.

When BIWT reads it

BIWT resolves its input at each import and holds a copy for that run, so a host change mid-run is not seen until the next one.

If your host outlives one run — an embedded tab rather than a popup — pass a zero-argument callable returning a BiwtInput (BiwtInputSource) instead of an instance, and you need no refresh hook:

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")

Keep it cheap and free of side effects; it runs inside the import path. If it raises, or returns anything that is not a BiwtInput, BIWT logs it and refuses the import — nothing is loaded, and the user is told your application could not supply its settings.

preferred_domain is the domain BIWT places into unless the user overrides it in the domain editor. It defaults to DomainSpec.default() — the ±500 µm × ±10 µm box from the PhysiCell XML defaults, the same fallback BIWT already used internally when it could not infer a domain from the data. Pass your own if your application has a meaningful one.

host_cell_type_names — the cell types your application already defines; optional, and never binding on the user. Used for rename suggestions and as candidates at the cell-templates step, where assigning one comes back marked HOST_SOURCE (see cell_templates below). A name defined both ways resolves to the host.

Widget setup

create_biwt_widget takes everything that is not your application's current state: its name, its template library, and how it decides two strings name the same cell type. All are read once, when the widget is built. None of them changes between runs, and the library is one the user edits, so re-asking per run would either be pointless or undo their work.

widget = create_biwt_widget(
    host_input,
    on_complete=save,
    host_name="My App",
    cell_template_paths=["/path/to/templates.toml"],
)

cell_template_paths — paths to TOML files, each mapping a template name to its content. The content is opaque to BIWT: read as text, never parsed, handed back verbatim.

BIWT ships no templates, so these files are the template library — pass them if you want the cell-templates step to offer anything. They seed the library listed on the landing screen, which the user owns from then on: files they add stay for every run, files they remove — yours included — stay gone. The result reports each template's source path either way.

Paths may be str or os.PathLike; anything else is dropped with a warning. The files are read at the cell-templates step and nowhere else, so an unreadable one costs a warning dialog there rather than anything at startup. A non-string value in the file (a stray [section] header, a number) is rejected with a message naming the key. Templates and name matching covers the file rules and a worked assembly example.

host_name — appears in the domain editor as Use <host_name> Domain. Set it; the default "Host" reads like a placeholder.

name_matches — a Callable[[str, str], bool] 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.

BIWT calls it once per (cell type, candidate) pair every time it resolves matches — at the cell-templates step that is one call per cell type per template — and it re-resolves whenever a template file is loaded or an auto-match button is pressed. Two requirements follow:

  • Deterministic. The same pair must always get the same answer — the call count is not part of the contract, so anything varying between calls can make two identical rows disagree.
  • No side effects. It runs inside widget construction and Qt signal handlers, so mutating the session, showing UI, or doing I/O from it fires at moments you did not choose.

name_match_cutoff — similarity threshold for BIWT's default matcher only; ignored when name_matches is given. Templates and name matching spells that default out, with the cases it rejects and the one gap it does not cover.

BiwtResult — BIWT to host

BiwtResult(
    coordinates=df,                  # DataFrame: x, y, z, type
    cell_type_map={...},             # original label -> final name | None
    domain_used=domain,              # DomainSpec actually applied
    cell_templates={},               # cell type -> (path, name, content)
)

coordinates — one row per placed cell, columns ["x", "y", "z", "type"]. The header is type, not cell_type, matching PhysiCell's CSV convention. 2D data has z = 0.0.

cell_type_map — every original label mapped to its final name, with None for deleted types.

domain_used — see source above.

cell_templates — the templates the user assigned, mapping each final cell-type name to (path, name, content): the absolute path of the .toml file it came from, its key in that file, and that key's value verbatim, surrounding whitespace included.

One value of path is not a path: HOST_SOURCE ("<host>") means the user picked one of the names you passed in host_cell_type_names, i.e. a cell type you already define. content is then "", so check the marker before using it — see HOST_SOURCE for the check to write.

Assembling anything out of that is yours to do — BIWT generates no XML; see templates and name matching for a worked example. Types the user left unassigned are absent, so {} is normal (the step has a Skip button and a per-type (none) option): check membership per type rather than assuming full coverage. Two types may carry the same name from different files, so path and name together identify a template while name alone does not.

to_csv(path)

A convenience for hosts that just want the file written:

result.to_csv("config/cells.csv")

Writes only the four PhysiCell columns, no index. The result carries no path field: BIWT does not choose an output location.

Reserved fields

substrate_data, gene_expression, and spatial_metadata are named in the docstring as future expansion but are not populated and not currently attributes. Do not code against them yet.

A complete minimal host

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow

from biwt.gui.walkthrough import create_biwt_widget
from biwt.types import BiwtInput, DomainSource, DomainSpec


def on_complete(result):
    print(f"{len(result.coordinates)} cells, "
          f"{result.coordinates['type'].nunique()} types")

    if result.domain_used.source != DomainSource.HOST:
        print(f"note: domain changed to {result.domain_used.source}")

    result.to_csv("cells.csv")
    for cell_type, (path, name, content) in result.cell_templates.items():
        print(f"{cell_type}: template {name!r} from {path}")


app = QApplication(sys.argv)
window = QMainWindow()
window.setCentralWidget(create_biwt_widget(
    BiwtInput(
        preferred_domain=DomainSpec(xmin=-500, xmax=500, ymin=-500, ymax=500),
        host_cell_type_names=["default", "tumor", "immune"],
    ),
    on_complete=on_complete,
    host_name="My App",
))
window.show()
sys.exit(app.exec_())

Stability

biwt.types and create_biwt_widget are the public API and changes to them will be treated as breaking. Everything under biwt.core and biwt.gui.windows is internal — useful to read, but not a contract.