Edit on GitHub

starplast.app

Desktop browser for gene evidence in Toxoplasma gondii and Plasmodium falciparum.

Each point represents a gene in a precomputed or user-built embedding. Colours show annotations or measurements; independently selectable edge layers show the type of relationship between genes. Proximity in the embedding suggests similar input features, but does not establish a physical interaction or shared function.

The window also hosts table import, feature selection, clustering, held-out recovery, and export tools. Long analyses run through the job runner so the interface stays responsive. Use :mod:starplast.discover for discovery searches without a display.

DATA = '/home/runner/work/starplast/starplast/starplast/data'
EDGE_TYPES = [('comention', 'co-mention (33,924 abstracts)'), ('comention_ft', 'co-mention (open-access full texts)'), ('orthogroup', 'shared orthogroup'), ('coexpression', 'co-expression (stage series)'), ('compartment', 'shared compartment (hyperLOPIT)'), ('cofitness', 'co-fitness (7 CRISPR screens)'), ('domain', 'shared InterPro domain'), ('xlms', 'crosslink MS — measured physical proximity'), ('ip_ms', 'IP-MS — replicated pulldown of a tagged bait'), ('struct', 'structural similarity (Foldseek TM ≥ 0.7)'), ('structural_hole', 'structural hole (biology links them, literature does not)'), ('unwritten_interaction', 'measured to bind, never written about')]
COMENTION = ('comention', 'comention_ft')
FIT = ['fit_invitro_hff', 'fit_invivo_PE', 'fit_invivo_lung', 'fit_invivo_liver', 'fit_invivo_spleen', 'fit_naive_bmdm', 'fit_ifng']
EDGE_CAP = 20000
COLOR_MODES = ['compartment', 'compartment (incl. transferred)', 'claims', 'clusters', 'in vitro fitness', 'publications', 'depth of attention', 'structure confidence (pLDDT)', 'cyst / tachyzoite expression', 'annotations']
POINT_SIZES = [('Automatic', None), ('Tiny (2 px)', 2.0), ('Small (4 px)', 4.0), ('Medium (7 px)', 7.0), ('Large (11 px)', 11.0), ('Huge (16 px)', 16.0)]
DEFAULT_POINT_SIZE = 7.0
DISPLAY_HELP = {'Theme': 'Choose interface colours. Paper gives a light background suitable for exported figures.', 'Colour map': 'Choose how numerical values or categories are coloured. Auto matches the data type.', 'Point style': 'Change point size and opacity. An explicit point size in View overrides the style size.', 'Overlap': 'Control how overlapping genes are drawn. Additive blending emphasizes density but can obscure colours.', 'Point render mode': 'Choose flat discs or shaded spheres. This changes appearance without changing the analysis.', 'Light render mode': 'Soft lighting adds depth; ray traced lighting adds shadows from dense regions and costs more rendering time.', 'Light target': 'Aim the light with the cursor or the selected gene. Lighting does not select genes or alter measurements.', 'Pointer beam': 'Change the width and softness of cursor-controlled lighting. Gene-centred lights are unaffected.', 'Pointer response': 'Choose how quickly the light follows the cursor. Smooth and cinematic add a delay.', 'Target marker': 'Show a visual marker at the light target. The marker represents no biological measurement.', 'Light mood': 'Choose neutral, cool, or warm lighting. Neutral preserves category colours most directly.', 'Background': 'Show or hide the animated panel background. Turn it off for a simpler display.', 'Window size': 'Choose the initial window size. Sizes larger than the monitor are reduced to fit.', 'Full screen': 'Use the whole monitor. Turn this off to restore a movable window with its title bar.', 'Fade with distance': 'Fade distant genes for depth. Turn this off when comparing the exact colours of points.', 'Reference grid': 'Show a plane below the map to make rotation easier to follow. Its spacing has no biological unit.'}
COLOR_MODE_HELP = {'compartment': 'Colour each gene by the category chosen in the find panel, compartment by default. Grey means unassigned or not measured, never a class of its own.', 'compartment (incl. transferred)': 'Compartments including labels transferred from orthologs in another species. Inferences, offered for coverage and kept apart from measured labels.', 'claims': "Measured labels in full colour; what Starplast claims for unlabelled genes in the claimed class's colour, faded toward grey by how uncertain the claim is. Pick the label in the Discoveries tab.", 'clusters': 'Colour by the clustering from the analysis panel. Genes left unclustered, and every gene before a clustering exists, stay grey.', 'in vitro fitness': 'Fitness in fibroblasts from the genome-wide CRISPR screen, on a sequential colour map. Genes the screen did not measure are grey.', 'publications': 'How much has been published about each gene, on a log scale. Attention, not importance: a colour to read beside the others.', 'depth of attention': 'Whether papers are about the gene, discuss it, or only mention it in passing. Categories, not a scale; never-named genes are grey.', 'structure confidence (pLDDT)': 'Mean AlphaFold confidence of the predicted structure. Low values often mark disordered regions, not bad data.', 'cyst / tachyzoite expression': 'Expression in the cyst stage minus the tachyzoite stage, on a diverging map. Grey where either stage is missing.', 'annotations': 'Genes with a saved annotation in their own colour; everything else grey, meaning nobody has proposed anything for it.'}
PANEL_HELP = {'console': 'Show the console: everything the program printed, including errors from jobs.', 'jobs': 'Show the jobs panel: every job this session, with stop buttons and memory use.', 'assistant': 'Show the assistant, which answers questions about the map on screen.', 'analysis': 'Show the analysis panel: data, maps, clusters, inference, search and validation.', 'strategies': 'Show the Strategies tab: named ways of inferring something, each self-testing.', 'gallery': 'Show the gallery of maps a walk or search built. Click one to display it.', 'evidence': 'Show the evidence panel for the selected gene, with links to source records.', 'find': 'Show the find panel: gene search, colour-by and the class filter.'}
EDGE_EXPLANATION = "They are kept as separate layers rather than added together into one 'interaction' edge.\n\nThese layers are not measurements of the same thing. A crosslink-MS edge is a measured physical contact. A co-mention edge is two genes appearing in one abstract, which happens to popular genes far more than to related ones. A co-expression edge is a correlation across a stage series.\n\nCombining them would let the weakest inference borrow the credibility of the strongest measurement: a merged score of 0.8 cannot tell you whether two proteins were measured touching or merely mentioned together, and there is no honest weighting that recovers the difference afterwards. So they are drawn in separate colors and toggled independently, and the answer to 'are these two related' is 'by which evidence'."
MAP_EXPLANATION = 'Position represents similarity under the saved feature recipe, reduced to three dimensions. The packaged maps use the same balanced embedding builder as interactive maps. Their archives record the selected features, ordered genes, data hashes and algorithm that actually ran.\n\nThe current default uses numeric biological features with median imputation, robust scaling and balanced blocks. Categorical compartment labels are not one-hot encoded. Numeric localization confidence can still be an input, so the display is not independent validation of localization. No literature column is an input, but uneven measurement coverage can still shape the map.\n\nProximity suggests a question; it does not establish function, interaction or a probability. Use Tools > Predict a trait for separate held-out evaluation, calibration and unsupported-call abstention. Its models fit their own inputs and exclude the target and derived evidence. Compare results with a shuffled-label negative control before interpreting performance. Grey means unknown, not a measured zero.'
MAX_CATEGORY_VALUES = 60
UI_SCALE_RANGE = (0.8, 1.6)
DIAGRAM_NOTE_HEIGHT = 54
MATCH_SCREEN = 'match screen'
WINDOW_SIZES = ('match screen', '1280 × 720', '1366 × 768', '1600 × 900', '1920 × 1080', '2560 × 1440', '3200 × 1800', '3840 × 2160')
CATEGORY_DENYLIST = {'product', 'orthogroup', 'symbol', 'gene_id'}
def as_text(s):

A plain string Series with missing values as "".

.astype(str) is not enough. Under pandas 3 a string column has dtype str and keeps NA through the cast, so the result mixes str with nan: sorting it raises TypeError, and the NaNs compare unequal to everything including themselves. Under pandas 2 the same cast produced the literal "nan". Neither is what the caller wants, so missing becomes "", which ABSENCE already treats as absence -- the same convention the held-out search uses.

RUN_PREFIX = 'clustering: '
BIN_PREFIX = 'binned: '
def numeric_columns(nodes) -> list[str]:

Every column that is a quantity worth binning, best first.

A quantity binned behaves like a category, which is what makes it comparable with a clustering -- the comparison the color-by panel exists for. Identifiers and flags are left out: binning gene_id is not a question anybody has.

def category_columns(nodes) -> list[str]:

Every column that can serve as a filter class, best first.

The compartment field used to be the only one, hardcoded, which meant the map could be filtered by the single worst-recovered property in it and by nothing else. Any column with a small number of repeated values is a legitimate way to slice the proteome, so the panel offers all of them and lets the user decide which question they are asking.

PALETTE = [(0.9, 0.24, 0.24), (0.2, 0.55, 0.9), (0.25, 0.75, 0.35), (0.95, 0.65, 0.15), (0.65, 0.35, 0.85), (0.15, 0.8, 0.78), (0.95, 0.45, 0.7), (0.55, 0.75, 0.2), (0.85, 0.35, 0.1), (0.35, 0.45, 0.85), (0.1, 0.65, 0.5), (0.8, 0.8, 0.2), (0.6, 0.2, 0.45), (0.3, 0.7, 0.95), (0.75, 0.55, 0.3), (0.45, 0.35, 0.7), (0.95, 0.8, 0.45), (0.2, 0.4, 0.35), (0.85, 0.55, 0.55), (0.4, 0.6, 0.5), (0.7, 0.7, 0.9), (0.55, 0.45, 0.2), (0.3, 0.85, 0.6), (0.9, 0.4, 0.45), (0.5, 0.5, 0.95), (0.65, 0.85, 0.35)]
GREY = (0.45, 0.45, 0.48)
EDGE_INK_TARGET = 1500
DEPTH_COLOR = {'focal': (0.98, 0.86, 0.3), 'substantive': (0.35, 0.7, 0.95), 'incidental': (0.55, 0.35, 0.6)}
SPECIES = {'Toxoplasma gondii': {'nodes': 'nodes.parquet', 'graph': 'graph.npz', 'code': 'Tg'}, 'Plasmodium falciparum': {'nodes': 'pf_nodes.parquet', 'graph': 'pf_graph.npz', 'code': 'Pf'}}
DEFAULT_SPECIES = 'Toxoplasma gondii'
def available_species() -> list:

The species whose cache is actually present, in menu order.

Checked rather than assumed: a checkout that has never built the Plasmodium arm should offer one species, not offer two and fail on the second.

def load(species: str = 'Toxoplasma gondii'):

Load one species' built cache: node table, coordinates, edge layers and stored models.

Raises with the command that builds the cache if it is absent, rather than letting pandas raise a file-not-found from inside a constructor, which tells the user nothing about what to do.

INTERACTION_MODES = ('navigate', 'select')
GATE_SHAPES = ('lasso (2D)', 'brush (3D)')
def inside_polygon(x, y, poly) -> numpy.ndarray:

Which of the points (x, y) fall inside a polygon. Ray casting, vectorised over the points.

Written out rather than taken from matplotlib: it runs over 8,140 points as the mouse moves, and the dependency is not otherwise needed at run time. Points with NaN coordinates -- genes behind the camera -- are outside by construction, which is the answer that matches what is on screen.

class Map3D(pyqtgraph.opengl.GLViewWidget.GLViewWidget):

GLViewWidget plus click-picking and gated selection, neither of which pyqtgraph provides.

Map3D(xyz)

Create a navigable OpenGL view for an (n_genes, 3) coordinate array.

def picked(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def gated(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

xyz
pickable
pointer
pointer_px
mode
axis
gate_shape
overlay
def resizeEvent(self, ev):

Keep the gate overlay the size of the view it is drawn on.

def fit_view(self, margin=1.35):

Frame the data rather than assuming a fixed distance.

The embedding is renormalized on every build and its extent changes with the feature set, so a hard-coded 170 left the map as a small island in a large empty viewport.

def animate_distance(self, target: float, ms: int = 420):

Ease the camera to a new distance instead of cutting to it.

A cut between levels of detail loses the viewer's place: the whole picture changes at once and nothing carries over, so the eye has to re-find the region it was looking at. Easing keeps the correspondence between what was on screen and what is now on screen.

def data_radius(self) -> float:

Radius of the point cloud about its center, for framing and grid sizing.

def project(self):

Node positions in widget (logical) pixels; NaN where behind the camera.

def begin_gate(self, x: float, y: float):

Start a gate at a widget-space point. The shape decides what that means.

Split from the mouse handler so the whole gating path can be driven -- and tested -- without a window manager. Every display claim in this project has to be checkable headlessly, and a gate that returns the wrong genes is a display claim.

def extend_gate(self, x: float, y: float):

Continue the gate to a new point.

def finish_gate(self):

Close the gate, emit the genes inside it, and return them.

2D lasso: a polygon in screen space, so it takes everything behind it as well. That is what "grab that visual cluster" means -- the cluster you can see is a screen-space object.

3D brush: a ball in world space around the gene under the press, with the drag setting its radius. Deep in the cloud a lasso also catches the far side, which looks like a selection of one structure and is a selection of two; a world-space ball cannot.

def nearest(self, x: float, y: float, within: float = 1000000000.0):

The gene nearest a widget point, or None. Shared by picking and by the brush anchor.

def under_pointer(self):

Where the gene under the pointer IS, in world coordinates, or None.

Deliberately the nearest gene on screen rather than the nearest along a ray: what a reader means by "that cluster" is the one they can see under the cursor, and the two answers differ only where a nearer gene projects closer to the pointer than the one being looked at.

def mousePressEvent(self, ev):

Start a selection gate or pass navigation input to the OpenGL view.

def camera_basis(self):

(eye, right, up, forward) in world coordinates, read off the camera.

Everything screen-relative needs this. A light asked for "top left" or "where the pointer is" is a statement about the SCREEN, and putting it at a fixed world position instead means it stops being top-left the moment the map is orbited -- which is exactly what "the corner lights do not seem to move" was.

def mouseMoveEvent(self, ev):

Update the mouse light and active gate, or rotate the map in navigation mode.

def mouseReleaseEvent(self, ev):

Complete a selection gate or select the gene under a navigation click.

class Window(PyQt6.QtWidgets.QMainWindow):

The application window: a 3D map, the panels around it, and everything they can do.

Holds the view state that the menus mutate -- level of detail, coloring, point size, which edge layers are active, the filter category -- and redraws from it. State lives here as plain attributes rather than being read back out of widgets, so a headless caller can drive the whole interface without a window manager, which is how the tests exercise it.

Window(species: str = None)

Load one organism and construct its map, evidence docks, menus, and analysis panel.

theme
point_style
point_mode
depth_cue
show_ground
cmap_name
species
n
sel
categories
numerics
bins
runs
category
comps
color_of
color_mode
point_size
edge_on
attn_on
all_edges_on
cluster_labels
placed
gated
imports
annotated
view
scatter
halo_item
emitter_item
grid_item
edge_items
jobs
status
def import_data(self, path: str = ''):

Read a user's table, offer the preprocessing, and add the columns to this session.

def build_import_dialog(self, path: str):

The import dialog, built but not shown.

Split from import_data for the reason every dialog here is: exec blocks until a human closes it, so a test that called it would hang rather than fail.

def apply_import(self, dialog):

Run the import the dialog describes and put its columns into this session.

def settings(self):

Where preferences persist. One place, so a new setting cannot invent its own file.

def work_area(self) -> PyQt6.QtCore.QRect:

The space a window may occupy on the monitor it is on.

availableGeometry, not geometry: the difference is the taskbar, and it is the difference between a window that fits and one whose title bar is off the top of the screen. Falls back to the primary screen because during __init__ the window has not been shown and has no screen of its own yet.

def window_settings(self) -> dict:

The stored window preferences, with the defaults that apply on a fresh install.

def apply_window_size(self, **changes) -> str:

Resize to the chosen size, or go full screen, and say what happened.

Every branch ends up clamped to work_area, including the fixed sizes, so no choice here can produce a window larger than the display. That is the whole point of the control: the previous behaviour was one hardcoded size for every monitor, and a size too large for a screen is not recoverable by dragging when the title bar is off the top.

Returns the sentence shown in Preferences, because a setting whose effect was silently clamped needs to say so -- otherwise choosing 4K on a 1080p screen looks like the setting was ignored.

def log_settings(self) -> dict:

The stored logging preferences, with the defaults that apply on a fresh install.

Off, and WARNING to the console. Off because writing files to someone's disk uninvited is how a tool loses trust; WARNING rather than silent because a warning nobody enabled a log to see is a silent failure with extra steps.

def apply_log_settings(self, **changes) -> str:

Apply the stored settings, with any changes, and persist what was applied.

Persisted only after configure has accepted it, so a directory that could not be written does not come back on the next launch as though it had worked.

def stylesheet(self) -> str:

The application stylesheet for the current theme, panel opacity, text size and display.

One place, because three settings rebuild it and each used to spell the call out: whether the display composites decides whether menus and tooltips are translucent black glass or the same colour made opaque (see starplast.glass).

def apply_theme(self, name: str):

Repaint everything from the palette -- widgets, GL background, and the compartment colors.

def apply_point_style(self):

Re-apply the point style and redraw.

def colour_by_claims(self, target: str):

Colour the map by one label: measured in full, claimed faded by uncertainty, rest grey.

def colors_for_genes(self, mask):

The current coloring, restricted to a subset of genes -- what the gallery paints with.

Taken from colors rather than reimplemented so a thumbnail is colored by exactly what the map is colored by, including the rule that gray means unknown. The visibility filter is deliberately not applied: a thumbnail showing only the filtered classes would look like a different embedding rather than the same one seen through a filter.

def use_embedding(self, coords, rows):

Swap the displayed map for one the analysis panel just built.

rows says which genes this embedding has coordinates for, and the rest are recorded as unplaced rather than drawn. A walk embeds a seeded subsample, so most of the proteome has no position in one of its maps -- and every unplaced gene used to be left at the origin, where 6,000 of them formed a dense lump in the middle of the map that could be clicked, filtered and counted like real structure. That is absence rendered as a value, which is the one thing this application exists not to do.

def show_walk_map(self, step):

Show one configuration from the gallery in the central view.

The expanded map is not a picture of a map: it goes through the same path as "build this map", so genes are clickable, every color mode applies and edges draw as usual.

def open_tutorials(self):

The tutorial index: from this checkout when there is one, otherwise the repository.

def shortcut_rows(self) -> list:

Every keyboard shortcut in the menu bar, as (keys, menu path) rows.

def show_shortcuts(self):

A glass window listing every key the menu bar binds.

def display_choices(self) -> list:

Every display setting that is a CHOICE, as (label, options, current, apply).

One table, so the right-click menu and Preferences cannot drift apart -- they used to be able to: a control added to the dialog was not in the menu, and the menu is where a reader already is when they want to change how the map looks, because they are right-clicking the map.

Numeric settings are deliberately absent. A slider is not a menu item, and text size, spin speed, panel opacity and the three blob numbers stay in Preferences where they can be dragged.

def display_toggles(self) -> list:

The display settings that are booleans, as (label, state, apply).

def set_depth_cue(self, on: bool) -> bool:

Whether distance fades and shrinks a point. Named so the checkbox in Preferences and the item in the right-click menu drive one function rather than two equivalent lambdas.

def set_show_ground(self, on: bool) -> bool:

Whether the horizon grid is drawn.

def build_context_menu(self):

Construct the menu without showing it.

Split from _context_menu for the same reason build_preferences is split from open_preferences: exec() enters a modal loop and does not return until a human closes the menu, so a test that called it hung forever instead of failing.

def set_color_mode(self, name: str):

Change what color encodes. Each mode is a different claim about the data.

def set_point_size(self, size):

Set an absolute point size, or None to follow the point style.

def set_edge(self, key: str, on: bool):

Turn one edge layer on or off. Layers are never merged.

def explain_edges(self):

Explain, in words, why relationship types are kept separate.

In a glass window beside the map rather than a modal box: the explanation is about the layers on screen, and a modal box froze them. Returns the window.

def explain_map(self):

Explain what the map is and what held-out testing says it does not support.

def about(self):

What this is, what it is standing on, and who drew the cell. Returns the text shown.

def about_text(self) -> str:

The About text, built rather than written down, so it cannot go stale.

Everything in it is read at the moment it is asked for: the version, what is doing the computing, how big the shipped cache is and how many genes it holds. A dialog that claims a version the program is not running is worse than no dialog.

def explain_scoring(self):

The precision/recall explainer, read from objectives.py so it cannot drift from the code.

Shown in a monospace, selectable box: it contains a table, and a table reflowed into a proportional font is unreadable.

def set_interaction_mode(self, name: str):

Switch what the left mouse button does, and say so -- a silent mode change is a trap.

def set_navigate_axis(self, name: str):

Constrain the orbit to one axis, or free it.

def set_gate_shape(self, name: str):

Choose the 2D lasso or the 3D brush.

def on_gated(self, idx):

Take a gated set of genes: mark them, report what is in it, and offer it for export.

def clear_gate(self):

Drop the gated set and go back to showing the whole map.

def describe_gate(self, idx) -> str:

What is in a gated set: its composition by the current category, and what carries none.

Composition rather than a list, because the question a gate is drawn to answer is "what is this clump". The unlabeled count leads, because those genes are the candidates a gate exists to produce and most of this proteome is among them.

def export_gated(self, path: str = '', columns=None):

Write the gated genes to CSV, with their coordinates.

A separate action from "export visible" rather than a mode of it: the two answer different questions -- everything passing the filter, versus the set someone drew a gate around -- and one export that silently meant whichever had happened last would be worse than either.

def refresh_annotations(self):

Re-read the annotations file and redraw, switching to the color that shows them.

Re-read rather than tracked: the file is meant to be shared and hand-edited, and a window holding its own idea of what is in it would disagree with the file the moment anyone did.

def use_clusters(self, labels):

Take a clustering from the analysis panel, KEEP it, and color the map by it.

Kept rather than only drawn: the second run used to replace the first with no way back, so the one comparison this application is for -- does this structure survive different settings -- could not be made by looking. It arrives in the color-by panel under a timestamp name.

The coloring switches automatically, because a user who has just pressed "cluster this map" wants to see the clusters and leaving it on compartment made the button look inert.

def set_ui_scale(self, scale: float) -> float:

Scale every piece of text in the program, and let the layouts follow.

On the QApplication's font rather than on a stylesheet: a stylesheet font-size does not change what a widget reports as its size hint, so the text grew and the boxes did not, and labels were cut off at the old width. Changing the application font invalidates every layout, which is the whole point -- a wider label makes a wider row.

def set_ambient(self, mode: str) -> str:

Turn the drifting background on or off, and remember the choice.

def set_ambient_option(self, key: str, value) -> None:

One of speed, size or density.

def eventFilter(self, obj, ev):

Keep the background the size of the panel it sits behind, and behind it.

def set_lighting(self, mode: str) -> str:

Choose off, soft illumination, or one of the density-ray shadow depths.

def set_lighting_option(self, key: str, value) -> str:

Set the interaction target, light mood, or point render mode and apply it now.

def set_container_opacity(self, value: float) -> float:

How much of the background shows through the panels.

def edge_neighbours(self, index: int) -> list:

Genes joined to this one by an edge type that is currently DRAWN.

Currently drawn, not merely present: the question the light is answering is "what is this gene connected to in the picture in front of me", and a light on a neighbour whose edge type is switched off would point at a relationship the reader cannot see.

def frame_lights(self) -> list:

The lights for this frame, from whichever source is chosen.

def occupancy(self):

The map binned into a coarse box, for asking what is in the way of what.

Cached against the coordinates themselves: switching embedding or filtering genes changes the cloud, and everything else -- orbiting, relighting, sixty frames a second -- does not.

def cast_rays(self, lit: list) -> list:

Trace volumetric shadow rays when the explicit ray-traced mode is selected.

Each output light carries one transmittance per gene, computed by marching the segment from that gene to the light through the cached density volume. Soft mode deliberately skips this cost and cannot cast shadows.

def open_preferences(self):

Appearance settings, in a window of their own.

Shown rather than exec'd, and kept on self: a modal dialog freezes the map behind it, so every setting had to be judged from memory of what the map looked like a moment ago. This one stays open beside the window, is moved independently of it, and changes take effect under it while it sits there.

def preferences_dialog(self):

The one Preferences window, built on first use and dressed as glass, but not shown.

Split from open_preferences so the search can read the settings it holds -- to find "spin speed" it has to know which tab the control is on -- without opening a window.

def toggle_slot_tree(self, on: bool):

Show or hide the slot tree. The menu item is a checkbox, so it has to be able to close it.

def open_slot_tree(self):

The slot tree, in a window of its own. Shown rather than exec'd, and kept on self.

A modal here would mean checking the catalog against the map from memory, which is the exact comparison it is for.

def open_species(self, name: str):

Open another species' map in a window of its own, and remember the choice.

Returns the new window. The old one is closed only once the new one exists: building it reads a cache and lays out several thousand genes, and closing first would leave nothing on screen if that failed.

def compare_backends(self, sample: int = 2000):

Run the same embedding both ways and show the two maps beside each other.

In a job, because it is two UMAPs and the window should stay usable; the dialog is built by build_comparison so a test can inspect it without entering a modal loop.

def build_comparison(self, result: dict):

The two maps, the two clocks, and how much they agree -- built, not shown.

def build_preferences(self):

Construct the dialog without showing it.

Split from open_preferences so the controls can be built and inspected without entering a modal event loop: exec() blocks until a human closes the window, so a test that called it hung forever rather than failing.

def selected_job(self):

The job selected in the Jobs panel, or None.

def stop_selected_job(self):

Ask the selected job to stop, reporting why if it cannot.

def stop_all_jobs(self):

Ask every running job to stop.

def build_job_menu(self):

The Jobs right-click menu. Built rather than shown, because exec() blocks.

def copy_job_error(self):

Copy a failed job's traceback to the clipboard.

def resource_summary(self) -> str:

Process memory, system memory and GPU memory, as one line.

Read from /proc and from torch where they exist, with no hard dependency on either: this is a status line, and a status line that raises on a machine without a GPU is worse than one that says nothing about GPUs.

def refresh_resources(self):

Update the memory and CPU line in the Jobs panel.

def free_memory(self) -> str:

Release what finished work left behind, and say what was actually released.

Deliberately does NOT touch running jobs. "Clear RAM" that silently killed a search would be a data-loss button wearing a housekeeping label.

def run_job(self, fn, name: str):

Submit background work. The one entry point, so everything slow is visible in one place.

def toggle_spin(self, on: bool):

Rotate the camera continuously. Depth in a 3D scatter only reads when it moves.

def set_bins(self, n: int):

Change how finely a binned quantity is cut, and redraw if that is what is showing.

def on_category_changed(self, name: str):

Switch what color means, rebuild its palette and its list of values.

def select_compartment(self, name: str):

Tick a compartment in the list, from the diagram. The other half of both directions.

Ticks it rather than merely highlighting it, because the list now filters on ticks: clicking an organelle in the drawing has to do the same thing as ticking its row, or the two halves of "both directions" would no longer be the same operation.

def color_sources(self) -> list:

Everything the map can be colored by, in one list: columns, runs, binned quantities.

One list rather than three controls, because they answer the same question -- what should color mean right now -- and having to know which of three places to look for an answer is the state this panel replaced.

def category_values(self) -> pandas.Series:

The current color source as a string column over every gene.

Absence is "" in all three cases and means the same thing each time: no value measured, no position in that run, no number to bin. It is drawn gray, never as a category.

def keep_run(self, labels, recipe=None, name: str = ''):

Keep a clustering, list it in the color-by panel, and color the map by it.

Kept rather than drawn and forgotten: the second run used to replace the first with no way back, which makes the one comparison this application is for -- does this structure survive different settings -- impossible to make by looking.

def rename_run(self, old: str, new: str) -> bool:

Rename a kept run and keep the panel pointing at it.

def visible_mask(self):

Boolean mask of the genes passing the current class filter AND having a position.

The two are combined here rather than at each drawing site because everything downstream -- points, edges, the gene count in the status bar -- reads this one mask. A gene the displayed embedding does not cover has no position to draw, and drawing it anyway would put absence on the map as though it were a measurement.

def colors(self, vis):

An RGBA color per gene under the current color mode. Gray always means unknown.

def redraw(self):

Redraw everything from the current state: gene points, ground, selection halo and edges.

def draw_edges(self, vis):

Draw the active edge layers, capped and alpha-budgeted so they do not hide the map.

def on_pick(self, i):

Select a gene and show its evidence.

def fly_to_compartment(self, item):

Centre the camera on the genes in the selected category.

def reset(self):

Clear the selection and every filter, and frame the whole map again.

def export_image(self, path: str = ''):

The current frame as a PNG, at the size it is on screen.

def default_export_columns(self) -> list[str]:

What to tick when the picker opens: identity, the active filter, the active coloring.

Taken from the current view rather than fixed, because the columns worth exporting are usually the ones being looked at.

def choose_export_columns(self, preselect=None):

A checkable list of every column, for picking what an export carries.

Built rather than shown, for the same reason as the context menu: exec() enters a modal loop and does not return until a human closes it.

def export_visible(self, path: str = '', columns=None):

The genes currently passing the filter, with their coordinates and chosen columns.

Coordinates travel with the rows because a gene list without them cannot reproduce what was on screen, and reproducing what was on screen is the point of exporting it.

def export_relationships(self, path: str = '', columns=None):

Every active edge between two visible genes, one row per edge, as CSV.

The GraphML export is for Cytoscape; this is for a spreadsheet, and it is what "export the relationships I can see" actually means. Both endpoints carry the chosen columns, suffixed _a and _b, so the file can be read without joining it back against anything.

The edge type is a column and is never collapsed. A row saying only that two genes are related at weight 0.8 has lost whether they were measured touching or merely mentioned together, which is the distinction the whole application is built around.

def export_graphml(self, path: str = ''):

The active edge types over the visible genes, as GraphML for Cytoscape or networkx.

Each edge keeps its type as an attribute rather than being merged into a generic edge, for the same reason the layers are never combined on screen -- see Edges ▸ Why are these never combined? An exported file outlives the session, so it is exactly where a merged score would do the most damage.

def describe_state(self) -> str:

A briefing for the assistant: what is on screen right now.

def show_detail(self, i):

Fill the evidence panel for one gene, distinguishing absence from zero throughout.

def run_my_list(self, genes: list):

Hide a list of genes together on a background job; show what was recovered.

The list's own labels are hidden, nothing else: this asks whether the data, not the list, knows what these genes are. The five fast strategies answer; the shipped record covers all.

def main():

Entry point for the starplast command: build the window and run the event loop.