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.
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.
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.
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.
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.
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.
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.
GLViewWidget plus click-picking and gated selection, neither of which pyqtgraph provides.
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.
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.
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.
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.
Radius of the point cloud about its center, for framing and grid sizing.
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.
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.
The gene nearest a widget point, or None. Shared by picking and by the brush anchor.
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.
(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.
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.
Load one organism and construct its map, evidence docks, menus, and analysis panel.
Read a user's table, offer the preprocessing, and add the columns to this session.
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.
Run the import the dialog describes and put its columns into this session.
Where preferences persist. One place, so a new setting cannot invent its own file.
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.
The stored window preferences, with the defaults that apply on a fresh install.
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.
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.
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.
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).
Repaint everything from the palette -- widgets, GL background, and the compartment colors.
Colour the map by one label: measured in full, claimed faded by uncertainty, rest grey.
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.
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.
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.
The tutorial index: from this checkout when there is one, otherwise the repository.
Every keyboard shortcut in the menu bar, as (keys, menu path) rows.
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.
The display settings that are booleans, as (label, state, apply).
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.
Change what color encodes. Each mode is a different claim about the data.
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.
What this is, what it is standing on, and who drew the cell. Returns the text shown.
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.
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.
Switch what the left mouse button does, and say so -- a silent mode change is a trap.
Take a gated set of genes: mark them, report what is in it, and offer it for export.
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.
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.
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.
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.
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.
Turn the drifting background on or off, and remember the choice.
Keep the background the size of the panel it sits behind, and behind it.
Choose off, soft illumination, or one of the density-ray shadow depths.
Set the interaction target, light mood, or point render mode and apply it now.
How much of the background shows through the panels.
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.
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.
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.
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.
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.
Show or hide the slot tree. The menu item is a checkbox, so it has to be able to close it.
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.
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.
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.
The two maps, the two clocks, and how much they agree -- built, not shown.
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.
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.
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.
Submit background work. The one entry point, so everything slow is visible in one place.
Rotate the camera continuously. Depth in a 3D scatter only reads when it moves.
Change how finely a binned quantity is cut, and redraw if that is what is showing.
Switch what color means, rebuild its palette and its list of values.
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.
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.
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.
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.
Rename a kept run and keep the panel pointing at it.
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.
An RGBA color per gene under the current color mode. Gray always means unknown.
Redraw everything from the current state: gene points, ground, selection halo and edges.
Draw the active edge layers, capped and alpha-budgeted so they do not hide the map.
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.
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.
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.
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.
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.
Entry point for the starplast command: build the window and run the event loop.