diff --git a/client/src/actions/embedding.js b/client/src/actions/embedding.js
index 555c69ab..5e882160 100644
--- a/client/src/actions/embedding.js
+++ b/client/src/actions/embedding.js
@@ -5,6 +5,23 @@ action creators related to embeddings choice
import { AnnoMatrixObsCrossfilter } from "../annoMatrix";
import { _setEmbeddingSubset } from "../util/stateManager/viewStackHelpers";
+export async function _switchEmbedding(prevAnnoMatrix, newEmbeddingName) {
+ /*
+ DRY helper used by this and reembedding action creators
+ */
+ const base = prevAnnoMatrix.base();
+ const embeddingDf = await base.fetch("emb", newEmbeddingName);
+ const annoMatrix = _setEmbeddingSubset(prevAnnoMatrix, embeddingDf);
+ const obsCrossfilter = await new AnnoMatrixObsCrossfilter(annoMatrix).select(
+ "emb",
+ newEmbeddingName,
+ {
+ mode: "all",
+ }
+ );
+ return [annoMatrix, obsCrossfilter];
+}
+
export const layoutChoiceAction = (newLayoutChoice) => async (
dispatch,
getState
@@ -14,15 +31,9 @@ export const layoutChoiceAction = (newLayoutChoice) => async (
layout.
*/
const { annoMatrix: prevAnnoMatrix } = getState();
-
- const embeddingDf = await prevAnnoMatrix.base().fetch("emb", newLayoutChoice);
- const annoMatrix = _setEmbeddingSubset(prevAnnoMatrix, embeddingDf);
- const obsCrossfilter = await new AnnoMatrixObsCrossfilter(annoMatrix).select(
- "emb",
- newLayoutChoice,
- {
- mode: "all",
- }
+ const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
+ prevAnnoMatrix,
+ newLayoutChoice
);
dispatch({
type: "set layout choice",
diff --git a/client/src/actions/reembed.js b/client/src/actions/reembed.js
index 43b38e1f..2212f380 100644
--- a/client/src/actions/reembed.js
+++ b/client/src/actions/reembed.js
@@ -1,10 +1,10 @@
import { API } from "../globals";
-import { MatrixFBS } from "../util/stateManager";
import {
postNetworkErrorToast,
postAsyncSuccessToast,
postAsyncFailureToast,
} from "../components/framework/toasters";
+import { _switchEmbedding } from "./embedding";
function abortableFetch(request, opts, timeout = 0) {
const controller = new AbortController();
@@ -24,7 +24,7 @@ function abortableFetch(request, opts, timeout = 0) {
async function doReembedFetch(dispatch, getState) {
const state = getState();
- let cells = state.world.obsAnnotations.rowIndex.labels();
+ let cells = state.annoMatrix.rowIndex.labels();
// These lines ensure that we convert any TypedArray to an Array.
// This is necessary because JSON.stringify() does some very strange
@@ -54,10 +54,7 @@ async function doReembedFetch(dispatch, getState) {
});
const res = await af.ready();
- if (
- res.ok &&
- res.headers.get("Content-Type").includes("application/octet-stream")
- ) {
+ if (res.ok && res.headers.get("Content-Type").includes("application/json")) {
return res;
}
@@ -67,7 +64,6 @@ async function doReembedFetch(dispatch, getState) {
if (body && body.length > 0) {
msg = `${msg} -- ${body}`;
}
- postNetworkErrorToast(msg);
throw new Error(msg);
}
@@ -78,17 +74,24 @@ export function requestReembed() {
return async (dispatch, getState) => {
try {
const res = await doReembedFetch(dispatch, getState);
- const schema = JSON.parse(res.headers.get("CxG-Schema"));
- const buffer = await res.arrayBuffer();
- const df = MatrixFBS.matrixFBSToDataframe(buffer);
+ const schema = await res.json();
dispatch({
type: "reembed: request completed",
});
+
+ const { annoMatrix: prevAnnoMatrix } = getState();
+ const base = prevAnnoMatrix.base().addEmbedding(schema);
+ const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
+ base,
+ schema.name
+ );
dispatch({
type: "reembed: add reembedding",
- embedding: df,
schema,
+ annoMatrix,
+ obsCrossfilter,
});
+
postAsyncSuccessToast("Re-embedding has completed.");
} catch (error) {
dispatch({
@@ -103,13 +106,3 @@ export function requestReembed() {
}
};
}
-
-/* disabled until reimplementation occurs
-export function reembedResetWorldToUniverse(dispatch, getState) {
- const { reembedController } = getState();
- if (reembedController.pendingFetch) reembedController.pendingFetch.abort();
- dispatch({
- type: "reembed: clear all reembeddings",
- });
-}
-*/
diff --git a/client/src/annoMatrix/annoMatrix.js b/client/src/annoMatrix/annoMatrix.js
index 15ddb008..092d9007 100644
--- a/client/src/annoMatrix/annoMatrix.js
+++ b/client/src/annoMatrix/annoMatrix.js
@@ -397,6 +397,19 @@ export default class AnnoMatrix {
_subclassResponsibility();
}
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars -- make sure subclass implements
+ addEmbedding(colSchema) {
+ /*
+ Add a new obs embedding to the AnnoMatrix, with provided schema.
+ Returns a new annomatrix.
+
+ Typical use will be to add a re-embedding that the server has calculated.
+
+ Will throw if the column schema is invalid (eg, duplicate name).
+ */
+ _subclassResponsibility();
+ }
+
/**
** Private interfaces below.
**/
diff --git a/client/src/annoMatrix/crossfilter.js b/client/src/annoMatrix/crossfilter.js
index b3972978..f9f5bc53 100644
--- a/client/src/annoMatrix/crossfilter.js
+++ b/client/src/annoMatrix/crossfilter.js
@@ -118,6 +118,11 @@ export default class AnnoMatrixObsCrossfilter {
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
}
+ addEmbedding(colSchema) {
+ const annoMatrix = this.annoMatrix.addEmbedding(colSchema);
+ return new AnnoMatrixObsCrossfilter(annoMatrix, this.obsCrossfilter);
+ }
+
/**
Selection state - API is identical to ImmutableTypedCrossfilter, as these
are just wrappers to lazy create indices.
diff --git a/client/src/annoMatrix/loader.js b/client/src/annoMatrix/loader.js
index e10e3ee1..0b5fedcf 100644
--- a/client/src/annoMatrix/loader.js
+++ b/client/src/annoMatrix/loader.js
@@ -6,6 +6,7 @@ import {
removeObsAnnoColumn,
addObsAnnoCategory,
removeObsAnnoCategory,
+ addObsLayout,
} from "../util/stateManager/schemaHelpers";
import { isArrayOrTypedArray } from "../util/typeHelpers";
import { _whereCacheCreate } from "./whereCache";
@@ -47,9 +48,9 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
const colSchema = _getColumnSchema(this.schema, "obs", col);
_writableCategoryTypeCheck(colSchema); // throws on error
- const o = this._clone();
- o.schema = addObsAnnoCategory(this.schema, col, category);
- return o;
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix.schema = addObsAnnoCategory(this.schema, col, category);
+ return newAnnoMatrix;
}
async removeObsAnnoCategory(col, category, unassignedCategory) {
@@ -59,13 +60,17 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
const colSchema = _getColumnSchema(this.schema, "obs", col);
_writableCategoryTypeCheck(colSchema); // throws on error
- const o = await this.resetObsColumnValues(
+ const newAnnoMatrix = await this.resetObsColumnValues(
col,
category,
unassignedCategory
);
- o.schema = removeObsAnnoCategory(o.schema, col, category);
- return o;
+ newAnnoMatrix.schema = removeObsAnnoCategory(
+ newAnnoMatrix.schema,
+ col,
+ category
+ );
+ return newAnnoMatrix;
}
dropObsColumn(col) {
@@ -75,10 +80,10 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
const colSchema = _getColumnSchema(this.schema, "obs", col);
_writableCheck(colSchema); // throws on error
- const o = this._clone();
- o._cache.obs = this._cache.obs.dropCol(col);
- o.schema = removeObsAnnoColumn(this.schema, col);
- return o;
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix._cache.obs = this._cache.obs.dropCol(col);
+ newAnnoMatrix.schema = removeObsAnnoColumn(this.schema, col);
+ return newAnnoMatrix;
}
addObsColumn(colSchema, Ctor, value) {
@@ -98,7 +103,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
throw new Error("column already exists");
}
- const o = this._clone();
+ const newAnnoMatrix = this._clone();
let data;
if (isArrayOrTypedArray(value)) {
if (value.constructor !== Ctor)
@@ -109,10 +114,13 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
} else {
data = new Ctor(this.nObs).fill(value);
}
- o._cache.obs = this._cache.obs.withCol(colName, data);
- _normalizeCategoricalSchema(colSchema, o._cache.obs.col(colName));
- o.schema = addObsAnnoColumn(this.schema, colName, colSchema);
- return o;
+ newAnnoMatrix._cache.obs = this._cache.obs.withCol(colName, data);
+ _normalizeCategoricalSchema(
+ colSchema,
+ newAnnoMatrix._cache.obs.col(colName)
+ );
+ newAnnoMatrix.schema = addObsAnnoColumn(this.schema, colName, colSchema);
+ return newAnnoMatrix;
}
renameObsColumn(oldCol, newCol) {
@@ -155,13 +163,13 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
data[idx] = value;
}
- const o = this._clone();
- o._cache.obs = this._cache.obs.replaceColData(col, data);
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix._cache.obs = this._cache.obs.replaceColData(col, data);
const { categories } = colSchema;
if (!categories?.includes(value)) {
- o.schema = addObsAnnoCategory(this.schema, col, value);
+ newAnnoMatrix.schema = addObsAnnoCategory(this.schema, col, value);
}
- return o;
+ return newAnnoMatrix;
}
async resetObsColumnValues(col, oldValue, newValue) {
@@ -185,13 +193,27 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
if (data[i] === oldValue) data[i] = newValue;
}
- const o = this._clone();
- o._cache.obs = this._cache.obs.replaceColData(col, data);
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix._cache.obs = this._cache.obs.replaceColData(col, data);
const { categories } = colSchema;
if (!categories?.includes(newValue)) {
- o.schema = addObsAnnoCategory(this.schema, col, newValue);
+ newAnnoMatrix.schema = addObsAnnoCategory(this.schema, col, newValue);
}
- return o;
+ return newAnnoMatrix;
+ }
+
+ addEmbedding(colSchema) {
+ /*
+ add new layout to the obs embeddings
+ */
+ const { name: colName } = colSchema;
+ if (_getColumnSchema(this.schema, "emb", colName)) {
+ throw new Error("column already exists");
+ }
+
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix.schema = addObsLayout(this.schema, colSchema);
+ return newAnnoMatrix;
}
/**
diff --git a/client/src/annoMatrix/views.js b/client/src/annoMatrix/views.js
index f97e05a0..5b9b9c26 100644
--- a/client/src/annoMatrix/views.js
+++ b/client/src/annoMatrix/views.js
@@ -17,59 +17,74 @@ class AnnoMatrixView extends AnnoMatrix {
}
addObsAnnoCategory(col, category) {
- const o = this._clone();
- o.viewOf = this.viewOf.addObsAnnoCategory(col, category);
- o.schema = o.viewOf.schema;
- return o;
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix.viewOf = this.viewOf.addObsAnnoCategory(col, category);
+ newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
+ return newAnnoMatrix;
}
async removeObsAnnoCategory(col, category, unassignedCategory) {
- const o = this._clone();
- o.viewOf = await this.viewOf.removeObsAnnoCategory(
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix.viewOf = await this.viewOf.removeObsAnnoCategory(
col,
category,
unassignedCategory
);
- o.schema = o.viewOf.schema;
- return o;
+ newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
+ return newAnnoMatrix;
}
dropObsColumn(col) {
- const o = this._clone();
- o.viewOf = this.viewOf.dropObsColumn(col);
- o._cache.obs = this._cache.obs.dropCol(col);
- o.schema = o.viewOf.schema;
- return o;
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix.viewOf = this.viewOf.dropObsColumn(col);
+ newAnnoMatrix._cache.obs = this._cache.obs.dropCol(col);
+ newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
+ return newAnnoMatrix;
}
addObsColumn(colSchema, Ctor, value) {
- const o = this._clone();
- o.viewOf = this.viewOf.addObsColumn(colSchema, Ctor, value);
- o.schema = o.viewOf.schema;
- return o;
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix.viewOf = this.viewOf.addObsColumn(colSchema, Ctor, value);
+ newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
+ return newAnnoMatrix;
}
renameObsColumn(oldCol, newCol) {
- const o = this._clone();
- o.viewOf = this.viewOf.renameObsColumn(oldCol, newCol);
- o.schema = o.viewOf.schema;
- return o;
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix.viewOf = this.viewOf.renameObsColumn(oldCol, newCol);
+ newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
+ return newAnnoMatrix;
}
async setObsColumnValues(col, rowLabels, value) {
- const o = this._clone();
- o.viewOf = await this.viewOf.setObsColumnValues(col, rowLabels, value);
- o._cache.obs = this._cache.obs.dropCol(col);
- o.schema = o.viewOf.schema;
- return o;
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix.viewOf = await this.viewOf.setObsColumnValues(
+ col,
+ rowLabels,
+ value
+ );
+ newAnnoMatrix._cache.obs = this._cache.obs.dropCol(col);
+ newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
+ return newAnnoMatrix;
}
async resetObsColumnValues(col, oldValue, newValue) {
- const o = this._clone();
- o.viewOf = await this.viewOf.resetObsColumnValues(col, oldValue, newValue);
- o._cache.obs = this._cache.obs.dropCol(col);
- o.schema = o.viewOf.schema;
- return o;
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix.viewOf = await this.viewOf.resetObsColumnValues(
+ col,
+ oldValue,
+ newValue
+ );
+ newAnnoMatrix._cache.obs = this._cache.obs.dropCol(col);
+ newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
+ return newAnnoMatrix;
+ }
+
+ addEmbedding(colSchema) {
+ const newAnnoMatrix = this._clone();
+ newAnnoMatrix.viewOf = this.viewOf.addEmbedding(colSchema);
+ newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
+ return newAnnoMatrix;
}
}
diff --git a/client/src/components/embedding/index.js b/client/src/components/embedding/index.js
index b71ae1a0..d4c7562c 100644
--- a/client/src/components/embedding/index.js
+++ b/client/src/components/embedding/index.js
@@ -101,7 +101,7 @@ export default Embedding;
const loadAllEmbeddingCounts = async ({ annoMatrix, available }) => {
const embeddings = await Promise.all(
- available.map((name) => annoMatrix.fetch("emb", name))
+ available.map((name) => annoMatrix.base().fetch("emb", name))
);
return available.map((name, idx) => ({
embeddingName: name,
diff --git a/client/src/components/menubar/index.js b/client/src/components/menubar/index.js
index 63d30b90..27e6ba7f 100644
--- a/client/src/components/menubar/index.js
+++ b/client/src/components/menubar/index.js
@@ -10,6 +10,7 @@ import InformationMenu from "./infoMenu";
import Subset from "./subset";
import UndoRedoReset from "./undoRedo";
import DiffexpButtons from "./diffexpButtons";
+import Reembedding from "./reembedding";
import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
@connect((state) => {
@@ -49,6 +50,8 @@ import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
tosURL: state.config?.parameters?.["about_legal_tos"],
privacyURL: state.config?.parameters?.["about_legal_privacy"],
categoricalSelection: state.categoricalSelection,
+ enableReembedding:
+ state.config?.parameters?.["enable-reembedding"] ?? false,
};
})
class MenuBar extends React.PureComponent {
@@ -214,6 +217,7 @@ class MenuBar extends React.PureComponent {
colorAccessor,
subsetPossible,
subsetResetPossible,
+ enableReembedding,
} = this.props;
const { pendingClipPercentiles } = this.state;
@@ -266,6 +270,7 @@ class MenuBar extends React.PureComponent {
this.handleClipPercentileMinValueChange
}
/>
+ {enableReembedding ? : null}
({
+ reembedController: state.reembedController,
+ annoMatrix: state.annoMatrix,
+}))
+class Reembedding extends React.PureComponent {
+ render() {
+ const { dispatch, annoMatrix, reembedController } = this.props;
+ const loading = !!reembedController?.pendingFetch;
+ const disabled = annoMatrix.nObs === annoMatrix.schema.dataframe.nObs;
+ const tipContent = disabled
+ ? "Subset cells first, then click to recompute UMAP embedding."
+ : "Click to recompute UMAP embedding on the current cell subset.";
+
+ return (
+
+
+ dispatch(actions.requestReembed())}
+ loading={loading}
+ />
+
+
+ );
+ }
+}
+
+export default Reembedding;
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index d910cef0..76d60f0c 100644
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -18,7 +18,7 @@ import autosave from "./autosave";
import ontology from "./ontology";
import centroidLabels from "./centroidLabels";
import pointDialation from "./pointDilation";
-import { reembedController, reembedding } from "./reembed";
+import { reembedController } from "./reembed";
import { gcMiddleware as annoMatrixGC } from "../annoMatrix";
import undoableConfig from "./undoableConfig";
@@ -30,7 +30,6 @@ const Reducer = undoable(
["obsCrossfilter", obsCrossfilter],
["ontology", ontology],
["annotations", annotations],
- ["reembedding", reembedding],
["layoutChoice", layoutChoice],
["categoricalSelection", categoricalSelection],
["continuousSelection", continuousSelection],
@@ -55,7 +54,6 @@ const Reducer = undoable(
"layoutChoice",
"centroidLabels",
"annotations",
- "reembedding",
],
undoableConfig
);
diff --git a/client/src/reducers/layoutChoice.js b/client/src/reducers/layoutChoice.js
index 48cff07e..14aa3f73 100644
--- a/client/src/reducers/layoutChoice.js
+++ b/client/src/reducers/layoutChoice.js
@@ -48,27 +48,15 @@ const LayoutChoice = (
}
case "reembed: add reembedding": {
+ const { schema } = nextSharedState.annoMatrix;
const { name } = action.schema;
const available = Array.from(new Set(state.available).add(name));
+ const currentDimNames = schema.layout.obsByName[name].dims;
return {
...state,
available,
- };
- }
-
- case "reembed: clear all reembeddings": {
- const { annoMatrix } = nextSharedState;
- const { current } = state;
- const dflt = setToDefaultLayout(annoMatrix.schema);
- if (dflt.available.includes(current)) {
- return {
- ...state,
- available: dflt.available,
- };
- }
- return {
- ...state,
- ...dflt,
+ current: name,
+ currentDimNames,
};
}
diff --git a/client/src/reducers/reembed.js b/client/src/reducers/reembed.js
index a12a1e9b..02f649dc 100644
--- a/client/src/reducers/reembed.js
+++ b/client/src/reducers/reembed.js
@@ -27,38 +27,3 @@ export const reembedController = (
}
}
};
-
-/*
-actual reembedding data is part of the undo/redo history
-*/
-export const reembedding = (
- state = {
- reembeddings: new Map(),
- },
- action
-) => {
- switch (action.type) {
- case "reembed: add reembedding": {
- const { schema, embedding } = action;
- const { name } = schema.name;
- const { reembeddings } = state;
- return {
- ...state,
- reembeddings: new Map(reembeddings).set(name, {
- name,
- schema,
- embedding,
- }),
- };
- }
- case "reembed: clear all reembeddings": {
- return {
- ...state,
- reembeddings: new Map(),
- };
- }
- default: {
- return state;
- }
- }
-};
diff --git a/server/common/rest.py b/server/common/rest.py
index 0a0c2b50..b2e30305 100644
--- a/server/common/rest.py
+++ b/server/common/rest.py
@@ -304,10 +304,6 @@ def layout_obs_put(request, data_adaptor):
if not data_adaptor.dataset_config.embeddings__enable_reembedding:
return abort(HTTPStatus.NOT_IMPLEMENTED)
- preferred_mimetype = request.accept_mimetypes.best_match(["application/octet-stream"])
- if preferred_mimetype != "application/octet-stream":
- return abort(HTTPStatus.NOT_ACCEPTABLE)
-
args = request.get_json()
filter = args["filter"] if args else None
if not filter:
@@ -315,17 +311,9 @@ def layout_obs_put(request, data_adaptor):
method = args["method"] if args else "umap"
try:
- schema, fbs = data_adaptor.compute_embedding(method, filter)
- return make_response(
- fbs,
- HTTPStatus.OK,
- {
- "Content-Type": "application/octet-stream",
- "CxG-Schema": json.dumps(schema),
- "Access-Control-Expose-Headers": "CxG-Schema",
- },
- )
+ schema = data_adaptor.compute_embedding(method, filter)
+ return make_response(jsonify(schema), HTTPStatus.OK, {"Content-Type": "application/json"})
except NotImplementedError as e:
- return abort_and_log(HTTPStatus.NOT_IMPLEMENTED, str(e), include_exc_info=True)
+ return abort_and_log(HTTPStatus.NOT_IMPLEMENTED, str(e))
except (ValueError, DisabledFeatureError, FilterError) as e:
return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True)
diff --git a/server/compute/scanpy.py b/server/compute/scanpy.py
index cebafabd..36c2e1a9 100644
--- a/server/compute/scanpy.py
+++ b/server/compute/scanpy.py
@@ -1,4 +1,5 @@
import importlib
+import numpy as np
"""
Wrapper for various scanpy modules. Will raise NotImplementedError if the scanpy
@@ -11,8 +12,8 @@ def get_scanpy_module():
sc = importlib.import_module("scanpy")
# Future: we could enforce versions here, eg, lookat sc.__version__
return sc
- except ModuleNotFoundError:
- raise NotImplementedError("Please install scanpy to enable UMAP re-embedding")
+ except ModuleNotFoundError as e:
+ raise NotImplementedError("Please install scanpy to enable UMAP re-embedding") from e
except Exception as e:
# will capture other ImportError corner cases
raise NotImplementedError() from e
@@ -46,4 +47,7 @@ def scanpy_umap(adata, obs_mask=None, pca_options={}, neighbors_options={}, umap
sc.pp.neighbors(adata, **neighbors_options)
sc.tl.umap(adata, **umap_options)
- return adata.obsm["X_umap"]
+ umap = adata.obsm["X_umap"]
+ result = np.full((obs_mask.shape[0], umap.shape[1]), np.NaN)
+ result[obs_mask] = umap
+ return result
diff --git a/server/data_anndata/anndata_adaptor.py b/server/data_anndata/anndata_adaptor.py
index 69496614..f9c781ad 100644
--- a/server/data_anndata/anndata_adaptor.py
+++ b/server/data_anndata/anndata_adaptor.py
@@ -1,7 +1,6 @@
import warnings
import numpy as np
-import pandas as pd
from pandas.core.dtypes.dtypes import CategoricalDtype
import anndata
from scipy import sparse
@@ -314,16 +313,15 @@ class AnndataAdaptor(DataAdaptor):
raise FilterError("Error parsing filter")
with ServerTiming.time("layout.compute"):
X_umap = scanpy_umap(self.data, obs_mask)
- normalized_layout = DataAdaptor.normalize_embedding(X_umap)
# Server picks reemedding name, which must not collide with any other
- # embedding name generated by this backed.
+ # embedding name generated by this backend.
name = f"reembed:{method}_{datetime.now().isoformat(timespec='milliseconds')}"
dims = [f"{name}_0", f"{name}_1"]
- df = pd.DataFrame(normalized_layout, columns=dims)
- fbs = encode_matrix_fbs(df, col_idx=df.columns, row_idx=None)
- schema = {"name": name, "type": "float32", "dims": dims}
- return (schema, fbs)
+ layout_schema = {"name": name, "type": "float32", "dims": dims}
+ self.schema["layout"]["obs"].append(layout_schema)
+ self.data.obsm[f"X_{name}"] = X_umap
+ return layout_schema
def compute_diffexp_ttest(self, maskA, maskB, top_n=None, lfc_cutoff=None):
if top_n is None:
diff --git a/server/data_common/data_adaptor.py b/server/data_common/data_adaptor.py
index 20f8b601..84ac5e64 100644
--- a/server/data_common/data_adaptor.py
+++ b/server/data_common/data_adaptor.py
@@ -71,8 +71,7 @@ class DataAdaptor(metaclass=ABCMeta):
@abstractmethod
def compute_embedding(self, method, filter):
- """compute a new embedding on the specified obs subset, and return a
- tuple of (schema, fbs)."""
+ """compute a new embedding on the specified obs subset, and return the embedding schema. """
pass
@abstractmethod
diff --git a/server/test/test_anndata_adaptor.py b/server/test/test_anndata_adaptor.py
index 3afe2ea3..440e9b82 100644
--- a/server/test/test_anndata_adaptor.py
+++ b/server/test/test_anndata_adaptor.py
@@ -237,14 +237,14 @@ class AdaptorTest(unittest.TestCase):
self.data.compute_embedding("umap", filter)
return
- (schema, fbs) = self.data.compute_embedding("umap", filter)
+ schema = self.data.compute_embedding("umap", filter)
self.assertIsInstance(schema["name"], str)
name = schema["name"]
self.assertEqual(schema["type"], "float32")
self.assertEqual(schema["dims"], [f"{name}_0", f"{name}_1"])
- emb = decode_fbs.decode_matrix_FBS(fbs)
- self.assertEqual(emb["n_rows"], 100)
- self.assertEqual(emb["n_cols"], 2)
- self.assertEqual(emb["col_idx"], [f"{name}_0", f"{name}_1"])
+ emb = self.data.data.obsm[f"X_{name}"]
+ self.assertEqual(emb.shape, (2638, 2))
+ self.assertTrue(np.isfinite(emb[0:100]).all())
+ self.assertTrue(np.isnan(emb[100:]).all())
diff --git a/server/test/test_api.py b/server/test/test_api.py
index d3ecb315..7b50c426 100644
--- a/server/test/test_api.py
+++ b/server/test/test_api.py
@@ -73,21 +73,23 @@ class EndPoints(object):
# attempt to reembed with umap over 100 cells.
endpoint = "layout/obs"
url = f"{self.URL_BASE}{endpoint}"
- header = {"Accept": "application/octet-stream"}
data = {}
data["filter"] = {}
data["filter"]["obs"] = {}
data["filter"]["obs"]["index"] = list(range(100))
data["method"] = "umap"
- result = self.session.put(url, headers=header, json=data)
+ result = self.session.put(url, json=data)
self.assertEqual(result.status_code, HTTPStatus.OK)
- df = decode_fbs.decode_matrix_FBS(result.content)
- self.assertEqual(df["n_rows"], 100)
- self.assertEqual(df["n_cols"], 2)
- cols = list(df["col_idx"])
- self.assertTrue(cols[0].startswith("reembed:umap_") and cols[0].endswith("_0"))
- self.assertTrue(cols[1].startswith("reembed:umap_") and cols[1].endswith("_1"))
+ result_data = result.json()
+ self.assertIsInstance(result_data, dict)
+ self.assertEqual(result_data["type"], "float32")
+ self.assertTrue(result_data["name"].startswith("reembed:umap_"))
+ self.assertIsInstance(result_data["dims"], list)
+ self.assertEqual(len(result_data["dims"]), 2)
+ dims = result_data["dims"]
+ self.assertTrue(dims[0].startswith("reembed:umap_") and dims[0].endswith("_0"))
+ self.assertTrue(dims[1].startswith("reembed:umap_") and dims[1].endswith("_1"))
def test_bad_filter(self):
endpoint = "data/var"