Edit on GitHub

starplast.pf_graph

Relation layers for the Plasmodium table: pairs of genes, in their own index space.

The Toxoplasma graph stores edges as index pairs into nodes.parquet. A falciparum gene has no index there, so the second species needs a second graph rather than more layers in the first -- the same reason instruction 39 gives one table per species. Nothing is merged and nothing is compared across the two files; an index means a row of the table it was built from and of no other.

Three layers, built the way the Toxoplasma ones are

orthogroup and domain join genes that share a category. coexpression joins genes whose abundance moves together across the life stages. The constructions are copied deliberately rather than re-invented: if the two arms are ever compared, a difference should mean the biology differs and not that the edges were drawn by different rules.

The self-edge trap, inherited on purpose

InterPro reports one row per domain MATCH, so a protein carrying two copies of a repeat domain shows up twice in a member list. Pairing a list that contains a gene twice emits (g, g) as an edge and duplicates every other pair through it -- which is how 31 self-edges and 106 duplicated pairs once shipped in the Toxoplasma graph. A self-edge draws as a zero-length line and a duplicate draws twice, reading as twice the evidence. Members are collected into a SET here for that reason, and a test asserts no edge joins a gene to itself.

GRAPH = 'pf_graph.npz'
GROUP_CAP = 60
CORRELATION = 0.95
NEIGHBOURS = 25
STAGE_COLUMNS = ('expr_ring', 'expr_early_trophozoite', 'expr_late_trophozoite', 'expr_schizont', 'expr_gametocyte_ii', 'expr_gametocyte_v', 'expr_ookinete', 'expr_asexual_blood', 'expr_oocyst', 'expr_sporozoite')
def orthogroup_edges(nodes: pandas.DataFrame) -> tuple:

Genes sharing an OrthoMCL group.

def domain_edges(nodes: pandas.DataFrame) -> tuple:

Genes sharing an InterPro domain. Members are a SET; see the module docstring.

RPF_COLUMNS = ('riboseq_rpf_ring', 'riboseq_rpf_early_trophozoite', 'riboseq_rpf_late_trophozoite', 'riboseq_rpf_schizont', 'riboseq_rpf_merozoite')
def correlation_edges( nodes: pandas.DataFrame, columns=('expr_ring', 'expr_early_trophozoite', 'expr_late_trophozoite', 'expr_schizont', 'expr_gametocyte_ii', 'expr_gametocyte_v', 'expr_ookinete', 'expr_asexual_blood', 'expr_oocyst', 'expr_sporozoite'), threshold: float = 0.95, neighbours: int = 25) -> tuple:

Genes whose abundance moves together, keeping each gene's strongest neighbours.

def build( nodes: pandas.DataFrame, log=<built-in function print>, dataset_root: str | None = None) -> dict:

Every layer, in the <name>__a / <name>__b / <name>__w shape the graph file uses.

def save( nodes: pandas.DataFrame, path: str, log=<built-in function print>, dataset_root: str | None = None) -> dict:

Build the layers AND the 3D layout, and write them together.

The layout was missing until 2026-08-17, and its absence is why the application could not open this arm at all: app.load reads xyz out of the graph file, and this file had every edge layer and no coordinates. The arm was 41 filled slots that nothing could draw.

Laid out by the same build_graph.embed the Toxoplasma table uses, so the two maps are made the same way -- not the same axes, which would be meaningless across species with different features, but the same construction, which is what makes comparing them honest.

def load(base: str):

The shipped Plasmodium graph, or None if it has not been built.

ACCESSION = re.compile('PF3D7_\\w+')
def ip_ms_edges( nodes: pandas.DataFrame, dataset_root: str, log=<built-in function print>) -> tuple:

Bait-to-partner edges from the co-immunoprecipitations, weighted by spectra in the bait.

Named ip_ms to match the Toxoplasma layer answering the same slot -- and, as everywhere on this arm, the shared name is why the species guard exists: these indices are positions in pf_nodes.parquet and mean different genes in the other graph.

A bait's own row is not an edge (a protein does not bind itself into the graph), and the pairs are undirected here even though the experiment is not: A pulled down B and B pulled down A are one line between two genes, and the direction is kept in the table the loader returns rather than in the layer.

def structure_edges( nodes: pandas.DataFrame, dataset_root: str, log=<built-in function print>) -> tuple:

Pairs whose AlphaFold models superpose, as a layer of this graph.

Named struct to match the Toxoplasma layer answering the same slot, and built at the same TM-score so the two mean one thing. It is the layer that needs no orthology, which in this genome matters more than in the other one: most of what is exported has no detectable homolog.

def host_bridge( nodes: pandas.DataFrame, dataset_root: str, log=<built-in function print>) -> pandas.DataFrame:

The crosslinks this layer throws away: parasite protein to HUMAN protein.

The same file the xlms layer reads, and the other half of its filter. A crosslink between a parasite protein and an erythrocyte one is a measured contact across the host boundary, which is a bridge rather than an edge -- the pair's two ends live in different tables and a human protein has no index in this one, exactly as instruction 39 describes.

Which end is which goes through the accession index, for the reason crosslink_edges records: reading it off a pattern classified Pfs16, a parasite protein, as human.

def host_degree( nodes: pandas.DataFrame, dataset_root: str, log=<built-in function print>) -> pandas.Series:

How many host proteins each parasite gene was crosslinked to.

Three states, and the middle one is the reason this is not a fillna(0). The Toxoplasma arm sets this to 0 for every gene without a curated host target, which is right there because its source is a curated table covering the whole literature. This source is ONE experiment: a gene it never detected has not been shown to lack host partners, it was simply not seen. So genes that appear in the crosslink data get a count -- zero included, because being crosslinked only to parasite proteins is a real observation -- and genes absent from it stay missing.