Codebase architecture#
This page explains the architecture of the cellucid Python package (repo folder: cellucid-python/): what the core subsystems are, how data flows through them, and which invariants you must preserve when changing code.
Audience
If you are a wet-lab user trying to “make the viewer appear”: read the Fast path and Troubleshooting sections only.
If you are a computational user: focus on the Cellucid-ready AnnData + server behavior.
If you are a developer: read the whole page before editing
prepare_data/,anndata_server.py, orjupyter/.
What the Python package is responsible for (and what it is not)#
Python package responsibilities#
cellucid-python provides:
A Cellucid-ready AnnData writer:
cellucid.prepare(...)writes one standard AnnData (.h5ad,.zarr,.zarr.zip), or from a MuData a Cellucid-ready MuData (.h5mu, MuData Zarr), laid out for the browser’s range reads.One server:
cellucid.anndata_server.AnnDataServerserves the bytes of one dataset in any format the web app reads, classified by the input format registry (input_formats.json: AnnData, MuData, Loom, 10x.h5/outs/, MatrixMarket folders, R objects), which the web app reads itself — with range requests where the format allows it, once from the start for an R object or a MatrixMarket folder.Notebook embedding + hooks:
cellucid.jupyter.AnnDataViewer/show_anndata(...)embed the web app in notebooks and enable Python ↔ frontend interaction.Session tooling: read
.cellucid-sessionfiles (the history of every step: trackers, branches, steps, notes, turns, the journal, the state at each step;read_session,cellucid.history), and plan, then apply, the state at any step onto AnnData or MuData, each apply recorded (apply_cellucid_session_to_anndata,ApplyPlan,ApplyReport,history_applied).Convenience utilities: vector-field derivation helpers, hosted web UI caching helpers.
Not Python package responsibilities#
Rendering, UI state machines, analysis panels, figure export internals live in the web app repo (
cellucid/). Start with: Developer Documentation (Web App Architecture).Community annotation workflows are primarily web-app-driven (repo template lives in
cellucid-annotation/).R is owned by
cellucid-r; itscellucid_prepare()writes the same Cellucid-ready AnnData the web app reads.
Source tree: the “one screen” map#
Most of the public API lives in cellucid-python/src/cellucid/:
File |
What it does |
Read when you… |
|---|---|---|
|
Lazy public API exports via |
want to change public imports without slowing CLI startup |
|
|
want to change what a Cellucid-ready AnnData holds or how it is laid out |
|
the launch-default check shared by |
want to add or change a launch default |
|
the data-mapping rules (validation, unique names, the default UMAP rule, vector-field keys), mirrored by the web app’s |
want to change what the viewer plots and reads |
|
dataset-id and display-text rules, standard library only |
want to change what counts as a valid id or name |
|
the counted NaN/Inf/float32-range diagnosis in refusals |
want to change how a non-finite input is reported |
|
classifies any file or folder by the input format registry ( |
want to change what input is accepted or how it is handed to the browser |
|
the one gene-major layout pass ( |
want to change how an expression matrix is laid out |
|
arrays of a source read by rows or ranges in any order (in memory, mapped in place, or decoded once into a scratch folder); the scratch folders, removed with their owner and by the next one when a process was killed |
want to change how a source array is read |
|
the data server: checks launch choices against AnnData/MuData metadata, serves the bytes with exact |
want to add/modify server endpoints |
|
the viewer API ( |
want to change notebook UX or hooks behavior |
|
the viewer channel: WebSocket upgrade and |
want to change how the viewer and Python talk |
|
RFC 6455 framing over an upgraded HTTP connection (masked, fragmented, in-place unmasking) |
want to change the WebSocket layer |
|
the HTTP server base: CORS, |
want to change security/transport behavior |
|
the |
want to read more of a session, or follow a change of the format |
|
the reading model: |
want to change how a history reads in Python |
|
the history model as frozen dataclasses, generated by |
want to follow a change of the history model (regenerate; never edit |
|
the codecs of a session’s array chunks (uvarint, cell sets), mirroring |
want to align a codec with the web app |
|
the round trip: |
want to change how results or a session’s state reach AnnData |
|
CellRank drift → vector fields |
want to add helpers for velocity/drift conventions |
|
exact inventory-driven viewer generation establishment and verification |
want to change source, staging, verification, or atomic publication behavior |
|
the |
want to add or change a command-line option |
|
|
want to change what an agent app is answered |
|
one agent connection without the transport: which viewer it drives, the tools ( |
want to change how a viewer is chosen or what is said when none is |
|
a viewer’s end for agents ( |
want to change how |
|
|
want to change which cells a subsample keeps (change all three) |
|
strong validators of a file’s version, read from the open file: the server’s |
want to change when a file counts as changed |
|
encoding-safe console output; the noun a count takes |
want to change user-facing console text |
The core contract: the dataset as stored#
Every dataset reaches the viewer as the file or folder it is on disk, read as
it is: the web app’s own readers (HDF5, Zarr, R serialization, MatrixMarket
text) read it directly, from a server route, a link, or a file picked in the
browser. An in-memory AnnData, and an AnnData file not in priority order, are
served by Python as a Zarr store in priority order made as the viewer reads it
(src/cellucid/_served_store.py), and so is an in-memory MuData, as a MuData
store. A file written by
prepare() follows the Cellucid-ready AnnData contract
(Cellucid-ready AnnData specification):
gene-major, chunked for range reads, and naming itself in uns['cellucid'].
A server only adds /_cellucid/datasets, which names the dataset and states
the launch choices — see
Server mode architecture, endpoints, and security.
Data flow diagrams (high level)#
B) AnnData server workflow (nothing converted)#
Python:
AnnDataServerclassifies the path by the input format registry (classify_anndata_path), presents an in-memory AnnData, an AnnData file not in priority order, and a MuData, Loom or 10x file as a Zarr store in priority order made as the browser reads it (_served_store.py,_served_formats.py; an in-memory MuData as a MuData store), reads its metadata (an R object and a MatrixMarket folder are read by the browser alone), and checks the launch choices against it.Browser: reads
/_cellucid/datasets, then opens the store at/_cellucid/anndata/<dataset_id><ending>(the file’s own ending, such as.h5ad,.zarr.zip,.loomor.rds.gz),<dataset_id>.zarr/<key>for a Zarr folder, or<dataset_id>/<file>for a folder of files, whose file list/_cellucid/datasetscarries.Browser: to show
FOXP1, its HDF5/Zarr reader issues HTTP range requests for exactly that gene’s bytes; Python answers each with206and the bytes on disk.
C) Notebook embedding + hooks workflow#
Python:
show_anndata()prepares the dataset (anAnnDataServerthat is never bound), creates aviewerIdandviewerToken, and shows aWidget.Notebook: the widget’s output frames
https://www.cellucid.com/?viewer=embeddedand hands the page the id, the token and aMessagePort.Frontend: the viewer reads the data through the port (the widget relays each request to the kernel), opens its channel and says hello with its
viewerIdandviewerToken.
A browser-tab viewer (AnnDataViewer) instead binds the server (port 8765, or
the next free port when another viewer holds it and no port was given), and
the page at <server>/?viewer=true&viewerId=…&viewerToken=…&anndata=true
opens its channel at /_cellucid/ws.
4) Python → frontend: commands on the channel; frontend → Python: events on
the same channel.
Architecture diagram (recommended)#
If you want a single figure you can refer to in bug reports and onboarding, add an architecture diagram.
Invariants (do not break these without a coordinated frontend change)#
1) Cell indices are positional#
Frontend selections/highlights are expressed as row indices (0-based positions).
Implications:
If you reorder cells, the same indices refer to different biological cells.
For “transfer state” between datasets, you need stable dataset identity + explicit mapping logic.
3) Notebook environments are hostile to assumptions#
Do not assume:
the browser can reach
http://127.0.0.1:<port>(remote kernels, Colab),mixed-content is allowed (HTTPS notebooks),
network access is available (airgapped environments),
or that the kernel hears the viewer while a cell runs (it does not: a widget’s messages arrive between cells).
Notebook constraints drive the existence of:
the notebook widget, which needs no port, address or proxy,
for browser-tab viewers, same-origin local viewer generation delivery and the explicit
client_server_urlargument,and the debugging helpers in
viewer.debug_connection().
Troubleshooting (quick routing)#
Symptom: “I changed Python and now the viewer fails to load data”#
Likely causes:
you broke an invariant of the Cellucid-ready AnnData (element names, encodings, codecs),
Python and the configured web source publish different current contracts,
you introduced a CORS/origin mismatch.
Start with:
prepare() writer architecture (writer invariants),
Server mode architecture, endpoints, and security (HTTP + CORS checks),
Debugging playbook (how to capture minimal repro).
Symptom: “Hooks/events stopped working”#
Start with:
then run
viewer.debug_connection()and follow Debugging playbook.