> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bioptimus.com/llms.txt
> Use this file to discover all available pages before exploring further.

# bioptimus.data.cohort

Cohort manifest for pairing WSIs with bulk RNA and clinical metadata.

Provides [`Cohort`](/sdk-reference/data/cohort#cohort), a typed registry that tracks the mapping between patients, wsis, bulk RNA samples, timepoints, labels, and arbitrary clinical metadata.  [`WSIRecord`](/sdk-reference/data/cohort#wsirecord) is the single source of truth for each WSI — including per-model outputs and processing status.

Construction paths:

1. **From a user-provided CSV** via [`Cohort.from_csv`](/sdk-reference/data/cohort#from_csv).
2. **Auto-matching from directories** via [`Cohort.from_directories`](/sdk-reference/data/cohort#from_directories).
3. **From a YAML manifest** via [`Cohort.load`](/sdk-reference/data/cohort#load) (for resume).

The cohort can be serialised to YAML via [`Cohort.save`](/sdk-reference/data/cohort#save) and loaded back for full reproducibility and resume support.

**Example**:

```python theme={null}
from bioptimus.data.cohort import Cohort

# Auto-match from directories:
cohort = Cohort.from_directories(
    wsi_dir="/data/wsis",
    bulk_rna_dir="/data/rna",
)

# Or load a saved manifest (resume):
cohort = Cohort.load(".output/brca/run_1/manifest.yaml")

# Add labels post-hoc:
cohort.add_labels({"patient_001": {"mutation": "BRAF"}})

# Iterate:
for record in cohort:
    print(record.patient_id, record.wsi_path)
```

## ModelOutput

```python theme={null}
@dataclass
class ModelOutput()
```

Per-model output record for a single WSI.

**Attributes**:

* `model_name` - Name of the model that produced this output.
* `embedding_path` - Path to the embedding output file.
* `prediction_path` - Path to the prediction output file.
* `tiles_csv_path` - Path to the tile coordinates CSV.
* `tiles_dir` - Directory containing exported tile images.
* `modalities` - Input modalities used for this output (e.g. `["image"]` or `["image", "bulk_rna"]`).
* `timestamp` - ISO 8601 string of when the output was produced.
* `status` - Processing status (`"pending"`, `"done"`, `"error"`).
* `error` - Error message if status is `"error"`.
* `base_dir` - Manifest directory that relative output paths are anchored to for existence checks.  Set by [`Cohort.load`](/sdk-reference/data/cohort#load) and never serialised.  `None` resolves relative paths against the current working directory.

***

#### is\_done

```python theme={null}
@property
def is_done() -> bool
```

Returns `True` if status is done.

***

#### has\_embedding

```python theme={null}
@property
def has_embedding() -> bool
```

Returns `True` if an embedding path is set and exists.

***

#### has\_prediction

```python theme={null}
@property
def has_prediction() -> bool
```

Returns `True` if a prediction path is set and exists.

***

#### set\_stage\_output

```python theme={null}
def set_stage_output(stage: InferenceMode | str,
                     modalities: list[str],
                     path: Path,
                     timestamp: str | None = None) -> None
```

Records an output for a specific stage and modality combo.

Updates both the per-modality tracking dict and the top-level convenience fields (`embedding_path` / `prediction_path`). The combined [`InferenceMode.PREDICTION_WITH_EMBEDDING`](/sdk-reference/inference/schemas#prediction_with_embedding) stage records the single output file under both fields, since it carries the prediction and the embedding together.

<ParamField body="stage">
  The output stage as an [`InferenceMode`](/sdk-reference/inference/schemas#inferencemode) (or a mode string; legacy short spellings are accepted).
</ParamField>

<ParamField body="modalities">
  Modalities used for this output.
</ParamField>

<ParamField body="path">
  Output file path.
</ParamField>

<ParamField body="timestamp">
  ISO 8601 timestamp.
</ParamField>

***

#### get\_stage\_path

```python theme={null}
def get_stage_path(stage: InferenceMode | str,
                   modalities: list[str] | None = None) -> Path | None
```

Returns the output path for a stage and modality combo.

For the combined stage both path fields point at the same file, so the embedding field is returned.

<ParamField body="stage">
  The output stage as an [`InferenceMode`](/sdk-reference/inference/schemas#inferencemode) (or a mode string; legacy short spellings are accepted).
</ParamField>

<ParamField body="modalities">
  Modality combination to look up. Falls back to the top-level path when `None`.
</ParamField>

**Returns**:

Path resolved against `base_dir`, or `None` if not
recorded.

***

#### to\_dict

```python theme={null}
def to_dict() -> dict[str, Any]
```

Serialises to a plain dict for YAML output.

**Returns**:

A dictionary representation of the model output.

***

#### from\_dict

```python theme={null}
@classmethod
def from_dict(cls,
              data: dict[str, Any],
              base_dir: Path | None = None) -> ModelOutput
```

Constructs from a plain dict (YAML round-trip).

<ParamField body="data">
  Dictionary with serialised model output fields.
</ParamField>

<ParamField body="base_dir">
  Manifest directory to anchor relative paths to for existence checks.
</ParamField>

**Returns**:

A new [`ModelOutput`](/sdk-reference/data/cohort#modeloutput) instance.

## WSIRecord

```python theme={null}
@dataclass
class WSIRecord()
```

Single WSI entry — the single source of truth.

Tracks inputs (WSI, RNA, mask), per-model outputs, and arbitrary annotations for one WSI within a cohort.

**Attributes**:

* `patient_id` - Unique patient identifier.
* `wsi_id` - Unique WSI identifier (typically the filename stem).
* `wsi_path` - Path to the WSI file.
* `bulk_rna_id` - Identifier for the paired bulk RNA sample.
* `bulk_rna_path` - Path to the bulk RNA CSV/TSV file.
* `mask_path` - Path to a tissue mask (pre-computed or cached).
* `mask_timestamp` - When the mask was computed/discovered.
* `timepoint` - Temporal ordering label (e.g. `"t0"`, `"t1"`).
* `outputs` - Per-model output records keyed by model name.
* `labels` - Arbitrary categorical annotations.
* `metadata` - Arbitrary clinical/treatment metadata.
* `base_dir` - Manifest directory that relative paths are anchored to for existence checks.  Set by [`Cohort.load`](/sdk-reference/data/cohort#load) and never serialised.  `None` resolves relative paths against the current working directory.

***

#### get\_output

```python theme={null}
def get_output(model_name: str) -> ModelOutput
```

Returns the output for *model\_name*, creating if absent.

<ParamField body="model_name">
  Model identifier string.
</ParamField>

**Returns**:

The [`ModelOutput`](/sdk-reference/data/cohort#modeloutput) for the given model.

***

#### is\_stage\_done

```python theme={null}
def is_stage_done(model_name: str, stage: InferenceMode | str) -> bool
```

Checks whether a stage output exists on disk.

When `modality_outputs` are tracked, the check is scoped to the record's current `available_modalities` so that linking new data (e.g. bulk RNA) automatically surfaces pending work without requiring `force=True`. The combined stage requires both its path fields, so it is done only once both outputs exist.

<ParamField body="model_name">
  Model identifier.
</ParamField>

<ParamField body="stage">
  The output stage as an [`InferenceMode`](/sdk-reference/inference/schemas#inferencemode) (or a mode string; legacy short spellings are accepted).
</ParamField>

**Returns**:

`True` if the stage's output path(s) exist on disk.

***

#### has\_mask

```python theme={null}
@property
def has_mask() -> bool
```

Returns `True` if a mask path is set and exists on disk.

***

#### merge\_saved\_outputs

```python theme={null}
def merge_saved_outputs(saved: WSIRecord) -> int
```

Merges output/mask metadata from a saved (manifest) record.

Used to resume after a restart: outputs recorded in a previous manifest are folded into this live record, which may already have some outputs from the current process. WSI/RNA paths are left untouched so the user's current directory layout wins.

Adopts `saved.base_dir` unconditionally so that relative paths merged from *saved* keep resolving against the manifest directory rather than the process CWD, regardless of whether a mask is present (existence checks read the record's `base_dir`, not the output's).

<ParamField body="saved">
  The corresponding record loaded from a manifest.
</ParamField>

**Returns**:

Number of output entries merged in (new models plus new
modality combinations added to an existing model output).

***

#### available\_modalities

```python theme={null}
@property
def available_modalities() -> list[str]
```

Returns the input modalities available for this record.

Always includes `"image"`.  Includes `"bulk_rna"` when `bulk_rna_path` is set.

***

#### to\_dict

```python theme={null}
def to_dict() -> dict[str, Any]
```

Serialises to a plain dict for YAML output.

**Returns**:

A dictionary representation of the WSI record.

***

#### from\_dict

```python theme={null}
@classmethod
def from_dict(cls,
              data: dict[str, Any],
              base_dir: Path | None = None) -> WSIRecord
```

Constructs from a plain dict (YAML round-trip).

<ParamField body="data">
  Dictionary with serialised WSI record fields.
</ParamField>

<ParamField body="base_dir">
  Manifest directory to anchor relative paths to for existence checks.
</ParamField>

**Returns**:

A new [`WSIRecord`](/sdk-reference/data/cohort#wsirecord) instance.

## PatientRecord

```python theme={null}
@dataclass
class PatientRecord()
```

Groups all WSIs belonging to a single patient.

**Attributes**:

* `patient_id` - Unique patient identifier.
* `wsis` - Ordered list of WSI records (by timepoint).
* `labels` - Patient-level categorical annotations.
* `metadata` - Patient-level clinical metadata.

## Cohort

```python theme={null}
class Cohort(records: list[WSIRecord] | None = None)
```

Typed cohort manifest — single source of truth for an experiment.

A cohort is an ordered collection of [`WSIRecord`](/sdk-reference/data/cohort#wsirecord) entries grouped by patient.  It stores all information needed to reproduce and resume an inference run: file locations, pairing logic, timepoints, per-model outputs, labels, and clinical metadata.

Construction: Use [`from_csv`](/sdk-reference/data/cohort#from_csv), [`from_directories`](/sdk-reference/data/cohort#from_directories), or [`load`](/sdk-reference/data/cohort#load) rather than calling the constructor directly.

<ParamField body="records">
  Initial list of [`WSIRecord`](/sdk-reference/data/cohort#wsirecord) entries.
</ParamField>

***

#### from\_csv

```python theme={null}
@classmethod
@log_failures
def from_csv(cls,
             path: str | Path,
             *,
             wsi_dir: str | Path | None = None,
             bulk_rna_dir: str | Path | None = None,
             columns: dict[str, str] | None = None) -> Cohort
```

Creates a cohort from a user-provided CSV manifest.

Required columns:

* `wsi_id`: WSI identifier (filename stem or full name).

Optional columns:

* `patient_id`: if absent, derived from `wsi_id`.
* `bulk_rna_id`: paired RNA sample identifier.
* `timepoint`: temporal label.
* `wsi_path`: explicit path override.
* `bulk_rna_path`: explicit path override.
* Any other columns are treated as labels if prefixed
  with `label_` or as metadata otherwise.

When *wsi\_dir* or *bulk\_rna\_dir* are provided, paths are resolved by joining the directory with the corresponding ID (plus matching extension found on disk).

The *columns* dict maps canonical field names to the actual CSV column names.  Only the keys that differ from the defaults need to be specified.  For example:

Recognised canonical keys: `wsi_id`, `patient_id`, `bulk_rna_id`, `timepoint`, `wsi_path`, `bulk_rna_path`.

```python theme={null}
Cohort.from_csv(
    "cohort.csv",
    columns={
        "wsi_id": "imageid",
        "patient_id": "subject_id",
    },
)
```

<ParamField body="path">
  Path to the CSV manifest.
</ParamField>

<ParamField body="wsi_dir">
  Directory to resolve WSI paths from.
</ParamField>

<ParamField body="bulk_rna_dir">
  Directory to resolve bulk RNA paths from.
</ParamField>

<ParamField body="columns">
  Mapping of canonical field names to actual CSV column names. Unmapped fields fall back to their canonical name.
</ParamField>

**Returns**:

A populated [`Cohort`](/sdk-reference/data/cohort#cohort).

***

#### from\_directories

```python theme={null}
@classmethod
@log_failures
def from_directories(
        cls,
        wsi_dir: str | Path,
        bulk_rna_dir: str | Path | None = None,
        *,
        mask_dir: str | Path | None = None,
        patient_id_fn: Callable[[str], str] | None = None) -> Cohort
```

Creates a cohort by auto-matching files from directories.

WSI and bulk RNA files are paired by **exact filename stem** equality (case-sensitive).  Patient IDs default to the shared stem.  When multiple wsis share the same patient ID, they are assigned sequential timepoints (`t0`, `t1`, ...) in alphabetical order.

<ParamField body="wsi_dir">
  Directory containing WSI files.
</ParamField>

<ParamField body="bulk_rna_dir">
  Directory containing bulk RNA files. When `None`, wsis are registered without RNA.
</ParamField>

<ParamField body="mask_dir">
  Directory containing pre-computed masks (PNG files whose stem matches the WSI stem).
</ParamField>

<ParamField body="patient_id_fn">
  Optional callable that maps a filename stem to a patient ID.
</ParamField>

**Returns**:

A populated [`Cohort`](/sdk-reference/data/cohort#cohort).

***

#### save

```python theme={null}
@log_failures
def save(path: str | Path) -> Path
```

Persists the cohort manifest to a YAML file.

The YAML includes all inputs, per-model outputs, status, labels, and metadata — everything needed to resume.

<ParamField body="path">
  Destination file path.
</ParamField>

**Returns**:

The resolved output path.

***

#### load

```python theme={null}
@classmethod
@log_failures
def load(cls, path: str | Path) -> Cohort
```

Loads a cohort from a previously saved YAML manifest.

<ParamField body="path">
  Path to the YAML manifest file.
</ParamField>

**Returns**:

A fully-populated [`Cohort`](/sdk-reference/data/cohort#cohort).

***

#### upsert

```python theme={null}
def upsert(wsi_id: str,
           *,
           patient_id: str | None = None,
           wsi_path: Path | None = None,
           bulk_rna_path: Path | None = None,
           mask_path: Path | None = None,
           timepoint: str | None = None) -> WSIRecord
```

Inserts or updates a WSI record.

If a record with *wsi\_id* exists, updates the non-None fields.  Otherwise creates a new record.

<ParamField body="wsi_id">
  WSI identifier.
</ParamField>

<ParamField body="patient_id">
  Patient identifier (defaults to wsi\_id).
</ParamField>

<ParamField body="wsi_path">
  Path to WSI file.
</ParamField>

<ParamField body="bulk_rna_path">
  Path to bulk RNA file.
</ParamField>

<ParamField body="mask_path">
  Path to tissue mask.
</ParamField>

<ParamField body="timepoint">
  Timepoint label.
</ParamField>

**Returns**:

The inserted or updated [`WSIRecord`](/sdk-reference/data/cohort#wsirecord).

***

#### get\_wsi

```python theme={null}
def get_wsi(wsi_id: str) -> WSIRecord | None
```

Returns the record for *wsi\_id*, or `None`.

<ParamField body="wsi_id">
  Unique WSI identifier (typically the file stem).
</ParamField>

**Returns**:

The matching [`WSIRecord`](/sdk-reference/data/cohort#wsirecord), or `None` if not found.

***

#### link\_bulk\_rna

```python theme={null}
def link_bulk_rna(bulk_rna_dir: str | Path,
                  *,
                  extensions: set[str] | None = None,
                  separator: str | None = None,
                  gene_column: str | None = None,
                  value_column: str | None = None,
                  strip_version: bool = False) -> int
```

Links bulk RNA files to existing WSI records by stem.

Scans *bulk\_rna\_dir* for recognised RNA files and pairs them with existing records by exact filename-stem equality. Records that already have a `bulk_rna_path` are skipped.

This enables late-binding of bulk RNA data: build a cohort from WSIs first, then call this method to attach RNA when it becomes available.

<ParamField body="bulk_rna_dir">
  Directory containing bulk RNA files.
</ParamField>

<ParamField body="extensions">
  File extensions to match. Defaults to `{".csv", ".tsv"}`.
</ParamField>

<ParamField body="separator">
  Column delimiter for parsing. When `None` the separator is inferred from the file extension.
</ParamField>

<ParamField body="gene_column">
  Column name containing gene identifiers. Required (with `value_column`) for long-format files (e.g. GDC/TCGA gene quantification TSVs).
</ParamField>

<ParamField body="value_column">
  Column name containing expression values to read (e.g. `"tpm_unstranded"`).
</ParamField>

<ParamField body="strip_version">
  When `True`, strips version suffixes from gene identifiers (e.g. `ENSG…00003.15` → `ENSG…00003`).
</ParamField>

**Returns**:

Number of records that were linked.

***

#### add\_labels

```python theme={null}
def add_labels(labels: dict[str, dict[str, Any]],
               *,
               by: str = "patient_id") -> None
```

Adds categorical labels to records.

<ParamField body="labels">
  Mapping of `{identifier: {label_name: value}}`.
</ParamField>

<ParamField body="by">
  Key to match on — `"patient_id"` or `"wsi_id"`.
</ParamField>

***

#### add\_metadata

```python theme={null}
def add_metadata(metadata: dict[str, dict[str, Any]],
                 *,
                 by: str = "patient_id") -> None
```

Adds clinical/treatment metadata to records.

<ParamField body="metadata">
  Mapping of `{identifier: {field: value}}`.
</ParamField>

<ParamField body="by">
  Key to match on — `"patient_id"` or `"wsi_id"`.
</ParamField>

***

#### add\_labels\_from\_csv

```python theme={null}
@log_failures
def add_labels_from_csv(path: str | Path, *, by: str = "patient_id") -> None
```

Adds labels and metadata from a CSV file.

The CSV must have a column matching *by* (default `patient_id`).  Columns prefixed with `label_` are treated as labels; remaining columns as metadata.

<ParamField body="path">
  Path to the labels CSV.
</ParamField>

<ParamField body="by">
  Join key column name.
</ParamField>

***

#### num\_patients

```python theme={null}
@property
def num_patients() -> int
```

Number of unique patients in the cohort.

***

#### patients

```python theme={null}
@property
def patients() -> dict[str, PatientRecord]
```

Patient-indexed view of the cohort.

***

#### get\_patient

```python theme={null}
def get_patient(patient_id: str) -> PatientRecord
```

Returns the [`PatientRecord`](/sdk-reference/data/cohort#patientrecord) for a given patient.

<ParamField body="patient_id">
  Patient identifier string.
</ParamField>

**Returns**:

The matching [`PatientRecord`](/sdk-reference/data/cohort#patientrecord).

**Raises**:

* `KeyError` - If *patient\_id* is not found.

***

#### wsi\_ids

```python theme={null}
@property
def wsi_ids() -> list[str]
```

Ordered list of all WSI IDs.

***

#### patient\_ids

```python theme={null}
@property
def patient_ids() -> list[str]
```

Ordered list of unique patient IDs.

***

#### wsi\_paths

```python theme={null}
@property
def wsi_paths() -> list[Path | None]
```

Ordered list of WSI paths (may contain `None`).

***

#### pending

```python theme={null}
def pending(model_name: str, stage: InferenceMode | str) -> list[WSIRecord]
```

Returns wsis that have not completed a stage for a model.

Checks whether the output file exists on disk.  Records with missing WSI paths are excluded.

<ParamField body="model_name">
  Model identifier.
</ParamField>

<ParamField body="stage">
  The output stage as an [`InferenceMode`](/sdk-reference/inference/schemas#inferencemode) (or a mode string; legacy short spellings are accepted).
</ParamField>

**Returns**:

List of [`WSIRecord`](/sdk-reference/data/cohort#wsirecord) entries still needing
processing.

***

#### to\_csv

```python theme={null}
@log_failures
def to_csv(path: str | Path) -> Path
```

Writes input columns to a CSV file (labels + metadata).

For full persistence including outputs, use [`save`](/sdk-reference/data/cohort#save).

<ParamField body="path">
  Destination file path.
</ParamField>

**Returns**:

The resolved output path.

***

#### summary

```python theme={null}
def summary() -> str
```

Returns a human-readable summary of the cohort.

**Returns**:

Multi-line summary string.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.