Architecture
This page is for whoever maintains cellmap-flow. It says what each module is
for, how the main requests travel through them, where state is kept, and the
conventions a change has to keep. It describes the package as the 0.3.0
cleanup (PR #103) leaves it. Each module’s docstring says more about it, and
CHANGELOG.md at the repository root says what changed and why.
Paths below are relative to cellmap_flow/.
Module map
Reading data and running a model
Module |
What it is for |
|---|---|
|
Reading datasets. It imports nothing from the rest of the package (no
Metadata, pyramids, chunk listing, reading and writing are
worldarray’s own ( |
|
|
|
|
|
|
|
|
|
|
|
|
Models
Module |
What it is for |
|---|---|
|
One module per model type. |
|
The classes’ public import path, which the docs and plugins use, and
|
|
A model entry, |
|
The model types (every |
|
|
|
A model’s geometry without building the model: from a running
server’s |
|
The cellmap models on the Hugging Face Hub, for the Models tab. |
|
Merging several models’ outputs in a blockwise run ( |
|
The Models tab’s catalog. |
|
A deprecated alias; see Deprecated in 0.3.0. |
Serving and jobs
Module |
What it is for |
|---|---|
|
|
|
The model and its chains as chunkmirage ops: |
|
|
|
|
|
What a layer URL carries to a server: |
|
|
|
What a model’s output range says about the postprocessing it needs. |
|
The secret a finetune job’s server takes a restart with. |
|
Running jobs on LSF or on this machine, without importing
|
The viewer and the dashboard
Module |
What it is for |
|---|---|
|
The neuroglancer viewer. It never imports or starts the dashboard;
callers pass what a layer needs. (The raw layer is a worldarray
array indexed through
|
|
The Flask app: its blueprints, the log panel’s handler, and
|
|
|
|
The typed bodies of the POST routes (pydantic), and |
|
How far a long request has got (a YAML crop import, a resume), under
the |
|
The job manager’s listener: a finetuned model’s viewer layer, and its
|
|
Hands the session’s MinIO state and volume records to
|
|
|
|
One blueprint per area:
|
|
The two pages; below. |
The pages are ES modules served as they are, with no bundler:
dashboard/templates/
index.html the dashboard page; includes _dashboard.html and the
_models_tab, _processing_tab, _finetune_tab, _run_tab
and _review_tab partials
pipeline_builder_v2.html the pipeline builder
_log_panel.html the dashboard's log at the foot of both pages, with
its stylesheet and script
bbox_json_template.html
dashboard/static/css/ tokens.css (the palette), dark.css, pipeline_builder.css,
log-panel.css
dashboard/static/js/
lib/ api, dom, page-data, poll, sortable, sse: know no page
shared/ gpu-queues, op-chain, server-config, log-panel: used by
both pages
dashboard/ main.js (the page's entry), connect, models-tab,
model-advice; finetune/ and review/, each an index.js
that wires one module per part of its tab
pipeline-builder/ main.js (the entry), state, canvas, nodes, palette,
io, blockwise, bbx, dialogs, messages,
model-config-modal, output-channels
vendor/js-yaml.js
A template hands its scripts data as JSON, in
<script type="application/json" id="page-data">, which
lib/page-data.js reads. Nothing is put on window and no markup has an
inline handler. Build DOM with lib/dom.js’s h() rather than HTML
strings, so that names from a file or a server never run as script.
Blockwise and finetuning
Module |
What it is for |
|---|---|
|
Running models over a whole volume, to disk, with daisy.
|
|
The training job’s entry point, |
|
The trainer’s flags ( |
|
|
|
What each iteration leaves: |
|
The lines the trainer prints for the job manager to find in its log. |
|
|
|
The masked losses; annotations (0 unannotated, 1 background, 2 and up objects) as training targets and masks. |
|
|
|
|
|
What the trainer trains on: |
|
A finetune session on disk, with no Flask, neuroglancer, torch or
|
|
Finetune jobs from the dashboard’s side: |
|
The schema of a YAML crops manifest; building a corrections directory from one without a dashboard. |
|
Folding a finetune into the base weights and re-exporting it at a larger tile. |
|
The YAML that serves a finetuned model. |
Entry points and process-wide modules
Module |
What it is for |
|---|---|
|
The |
|
|
|
Plugins in |
|
|
|
Deprecated, and gone after 0.3.0: |
|
The Review tab’s SQLite index: reading it, and building one
( |
|
|
Request flows
A chunk served to Neuroglancer
A prediction layer’s source is
zarr://<server>/<model>__CFLOW_ARGS__<blob>__CFLOW_ARGS__/zarr(viewer.layers.prediction_source): the layer’s name, then the format the server serves it in (serving.protocol.FORMAT). The blob is the layer’s chain with the dashboard’s address and the chain’s digest,Session.layer_blob()(PipelineSpec.to_url_blob): minified JSON in URL-safe base64 (serving.protocol.encode_to_str). Every request Neuroglancer makes for the layer carries it, and every path that draws a layer builds it the same way.The server is
serving.chunkmirage_server.ChunkmirageServer, built on chunkmirage, which reads, caches, schedules and serves; cellmap-flow keeps the model, its chains and the routes the dashboard reads. Neuroglancer asks it for<name>/zarr/.zattrs,<name>/zarr/s0/.zarray, then chunks<name>/zarr/s0/[<c>/]<z>/<y>/<x>: one level, s0, at the output voxel size from the corner of the level the model reads, with any channel axis first. A level read as if it were at the model’s voxel size (no--resample) is served where it really lies, and model_info says so (input_relabelled_from).chunkmirage resolves an unknown name on its first request (
ChunkmirageServer.pipeline_for) into a pipeline ofserving.chunkmirage_ops.layer_ops: the raw data (the levelio.raw.open_rawchose, as a chunkmirage source placed where it is stored); anInferenceOp, the model on the layer’s normalizers; onePostprocessOpper postprocessing step (thePostProcessorsteps the Output tab builds, run as they always were, with their state kept for the layer’s lifetime); and chunkmirage’scastif the chain leaves another dtype than the one served. The blob’s chain is the layer’s own (serving.protocol.split_dataset_urlfinds it in the name), so layers sharing a server never share a chain. A name without a blob gets the process’s chain,process_chain(), which is empty in a server started from the command line; the log then warns that the model is fed raw voxel values.chunkmirage reads each chunk’s input with the model’s halo (the data’s outermost voxels repeated past it) and queues it for the device, finest level first, dropping chunks nobody waits for any more (
InferenceOp.slots, fromCELLMAP_FLOW_GPU_SLOTS, plus a few chunks whose normalization overlaps the running forward). The forward itself runs inside aDeviceSlotsslot: by default one chunk at a time on the GPU, first come first served.InferenceOphands the model aBlockInputfor itsto_ndarray_ts, which applies the normalizers to what lies inside the data and repeats its edge after, asio.raw.RawInput.to_ndarray_tsdoes over an arrayopen_rawpadded with"edge"; a config with its ownprocess_chunk(TensorFlow, ONNX, cellpose, bioimage.io) reads through the same. A chunk the data ends in is computed whole and cropped.The model’s output is cached (
CELLMAP_FLOW_PREDICTION_CACHE_BYTES, 16 GiB by default, 0 for none;CELLMAP_FLOW_RAW_CACHE_BYTES, 1 GiB, bounds the raw data kept): panning back, or changing only the postprocessing, does not run the model again. Its cache key includes the model’s weights version, which the finetune loop bumps after each iteration (ChunkmirageServer.weights_changed). A postprocessor that merges segments across chunks posts its equivalences to<dashboard_url>/update/equivalences, at most every 5 s, from a thread of its own.
Apply, or PUT /api/pipeline, and the layers
The dashboard page’s Apply, at the top of the Processing tab (
templates/_processing_tab.html), reads the ticked ops of both its lists (static/js/shared/op-chain.js) and sendsPUT /api/pipelinewith{input_norm, postprocess}(static/js/dashboard/main.js). Until then the bar counts the edits not applied (op-chain.jschainChanges, against the chain last applied or shown). A model advice suggestion’s Apply (static/js/dashboard/model-advice.js) sets its steps in its list and goes through the same Apply. The pipeline builder sends the same, and its canvas asbuilder, 2 s after its last edit (static/js/pipeline-builder/state.js).dashboard/routes/pipeline.put_pipelineparses the body as aPipelineUpdatethroughdashboard.requests.parse(an unknown op is a 400), then builds the steps (a value an op’s class refuses is a 400). Nothing has changed yet._set_chain_and_redrawmakes it the chain withSession.set_pipeline, which sets the live steps and their config together, and keeps the canvas if one was sent. With no viewer yet (no dataset opened) it stops there.The layers’ source is named by the chain’s digest,
Session.layer_blob(). The same chain sent again gives the same URL, so Neuroglancer keeps the chunks it has, and each server reuses the chain it built; a layer a launch drew over this chain is left alone.It keeps the shaders the user set in the viewer, dropping a layer’s contrast range when its chain changed. Then, in one
viewer.txn(), it rebuilds the raw layer (viewer.layers.raw_layer) and a prediction layer for each job that has a host (viewer.layers.prediction_layer, given the server’smodel_infofromserving.client.fetch_model_info). A chain that ends in labels gives a segmentation layer, and the server’seffective_output_voxel_sizeplaces the layer.The answer is
{"success": true, "pipeline", "digest", "layers"}; see Viewer layer API. The layers’ next chunk requests carry the new blob.
The one other change of the chain a model makes: a model started from the
Models tab when the chain has no input normalizers sets the ones it declares
(ModelConfig.declared_input_norm: an onnx model’s manifest, or a
cellmap-models export’s metadata.json input_normalization, read by
serving/probe.metadata_input_norm), or, for a CellMap or DaCapo model
that declares none, their convention ([-1, 1]), before its layer is drawn
(routes/pipeline.set_declared_input_norm, through
_set_chain_and_redraw; the browser’s serving.ts
setDeclaredInputNorm, through setChain, whose models all declare
theirs). After every model change the page reads the chain back from
GET /api/models/loaded’s pipeline, and a list whose chain the
backend changed shows it, saying so above it, and whether the model
declared it (input_norm_declared).
Launching a model server
A model config comes from
cellmap_flow infer <type>(cli/infer.py: a command per type inmodels.registry.model_types(), its options fromregistry.click_options, its options made a model entry), from a YAML (cli/yaml_cli.py:config.yaml.load_config,registry.build_models), or from the Models tab (POST /api/models,dashboard/services/launch.py: each catalog, Hugging Face, zoo and Cellpose pick is an entry, built the same way).registry.build_modelis the one constructor path.serving.launch.launchis the one launcher: it applies the model’sscaleto the data path (config.yaml.resolve_data_path), falls back to the saved queue and charge group, builds the command line (the words ofjobs.launch.SERVER_COMMAND,cellmap_flow serveby default, then--modeland the model’s entry as JSON,ModelConfig.launch_entry, then-d <data path>) and starts the job.jobs.launch.start_hostssubmits it to LSF with one GPU (submit_bsub_job, thenjobs.lsf.submitandbsub_argv). When a queue does not start the job it tries the next ofjobs.queues.candidates. The caller gives the queue and charge group (the CLIs default to the saved settings); the walltime is the caller’s, else the saved setting (jobs.settings.launcher_settings()), elsejobs.site’s. It runs the server on this machine (jobs.local) only when bsub is not installed; when bsub is there and every queue fails, it raisesJobStartError.It waits for the server’s address: from the ready file named in the job’s environment (
jobs.ready), else from theCELLMAP_FLOW_SERVER_IP(...)marker in the job’s output, through bpeek. Then it adds the job tojobs.launch.started_jobs().On the GPU node,
cellmap_flow serve --model <entry> -d <data path>(cli/server_cli.py) rebuilds the config withregistry.build_model, as a YAML’s model entry is rebuilt, and starts the server (serving.engine.make_server). ItsInferencerloads the model, and the warmup forward checks the declared shapes, probes the output range (formodel_info) and keeps fp16 only if it agrees with fp32 (CELLMAP_FLOW_HALF_PRECISION).run()serves; once the port is bound it prints the address marker and writes the ready file.The launcher adds the model’s layer (
viewer.layers.prediction_layer). The CLIs do it indashboard.services.startup.generate_neuroglancer_url, which then serves the dashboard and does not return.
A blockwise run
The pipeline builder’s blockwise steps post to
/api/blockwise/validate,/generate,/precheckand/submit(dashboard/routes/blockwise.py). Each answers 200, withvalidorsuccessfalse and anerrorwhen it cannot go on. Generate writes the task YAML underblockwise_tasks_dir; precheck isblockwise_processor.precheck, which loads no model and writes nothing. Submit sends the master to LSF as a CPU job:python -m cellmap_flow.blockwise.multiple_cli <task YAMLs>. From a shell,cellmap_flow blockwise <task.yaml>runs one master.The master,
CellMapFlowBlockwiseProcessor(yaml, create=True), reads the YAML (config.yaml.load_config,registry.build_models,resolve_data_path), builds noInferencer(it needs the geometry only), and creates the output arrays withworldarray.create, which writes their OME attributes, on a grid that starts at the corner of the raw level the model reads. An output covers the raw extent, or only the bounding boxes: their bounding box, or each box in its own<output_path>/box_<n>withseparate_bounding_boxes_zarrs.run()makes one daisy task per region (the whole extent, or each bounding box), with the idpredict_<model>_<task>(_roi<n>after it for each of several boxes), its blocks laid from its output’s corner and the last ones cut at the region’s end, so they meet the output’s chunks and the task sets noread_write_conflict. Daisy spawns the workers throughspawn_worker: each is an LSF job, submitted withjobs.launch.submit_bsub_job, that runscellmap_flow blockwise <task.yaml> --client, with a walltime and its own log.A worker builds an
Inferencerper model, takes blocks from daisy and runsInferencer.process_chunkon each, merging several models withmodels.model_merger. Withtrack_progressit writes a marker per finished block undertmp_dir, and a resumed run skips the blocks that have one.The master exits 1 if any block failed or never ran.
A finetune job
The Finetune tab posts
/api/finetune/submit.dashboard/routes/finetune/training.pyparses it (requests.FinetuneSubmit), brings the session’s_virtual_sources.jsonup to date, settles the loss and target type (painted, sparse annotations change them), has the viewer’s listener follow the manager (finetune_layers.follow_jobs), and callsFinetuneJobManager.submit_finetuning_job.The manager (
finetune/job_manager/manager.py) works out what to run withsubmit(the model type, checkpoint, corrections, channels and voxel sizes), makes the run’s directory<session>/runs/<model>_<ts>/with itsrestart_tokenandmetadata.json(persistence), and launchessubmit.build_command’s line withsubmit.launch: on LSF with one GPU, or here without bsub. The line runspython -m cellmap_flow.finetune.finetune_cliand pipes it throughteeinto the run’straining_log.txt. A monitor thread follows the job.The trainer (
finetune_cli.main) parses its flags (finetune.cli), loads the model (model_loading.load_trainable_model), prepares it (adaptation), and runssession_loop.TrainingSession. Each iteration trains onfinetune.data’s patches withlora_trainer, exports intoiterations/NNN_<ts>/and writes a serving YAML (run_outputs), and printsFINETUNED_MODEL_YAMLandTRAINING_ITERATION_COMPLETE(markers.emit). Without--auto-servethe job ends there. With it, the job starts an inference server (serving.engine.make_server) on the model it trained, in the same process (its address marker goes into the same log; each later iteration tells the server its weights changed), printsWAITING_FOR_RESTART, and waits for/__control__/restartor arestart_signal.json.Every 3 s the monitor (
job_manager/monitor.py) asks the scheduler about the job (state.on_scheduler_status) and reads the log’s new lines (tailer.LogTailer): epochs and loss, status markers (state.on_status_marker), the server’s address and each finished iteration. It records each change inmetadata.json. A job the scheduler says has completed is COMPLETED only once its export is found (monitor.complete_job), and FAILED if it is not.The monitor tells the listeners
on_server_readyandon_iteration_complete. The dashboard’sFinetuneLayerListener(dashboard/finetune_layers.py) answers with the model’s layer, served by the job’s server, and aFinetuneModelConfigamong the session’s models, so the pipeline builder offers it.The tab polls
/api/finetune/job/<id>/statusand streams.../logs/stream(server-sent events,static/js/lib/sse.js), and stops at a final status. A restart (.../restart) syncs the annotations from MinIO, thenjob_manager/restart.pyPOSTs the new settings to the job’s/__control__/restartwith the run’s token inX-Restart-Token, or writes them torestart_signal.jsonwhen the server can’t be reached. A dashboard started later finds a session’s jobs again from theirmetadata.jsonand one bjobs call (persistence.rehydrate). Shift+R over the viewer (routes/finetune/restart_key.py) counts a press against the job the tab polls, if it can take a restart, and the status answer carries the count (restart_requests): the tab sends the restart with its form’s values when the count goes up, and the restart route says in the viewer’s status bar how it answered.
Annotation volumes and MinIO sync
New Volume posts
/api/finetune/create-volume(routes/finetune/annotation_core.py). The model’s geometry comes frommodels.geometry_cache.resolve_model_geometry: its running server, else the cache, else a build.finetune.session.volume.plan_volumesizes the volume over the whole raw dataset, one chunk per model output, andcreate_volume_zarrwrites<id>.zarrinto the session’scorrections/.dashboard.finetune_utils.ensure_minio_servingcallsfinetune.session.minio.MinioServer.ensure_serving. It startsminio serverif none is running, with the bucketannotationsopen to anonymous access and the periodic sync thread. Then it pulls any chunks painted since the last sync, and mirrors the volume up withmc mirror. The volume is registered in the session’sannotation_volumes(session.store.SessionStore), and its_virtual_sources.jsonis written (session.volume.build_manifest,session.manifest.write_manifest); the trainer finds the volume only through that file.The page then posts
/api/finetune/add-to-viewer(routes/finetune/overlay.py): a writable segmentation layer ons3+<MinIO URL>, selected, with the painting tools bound. The browser paints into MinIO directly.finetune.session.syncbrings the painted chunks back to the volume on disk: every 30 s, on Save (/api/finetune/sync-annotations), before a restart, and before each mirror. It lists the volume’s chunks in the bucket with their ETags and copies those that changed (through.partfiles), recording what it saw in the volume’schunk_sync_state. Nothing on disk is deleted because MinIO lacks it.The trainer reads the volume on disk, never MinIO. Volumes are also filled by a YAML crop import (
/api/finetune/load-crops,session.volume.write_crop_into_volume), resumed from an earlier session (annotation_sessions), seeded from a segmentation (session.instance), or built without a dashboard (python -m cellmap_flow.finetune.build_corrections).
Where state lives
Its owners
Four objects hold what a process shares. Each is read through a function, at
call time and never into a module global, because tests/conftest.py
swaps in fresh ones for every test (_fresh_process_state).
jobs.settings.launcher_settings(): the launcher settings, one attribute per key ofSERVER_CONFIG_DEFAULTS(queue,charge_group,walltime, …), loaded from~/.cellmap_flow/server_config.yamlthe first time the process asks, andsave(). The CLIs and/api/server-configwrite them;start_hosts, blockwise and the dashboard’s finetune submit read them.process_chain.process_chain(): the process’s chain.input_normsandpostprocessare the live steps, which can hold state;input_norm_configandpostprocess_configthe steps as the dashboard received them;specthe chain as data, from the configs or else the live steps;set(spec, built=None)the one way to change it. In the dashboard it is the chain the user submitted. In a server or a blockwise worker it is the fallback for a layer URL or model script that gives no chain.jobs.launch.started_jobs(): the servers this process started, whichstart_hostsappends to,cleanup_handlerkills, andserving.client.running_job_hostlooks a model’s host up in.dashboard.state.get_session(): the dashboard’s own state (below).
The dashboard’s Session
Dashboard code reads and writes its state through
dashboard.state.get_session(), a module registry rather than Flask’s
app.extensions: the CLIs fill it before dashboard.app is imported,
and launch threads, job monitors, the MinIO sync thread and the log panel’s
handler run without an app context. It holds:
the dataset and the viewer:
dataset_path,viewer,raw,shaders,shader_controls,extra_layers,neuroglancer_url;the models:
models_config,model_catalog;the pipeline builder’s last apply:
builder_state,builder_model_configs;blockwise’s
tmp_dirandblockwise_tasks_dir, andtasks_dir();the finetune and review tabs:
minio_state,annotation_volumes,output_sessions,finetune_job_manager(made when first asked for),review;the log panel’s
log_buffer,log_idsandlog_clients, and the box tool’sbbx_generator_state.
It forwards the rest to the owners above: each setting, settings,
server_config and save_server_config(); input_norms and
postprocess (read-only), pipeline_spec and set_pipeline(spec);
and jobs, whose assignment replaces the started list’s contents. It has
__slots__, so a misspelt attribute raises instead of being stored.
The deprecated g
cellmap_flow.globals.g stays for one release, for scripts and plugins.
Each name it had forwards to its owner with a DeprecationWarning that
names the replacement. The warnings are silent in the servers and the
dashboard, whose code is not __main__, and show under pytest.
Nothing in the package imports globals, and test_import_targets
checks it. Importing globals also configures logging, so a module that
imported it reset a CLI’s --log-level to INFO whenever the CLI imported
that module after parsing the flag, as cellmap_flow_server imports the
server. New code uses the owners:
no
import cellmap_flow.globals, and nog.x;dashboard code reads state through
get_session();everything else takes what it needs as arguments, as
finetune.sessiondoes: its MinIO and sync functions are given the dashboard’sminio_stateand volume records, and keep none of their own.
On disk
Where |
What |
|---|---|
|
The saved job settings: read by |
|
Registered plugins. |
|
Inference-server job logs and ready files ( |
|
The geometry cache and the Hugging Face listing. |
|
The dashboard’s blockwise task YAMLs and master logs, and its default
|
|
The Finetune tab’s saved form, and the corrections directory used when no output path is given. |
|
|
The session layout, metadata.json, the markers and the annotation volume
format are read by dashboards older and newer than whatever wrote them, so
they change only compatibly.
Conventions
An OME translation is voxel 0’s centre
OME-NGFF, and Neuroglancer, put translation at the centre of voxel 0,
and so does worldarray: a WorldArray’s and an ArrayMeta’s
translation is that centre and corner the lower corner of voxel 0,
and worldarray converts when it reads a store’s documents and when it
writes them (create); no code in cellmap-flow adds or subtracts half a
voxel. Janelia pyramids have
translation = scale/2 − 4 nm, so every level’s corner is at −4 nm. The
served zarr and the blockwise output start at the corner of the raw level the
model reads. The legacy
resolution/offset and N5 transform attributes keep their old
meaning. An annotation volume’s root attribute dataset_offset_nm is voxel
0’s centre, and session.volume.volume_corner_nm gives the corner.
The chain format
A chain is {"input_norm": [...], "postprocess": [...]}, each an ordered
list of steps, each step a flat {"name": <op class>, **constructor
arguments}. It is the same in layer URL blobs, exported YAMLs, blockwise
task YAMLs’ json_data and finetune manifests, and it is read and written
through pipeline_spec.PipelineSpec. A step is never nested as
{"name", "params"} on the wire, because older servers pass every key but
name to the constructor. Values are kept as given: the dashboard’s forms
send strings and the constructors parse them. Files may still hold the older
{Name: {params}} dict, which the readers accept (it can’t hold the same
op twice); PUT /api/pipeline refuses it.
“Behaviour change:” commits
A change to anything a user, a file or another version of cellmap-flow sees
(served data or metadata, files written, HTTP answers, the CLI, training
results) is its own commit, whose subject starts Behaviour change:. It
carries a test that fails on its parent, and it is listed under
“Behaviour-change commits” in CHANGELOG.md. A refactor leaves every test
below passing unchanged, so that a behaviour change can be reviewed, reverted
or dropped on its own.
Tests
One behavioural test per feature, in each mode. Each user-facing feature has a test that goes through a public entry point (a dashboard route, the CLI, a served chunk, a training run; in the browser app, a backend route over the fakes in
web/test/unit/backend-fakes.ts) and that a refactor keeping the behaviour leaves passing. A behaviour is tested once, at its outermost layer, not again at each layer beneath it (the route, then the job manager, then the command line, then the CLI).Tests live next to their subject, in
tests/utils/(most of the package, whatever its package),tests/cli/,tests/finetune/,tests/blockwise/,tests/ai_annotate/ortests/web/, and test through the public interface: nothing on private helpers, and no test that would fail only because of an internal rename.Tables with readable row ids: near-duplicates are one parametrized test, each
pytest.param(..., id="...")named for its case, and a test’s name says the behaviour it protects.No wording or shape pins. A test checks what a user or another program acts on: a status code and the kind of error, the values served, the files written, a YAML read back with
yaml.safe_load. It does not freeze an error’s sentence, help text or a prompt, a command line or a task YAML byte for byte, a list of element ids, a hash of trained weights or a table of per-epoch losses. Where another party reads a contract, the test is that party’s check:test_deployabilityruns what Fileglancer’srunnables.yamlruns, andtest_import_hygieneandtest_import_targets(everyfrom cellmap_flow... importin the package and the tests resolves) guard the imports.Parity where both modes must agree. The browser app’s ports are checked in
web/test/parity/against JSON thatweb/test/fixtures/make_*.pywrites from the Python, for what a user sees the same in either mode: the__CFLOW_ARGS__blob and its digest (a shared link names the same layer), the input norms and the postprocessors, the ids from Morton codes, connected components, mutex watershed and the merger, the finetune targets, Label it, and AI-annotate’s planes and paint.tests/web/test_fixtures_fresh.pyfails while a fixture is stale. An op that parity covers gets no TypeScript unit test of hand-copied Python values.Characterization tests stay on their branch. A refactor that wants the current behaviour pinned first writes the snapshot on its branch, runs it on the code before and after the change, and drops it before the merge.
Shared fixtures are in the conftest files.
tests/conftest.pypointsHOMEat a temporary directory before anything imports cellmap_flow, gives every test fresh owners of the process’s state (_fresh_process_state), restores the root logger after it, and hasraw_zarr,ome_pyramid,write_array,model_script,fake_lsf,vieweranddashboard.tests/finetune/conftest.pyandtests/blockwise/conftest.pyhave their areas’ fixtures.Patch where the name is used. A test that monkeypatches a name patches it on the module that calls it, and asserts its fake was called, so a move can’t turn it into a test of nothing, or of real bsub or
mc.Markers:
finetune(needs peft, skipped without it),gpu,lsf,minio,network(opt in withCELLMAP_FLOW_NETWORK_TESTS=1) andslow. CI runspytest -n auto -m "not gpu and not lsf and not minio and not network"with and without thefinetuneextra;-n autospreads the tests over the machine’s cores with pytest-xdist (in thetestextra), so a test may not rely on another having run before it in the same process.Import hygiene: what jobs, servers, finetune runs and the dashboard’s request threads import must not drag in the dashboard or a model. No module imports
globals(test_import_targets). A new module imports Flask, neuroglancer, huggingface_hub and peft only where it needs them, never at module level, and torch likewise outsidefinetune/andinference/.test_import_hygienechecks it in a fresh interpreter per group:jobs/and the job manager (throughjobs.launchandjob_manager.manager, which import the rest),serving/(withmodels.geometry,geometry_cache,inference.runner, theInferencerandio.raw), the registry (withconfig.yamlandserving.launch),models.resolve, the state’s owners (withprocess_chain),finetune/session/and a finetune run’s modules (adaptation,losses,dataand the training loop); that importing the server,blockwise/or the finetune CLI leaves logging alone, andjobs.launchimported off the main thread leaves the signal handlers alone; that plugins run when a command starts, not on import; and thatimport cellmap_flowwrites nothing underHOME.
The dashboard’s JavaScript
The repository has no JavaScript test runner. CI checks that every file in
static/js parses as an ES module (node --input-type=module --check;
plain node --check passes a module with a syntax error), and pytest
checks what it can without a browser: what the two pages hand their scripts
(test_dashboard_pages), that every element id the builder’s and the
tabs’ scripts look up is on the page and that no markup runs an inline
script (test_dashboard_tab_scripts), and that every module the pages
load and every import resolves (test_static_assets). That the wheel
ships every file is CI’s package job.
What a page does was checked during the cleanup with a headless-Chrome
harness. It renders each page from the tree with Flask’s test client and
loads it in chrome-headless-shell with the tree’s own scripts and styles.
fetch, EventSource and the dialogs are stubbed, timers run on
virtual time, and scripted scenarios drive the page. Each scenario records
every request (method, URL, body, headers and the script that sent it),
every error and alert, and a signature of each rendered element;
compare.py diffs a run on the base against one on the change, and a
refactor must show no difference. The harness is not in the repository: it
is kept with the cleanup’s working notes, in
cleanup_review/tools/fe_harness/ (harness.py, compare.py, the
*_scenarios.py files and libcheck/), with one machine’s Chrome and
node paths written into it.
The deployability contract
Fileglancer runs cellmap-flow from this repository with pixi, and
tests/utils/test_deployability.py pins what that needs:
runnables.yamlrunspixi run <console script> [<subcommand>]for scripts in[project.scripts], with flags they accept (-dforcellmap_flow view, a positional config forcellmap_flow yaml). A change to a command’s name or flags changes the manifest in the same commit.The dashboard binds a free port with a threaded server, prints its URL, and writes it to the file named by
SERVICE_URL_PATHwhen that is set.jobs.launch.SERVER_COMMANDcomes fromCELLMAP_FLOW_SERVER_COMMAND, which pixi’s activation sets topixi run cellmap_flow serve. It is read at import and used at call time; no other module keeps a copy of it. The value before 0.3.0,pixi run cellmap_flow_server, still takes the--modelthe launchers pass.cellmap_flow viewbills the models it launches toLSB_PROJECT_NAME, the Fileglancer job’s project, unless-Pis given.CI runs
pixi lock --check, so a change to the environment is re-locked in the same commit.
See Installation for running from pixi.
Deprecated in 0.3.0
Each of these still works in 0.3.0, warns, and goes in the next release.
CHANGELOG.md has the details.
Deprecated |
Use instead |
|---|---|
The separate console scripts ( |
The subcommands of the one |
|
|
|
|
|
|
|
The same names in |
|
|
Using a name through one of the three alias modules raises a
DeprecationWarning that names its new path;
tests/utils/test_deprecated_imports.py checks that each name still gives
the very same object. These went in 0.3.0 without an alias; nothing in the
docs or the examples used them:
the rest of
cellmap_flow.utils, dissolved intoio/,jobs/,serving/,config/yaml.py,models/,norm/safe_expression,dashboard/services/,pluginsandlogging_setup;python -m cellmap_flow.utils.doctorispython -m cellmap_flow.cli.doctor;finetune/finetune_job_manager.py, nowfinetune/job_manager/, andfinetune/virtual_dataset.py, nowfinetune/data/;lora_wrapper.merge_lora_into_base(K19), replaced byadaptation.LoraStrategy.merge.