Single source of truth for the shared interfaces created in WP-0. Downstream work packages (WP-A … WP-F) implement against exactly what is written here. If a downstream WP needs to change a contract, update this file first.
Written 2026-07-02 (WP-0). Companion to IMPLEMENTATION_PLAN_1.0.md.
-
preferred:"cpu" | "cuda" | "auto"(case-insensitive). WhenNone, readsLATENT_SCOPE_DEVICEviaconfiguration.get_device_preference()(default"auto"). Unrecognised values are coerced to"auto". -
Returns a
DeviceResolutiondataclass that also supports mapping access (res["torch_device"],dict(res),**resunpacking) andres.to_dict():DeviceResolution( torch_device="cuda" | "cpu", # pass to torch / model.to() use_cuml=bool, # use cuML/cuvs accelerated UMAP/HDBSCAN reason=str, # human-readable explanation (logged) )
-
Semantics:
auto→cuda+use_cuml=Trueiff both torch-CUDA and cuML import succeed; else CPU (or GPU-torch + CPU-cuml if torch-CUDA works but cuML does not).cpu→ alwaystorch_device="cpu",use_cuml=False.cuda→torch_device="cuda"when torch-CUDA is available;use_cuml=Trueonly if cuML is importable, otherwiseuse_cuml=Falsewith a logged fallback reason. If torch-CUDA is unavailable, falls back to CPU (logged).
-
Guarantees: no torch/cuML import at module top level; both are probed with guarded
try/exceptinside the function.import latentscopeandfrom latentscope.util.device import resolve_devicework with no cuML installed. The decision is logged to stdout viaprint(...)(captured by the job runner), format:resolve_device: backend=<cpu|cuda> use_cuml=<bool> (<reason>).
- WP-A (
umapper.py) and WP-B (cluster.py) callresolve_device()once and branch:res.use_cuml→cuml.manifold.UMAP/cuml.cluster.HDBSCAN; else the CPUumap-learn/hdbscanpaths. EVoC has no cuML equivalent — always CPU (log it). - Env var registered in
configuration.pyasDEVICE_ENV_VAR = "LATENT_SCOPE_DEVICE"; read it only throughget_device_preference().
All fields below are optional and additive; existing readers/consumers must ignore unknown keys and tolerate their absence (nothing is removed or renamed).
| field | type | notes |
|---|---|---|
name |
string | human-friendly title for the run; omitted when not provided |
description |
string | free-text; omitted when not provided |
| field | type | notes |
|---|---|---|
name |
string | human-friendly title; omitted when not provided |
description |
string | free-text; omitted when not provided |
cluster_on |
"umap" | "embedding" |
input space clustered on; always written (records the effective value even when the CLI flag was defaulted) |
Existing cluster fields (method, samples, min_samples,
cluster_selection_epsilon, n_clusters, n_noise, and evoc's n_neighbors /
noise_level) are unchanged. method now additionally may be "kmeans" or
"gmm".
WP-0 wires the server → CLI passthrough in server/jobs.py. WP-A/WP-B must
add the argparse entries below so the emitted commands parse.
New optional flags (append after existing positionals/flags):
| flag | type | default | notes |
|---|---|---|---|
--name |
str | None |
written to umap-NNN.json["name"] when set |
--description |
str | None |
written to umap-NNN.json["description"] when set |
Existing surface unchanged: positional dataset_id embedding_id [neighbors] [min_dist]; flags --init --align --save --seed --sae_id.
| flag | type | choices | default | notes |
|---|---|---|---|---|
--method |
str | evoc, hdbscan, kmeans, gmm |
evoc |
extend existing choices with kmeans, gmm |
--cluster_on |
str | umap, embedding |
None → per-method default |
None/omitted preserves today's behavior: evoc→embedding, hdbscan/kmeans/gmm→umap. Resolve the effective value and write it to cluster_on in the meta JSON. |
--name |
str | — | None |
written to cluster-NNN.json["name"] when set |
--description |
str | — | None |
written to cluster-NNN.json["description"] when set |
Existing surface unchanged: positionals dataset_id umap_id samples min_samples cluster_selection_epsilon [column]; flags --n_neighbors --noise_level (evoc).
Server passthrough already implemented in jobs.py (/api/jobs/cluster):
method is whitelisted to {evoc,hdbscan,kmeans,gmm} (falls back to evoc
otherwise); cluster_on is whitelisted to {umap,embedding} and only appended
when provided; --name/--description appended only when non-empty. The
/api/jobs/umap builder appends --name/--description only when non-empty.
All commands remain list-based (no shell=True).
Base prefix is /api/datasets (blueprint datasets_bp read / datasets_write_bp
write). apiUrl in the frontend is the /api base.
GET /api/datasets/<dataset>/column/<column>?scope=<idOrScope>
<column>: the dataset column to read (URL-encoded by the client).?scope=: optional scope/umap id to align/subset values to. When omitted, return values for the full dataset inls_indexorder.- Response (numeric column):
{ "column": "score", "values": [0.12, 0.98, ...], // per-point, aligned to ls_index order "extent": [min, max], // numeric min/max "type": "numeric" } - Response (categorical column):
{ "column": "label", "values": [0, 2, 1, ...], // per-point category indices "type": "categorical", "categorical": { "categories": ["a", "b", "c"], // index -> label "counts": [10, 5, 3] } } valueslength must equal the scope/dataset row count. Extent is sourced frommeta.json → column_metadata(already stored byingest.py); categorical columns exposecategories/counts.
POST /api/datasets/<dataset>/umaps/<umap>/meta
Content-Type: application/json
{ "name": "My projection", "description": "..." }
- Merges
name/descriptionintoumaps/<umap>.json(create keys if absent, overwrite if present; leave other fields untouched). - Response:
{ "success": true }(mirroroverwrite_scope_description).
POST /api/datasets/<dataset>/clusters/<cluster>/meta
Content-Type: application/json
{ "name": "...", "description": "..." }
- Same merge semantics into
clusters/<cluster>.json. - Response:
{ "success": true }.
Decision (meta-update route shape): name/description are set at creation time through the job command builders (
jobs.py, §3) and written by the scripts. ThePOST .../metaroutes above exist for editing an existing run without re-running it (the gallery's inline rename). They live indatasets.py(owner WP-C) as new write routes — they do not touch the existing umap/cluster GET routes. POST + JSON body was chosen over the query-string GET style ofoverwrite_scope_descriptionbecause these carry free-text values; the JSON body avoids URL-length/encoding pitfalls and is consistent with the existingpostJsonOptionshelper inapiService.js.
Already added and wired to the §4 endpoints:
apiService.updateUmapMeta(datasetId, umapId, { name, description })
// POST /api/datasets/<dataset>/umaps/<umapId>/meta (JSON body)
// -> { success: true }
apiService.updateClusterMeta(datasetId, clusterId, { name, description })
// POST /api/datasets/<dataset>/clusters/<clusterId>/meta (JSON body)
// -> { success: true }
apiService.fetchColumnValues(datasetId, idOrScope, column)
// GET /api/datasets/<dataset>/column/<column>?scope=<idOrScope>
// -> { column, values: number[], extent?: [min,max], type, categorical? }
// idOrScope may be null/undefined to omit the ?scope= filter.All three go through the existing fetchJson helper (ok-status checks + error
body snippet) and postJsonOptions for the writes.
scikit-learnconfirmed as a direct dependency (already present:scikit-learn>=1.3,<2) — required for WP-B'sKMeans/GaussianMixture.- New optional extra (NOT default):
Install with
[project.optional-dependencies] gpu = ["cuml-cu12>=25.2", "cuvs-cu12>=25.2"]
pip install "latentscope[gpu]"plus--extra-index-url=https://pypi.nvidia.com. Pins need validation on the CUDA 12.8 gsv box before release (RAPIDS ships fast; prefer newest 25.x that resolves against the driver). The guarded import indevice.pymeans absence of this extra never breaks anything.