starplast.star_edges
Gene-gene links with their provenance: what the star map draws, and where each link came from.
The 3D map places genes; it does not say WHY two genes are related. The strategies do, but each one says it in its own table -- a list of predicted pairs, a module assignment, a set of candidates grown from a seed list, a call backed by partners. This module turns all of those into one kind of thing, a link between two genes, and keeps with every link the answer to "who says so":
kind "measured" an edge layer of the space's graph (`graph.npz`: crosslinks, co-fitness,
co-expression, shared domain, co-mention ...)
"inferred" a link a strategy derived from those measurements
source the layer key, or the strategy key
how what kind of statement the link is (see `HOW`)
setting the settings the strategy ran with, as text
score the strategy's own number for the link (probability, logit, count, residual ...)
strength that score as a percentile within its run, 0..1, so links from different strategies
can share one width scale without pretending their scores are comparable
run which run produced it: "" for a measured layer, ``starplast:<key>`` for the runs shipped
with the package, ``you:<key>:<timestamp>`` for a run made in the application
origin "data", "starplast" or "you"
Three stores feed the map. MEASURED links are read from the space's own graph on demand and never
copied. SHIPPED links are the default-setting runs of the strategies below on the packaged tables,
precomputed by scripts/build_star_edges.py into data/star_edges.parquet (recorded as an
executed notebook). USER links are written by :class:UserEdgeStore whenever a strategy is run in
the application, one file per run under the user cache, and appear in the map at once.
Everything here is bounded, and every bound is a named constant. A strategy that says "these
1,800 genes form a community" is not saying there are 1.6 million pairwise links; drawing it as a
clique would bury every other source. A module becomes, for each member, links to its MODULE_K
nearest co-members -- nearest first by how many measured layers already join them, then by distance
on the map the strategy built (or in the permitted measurements when it built none). A set expansion
links each candidate to its SEED_K nearest seeds by the same rule. A neighbour vote (07) links a
called gene to its LABEL_K nearest labelled genes that carry the label it was called with, found
in the same measurement space the vote used. A partner vote (13) links a call to the partners it
names, at most PARTNER_MAX. No run contributes more than MAX_EDGES_PER_RUN links.
Qt-free, so the build script and the tests use it without a display.
An edge frame with no rows and every column.
Scores as percentiles within their own set, 0..1 (missing scores sit at the middle).
A logit, a probability, a crosslink count and a residual cannot share a width scale; their ranks can. One link alone gets 1.0.
A run's settings as one short line; a pasted gene list is shown as its size.
What a measured layer is, in words.
'16 link_prediction: Predict the contacts ...' for a strategy key, or the key alone.
A space's edge layers as layer__a/b/w arrays, or {} if the graph is over another table.
Every measured layer as links: the layer is the source and its weight the score.
Co-membership as bounded links: each member to its k nearest co-members (-1 = no module).
Set expansion as links: each candidate to its k nearest seeds, scored by its own rank.
A neighbour-vote call as links to its k nearest labelled genes that carry the called label.
Found in the same space the vote used (Context.features(target)), so the links are the genes
that made the call, not a new guess.
A partner-vote call as links to the partners it names, weighted by their count.
A table of gene pairs (gene_a/gene_b, or gene_id/neighbour) as links.
Every gene-gene link a strategy's result states, bounded, with its provenance.
Reads what the result exposes: pair tables, a module or cluster assignment (labels), a
candidate list grown from settings['genes'], a neighbour-vote's calls (07) and a partner
vote's named partners. A result that relates no genes to each other gives an empty frame.
:param run: the run id every link carries.
:param origin: "starplast" for a shipped run, "you" for one made in the application.
:param group: the toggle it is drawn under; defaults to the strategy key for a shipped run and to
:data:YOUR_RUNS for the user's.
Positions to gene ids, for writing: a stored file must survive a re-ordered node table.
A stored file back to positions in this node table; links to genes it lacks are dropped.
Where the shipped links live (inside the package).
The links precomputed from the shipped strategy runs, for one organism's table.
you:<strategy>:<YYYYmmdd_HHMMSS>, the id a user run's links carry.
The links of the user's own strategy runs: one parquet and one JSON per run, kept on disk.
Beside the other user state (paths.user_cache_dir()), never inside the package, so a rebuild or
an upgrade does not lose them. The JSON carries what the hover text shows: the strategy, its
settings, when it ran and its summary.
Keep runs under root (created on first write); None keeps them in memory only.
Derive a run's links, keep them, and return them (positions in ctx's table).
All links of one table, from any number of sources, looked up by gene in constant time.
Blocks are added whole (measured, shipped, each user run) and the lookup is rebuilt lazily, so adding a run costs one concatenation, not a copy per gene.
Each group (layer, strategy, or your runs) with its kind and number of links.
The neighbourhood the star map draws: (nodes, edges).
nodes is a frame of gene, hop (0 centre, 1, 2) and parent; edges the links used, from
enabled groups only. Around each expanded gene every group keeps its per_group strongest
links -- so a dense layer (shared compartment joins hundreds of genes) cannot crowd out a sparse
one (crosslinks) -- and the second hop keeps a third as many. At most max_nodes genes in all.
Which links count as an edge: the user's own definition of the network.
Every field is one rule, and every rule removes links; nothing here invents them.
groups the sources that may contribute at all -- each measured layer, each strategy,
and "your runs" -- or None for every source, including ones added later.
min_strength a link is kept only when its strength (its score's percentile within its own
run) is at least this. 0 keeps everything.
max_per_gene each gene keeps at most this many links, its strongest first; 0 means no cap.
Applied after the other rules, so the cap is spent on links that survived.
min_sources a pair of genes is drawn only when at least this many DIFFERENT sources say so
-- the "two independent sources" rule. 1 asks for no agreement.
A definition is data: it is hashable, it compares by value, it round-trips through JSON, and
:func:apply is a pure function of (links, definition). Qt never sees it.
Everything that changes the resulting edges -- a cache key, without the name.
A definition read back from :meth:to_json; unknown keys are ignored.
edges narrowed to what definition calls an edge: (kept frame, counts).
The counts say what each rule removed, so the view can be honest about it:
{"links", "pairs", "sources", "genes", "from_sources", "dropped_source",
"dropped_strength", "dropped_agreement", "dropped_cap"}.
The live headline: "4,812 links between 3,104 genes from 3 sources".
The user's named connection definitions, one JSON file under the user cache.
A definition is remembered by name, so "crosslink + co-expression, 2+ sources" can be picked
again next session. root of None keeps them for this session only, which is what the tests and
a read-only install want.
(neighbours, starts): every gene's neighbours in one array, indexable by gene.
neighbours[starts[g]:starts[g + 1]] are the genes joined to g, strongest link first.
The genes within hops of seeds, as a frame of gene, hop and parent.
A breadth-first walk over the edges the definition kept, strongest link first at every step, so
a cut at max_nodes keeps the best-supported part of the neighbourhood rather than an arbitrary
slice. hop is 0 for a seed.
The whole network, cut down to something drawable: (nodes, edges, note).
The strongest max_edges links are kept, then the max_nodes genes with the most of them, then
the links between those genes. note says in words what was left out, so the view can print it;
it is "" when nothing was.
A cluster number per gene of genes, by label propagation over the kept links.
Deterministic: genes are visited in id order and ties go to the smallest label, so the same network always gives the same clusters and the layout below never moves on its own.
Positions for genes as an (m, 2) array: a spring layout, computed once and reusable.
Deterministic from the genes and the links alone -- the starting ring is by cluster and then by gene id, never random -- so the same network lays out identically every time and a cached layout can be trusted. This is the expensive part of a large view, so callers compute it once per (network, definition) and keep it: it must never run from a paint handler.