Skip to content

kaptive.serotyping

Surface antigen serotyping and locus matching engine.

The kaptive.serotyping sub-package implements surface polysaccharide locus typing (such as Klebsiella K and O loci, Acinetobacter K and OC loci) by matching assembly contigs against reference locus databases, scoring gene presence, and evaluating locus integrity.

Exports:

  • Serotyper: Main serotyping execution engine (Serotyper).
  • SerotypingProblem: Problem definition pairing a genome assembly with a database (SerotypingProblem).
  • SerotypingResult: Comprehensive outcome of locus typing and gene scoring (SerotypingResult).
  • GeneState: Enumeration of gene call integrity states (GeneState).
  • GeneHits: Container for mapped gene alignment records (GeneHits).
  • LocusPieces: Container for assembly locus fragment matches (LocusPieces).
  • ReportRow: Base container for structured output report serialization (ReportRow).
  • KaptiveRow: Standard Kaptive TSV report format row (KaptiveRow).
  • Pha4geRow: PHA4GE-compliant tabular report format row (Pha4geRow).

Modules:

  • cli –

    Command line interface commands and exporter for serotyping.

  • core –

    Core engine for in silico serotyping of bacterial genome assemblies.

  • io –

    I/O formatting and TSV report generation for in silico serotyping results.

  • models –

    Data models and container classes for serotyping analysis.

Classes:

  • GeneHits –

    A high-performance SoA container for classified gene alignments.

  • GeneState –

    Mutually exclusive states for locus genes found in a genome assembly.

  • KaptiveRow –

    Report row representation matching the classic Kaptive TSV output format.

  • LocusPieces –

    A high-performance SoA container for bounding coordinates of locus fragments.

  • Pha4geRow –

    Report row representation adhering to Public Health Alliance for Genomic Epidemiology (PHA4GE) standards.

  • ReportRow –

    Abstract base class for tabular in silico serotyping report rows.

  • Serotyper –

    High-performance in silico serotyping engine for bacterial genome assemblies.

  • SerotypingProblem –

    Symbolic problems with the serotype call used for report formatting.

  • SerotypingResult –

    Efficient, immutable container representing an in silico serotyping call.

GeneHits dataclass

GeneHits(gene_indices: NDArray[int32], q_starts: NDArray[int32], q_ends: NDArray[int32], t_indices: NDArray[uint32], t_starts: NDArray[int32], t_ends: NDArray[int32], strands: NDArray[int8], is_expected: NDArray[bool_], is_inside: NDArray[bool_], is_extra: NDArray[bool_], expected_positions: NDArray[int32], expected_strands: NDArray[int8], gene_ids: NDArray[bytes_], cluster_names: NDArray[bytes_], product_descriptions: NDArray[bytes_], coverages: NDArray[float32])

              flowchart TD
              kaptive.serotyping.GeneHits[GeneHits]
              kaptive.core.collections.BatchedContainer[BatchedContainer]

                              kaptive.core.collections.BatchedContainer --> kaptive.serotyping.GeneHits
                


              click kaptive.serotyping.GeneHits href "" "kaptive.serotyping.GeneHits"
              click kaptive.core.collections.BatchedContainer href "" "kaptive.core.collections.BatchedContainer"
            

A high-performance SoA container for classified gene alignments.

Encapsulates parallel NumPy arrays and metadata tuples for gene alignments, enabling synchronized vectorised filtering and dynamic interval calculations. Inherits from BatchedContainer.

Attributes:

  • gene_indices (NDArray[int32]) –

    Global database gene indices.

  • q_starts (NDArray[int32]) –

    Alignment start positions on query contigs (0-indexed).

  • q_ends (NDArray[int32]) –

    Alignment end positions on query contigs (0-indexed).

  • t_indices (NDArray[uint32]) –

    Target contig indices in genome assembly.

  • t_starts (NDArray[int32]) –

    Alignment start positions on database reference genes.

  • t_ends (NDArray[int32]) –

    Alignment end positions on database reference genes.

  • strands (NDArray[int8]) –

    Alignment strand orientations (+1 or -1).

  • is_expected (NDArray[bool_]) –

    Boolean mask indicating expected locus genes.

  • is_inside (NDArray[bool_]) –

    Boolean mask indicating hits within locus boundaries.

  • is_extra (NDArray[bool_]) –

    Boolean mask indicating extra allowed genes.

  • expected_positions (NDArray[int32]) –

    Expected relative gene order positions.

  • expected_strands (NDArray[int8]) –

    Expected strand orientations (+1 or -1).

  • gene_ids (NDArray[bytes_]) –

    1D byte string array (S32) of gene identifier strings.

  • cluster_names (NDArray[bytes_]) –

    1D byte string array (S10) of gene cluster or family names.

  • product_descriptions (NDArray[bytes_]) –

    1D byte string array (S64) of functional gene product annotations.

  • coverages (NDArray[float32]) –

    Gene alignment coverage proportions.

Methods:

  • __getitem__ –

    Slice or boolean-mask all parallel array fields simultaneously.

  • __len__ –

    Return total number of gene hit alignments in container.

  • concat –

    Concatenate multiple GeneHits batches into a single container.

  • empty –

    Create an empty GeneHits container with zero-length arrays and empty tuples.

  • from_dict –

    Reconstruct a GeneHits container from a deserialized dictionary.

  • to_dict –

    Convert SoA array fields to a dictionary for JSON serialization.

frames property

frames: NDArray[int32]

Calculate reading frame offsets for query alignments.

Returns:

  • NDArray[int32] –

    npt.NDArray[np.int32]: Reading frame offsets calculated as (-q_starts) % 3.

q_intervals property

q_intervals: Intervals

Construct query genomic intervals container.

Returns:

  • Intervals ( Intervals ) –

    An Intervals object wrapper for q_starts, q_ends, and strands.

query_lengths property

query_lengths: NDArray[int32]

Calculate alignment spans on query assembly contigs.

Returns:

  • NDArray[int32] –

    npt.NDArray[np.int32]: Alignment spans calculated as q_ends - q_starts.

t_intervals property

t_intervals: Intervals

Construct database target intervals container.

Returns:

  • Intervals ( Intervals ) –

    An Intervals object wrapper for t_starts, t_ends, and strands.

target_lengths property

target_lengths: NDArray[int32]

Calculate alignment spans on database target references.

Returns:

  • NDArray[int32] –

    npt.NDArray[np.int32]: Alignment spans calculated as t_ends - t_starts.

__getitem__

__getitem__(item: Any) -> GeneHits

Slice or boolean-mask all parallel array fields simultaneously.

Parameters:

  • item

    (Any) –

    Slice, integer array, or boolean mask.

Returns:

Source code in src/kaptive/serotyping/models.py
def __getitem__(self, item: Any) -> "GeneHits":
    r"""Slice or boolean-mask all parallel array fields simultaneously.

    Args:
        item (Any): Slice, integer array, or boolean mask.

    Returns:
        GeneHits: A sliced [`GeneHits`][kaptive.serotyping.models.GeneHits] instance.
    """
    return GeneHits(
        gene_indices=self.gene_indices[item],
        q_starts=self.q_starts[item],
        q_ends=self.q_ends[item],
        t_indices=self.t_indices[item],
        t_starts=self.t_starts[item],
        t_ends=self.t_ends[item],
        strands=self.strands[item],
        is_expected=self.is_expected[item],
        is_inside=self.is_inside[item],
        is_extra=self.is_extra[item],
        expected_positions=self.expected_positions[item],
        expected_strands=self.expected_strands[item],
        gene_ids=self.gene_ids[item],
        cluster_names=self.cluster_names[item],
        product_descriptions=self.product_descriptions[item],
        coverages=self.coverages[item],
    )

__len__

__len__() -> int

Return total number of gene hit alignments in container.

Returns:

  • int ( int ) –

    Number of elements in parallel arrays.

Source code in src/kaptive/serotyping/models.py
def __len__(self) -> int:
    r"""Return total number of gene hit alignments in container.

    Returns:
        int: Number of elements in parallel arrays.
    """
    return len(self.gene_indices)

concat classmethod

concat(batches: Iterable[Self]) -> Self

Concatenate multiple GeneHits batches into a single container.

Parameters:

Returns:

Source code in src/kaptive/serotyping/models.py
@classmethod
def concat(cls, batches: Iterable[Self]) -> Self:  # type: ignore
    r"""Concatenate multiple `GeneHits` batches into a single container.

    Args:
        batches (Iterable[GeneHits]): An iterable of [`GeneHits`][kaptive.serotyping.models.GeneHits] instances.

    Returns:
        GeneHits: Combined [`GeneHits`][kaptive.serotyping.models.GeneHits] container.
    """
    batches_list = list(batches)
    if not batches_list:
        return cls.empty()  # type: ignore
    return cls(
        gene_indices=np.concatenate([b.gene_indices for b in batches_list]),
        q_starts=np.concatenate([b.q_starts for b in batches_list]),
        q_ends=np.concatenate([b.q_ends for b in batches_list]),
        t_indices=np.concatenate([b.t_indices for b in batches_list]),
        t_starts=np.concatenate([b.t_starts for b in batches_list]),
        t_ends=np.concatenate([b.t_ends for b in batches_list]),
        strands=np.concatenate([b.strands for b in batches_list]),
        is_expected=np.concatenate([b.is_expected for b in batches_list]),
        is_inside=np.concatenate([b.is_inside for b in batches_list]),
        is_extra=np.concatenate([b.is_extra for b in batches_list]),
        expected_positions=np.concatenate([b.expected_positions for b in batches_list]),
        expected_strands=np.concatenate([b.expected_strands for b in batches_list]),
        gene_ids=np.concatenate([b.gene_ids for b in batches_list]),
        cluster_names=np.concatenate([b.cluster_names for b in batches_list]),
        product_descriptions=np.concatenate([b.product_descriptions for b in batches_list]),
        coverages=np.concatenate([b.coverages for b in batches_list]),
    )

empty classmethod

empty() -> GeneHits

Create an empty GeneHits container with zero-length arrays and empty tuples.

Returns:

Source code in src/kaptive/serotyping/models.py
@classmethod
def empty(cls) -> "GeneHits":
    r"""Create an empty `GeneHits` container with zero-length arrays and empty tuples.

    Returns:
        GeneHits: An empty [`GeneHits`][kaptive.serotyping.models.GeneHits] instance.
    """
    return cls(
        np.empty(0, dtype=np.int32),
        np.empty(0, dtype=np.int32),
        np.empty(0, dtype=np.int32),
        np.empty(0, dtype=np.uint32),
        np.empty(0, dtype=np.int32),
        np.empty(0, dtype=np.int32),
        np.empty(0, dtype=np.int8),
        np.empty(0, dtype=bool),
        np.empty(0, dtype=bool),
        np.empty(0, dtype=bool),
        np.empty(0, dtype=np.int32),
        np.empty(0, dtype=np.int8),
        np.empty(0, dtype="S32"),
        np.empty(0, dtype="S10"),
        np.empty(0, dtype="S64"),
        np.empty(0, dtype=np.float32),
    )

from_dict classmethod

from_dict(data: dict[str, Any]) -> GeneHits

Reconstruct a GeneHits container from a deserialized dictionary.

Parameters:

  • data

    (dict[str, Any]) –

    Dictionary containing array data lists and tuple metadata.

Returns:

Source code in src/kaptive/serotyping/models.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "GeneHits":
    r"""Reconstruct a `GeneHits` container from a deserialized dictionary.

    Args:
        data (dict[str, Any]): Dictionary containing array data lists and tuple metadata.

    Returns:
        GeneHits: Reconstructed [`GeneHits`][kaptive.serotyping.models.GeneHits] instance.
    """

    def _to_bytes_array(val: Any, dtype: str) -> npt.NDArray[np.bytes_]:
        if val is None or len(val) == 0:
            return np.empty(0, dtype=dtype)
        if isinstance(val, np.ndarray) and val.dtype.kind in ("S", "a"):
            return val.astype(dtype)
        encoded = [x.encode("utf-8") if isinstance(x, str) else x for x in val]
        return np.array(encoded, dtype=dtype)

    return cls(
        gene_indices=np.array(data["gene_indices"], dtype=np.int32),
        q_starts=np.array(data["q_starts"], dtype=np.int32),
        q_ends=np.array(data["q_ends"], dtype=np.int32),
        t_indices=np.array(data["t_indices"], dtype=np.uint32),
        t_starts=np.array(data["t_starts"], dtype=np.int32),
        t_ends=np.array(data["t_ends"], dtype=np.int32),
        strands=np.array(data["strands"], dtype=np.int8),
        is_expected=np.array(data["is_expected"], dtype=bool),
        is_inside=np.array(data["is_inside"], dtype=bool),
        is_extra=np.array(data["is_extra"], dtype=bool),
        expected_positions=np.array(data.get("expected_positions", []), dtype=np.int32),
        expected_strands=np.array(data.get("expected_strands", []), dtype=np.int8),
        gene_ids=_to_bytes_array(data.get("gene_ids", []), "S32"),
        cluster_names=_to_bytes_array(data.get("cluster_names", []), "S10"),
        product_descriptions=_to_bytes_array(data.get("product_descriptions", []), "S64"),
        coverages=np.array(data.get("coverages", []), dtype=np.float32),
    )

to_dict

to_dict() -> dict[str, Any]

Convert SoA array fields to a dictionary for JSON serialization.

Returns:

  • dict[str, Any] –

    dict[str, Any]: Dictionary mapping field names to NumPy arrays and metadata tuples.

Source code in src/kaptive/serotyping/models.py
def to_dict(self) -> dict[str, Any]:
    r"""Convert SoA array fields to a dictionary for JSON serialization.

    Returns:
        dict[str, Any]: Dictionary mapping field names to NumPy arrays and metadata tuples.
    """
    d = {
        k: getattr(self, k)
        for k in (
            "gene_indices",
            "q_starts",
            "q_ends",
            "t_indices",
            "t_starts",
            "t_ends",
            "strands",
            "is_expected",
            "is_inside",
            "is_extra",
            "expected_positions",
            "expected_strands",
            "coverages",
        )
    }
    d["gene_ids"] = np.char.decode(self.gene_ids, "utf-8").tolist()
    d["cluster_names"] = np.char.decode(self.cluster_names, "utf-8").tolist()
    d["product_descriptions"] = np.char.decode(self.product_descriptions, "utf-8").tolist()
    return d

GeneState


              flowchart TD
              kaptive.serotyping.GeneState[GeneState]

              

              click kaptive.serotyping.GeneState href "" "kaptive.serotyping.GeneState"
            

Mutually exclusive states for locus genes found in a genome assembly.

Attributes:

  • NORMAL (int) –

    The gene was found intact as expected.

  • PARTIAL (int) –

    The gene was broken up over a contig edge.

  • TRUNCATED (int) –

    The gene does not form a complete amino acid sequence.

  • NOVEL (int) –

    The gene translation diverges significantly from the closest reference.

KaptiveRow dataclass

KaptiveRow(Kaptive_version: bytes, Database_name: bytes, Database_version: bytes, Assembly: bytes, Best_match_locus: bytes, Best_match_type: bytes, Match_confidence: bytes, Problems: bytes, Identity: bytes, Coverage: bytes, Length_discrepancy: bytes, Expected_genes_in_locus: bytes, Expected_genes_in_locus_details: bytes, Missing_expected_genes: bytes, Other_genes_in_locus: bytes, Other_genes_in_locus_details: bytes, Expected_genes_outside_locus: bytes, Expected_genes_outside_locus_details: bytes, Other_genes_outside_locus: bytes, Other_genes_outside_locus_details: bytes, Truncated_genes_details: bytes, Extra_genes_details: bytes)

              flowchart TD
              kaptive.serotyping.KaptiveRow[KaptiveRow]
              kaptive.serotyping.io.ReportRow[ReportRow]

                              kaptive.serotyping.io.ReportRow --> kaptive.serotyping.KaptiveRow
                


              click kaptive.serotyping.KaptiveRow href "" "kaptive.serotyping.KaptiveRow"
              click kaptive.serotyping.io.ReportRow href "" "kaptive.serotyping.io.ReportRow"
            

Report row representation matching the classic Kaptive TSV output format.

Encapsulates all summary statistics, locus match calls, problem flags, gene details, and coverage metrics for a single genome assembly in tab-separated binary format compatible with traditional Kaptive output parsers.

Attributes:

  • Kaptive_version (bytes) –

    The version of Kaptive used to perform serotyping.

  • Database_name (bytes) –

    Name of the reference database used for serotyping.

  • Database_version (bytes) –

    Version of the reference database used.

  • Assembly (bytes) –

    Identifier/filename of the analyzed genome assembly.

  • Best_match_locus (bytes) –

    Best matching reference locus type identifier.

  • Best_match_type (bytes) –

    Predicted serotype/phenotype call for the genome.

  • Match_confidence (bytes) –

    Confidence classification (b"Typeable" or b"Untypeable").

  • Problems (bytes) –

    Symbolic character flags representing SerotypingProblem locus match issues (?, +, -, *, !).

  • Identity (bytes) –

    Mean percentage amino acid identity across intact expected locus genes.

  • Coverage (bytes) –

    Percentage coverage of the best matching reference locus by assembly contigs.

  • Length_discrepancy (bytes) –

    Difference in base pairs between assembly locus length and reference locus length (or "n/a").

  • Expected_genes_in_locus (bytes) –

    Count and fraction of expected locus genes found inside locus boundary.

  • Expected_genes_in_locus_details (bytes) –

    Detailed identity and coverage specs for expected genes inside locus.

  • Missing_expected_genes (bytes) –

    Semicolon-separated names of expected genes not found.

  • Other_genes_in_locus (bytes) –

    Count of unexpected genes from other loci found inside locus boundary.

  • Other_genes_in_locus_details (bytes) –

    Detailed specs for unexpected genes inside locus.

  • Expected_genes_outside_locus (bytes) –

    Count and fraction of expected locus genes found outside locus boundary.

  • Expected_genes_outside_locus_details (bytes) –

    Detailed specs for expected genes found outside locus.

  • Other_genes_outside_locus (bytes) –

    Count of unexpected genes found outside locus boundary.

  • Other_genes_outside_locus_details (bytes) –

    Detailed specs for unexpected genes found outside locus.

  • Truncated_genes_details (bytes) –

    Detailed specs for truncated or partial genes.

  • Extra_genes_details (bytes) –

    Detailed specs for allowed extra database genes.

Note

Numbers beside gene names indicate percentage identity and percentage coverage of the gene in the genome.

Warning

You may sometimes see two copies of the same gene in the Expected_genes_in_locus_details column. These represent parts of the same gene split over contig boundaries.

Methods:

  • __bytes__ –

    Serialize the report row fields into a tab-separated binary TSV row.

  • from_result –

    Construct a classic KaptiveRow from a serotyping result.

  • header –

    Generate backwards-compatible column header bytes for classic Kaptive reports.

__bytes__

__bytes__() -> bytes

Serialize the report row fields into a tab-separated binary TSV row.

Returns:

  • bytes ( bytes ) –

    Tab-separated field values ending with a newline (b"\n").

Source code in src/kaptive/serotyping/io.py
def __bytes__(self) -> bytes:
    r"""Serialize the report row fields into a tab-separated binary TSV row.

    Returns:
        bytes: Tab-separated field values ending with a newline (`b"\n"`).
    """
    return b"\t".join(getattr(self, f.name) for f in fields(self)) + b"\n"

from_result classmethod

Construct a classic KaptiveRow from a serotyping result.

Calculates gene counts, percentage coverages, identity metrics, and problem symbol codes, formatting all fields into UTF-8 encoded bytes for backwards-compatible TSV output.

Parameters:

Returns:

  • KaptiveRow ( KaptiveRow ) –

    Formatted report row object.

Source code in src/kaptive/serotyping/io.py
@classmethod
def from_result(cls, result: SerotypingResult) -> "KaptiveRow":
    r"""Construct a classic [`KaptiveRow`][kaptive.serotyping.io.KaptiveRow] from a serotyping result.

    Calculates gene counts, percentage coverages, identity metrics, and problem symbol codes, formatting all fields
    into UTF-8 encoded bytes for backwards-compatible TSV output.

    Args:
        result (SerotypingResult): The serotyping call result.
            See [`SerotypingResult`][kaptive.serotyping.models.SerotypingResult].

    Returns:
        KaptiveRow: Formatted report row object.
    """
    hits = result.gene_hits
    states = result.gene_states

    # High-performance boolean masks
    in_loc = hits.is_inside
    out_loc = ~hits.is_inside
    exp = hits.is_expected
    extra = hits.is_extra
    unexp = ~exp & ~extra

    def _format_genes(mask: np.ndarray) -> bytes:
        r"""Helper to rapidly construct the details string for a specific masked subset of genes.

        Args:
            mask (np.ndarray): Boolean mask selecting the subset of gene hits to format.

        Returns:
            bytes: Semicolon-separated details string for the selected genes.
        """
        indices = np.where(mask)[0]
        if indices.size == 0:
            return b""

        details = []
        for i in indices:
            gene_name = result.gene_seqs.ids[i].encode("utf-8")
            parts = [
                gene_name,
                b"%.2f%%" % result.protein_identities[i],
                b"%.2f%%" % result.gene_hits.coverages[i],
            ]

            if states[i] == GeneState.PARTIAL.value:
                parts.append(b"partial")
            elif states[i] == GeneState.TRUNCATED.value:
                parts.append(b"truncated")
            elif states[i] == GeneState.NOVEL.value:
                parts.append(b"below_id_threshold")

            details.append(b",".join(parts))
        return b";".join(details)

    # Expected Inside
    mask_exp_in = in_loc & exp
    n_exp_in = len(np.unique(result.gene_hits.gene_indices[mask_exp_in]))

    # Expected Outside
    mask_exp_out = out_loc & exp
    n_exp_out = len(np.unique(result.gene_hits.gene_indices[mask_exp_out]))

    expected_total = n_exp_in + n_exp_out + len(result.missing_expected_genes)

    in_comp = (n_exp_in / expected_total * 100.0) if expected_total > 0 else 0.0
    exp_in_str = b"%d / %d (%.2f%%)" % (n_exp_in, expected_total, in_comp) if expected_total else b"0 / 0 (0.00%)"

    out_comp = (n_exp_out / expected_total * 100.0) if expected_total > 0 else 0.0
    exp_out_str = (
        b"%d / %d (%.2f%%)" % (n_exp_out, expected_total, out_comp) if expected_total else b"0 / 0 (0.00%)"
    )

    # Other counts
    n_unexp_in = len(np.unique(result.gene_hits.gene_indices[in_loc & unexp]))
    n_unexp_out = len(np.unique(result.gene_hits.gene_indices[out_loc & unexp]))

    return cls(
        Kaptive_version=result.kaptive_version.encode(),
        Database_name=result.database_name.encode(),
        Database_version=result.database_version.encode(),
        Assembly=result.genome.encode(),
        Best_match_locus=result.best_locus_name.encode(),
        Best_match_type=result.phenotype.encode(),
        Match_confidence=b"Typeable" if result.typeable else b"Untypeable",
        Problems=result.problems.to_symbols(),
        Identity=b"%.2f%%" % result.percent_identity,
        Coverage=b"%.2f%%" % result.percent_coverage,
        Length_discrepancy=b"n/a"
        if (result.length_discrepancy is None or np.isnan(result.length_discrepancy))
        else b"%d" % int(result.length_discrepancy),
        Expected_genes_in_locus=exp_in_str,
        Expected_genes_in_locus_details=_format_genes(mask_exp_in),
        Missing_expected_genes=b";".join(g.encode("utf-8") for g in result.missing_expected_genes),
        Other_genes_in_locus=b"%d" % n_unexp_in,
        Other_genes_in_locus_details=_format_genes(in_loc & unexp),
        Expected_genes_outside_locus=exp_out_str,
        Expected_genes_outside_locus_details=_format_genes(mask_exp_out),
        Other_genes_outside_locus=b"%d" % n_unexp_out,
        Other_genes_outside_locus_details=_format_genes(out_loc & unexp),
        Truncated_genes_details=_format_genes(
            (states == GeneState.TRUNCATED.value) | (states == GeneState.PARTIAL.value)
        ),
        Extra_genes_details=_format_genes(extra),
    )

header classmethod

header() -> bytes

Generate backwards-compatible column header bytes for classic Kaptive reports.

Replaces internal field name underscores with spaces and _details with , details to maintain exact compatibility with legacy Kaptive TSV headers.

Returns:

  • bytes ( bytes ) –

    Tab-separated legacy header line ending with a newline (b"\n").

Source code in src/kaptive/serotyping/io.py
@classmethod
def header(cls) -> bytes:
    r"""Generate backwards-compatible column header bytes for classic Kaptive reports.

    Replaces internal field name underscores with spaces and `_details` with `, details` to maintain exact
    compatibility with legacy Kaptive TSV headers.

    Returns:
        bytes: Tab-separated legacy header line ending with a newline (`b"\n"`).
    """
    headers = [f.name.encode("utf-8").replace(b"_details", b", details").replace(b"_", b" ") for f in fields(cls)]
    return b"\t".join(headers) + b"\n"

LocusPieces dataclass

LocusPieces(ctg_indices: NDArray[uint32], starts: NDArray[int32], ends: NDArray[int32], strands: NDArray[int8])

              flowchart TD
              kaptive.serotyping.LocusPieces[LocusPieces]
              kaptive.core.collections.BatchedContainer[BatchedContainer]

                              kaptive.core.collections.BatchedContainer --> kaptive.serotyping.LocusPieces
                


              click kaptive.serotyping.LocusPieces href "" "kaptive.serotyping.LocusPieces"
              click kaptive.core.collections.BatchedContainer href "" "kaptive.core.collections.BatchedContainer"
            

A high-performance SoA container for bounding coordinates of locus fragments.

Stores contig indices, coordinate spans, and strand directions for locus pieces when a locus is fragmented across multiple contigs. Inherits from BatchedContainer.

Attributes:

  • ctg_indices (NDArray[uint32]) –

    Target contig indices in assembly.

  • starts (NDArray[int32]) –

    Locus fragment start coordinates (0-indexed).

  • ends (NDArray[int32]) –

    Locus fragment end coordinates (0-indexed).

  • strands (NDArray[int8]) –

    Locus fragment strand orientations (+1 or -1).

Methods:

  • __getitem__ –

    Slice or array-mask all parallel fields of locus pieces simultaneously.

  • __len__ –

    Return total number of locus pieces in container.

  • concat –

    Concatenate multiple LocusPieces batches into a single container.

  • empty –

    Create an empty LocusPieces container with zero-length arrays.

  • from_dict –

    Reconstruct a LocusPieces container from a deserialized dictionary.

  • to_dict –

    Convert array fields to a dictionary for JSON serialization.

__getitem__

__getitem__(item: int | slice | NDArray[Any] | list[int]) -> Any | LocusPieces

Slice or array-mask all parallel fields of locus pieces simultaneously.

Parameters:

  • item

    (int | slice | NDArray | list) –

    Slice range, boolean mask, or index list.

Returns:

Raises:

Source code in src/kaptive/serotyping/models.py
def __getitem__(self, item: int | slice | npt.NDArray[Any] | list[int]) -> "Any | LocusPieces":
    r"""Slice or array-mask all parallel fields of locus pieces simultaneously.

    Args:
        item (int | slice | npt.NDArray | list): Slice range, boolean mask, or index list.

    Returns:
        LocusPieces: A sliced [`LocusPieces`][kaptive.serotyping.models.LocusPieces] instance.

    Raises:
        NotImplementedError: If single integer key access is attempted.
    """
    if isinstance(item, (int, np.integer)):
        raise NotImplementedError("Single item access not implemented for LocusPieces")
    return LocusPieces(
        ctg_indices=self.ctg_indices[item],
        starts=self.starts[item],
        ends=self.ends[item],
        strands=self.strands[item],
    )

__len__

__len__() -> int

Return total number of locus pieces in container.

Returns:

  • int ( int ) –

    Number of fragment elements.

Source code in src/kaptive/serotyping/models.py
def __len__(self) -> int:
    r"""Return total number of locus pieces in container.

    Returns:
        int: Number of fragment elements.
    """
    return len(self.ctg_indices)

concat classmethod

concat(batches: Iterable[Self]) -> Self

Concatenate multiple LocusPieces batches into a single container.

Parameters:

Returns:

Source code in src/kaptive/serotyping/models.py
@classmethod
def concat(cls, batches: Iterable[Self]) -> Self:  # type: ignore
    r"""Concatenate multiple `LocusPieces` batches into a single container.

    Args:
        batches (Iterable[LocusPieces]): An iterable of
            [`LocusPieces`][kaptive.serotyping.models.LocusPieces] instances.

    Returns:
        LocusPieces: Combined [`LocusPieces`][kaptive.serotyping.models.LocusPieces] container.
    """
    batches_list = list(batches)
    if not batches_list:
        return cls.empty()  # type: ignore
    return cls(
        ctg_indices=np.concatenate([b.ctg_indices for b in batches_list]),
        starts=np.concatenate([b.starts for b in batches_list]),
        ends=np.concatenate([b.ends for b in batches_list]),
        strands=np.concatenate([b.strands for b in batches_list]),
    )

empty classmethod

empty() -> LocusPieces

Create an empty LocusPieces container with zero-length arrays.

Returns:

Source code in src/kaptive/serotyping/models.py
@classmethod
def empty(cls) -> "LocusPieces":
    r"""Create an empty `LocusPieces` container with zero-length arrays.

    Returns:
        LocusPieces: An empty [`LocusPieces`][kaptive.serotyping.models.LocusPieces] instance.
    """
    return cls(
        np.empty(0, dtype=np.uint32),
        np.empty(0, dtype=np.int32),
        np.empty(0, dtype=np.int32),
        np.empty(0, dtype=np.int8),
    )

from_dict classmethod

from_dict(data: dict[str, Any]) -> LocusPieces

Reconstruct a LocusPieces container from a deserialized dictionary.

Parameters:

  • data

    (dict[str, Any]) –

    Dictionary containing coordinate array lists.

Returns:

Source code in src/kaptive/serotyping/models.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "LocusPieces":
    r"""Reconstruct a `LocusPieces` container from a deserialized dictionary.

    Args:
        data (dict[str, Any]): Dictionary containing coordinate array lists.

    Returns:
        LocusPieces: Reconstructed [`LocusPieces`][kaptive.serotyping.models.LocusPieces] instance.
    """
    return cls(
        ctg_indices=np.array(data["ctg_indices"], dtype=np.uint32),
        starts=np.array(data["starts"], dtype=np.int32),
        ends=np.array(data["ends"], dtype=np.int32),
        strands=np.array(data["strands"], dtype=np.int8),
    )

to_dict

to_dict() -> dict[str, Any]

Convert array fields to a dictionary for JSON serialization.

Returns:

  • dict[str, Any] –

    dict[str, Any]: Dictionary mapping field names to NumPy array data.

Source code in src/kaptive/serotyping/models.py
def to_dict(self) -> dict[str, Any]:
    r"""Convert array fields to a dictionary for JSON serialization.

    Returns:
        dict[str, Any]: Dictionary mapping field names to NumPy array data.
    """
    return {k: getattr(self, k) for k in ("ctg_indices", "starts", "ends", "strands")}

Pha4geRow dataclass

Pha4geRow(*, sample: bytes, genotyping_method: bytes = b'In silico serotyping', genotyping_schema_taxon: bytes, genotyping_database_name: bytes, genotyping_database_version: bytes, genotyping_schema_name: bytes = b'Kaptive', genotyping_software_name: bytes = b'Kaptive', genotyping_software_version: bytes, genotype: bytes, genotype_predicted_phenotype: bytes, genotype_confidence_value: bytes, genotyping_details: bytes, genotyping_method_url: bytes = b'https://github.com/klebgenomics/Kaptive')

              flowchart TD
              kaptive.serotyping.Pha4geRow[Pha4geRow]
              kaptive.serotyping.io.ReportRow[ReportRow]

                              kaptive.serotyping.io.ReportRow --> kaptive.serotyping.Pha4geRow
                


              click kaptive.serotyping.Pha4geRow href "" "kaptive.serotyping.Pha4geRow"
              click kaptive.serotyping.io.ReportRow href "" "kaptive.serotyping.io.ReportRow"
            

Report row representation adhering to Public Health Alliance for Genomic Epidemiology (PHA4GE) standards.

Encapsulates sample metadata, taxonomy, software versioning, genotype calls, and confidence values in tab-separated binary format standardized for public health surveillance data exchange.

For more information on the rationale and specifics of the PHA4GE genotyping specification, please see: https://github.com/pha4ge/genotyping-specification

Attributes:

  • sample (bytes) –

    Sample identifier taken from genome assembly filename.

  • genotyping_method (bytes) –

    Genotyping methodology string (default b"In silico serotyping").

  • genotyping_schema_taxon (bytes) –

    NCBITaxon formatted organism species string and taxon ID.

  • genotyping_database_name (bytes) –

    Name of reference database used for serotyping.

  • genotyping_database_version (bytes) –

    Version of reference database used.

  • genotyping_schema_name (bytes) –

    Schema name (default b"Kaptive").

  • genotyping_software_name (bytes) –

    Software name (default b"Kaptive").

  • genotyping_software_version (bytes) –

    Kaptive software version used for analysis.

  • genotype (bytes) –

    Best matching locus type identifier call.

  • genotype_predicted_phenotype (bytes) –

    Predicted surface antigen phenotype/serotype string.

  • genotype_confidence_value (bytes) –

    Confidence assessment (b"Typeable" or b"Untypeable").

  • genotyping_details (bytes) –

    Human-readable descriptions of any locus match problems detected.

  • genotyping_method_url (bytes) –

    Repository URL for methodology documentation.

Methods:

  • __bytes__ –

    Serialize the report row fields into a tab-separated binary TSV row.

  • from_result –

    Construct a standardized Pha4geRow from a serotyping result.

  • header –

    Generate the TSV header row as UTF-8 encoded bytes.

__bytes__

__bytes__() -> bytes

Serialize the report row fields into a tab-separated binary TSV row.

Returns:

  • bytes ( bytes ) –

    Tab-separated field values ending with a newline (b"\n").

Source code in src/kaptive/serotyping/io.py
def __bytes__(self) -> bytes:
    r"""Serialize the report row fields into a tab-separated binary TSV row.

    Returns:
        bytes: Tab-separated field values ending with a newline (`b"\n"`).
    """
    return b"\t".join(getattr(self, f.name) for f in fields(self)) + b"\n"

from_result classmethod

Construct a standardized Pha4geRow from a serotyping result.

Transforms numeric taxon IDs and problem flags into human-readable PHA4GE-compliant strings and binary bytes.

Parameters:

Returns:

  • Pha4geRow ( Pha4geRow ) –

    Formatted PHA4GE report row object.

Source code in src/kaptive/serotyping/io.py
@classmethod
def from_result(cls, result: SerotypingResult) -> "Pha4geRow":
    r"""Construct a standardized [`Pha4geRow`][kaptive.serotyping.io.Pha4geRow] from a serotyping result.

    Transforms numeric taxon IDs and problem flags into human-readable PHA4GE-compliant strings and binary bytes.

    Args:
        result (SerotypingResult): The serotyping call result.
            See [`SerotypingResult`][kaptive.serotyping.models.SerotypingResult].

    Returns:
        Pha4geRow: Formatted PHA4GE report row object.
    """
    # Transform problem enum into human-readable string
    if result.problems:
        detail_parts = []
        if SerotypingProblem.TRUNCATED_GENES in result.problems:
            detail_parts.append(b"truncated gene/s in locus")
        if SerotypingProblem.NOVEL_GENES in result.problems:
            detail_parts.append(b"low identity gene/s")
        if SerotypingProblem.FRAGMENTED in result.problems:
            detail_parts.append(b"match broken into %d pieces" % len(result.locus_pieces))
        if SerotypingProblem.MISSING_GENES in result.problems:
            detail_parts.append(b"missing expected gene/s")
        if SerotypingProblem.UNEXPECTED_GENES in result.problems:
            detail_parts.append(b"unexpected gene/s in locus")
        details = b"Best locus match: %b. Problems: %b" % (
            result.best_locus_name.encode(),
            b", ".join(detail_parts),
        )
    else:
        details = b"Best locus match: %b." % result.best_locus_name.encode()

    return cls(
        sample=result.genome.encode(),
        genotyping_schema_taxon=b"%s [NCBITaxon:%d]" % (result.database_organism.encode(), result.database_taxon),
        genotyping_database_name=result.database_name.encode(),
        genotyping_database_version=result.database_version.encode(),
        genotyping_software_version=result.kaptive_version.encode(),
        genotype=result.best_locus_name.encode(),
        genotype_confidence_value=b"Typeable" if result.typeable else b"Untypeable",
        genotype_predicted_phenotype=result.phenotype.encode(),
        genotyping_details=details,
    )

header classmethod

header() -> bytes

Generate the TSV header row as UTF-8 encoded bytes.

Returns:

  • bytes ( bytes ) –

    Tab-separated column header line ending with a newline (b"\n").

Source code in src/kaptive/serotyping/io.py
@classmethod
def header(cls) -> bytes:
    r"""Generate the TSV header row as UTF-8 encoded bytes.

    Returns:
        bytes: Tab-separated column header line ending with a newline (`b"\n"`).
    """
    return ("\t".join(f.name for f in fields(cls)) + "\n").encode("utf-8")

ReportRow dataclass

ReportRow()

              flowchart TD
              kaptive.serotyping.ReportRow[ReportRow]

              

              click kaptive.serotyping.ReportRow href "" "kaptive.serotyping.ReportRow"
            

Abstract base class for tabular in silico serotyping report rows.

Provides a uniform interface and binary serialization methods (__bytes__ and header) for converting SerotypingResult instances into tab-separated (TSV) outputs. Attributes documented in subclass docstrings correspond directly to TSV report column headers.

Methods:

  • __bytes__ –

    Serialize the report row fields into a tab-separated binary TSV row.

  • from_result –

    Construct a report row instance from a serotyping result.

  • header –

    Generate the TSV header row as UTF-8 encoded bytes.

__bytes__

__bytes__() -> bytes

Serialize the report row fields into a tab-separated binary TSV row.

Returns:

  • bytes ( bytes ) –

    Tab-separated field values ending with a newline (b"\n").

Source code in src/kaptive/serotyping/io.py
def __bytes__(self) -> bytes:
    r"""Serialize the report row fields into a tab-separated binary TSV row.

    Returns:
        bytes: Tab-separated field values ending with a newline (`b"\n"`).
    """
    return b"\t".join(getattr(self, f.name) for f in fields(self)) + b"\n"

from_result abstractmethod classmethod

from_result(result: SerotypingResult) -> Self

Construct a report row instance from a serotyping result.

Parameters:

Returns:

  • Self ( Self ) –

    Instance of the concrete ReportRow subclass populated with result fields.

Source code in src/kaptive/serotyping/io.py
@classmethod
@abstractmethod
def from_result(cls, result: SerotypingResult) -> Self:
    r"""Construct a report row instance from a serotyping result.

    Args:
        result (SerotypingResult): The serotyping analysis result to format.
            See [`SerotypingResult`][kaptive.serotyping.models.SerotypingResult].

    Returns:
        Self: Instance of the concrete [`ReportRow`][kaptive.serotyping.io.ReportRow] subclass
            populated with result fields.
    """
    ...

header classmethod

header() -> bytes

Generate the TSV header row as UTF-8 encoded bytes.

Returns:

  • bytes ( bytes ) –

    Tab-separated column header line ending with a newline (b"\n").

Source code in src/kaptive/serotyping/io.py
@classmethod
def header(cls) -> bytes:
    r"""Generate the TSV header row as UTF-8 encoded bytes.

    Returns:
        bytes: Tab-separated column header line ending with a newline (`b"\n"`).
    """
    return ("\t".join(f.name for f in fields(cls)) + "\n").encode("utf-8")

Serotyper

Serotyper(db: Database, max_other_genes: int = 1, min_completeness: float = 0.5, allow_below_threshold: bool = False, preset: Preset | None = None, scoring_metric: str = 'scores', min_gene_coverage: float = 0.2, partial_edge_tolerance: int = 5)

High-performance in silico serotyping engine for bacterial genome assemblies.

The Serotyper utilizes a reference database (Database) containing surface antigen locus definitions, reference gene sequences, and phenotypic rules to evaluate input assemblies (GenomeAssembly).

It executes a four-phase serotyping pipeline:

  1. Mapping & Scoring: Maps reference genes to assembly contigs using rammappy, culls overlapping hits, and ranks candidate loci based on gene coverage and locus completeness.
  2. Locus Reconstruction: Clusters gene hits spatially to bound locus regions into LocusPieces and identifies missing or unexpected genes inside/outside locus boundaries.
  3. Gene State & Identity Evaluation: Translates gene alignments, performs protein-level pairwise alignment with PairwiseAligner, assesses frame shifts and truncations, and assigns GeneState (NORMAL, PARTIAL, TRUNCATED, NOVEL).
  4. Phenotype & Confidence Scoring: Applies phenotypic rules (e.g. active/inactive gene clusters) and determines overall serotype typeability.

Attributes:

  • max_other_genes (int) –

    Maximum allowed unexpected genes inside locus before classifying sample as untypeable.

  • min_completeness (float) –

    Minimum locus completeness fraction required for typeability call.

  • allow_below_threshold (bool) –

    Whether to permit genes falling below identity threshold while remaining typeable.

  • preset (Preset | None) –

    Custom rammappy alignment preset, if specified.

  • scoring_metric (str) –

    Scoring metric used for locus scoring (default "scores").

  • min_gene_coverage (float) –

    Minimum query coverage fraction required for gene alignments to be considered valid.

  • partial_edge_tolerance (int) –

    Base pair distance tolerance from contig boundaries for identifying partial genes.

Parameters:

  • db

    (Database) –

    The reference surface antigen database containing loci, genes, and phenotype definitions. See Database.

  • max_other_genes

    (int, default: 1 ) –

    Maximum allowed unexpected genes inside the locus boundary before flagging as untypeable. Defaults to 1.

  • min_completeness

    (float, default: 0.5 ) –

    Minimum proportion of expected locus genes required to consider the call typeable. Defaults to 0.5.

  • allow_below_threshold

    (bool, default: False ) –

    If False, any gene inside the locus falling below the identity threshold makes the result untypeable. Defaults to False.

  • preset

    (Preset | None, default: None ) –

    Optional rammappy mapping preset. Defaults to None.

  • scoring_metric

    (str, default: 'scores' ) –

    Scoring metric used for candidate locus ranking. Defaults to "scores".

  • min_gene_coverage

    (float, default: 0.2 ) –

    Minimum gene alignment query coverage fraction (0.0 to 1.0) for valid scoring. Defaults to 0.20.

  • partial_edge_tolerance

    (int, default: 5 ) –

    Distance tolerance in base pairs from contig edges to classify a hit as partial. Defaults to 5.

Methods:

  • __call__ –

    Perform in silico serotyping on a target bacterial genome assembly.

Source code in src/kaptive/serotyping/core.py
def __init__(
    self,
    db: Database,
    max_other_genes: int = 1,
    min_completeness: float = 0.5,
    allow_below_threshold: bool = False,
    preset: rammappy.Preset | None = None,
    scoring_metric: str = "scores",
    min_gene_coverage: float = 0.20,
    partial_edge_tolerance: int = 5,
) -> None:
    r"""Initialize the serotyping engine with a reference database and search parameters.

    Args:
        db (Database): The reference surface antigen database containing loci, genes, and phenotype definitions.
            See [`Database`][kaptive.db.Database].
        max_other_genes (int): Maximum allowed unexpected genes inside the locus boundary before flagging as
            untypeable. Defaults to `1`.
        min_completeness (float): Minimum proportion of expected locus genes required to consider the call
            typeable. Defaults to `0.5`.
        allow_below_threshold (bool): If `False`, any gene inside the locus falling below the identity threshold
            makes the result untypeable. Defaults to `False`.
        preset (rammappy.Preset | None): Optional `rammappy` mapping preset. Defaults to `None`.
        scoring_metric (str): Scoring metric used for candidate locus ranking. Defaults to `"scores"`.
        min_gene_coverage (float): Minimum gene alignment query coverage fraction (0.0 to 1.0) for valid scoring.
            Defaults to `0.20`.
        partial_edge_tolerance (int): Distance tolerance in base pairs from contig edges to classify a hit
            as partial. Defaults to `5`.
    """
    self._db: Database = db
    self.max_other_genes: int = max_other_genes
    self.min_completeness: float = min_completeness
    self.allow_below_threshold: bool = allow_below_threshold
    self.preset: rammappy.Preset | None = preset
    self.scoring_metric: str = scoring_metric
    self.min_gene_coverage: float = min_gene_coverage
    self.partial_edge_tolerance: int = partial_edge_tolerance
    self._protein_aligner = PairwiseAligner()

    # Count expected genes per locus for weighting
    self._expected_genes_per_locus = np.zeros(len(self._db.loci), dtype=np.float32)
    np.add.at(
        self._expected_genes_per_locus,
        self._db.gene_locus_indices[~self._db.extra_genes],
        1.0,
    )
    self._expected_genes_per_locus = np.maximum(self._expected_genes_per_locus, 1.0)

    # Prepare metadata for alignment iterator parsing
    self._gene_seqs = [
        (
            str(i).encode(),
            bytes(
                self._db.genes.seqs[
                    self._db.genes.offsets[i] : self._db.genes.offsets[i] + self._db.genes.lengths[i]
                ]
            ),
        )
        for i in range(len(self._db.genes))
    ]
    self._gene_meta = [(str(i), self._db.genes.lengths[i]) for i in range(len(self._db.genes))]

__call__

__call__(genome: GenomeAssembly | str | Path) -> SerotypingResult | None

Perform in silico serotyping on a target bacterial genome assembly.

Maps reference locus genes against the provided genome assembly, ranks candidate loci, reconstructs locus boundaries, evaluates gene integrity and amino acid identity, and resolves the predicted serotype phenotype.

Parameters:

Returns:

  • SerotypingResult | None –

    SerotypingResult | None: Complete serotyping analysis result containing best matching locus, predicted phenotype, gene hit classifications, spatial locus pieces, and confidence metrics. See SerotypingResult.

Raises:

  • FileNotFoundError –

    If genome is passed as a file path that does not exist on disk.

  • ValueError –

    If the genome assembly contains no valid contigs or sequence data cannot be parsed.

Source code in src/kaptive/serotyping/core.py
def __call__(self, genome: GenomeAssembly | str | Path) -> SerotypingResult | None:
    r"""Perform *in silico* serotyping on a target bacterial genome assembly.

    Maps reference locus genes against the provided genome assembly, ranks candidate loci,
    reconstructs locus boundaries, evaluates gene integrity and amino acid identity, and resolves the predicted
    serotype phenotype.

    Args:
        genome (GenomeAssembly | str | Path): Target genome assembly as a
            [`GenomeAssembly`][kaptive.core.genome.GenomeAssembly] instance or filesystem path (`str` or `Path`)
            to a FASTA file.

    Returns:
        SerotypingResult | None: Complete serotyping analysis result containing best matching locus, predicted
            phenotype, gene hit classifications, spatial locus pieces, and confidence metrics.
            See [`SerotypingResult`][kaptive.serotyping.models.SerotypingResult].

    Raises:
        FileNotFoundError: If `genome` is passed as a file path that does not exist on disk.
        ValueError: If the genome assembly contains no valid contigs or sequence data cannot be parsed.
    """
    genome = GenomeAssembly.ensure(genome)

    contig_index = genome.get_rammappy_index()
    aligner = Aligner(index=contig_index, preset=None, do_cigar=True, do_cs=False, do_md=False)
    opts = aligner.options
    opts.filtering.best_n = 50000
    opts.filtering.pri_ratio = 0.0
    aligner.options = opts

    gene_aln_batch = aligner.map_batch(self._gene_seqs)
    gene_alns = Alignments.from_mapping_iterators(self._gene_meta, gene_aln_batch)

    # Calculate total coverage per gene across all alignments for reporting
    q_indices = gene_alns.q_names.astype(np.int32)
    q_lengths = gene_alns.q_aln_lens
    total_q_covs = np.zeros(len(self._db.genes), dtype=np.float32)
    np.add.at(total_q_covs, q_indices, q_lengths)
    total_q_covs /= self._db.genes.lengths

    # Scoring phase ------------------------------------------------------------------------------------------------
    # Calculate alignment query coverage per alignment
    q_covs = gene_alns.q_covs
    valid_cov_mask = q_covs >= self.min_gene_coverage

    valid_alns = gene_alns[valid_cov_mask]
    valid_q_covs = q_covs[valid_cov_mask]
    valid_gene_indices = valid_alns.q_names.astype(np.int32)  # type: ignore

    # Sort to find the best alignment per gene (highest q_cov, tie-breaker: highest score)
    order = np.lexsort((-valid_alns.scores, -valid_q_covs, valid_gene_indices))  # type: ignore
    valid_alns = valid_alns[order]
    valid_gene_indices = valid_gene_indices[order]
    valid_q_covs = valid_q_covs[order]

    # Take the first (best) alignment for each gene
    _, unique_indices = np.unique(valid_gene_indices, return_index=True)
    best_gene_indices = valid_gene_indices[unique_indices]
    best_q_covs = valid_q_covs[unique_indices]

    valid_locus_indices = self._db.gene_locus_indices[best_gene_indices]
    valid_not_extra = ~self._db.extra_genes[best_gene_indices]

    # Calculate locus scores by summing best q_covs of valid expected genes
    locus_scores = np.zeros(len(self._db.loci), dtype=np.float64)
    np.add.at(
        locus_scores,
        valid_locus_indices[valid_not_extra],
        best_q_covs[valid_not_extra],
    )

    # Calculate completeness count per locus (unique expected genes matched per locus)
    locus_counts = np.zeros(len(self._db.loci), dtype=np.float32)
    matched_expected_genes = best_gene_indices[valid_not_extra]
    np.add.at(locus_counts, self._db.gene_locus_indices[matched_expected_genes], 1.0)

    locus_completeness = locus_counts / self._expected_genes_per_locus
    final_locus_scores = locus_scores * (locus_completeness**3)

    self._last_scores = final_locus_scores.copy()
    self._last_completeness = locus_completeness.copy()

    best_locus_idx = int(np.argmax(final_locus_scores))
    best_locus_name = self._db.loci.ids[best_locus_idx]

    # Reconstruction phase -----------------------------------------------------------------------------------------
    valid_alns = gene_alns

    # Cull alignments, prioritizing genes belonging to the best match locus
    valid_indices = valid_alns.q_names.astype(np.int32)
    priority_mask = self._db.gene_locus_indices[valid_indices] == best_locus_idx

    culled_alns = valid_alns.cull_overlaps(by_query=False, priority_mask=priority_mask, max_overlap_fraction=0.1)

    # Re-extract arrays for the culled batch
    culled_gene_indices = culled_alns.q_names.astype(np.int32)
    t_indices = np.array([genome.id_map[n] for n in culled_alns.t_names], dtype=np.uint32)
    # Cluster intervals by contig using max_locus_length as tolerance
    culled_intervals = culled_alns.to_intervals(by_query=False)
    piece_ids = culled_intervals.cluster_spatial(tolerance=self._db.max_locus_length, group_by=t_indices)

    # Identify expected genes and the clusters they fall into
    is_expected = (self._db.gene_locus_indices[culled_gene_indices] == best_locus_idx) & ~self._db.extra_genes[
        culled_gene_indices
    ]
    valid_cluster_ids = np.unique(piece_ids[is_expected])
    is_extra = self._db.extra_genes[culled_gene_indices]

    # Calculate gene alignment coverages based on total coverage across all non-overlapping fragments
    coverages = np.clip(total_q_covs[culled_gene_indices] * 100.0, 0.0, 100.0)

    # Identify the primary hit for each expected gene for bounding box calculation
    primary_expected = np.zeros(len(culled_alns), dtype=bool)
    is_expected_hits = np.where(is_expected)[0]
    if len(is_expected_hits) > 0:
        exp_gene_indices = culled_gene_indices[is_expected_hits]
        exp_scores = culled_alns.scores[is_expected_hits]
        order = np.lexsort((-exp_scores, exp_gene_indices))
        sorted_exp_gene_indices = exp_gene_indices[order]
        _, unique_indices = np.unique(sorted_exp_gene_indices, return_index=True)
        best_hits = is_expected_hits[order[unique_indices]]
        primary_expected[best_hits] = True

    # Construct bounding locus pieces from valid pieces
    l_ctg_indices, l_starts, l_ends, l_strands = [], [], [], []
    l_expected_means = []
    for c_id in valid_cluster_ids:
        piece_mask = piece_ids == c_id

        piece_primary = piece_mask & primary_expected
        if np.any(piece_primary):
            ctg_idx = t_indices[piece_mask][0]
            l_ctg_indices.append(ctg_idx)
            start = np.min(culled_intervals.starts[piece_primary])
            end = np.max(culled_intervals.ends[piece_primary])
            l_starts.append(start)
            l_ends.append(end)

            exp_genes = culled_gene_indices[piece_primary]
            l_expected_means.append(np.mean(self._db.gene_positions[exp_genes]))

            exp_strands = self._db.gene_intervals.strands[exp_genes]
            found_strands = culled_alns.strands[piece_primary]
            if np.sum(found_strands * exp_strands) < 0:
                l_strands.append(-1)
            else:
                l_strands.append(1)

    # Recompute is_inside using the strict expected boundaries
    is_inside = np.zeros(len(culled_alns), dtype=bool)
    for ctg_idx, start, end in zip(l_ctg_indices, l_starts, l_ends):
        on_ctg = t_indices == ctg_idx
        # An alignment is inside if it overlaps the bounding region
        inside_this = on_ctg & (culled_intervals.starts <= end) & (culled_intervals.ends >= start)
        is_inside |= inside_this

    # Sort pieces by expected mean position
    piece_order = np.argsort(l_expected_means)

    locus_pieces = LocusPieces(
        ctg_indices=np.array(l_ctg_indices, dtype=np.uint32)[piece_order],
        starts=np.array(l_starts, dtype=np.int32)[piece_order],
        ends=np.array(l_ends, dtype=np.int32)[piece_order],
        strands=np.array(l_strands, dtype=np.int8)[piece_order],
    )

    # Identify missing expected genes
    expected_genes_mask = (self._db.gene_locus_indices == best_locus_idx) & ~self._db.extra_genes
    expected_gene_indices = np.where(expected_genes_mask)[0]
    # Which ones did we find inside the locus?
    found_expected_gene_indices = culled_gene_indices[is_expected & is_inside]
    missing_indices = np.setdiff1d(expected_gene_indices, found_expected_gene_indices, assume_unique=True)
    missing_expected_genes = tuple(self._db.genes.ids[i] for i in missing_indices)

    # Calculate actual completeness based on reconstructed locus
    actual_locus_completeness = (
        1.0 - (len(missing_indices) / len(expected_gene_indices)) if len(expected_gene_indices) > 0 else 1.0
    )

    gene_hits = GeneHits(
        gene_indices=culled_gene_indices,
        q_starts=culled_alns.q_starts,
        q_ends=culled_alns.q_ends,
        t_indices=t_indices,
        t_starts=culled_alns.t_starts,
        t_ends=culled_alns.t_ends,
        strands=culled_alns.strands,
        is_expected=is_expected,
        is_inside=is_inside,
        is_extra=is_extra,
        expected_positions=self._db.gene_positions[culled_gene_indices].astype(np.int32),
        expected_strands=self._db.gene_intervals.strands[culled_gene_indices],
        gene_ids=np.array([self._db.genes.ids[i].encode("utf-8") for i in culled_gene_indices], dtype="S32"),
        cluster_names=np.array(
            [self._db.cluster_keys[self._db.gene_cluster_ids[i]].encode("utf-8") for i in culled_gene_indices],
            dtype="S10",
        ),
        product_descriptions=np.array(
            [
                self._db.description_keys[self._db.gene_description_ids[i]].encode("utf-8")
                for i in culled_gene_indices
            ],
            dtype="S64",
        ),
        coverages=coverages,
    )

    # Locus extraction phase ---------------------------------------------------------------------------------------
    if len(locus_pieces) > 0:  # Extract locus sequences using the batched SoA locus pieces
        locus_seqs = genome.contigs.extract(
            locus_pieces.ctg_indices,  # type: ignore
            locus_pieces.starts,
            locus_pieces.ends,
            locus_pieces.strands,
        )
    else:
        locus_seqs = Sequences.empty()

    # Calculate coverage and length discrepancy
    assem_len = np.sum(locus_pieces.ends - locus_pieces.starts)
    ref_len = self._db.loci.lengths[best_locus_idx]
    pcov = float(min(100.0, (assem_len / ref_len) * 100.0)) if ref_len > 0 else 0.0
    if len(locus_pieces) == 1:
        length_discrepancy = float(assem_len - ref_len)
    else:
        length_discrepancy = float("nan")

    # Gene state phase ---------------------------------------------------------------------------------------------
    gene_seqs = genome.contigs.extract_intervals(  # Extract gene nucleotides from their contigs
        gene_hits.t_indices,
        gene_hits.t_intervals,
        new_ids=tuple(self._db.genes.ids[i] for i in gene_hits.gene_indices),
    )
    # Translate nucleotides to amino acids, compensating for the reading frames of the alignments
    # Truncate at the first stop codon to match Old Kaptive's behavior, which prevents frameshifts
    # from pulling down the identity score of the valid upstream alignment.
    prot_seqs = gene_seqs.translate(frames=gene_hits.frames, to_stop=True)  # type: ignore

    # Initialize states
    gene_states = np.full(len(gene_hits), GeneState.NORMAL.value, dtype=np.int8)
    is_partial = culled_alns.is_partial(self.partial_edge_tolerance)
    db_gene_lengths = self._db.genes.lengths[gene_hits.gene_indices]

    # In Old Kaptive, truncation was calculated based on the *translated protein* length
    # (up to the first stop codon) divided by the reference protein length.
    prot_covs = (prot_seqs.lengths * 3.0) / db_gene_lengths

    # We must also update the coverage to reflect the protein coverage, so it prints correctly in the output
    gene_hits.coverages[:] = np.clip(prot_covs * 100.0, 0.0, 100.0)

    # A partial gene colliding with a contig edge is excluded from being truncated
    is_truncated = (~is_partial) & (prot_covs < 0.90)
    gene_states[is_partial] = GeneState.PARTIAL.value
    gene_states[is_truncated] = GeneState.TRUNCATED.value
    prot_alns = self._protein_aligner(prot_seqs, self._db.translations[gene_hits.gene_indices])  # type: ignore
    prot_idents = prot_alns.pidents.astype(np.float32)

    # Drop genes outside the locus that fall below the identity threshold,
    # mirroring Old Kaptive's behavior of ignoring spurious homologies
    is_spurious = (~gene_hits.is_inside) & (prot_idents < self._db.metadata.id_threshold)
    if np.any(is_spurious):
        keep_mask = ~is_spurious
        gene_hits = gene_hits[keep_mask]
        gene_seqs = gene_seqs[keep_mask]
        prot_seqs = prot_seqs[keep_mask]
        gene_states = gene_states[keep_mask]
        prot_idents = prot_idents[keep_mask]

    # Normal genes that fall below the identity threshold are considered NOVEL
    below_threshold = (gene_states == GeneState.NORMAL.value) & (prot_idents < self._db.metadata.id_threshold)
    gene_states[below_threshold] = GeneState.NOVEL.value
    valid_pidents = prot_idents[gene_states == GeneState.NORMAL.value]
    pident = float(np.mean(valid_pidents)) if valid_pidents.size > 0 else 0.0

    # Phenotype Evaluation phase -----------------------------------------------------------------------------------
    base_phenotype = self._db.serotypes[best_locus_idx]
    phenotypes = self._db.phenotypes

    if len(phenotypes) > 0:
        # A cluster is considered 'active' if it's found NORMAL or PARTIAL
        q_active = np.zeros(len(self._db.cluster_keys), dtype=bool)
        is_active = (gene_states == GeneState.NORMAL.value) | (gene_states == GeneState.PARTIAL.value)
        if np.any(is_active):
            active_clusters = self._db.gene_cluster_ids[gene_hits.gene_indices[is_active]]
            q_active[active_clusters] = True

        # Vectorized rule evaluation using BLAS matrix multiplication
        locus_match = phenotypes.locus_masks[:, best_locus_idx]
        q_active_int = q_active.astype(np.int8)
        extra_match = np.dot(phenotypes.extra_masks, q_active_int) == phenotypes.extra_counts

        has_inactive_rule = phenotypes.inactive_masks.sum(axis=1) > 0

        expected_mask = np.zeros(len(self._db.cluster_keys), dtype=np.int8)
        offset = self._db.locus_gene_offsets[best_locus_idx]
        length = self._db.locus_gene_lengths[best_locus_idx]
        expected_clusters = self._db.gene_cluster_ids[offset : offset + length]
        expected_mask[expected_clusters] = 1

        applicable_inactive_masks = phenotypes.inactive_masks & expected_mask
        has_applicable_inactive = applicable_inactive_masks.sum(axis=1) > 0

        q_inactive_int = (~q_active).astype(np.int8)
        inactive_hits = np.dot(applicable_inactive_masks, q_inactive_int)

        inactive_match = (~has_inactive_rule) | (has_applicable_inactive & (inactive_hits > 0))

        if np.any(valid_mask := locus_match & extra_match & inactive_match):
            valid_indices = np.where(valid_mask)[0]
            is_suffix = phenotypes.as_suffix[valid_indices]

            if len(replacements := valid_indices[~is_suffix]) > 0:
                best_rep_idx = replacements[np.argmax(phenotypes.priorities[replacements])]
                base_phenotype = phenotypes.ids[best_rep_idx].decode("utf-8")

            if len(suffixes := valid_indices[is_suffix]) > 0:
                sorted_suffixes = suffixes[np.argsort(-phenotypes.priorities[suffixes])]
                suffix_strs = [phenotypes.ids[i].decode("utf-8") for i in sorted_suffixes]
                base_phenotype = f"{base_phenotype}{''.join(suffix_strs)}"

    # Confidence evaluation phase ----------------------------------------------------------------------------------
    typeable = True
    if actual_locus_completeness < self.min_completeness:
        typeable = False

    # Truncated unexpected genes do not count towards the limit, mirroring Old Kaptive
    is_unexpected = gene_hits.is_inside & ~gene_hits.is_expected & ~gene_hits.is_extra
    is_not_truncated = gene_states != GeneState.TRUNCATED.value
    unexpected_count = np.count_nonzero(is_unexpected & is_not_truncated)
    if unexpected_count > self.max_other_genes:
        typeable = False

    # 3. Check for any genes falling below the identity threshold
    if not self.allow_below_threshold:
        if np.any(gene_hits.is_inside & (gene_states == GeneState.NOVEL.value)):
            typeable = False

    # Return result object -----------------------------------------------------------------------------------------
    return SerotypingResult(
        kaptive_version=__version__,
        database_name=self._db.metadata.name,
        database_version=self._db.metadata.version,
        database_organism=self._db.metadata.organism,
        database_taxon=self._db.metadata.taxon,
        genome=genome.id,
        best_locus_idx=best_locus_idx,
        best_locus_name=best_locus_name,
        best_locus_score=locus_scores[best_locus_idx],
        best_locus_completeness=actual_locus_completeness,
        length_discrepancy=length_discrepancy,
        gene_hits=gene_hits,
        gene_states=gene_states,
        locus_pieces=locus_pieces,
        locus_seqs=locus_seqs,
        gene_seqs=gene_seqs,  # type: ignore
        translations=prot_seqs,  # type: ignore
        percent_identity=pident,
        percent_coverage=pcov,
        protein_identities=prot_idents,
        phenotype=base_phenotype,
        typeable=typeable,
        missing_expected_genes=missing_expected_genes,
    )

SerotypingProblem


              flowchart TD
              kaptive.serotyping.SerotypingProblem[SerotypingProblem]

              

              click kaptive.serotyping.SerotypingProblem href "" "kaptive.serotyping.SerotypingProblem"
            

Symbolic problems with the serotype call used for report formatting.

Bitflag values represent distinct issues detected during locus assembly analysis and can be combined bitwise.

Attributes:

  • NONE (int) –

    No problems detected in the serotype call.

  • FRAGMENTED (int) –

    Locus is broken up into multiple pieces across contigs (Symbol: ?).

  • UNEXPECTED_GENES (int) –

    Unexpected genes from non-target loci present inside locus boundary (Symbol: +).

  • MISSING_GENES (int) –

    Expected genes from target locus missing inside locus boundary (Symbol: -).

  • NOVEL_GENES (int) –

    Genes inside locus boundary falling below identity threshold (Symbol: *).

  • TRUNCATED_GENES (int) –

    Genes inside locus boundary that are truncated or partial (Symbol: !).

  • SYMBOLS (ClassVar[tuple[bytes, ...]]) –

    Precomputed lookup table mapping integer bitflag combinations to symbol byte strings.

Methods:

  • to_symbols –

    Render the bitflag combination into formatted symbol bytes for TSV reporting.

to_symbols

to_symbols() -> bytes

Render the bitflag combination into formatted symbol bytes for TSV reporting.

Returns:

  • bytes ( bytes ) –

    ASCII byte string containing concatenation of active problem symbols.

Source code in src/kaptive/serotyping/models.py
def to_symbols(self) -> bytes:
    r"""Render the bitflag combination into formatted symbol bytes for TSV reporting.

    Returns:
        bytes: ASCII byte string containing concatenation of active problem symbols.
    """
    return self.SYMBOLS[self.value]

SerotypingResult dataclass

SerotypingResult(kaptive_version: str, database_name: str, database_version: str, database_organism: str, database_taxon: int, genome: str, best_locus_idx: int, best_locus_name: str, best_locus_score: float, best_locus_completeness: float, locus_pieces: LocusPieces, length_discrepancy: float, locus_seqs: Sequences, gene_hits: GeneHits, gene_states: NDArray[int8], gene_seqs: Sequences, translations: Sequences, percent_identity: float, percent_coverage: float, protein_identities: NDArray[float32], phenotype: str, typeable: bool, missing_expected_genes: tuple[str, ...])

Efficient, immutable container representing an in silico serotyping call.

Designed to be lightweight for JSON serialization and database storage while retaining full information needed to inspect and reconstruct alignment details. Houses nested SoA containers (LocusPieces and GeneHits) and sequence objects (Sequences) for downstream processing.

Attributes:

  • kaptive_version (str) –

    Version of Kaptive software that produced result.

  • database_name (str) –

    Name of target locus reference database.

  • database_version (str) –

    Version tag of reference database.

  • database_organism (str) –

    Target organism description in database.

  • database_taxon (int) –

    NCBI taxonomy ID of database.

  • genome (str) –

    Sample genome assembly identifier or filename.

  • best_locus_idx (int) –

    Index of best-matching locus in database.

  • best_locus_name (str) –

    Identifier name of best-matching locus.

  • best_locus_score (float) –

    Alignment score for best-matching locus.

  • best_locus_completeness (float) –

    Proportion of expected genes found in locus (0.0 to 1.0).

  • locus_pieces (LocusPieces) –

    Locus piece bounding coordinates container.

  • length_discrepancy (float) –

    Length discrepancy relative to reference locus.

  • locus_seqs (Sequences) –

    Sequences of identified locus region fragments.

  • gene_hits (GeneHits) –

    High-performance SoA container for gene alignment hits.

  • gene_states (NDArray[int8]) –

    Gene classification state array matching GeneState values.

  • gene_seqs (Sequences) –

    Extracted nucleotide sequences of locus genes.

  • translations (Sequences) –

    Translated amino acid sequences of locus genes.

  • percent_identity (float) –

    Overall nucleotide identity percentage across locus.

  • percent_coverage (float) –

    Overall reference coverage percentage.

  • protein_identities (NDArray[float32]) –

    Per-gene protein identity percentages.

  • phenotype (str) –

    Inferred serotype phenotype description.

  • typeable (bool) –

    Flag indicating if confidence criteria for serotype call were met.

  • missing_expected_genes (tuple[str, ...]) –

    Identifiers of missing expected locus genes.

Methods:

  • from_dict –

    Reconstruct a SerotypingResult instance from a deserialized dictionary.

  • to_dict –

    Convert serotyping result into a dictionary suitable for JSON serialization.

  • to_locus_data –

    Convert result into a LocusData container for comparative multi-locus visualization.

from_dict classmethod

from_dict(data: dict[str, Any]) -> SerotypingResult

Reconstruct a SerotypingResult instance from a deserialized dictionary.

Parameters:

  • data

    (dict[str, Any]) –

    Dictionary containing serialized fields and nested sub-dictionaries.

Returns:

Source code in src/kaptive/serotyping/models.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "SerotypingResult":
    r"""Reconstruct a `SerotypingResult` instance from a deserialized dictionary.

    Args:
        data (dict[str, Any]): Dictionary containing serialized fields and nested sub-dictionaries.

    Returns:
        SerotypingResult: Reconstructed [`SerotypingResult`][kaptive.serotyping.models.SerotypingResult] instance.
    """
    return cls(
        kaptive_version=data["kaptive_version"],
        database_name=data["database_name"],
        database_version=data["database_version"],
        database_organism=data["database_organism"],
        database_taxon=data["database_taxon"],
        genome=data["genome"],
        best_locus_idx=data["best_locus_idx"],
        best_locus_name=data["best_locus_name"],
        best_locus_score=data["best_locus_score"],
        best_locus_completeness=data["best_locus_completeness"],
        length_discrepancy=data["length_discrepancy"],
        locus_pieces=LocusPieces.from_dict(data["locus_pieces"]),
        gene_hits=GeneHits.from_dict(data["gene_hits"]),
        gene_states=np.array(data["gene_states"], dtype=np.int8),
        percent_identity=data["percent_identity"],
        percent_coverage=data["percent_coverage"],
        phenotype=data["phenotype"],
        typeable=data["typeable"],
        missing_expected_genes=tuple(data.get("missing_expected_genes", [])),
        locus_seqs=Sequences.from_dict(data["locus_seqs"]),
        gene_seqs=Sequences.from_dict(data["gene_seqs"]),
        translations=Sequences.from_dict(data["translations"]),
        protein_identities=np.array(data["protein_identities"], dtype=np.float32),
    )

to_dict

to_dict() -> dict[str, Any]

Convert serotyping result into a dictionary suitable for JSON serialization.

Returns:

  • dict[str, Any] –

    dict[str, Any]: Lightweight dictionary containing primitive types, lists, and nested dictionaries.

Source code in src/kaptive/serotyping/models.py
def to_dict(self) -> dict[str, Any]:
    r"""Convert serotyping result into a dictionary suitable for JSON serialization.

    Returns:
        dict[str, Any]: Lightweight dictionary containing primitive types, lists, and nested dictionaries.
    """
    return {
        "kaptive_version": self.kaptive_version,
        "database_name": self.database_name,
        "database_version": self.database_version,
        "database_organism": self.database_organism,
        "database_taxon": self.database_taxon,
        "genome": self.genome,
        "best_locus_idx": self.best_locus_idx,
        "best_locus_name": self.best_locus_name,
        "best_locus_score": self.best_locus_score,
        "best_locus_completeness": self.best_locus_completeness,
        "length_discrepancy": self.length_discrepancy,
        "percent_identity": self.percent_identity,
        "percent_coverage": self.percent_coverage,
        "phenotype": self.phenotype,
        "typeable": self.typeable,
        "missing_expected_genes": self.missing_expected_genes,
        "problems": self.problems,
        "locus_pieces": self.locus_pieces.to_dict(),
        "gene_hits": self.gene_hits.to_dict(),
        "gene_states": self.gene_states,
        "protein_identities": self.protein_identities,
        "locus_seqs": self.locus_seqs.to_dict(),
        "gene_seqs": self.gene_seqs.to_dict(),
        "translations": self.translations.to_dict(),
    }

to_locus_data

to_locus_data() -> LocusData

Convert result into a LocusData container for comparative multi-locus visualization.

Extracts translations, locus backbone intervals, locus pieces, contig indices, gene states, and functional product descriptions for non-extra inside genes.

Returns:

Source code in src/kaptive/serotyping/models.py
def to_locus_data(self) -> "LocusData":
    r"""Convert result into a `LocusData` container for comparative multi-locus visualization.

    Extracts translations, locus backbone intervals, locus pieces, contig indices, gene states,
    and functional product descriptions for non-extra inside genes.

    Returns:
        LocusData: A [`LocusData`][kaptive.compare.LocusData] instance for multi-locus alignment plotting.
    """
    from kaptive.compare import LocusData

    mask = self.gene_hits.is_inside & ~self.gene_hits.is_extra
    descriptions = np.asarray(
        np.char.decode(self.gene_hits.product_descriptions[mask], "utf-8"),
        dtype=object,
    )

    return LocusData(
        proteins=self.translations[mask],  # type: ignore
        name=self.genome,
        backbone=self.gene_hits.t_intervals[mask],  # type: ignore
        pieces=self.locus_pieces,
        gene_ctg_indices=self.gene_hits.t_indices[mask],
        gene_states=self.gene_states[mask],
        gene_descriptions=descriptions,
    )