@sjcrh/proteinpaint-server 2.84.0 → 2.85.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/package.json +4 -5
  2. package/routes/_template_.js +14 -11
  3. package/routes/brainImaging.js +5 -14
  4. package/routes/brainImagingSamples.js +3 -3
  5. package/routes/burden.js +27 -59
  6. package/routes/dataset.js +9 -17
  7. package/routes/dsdata.js +11 -14
  8. package/routes/dzimages.js +11 -16
  9. package/routes/gdc.maf.js +8 -23
  10. package/routes/gdc.mafBuild.js +9 -9
  11. package/routes/gdc.topMutatedGenes.js +7 -7
  12. package/routes/genelookup.js +16 -34
  13. package/routes/genesetEnrichment.js +18 -14
  14. package/routes/genesetOverrepresentation.js +9 -14
  15. package/routes/healthcheck.js +26 -32
  16. package/routes/hicdata.js +7 -28
  17. package/routes/hicgenome.js +6 -27
  18. package/routes/hicstat.js +4 -22
  19. package/routes/isoformlst.js +8 -11
  20. package/routes/ntseq.js +8 -11
  21. package/routes/pdomain.js +8 -11
  22. package/routes/sampledzimages.js +12 -12
  23. package/routes/samplewsimages.js +6 -10
  24. package/routes/snp.js +8 -10
  25. package/routes/termdb.DE.js +8 -10
  26. package/routes/termdb.boxplot.js +8 -9
  27. package/routes/termdb.categories.js +4 -45
  28. package/routes/termdb.cluster.js +9 -9
  29. package/routes/termdb.cohort.summary.js +5 -8
  30. package/routes/termdb.cohorts.js +3 -7
  31. package/routes/{termdb.getdescrstats.js → termdb.descrstats.js} +8 -45
  32. package/routes/termdb.numericcategories.js +51 -0
  33. package/routes/{termdb.getpercentile.js → termdb.percentile.js} +4 -46
  34. package/routes/{termdb.getrootterm.js → termdb.rootterm.js} +4 -24
  35. package/routes/{termdb.getSampleImages.js → termdb.sampleImages.js} +9 -9
  36. package/routes/termdb.singleSampleMutation.js +3 -7
  37. package/routes/termdb.singlecellDEgenes.js +8 -8
  38. package/routes/termdb.singlecellData.js +4 -8
  39. package/routes/termdb.singlecellSamples.js +8 -8
  40. package/routes/{termdb.gettermchildren.js → termdb.termchildren.js} +8 -28
  41. package/routes/termdb.termsbyids.js +9 -16
  42. package/routes/{termdb.getTopTermsByType.js → termdb.topTermsByType.js} +9 -10
  43. package/routes/termdb.topVariablyExpressedGenes.js +8 -8
  44. package/routes/termdb.violin.js +8 -46
  45. package/routes/tileserver.js +5 -10
  46. package/routes/wsimages.js +10 -9
  47. package/src/app.js +1286 -2148
  48. package/routes/termdb.getnumericcategories.js +0 -91
@@ -1,29 +1,24 @@
1
+ import { genesetOverrepresentationPayload } from "#types";
1
2
  import { run_rust } from "@sjcrh/proteinpaint-rust";
2
3
  import serverconfig from "#src/serverconfig.js";
3
4
  import path from "path";
4
5
  const api = {
5
6
  endpoint: "genesetOverrepresentation",
6
7
  methods: {
7
- all: {
8
- init,
9
- request: {
10
- typeId: "genesetOverrepresentationRequest"
11
- },
12
- response: {
13
- typeId: "genesetOverrepresentationResponse"
14
- // will combine this with type checker
15
- //valid: (t) => {}
16
- }
8
+ get: {
9
+ ...genesetOverrepresentationPayload,
10
+ init
11
+ },
12
+ post: {
13
+ ...genesetOverrepresentationPayload,
14
+ init
17
15
  }
18
16
  }
19
17
  };
20
18
  function init({ genomes }) {
21
19
  return async (req, res) => {
22
20
  try {
23
- const results = await run_genesetOverrepresentation_analysis(
24
- req.query,
25
- genomes
26
- );
21
+ const results = await run_genesetOverrepresentation_analysis(req.query, genomes);
27
22
  res.send(results);
28
23
  } catch (e) {
29
24
  res.send({ status: "error", error: e.message || e });
@@ -1,43 +1,37 @@
1
1
  import { getStat } from "#src/health.ts";
2
+ import { healthcheckPayload } from "#types";
2
3
  const api = {
3
4
  endpoint: "healthcheck",
4
5
  methods: {
5
6
  get: {
6
- init({ genomes }) {
7
- return async (req, res) => {
8
- try {
9
- const health = await getStat(genomes);
10
- const q = req.query;
11
- if (q.dslabel) {
12
- for (const gn in genomes) {
13
- const ds = genomes[gn]?.datasets?.[q.dslabel];
14
- if (!ds?.getHealth)
15
- continue;
16
- if (!health.byDataset)
17
- health.byDataset = {};
18
- if (!health.byDataset[q.dslabel])
19
- health.byDataset[q.dslabel] = {};
20
- health.byDataset[q.dslabel][gn] = ds.getHealth(ds);
21
- }
22
- }
23
- res.send(health);
24
- } catch (e) {
25
- res.send({ status: "error", error: e.message || e });
26
- }
27
- };
28
- },
29
- request: {
30
- typeId: null
31
- //valid: default to type checker
32
- },
33
- response: {
34
- typeId: "HealthCheckResponse"
35
- // will combine this with type checker
36
- //valid: (t) => {}
37
- }
7
+ ...healthcheckPayload,
8
+ init
38
9
  }
39
10
  }
40
11
  };
12
+ function init({ genomes }) {
13
+ return async (req, res) => {
14
+ try {
15
+ const q = req.query;
16
+ const health = await getStat(genomes);
17
+ if (q.dslabel) {
18
+ for (const gn in genomes) {
19
+ const ds = genomes[gn]?.datasets?.[q.dslabel];
20
+ if (!ds?.getHealth)
21
+ continue;
22
+ if (!health.byDataset)
23
+ health.byDataset = {};
24
+ if (!health.byDataset[q.dslabel])
25
+ health.byDataset[q.dslabel] = {};
26
+ health.byDataset[q.dslabel][gn] = ds.getHealth(ds);
27
+ }
28
+ }
29
+ res.send(health);
30
+ } catch (e) {
31
+ res.send({ status: "error", error: e.message || e });
32
+ }
33
+ };
34
+ }
41
35
  export {
42
36
  api
43
37
  };
package/routes/hicdata.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { hicdataPayload } from "#types";
1
2
  import { fileurl } from "#src/utils.js";
2
3
  import { spawn } from "child_process";
3
4
  import readline from "readline";
@@ -6,34 +7,11 @@ const api = {
6
7
  endpoint: "hicdata",
7
8
  methods: {
8
9
  get: {
9
- init,
10
- request: {
11
- typeId: "HicdataRequest"
12
- },
13
- response: {
14
- typeId: "HicdataResponse"
15
- },
16
- examples: [
17
- {
18
- request: {
19
- body: {
20
- embedder: "localhost",
21
- url: "https://proteinpaint.stjude.org/ppdemo/hg19/hic/hic_demo.hic",
22
- matrixType: "observed",
23
- nmeth: "NONE",
24
- pos1: "3",
25
- pos2: "2",
26
- resolution: 1e6
27
- }
28
- },
29
- response: {
30
- header: { status: 200 }
31
- }
32
- }
33
- ]
10
+ ...hicdataPayload,
11
+ init
34
12
  },
35
13
  post: {
36
- alternativeFor: "get",
14
+ ...hicdataPayload,
37
15
  init
38
16
  }
39
17
  }
@@ -41,7 +19,8 @@ const api = {
41
19
  function init() {
42
20
  return async (req, res) => {
43
21
  try {
44
- const payload = await handle_hicdata(req.query);
22
+ const q = req.query;
23
+ const payload = await handle_hicdata(q);
45
24
  res.send(payload);
46
25
  } catch (e) {
47
26
  res.send({ error: e?.message || e });
@@ -52,7 +31,7 @@ function init() {
52
31
  }
53
32
  function handle_hicdata(q) {
54
33
  return new Promise((resolve, reject) => {
55
- const [e, file, isurl] = fileurl({ query: q });
34
+ const [e, file] = fileurl({ query: q });
56
35
  if (e)
57
36
  reject({ error: "illegal file name" });
58
37
  const matrixType = q.matrixType == "log(oe)" ? "oe" : q.matrixType ? q.matrixType : "observed";
@@ -1,3 +1,4 @@
1
+ import { hicGenomePayload } from "#types";
1
2
  import { fileurl } from "#src/utils.js";
2
3
  import { spawn } from "child_process";
3
4
  import readline from "readline";
@@ -6,43 +7,21 @@ const api = {
6
7
  endpoint: "hicgenome",
7
8
  methods: {
8
9
  get: {
9
- init,
10
- request: {
11
- typeId: "HicGenomeRequest"
12
- },
13
- response: {
14
- typeId: "HicGenomeResponse"
15
- },
16
- examples: [
17
- {
18
- request: {
19
- body: {
20
- chrlst: ["chr1", "chr2"],
21
- embedder: "localhost",
22
- url: "https://proteinpaint.stjude.org/ppdemo/hg19/hic/hic_demo.hic",
23
- matrixType: "observed",
24
- nmeth: "NONE",
25
- nochr: true,
26
- resolution: 25e5
27
- }
28
- },
29
- response: {
30
- header: { status: 200 }
31
- }
32
- }
33
- ]
10
+ ...hicGenomePayload,
11
+ init
34
12
  },
35
13
  post: {
36
- alternativeFor: "get",
14
+ ...hicGenomePayload,
37
15
  init
38
16
  }
39
17
  }
40
18
  };
41
19
  function init() {
42
20
  return async (req, res) => {
21
+ const query = req.query;
43
22
  const data = [];
44
23
  const erroutput = [];
45
- const [e, file, isurl] = fileurl({ query: req.query });
24
+ const [e, file] = fileurl({ query });
46
25
  if (e)
47
26
  res.send({ error: "illegal file name" });
48
27
  const matrixType = req.query.matrixType == "log(oe)" ? "oe" : req.query.matrixType ? req.query.matrixType : "observed";
package/routes/hicstat.js CHANGED
@@ -1,33 +1,15 @@
1
1
  import { fileurl, file_is_readable } from "#src/utils.js";
2
2
  import { do_hicstat } from "#src/hicstat.ts";
3
+ import { hicstatPayload } from "#types";
3
4
  const api = {
4
5
  endpoint: "hicstat",
5
6
  methods: {
6
7
  get: {
7
- init,
8
- request: {
9
- typeId: "HicstatRequest"
10
- },
11
- response: {
12
- typeId: "HicstatResponse"
13
- },
14
- examples: [
15
- {
16
- request: {
17
- body: {
18
- genome: "hg19",
19
- file: "proteinpaint_demo/hg19/hic/hic_demo.hic",
20
- embedder: "localhost"
21
- }
22
- },
23
- response: {
24
- header: { status: 200 }
25
- }
26
- }
27
- ]
8
+ ...hicstatPayload,
9
+ init
28
10
  },
29
11
  post: {
30
- alternativeFor: "get",
12
+ ...hicstatPayload,
31
13
  init
32
14
  }
33
15
  }
@@ -1,3 +1,4 @@
1
+ import { isoformlstPayload } from "#types";
1
2
  const api = {
2
3
  // route endpoint
3
4
  // - no need for trailing slash
@@ -6,16 +7,11 @@ const api = {
6
7
  endpoint: "isoformlst",
7
8
  methods: {
8
9
  get: {
9
- init,
10
- request: {
11
- typeId: "any"
12
- },
13
- response: {
14
- typeId: "any"
15
- }
10
+ ...isoformlstPayload,
11
+ init
16
12
  },
17
13
  post: {
18
- alternativeFor: "get",
14
+ ...isoformlstPayload,
19
15
  init
20
16
  }
21
17
  }
@@ -23,13 +19,14 @@ const api = {
23
19
  function init({ genomes }) {
24
20
  return function handle_isoformlst(req, res) {
25
21
  try {
26
- const g = genomes[req.query.genome];
22
+ const q = req.query;
23
+ const g = genomes[q.genome];
27
24
  if (!g)
28
25
  throw "invalid genome";
29
- if (!Array.isArray(req.query.lst))
26
+ if (!Array.isArray(q.lst))
30
27
  throw ".lst missing";
31
28
  const lst = [];
32
- for (const isoform of req.query.lst) {
29
+ for (const isoform of q.lst) {
33
30
  if (g.genomicNameRegexp.test(isoform))
34
31
  continue;
35
32
  const tmp = g.genedb.getjsonbyisoform.all(isoform);
package/routes/ntseq.js CHANGED
@@ -1,18 +1,14 @@
1
+ import { ntseqPayload } from "#types";
1
2
  import { get_fasta } from "#src/utils.js";
2
3
  const api = {
3
4
  endpoint: "ntseq",
4
5
  methods: {
5
6
  get: {
6
- init,
7
- request: {
8
- typeId: "any"
9
- },
10
- response: {
11
- typeId: "any"
12
- }
7
+ ...ntseqPayload,
8
+ init
13
9
  },
14
10
  post: {
15
- alternativeFor: "get",
11
+ ...ntseqPayload,
16
12
  init
17
13
  }
18
14
  }
@@ -20,14 +16,15 @@ const api = {
20
16
  function init({ genomes }) {
21
17
  return async function handle_ntseq(req, res) {
22
18
  try {
23
- if (!req.query.coord)
19
+ const q = req.query;
20
+ if (!q.coord)
24
21
  throw "coord missing";
25
- const g = genomes[req.query.genome];
22
+ const g = genomes[q.genome];
26
23
  if (!g)
27
24
  throw "invalid genome";
28
25
  if (!g.genomefile)
29
26
  throw "no sequence file available";
30
- const seq = await get_fasta(g, req.query.coord);
27
+ const seq = await get_fasta(g, q.coord);
31
28
  res.send({
32
29
  seq: seq.split("\n").slice(1).join("")
33
30
  });
package/routes/pdomain.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { pdomainPayload } from "#types";
1
2
  const api = {
2
3
  // route endpoint
3
4
  // - no need for trailing slash
@@ -6,16 +7,11 @@ const api = {
6
7
  endpoint: "pdomain",
7
8
  methods: {
8
9
  get: {
9
- init,
10
- request: {
11
- typeId: "any"
12
- },
13
- response: {
14
- typeId: "any"
15
- }
10
+ ...pdomainPayload,
11
+ init
16
12
  },
17
13
  post: {
18
- alternativeFor: "get",
14
+ ...pdomainPayload,
19
15
  init
20
16
  }
21
17
  }
@@ -23,7 +19,8 @@ const api = {
23
19
  function init({ genomes }) {
24
20
  return function handle_pdomain(req, res) {
25
21
  try {
26
- const gn = req.query.genome;
22
+ const q = req.query;
23
+ const gn = q.genome;
27
24
  if (!gn)
28
25
  throw "no genome";
29
26
  const g = genomes[gn];
@@ -32,10 +29,10 @@ function init({ genomes }) {
32
29
  if (!g.proteindomain) {
33
30
  return res.send({ lst: [] });
34
31
  }
35
- if (!Array.isArray(req.query.isoforms))
32
+ if (!Array.isArray(q.isoforms))
36
33
  throw "isoforms[] missing";
37
34
  const lst = [];
38
- for (const isoform of req.query.isoforms) {
35
+ for (const isoform of q.isoforms) {
39
36
  if (g.genomicNameRegexp.test(isoform))
40
37
  continue;
41
38
  const tmp = g.proteindomain.getbyisoform.all(isoform);
@@ -1,40 +1,40 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import serverconfig from "#src/serverconfig.js";
4
+ import { dzImagesPayload } from "#types";
4
5
  const api = {
5
6
  endpoint: "sampledzimages",
6
7
  methods: {
7
8
  get: {
8
9
  init,
9
- request: {
10
- typeId: "GetSampleDZImagesRequest"
11
- },
12
- response: {
13
- typeId: "GetSampleDZImagesResponse"
14
- }
10
+ ...dzImagesPayload
15
11
  },
16
12
  post: {
17
- alternativeFor: "get",
18
- init
13
+ init,
14
+ ...dzImagesPayload
19
15
  }
20
16
  }
21
17
  };
22
18
  function init({ genomes }) {
23
19
  return async (req, res) => {
20
+ const q = req.query;
24
21
  try {
25
- const g = genomes[req.query.genome];
22
+ const g = genomes[q.genome];
26
23
  if (!g)
27
24
  throw "invalid genome name";
28
- const ds = g.datasets[req.query.dslabel];
25
+ const ds = g.datasets[q.dslabel];
29
26
  if (!ds)
30
27
  throw "invalid dataset name";
31
- const sampleId = req.query.sample_id;
28
+ const sampleId = q.sample_id || "";
32
29
  const sampleDZImagesPath = path.join(
33
30
  `${serverconfig.tpmasterdir}/${ds.queries.DZImages.imageBySampleFolder}`,
34
31
  sampleId
35
32
  );
36
33
  const sampleDZImages = getDZImages(sampleDZImagesPath);
37
- res.send({ sampleDZImages });
34
+ res.send(
35
+ { sampleDZImages }
36
+ /*satisfies DZImagesResponse*/
37
+ );
38
38
  } catch (e) {
39
39
  console.log(e);
40
40
  res.status(404).send("Sample images not found");
@@ -1,17 +1,13 @@
1
+ import { sampleWSImagesPayload } from "#types";
1
2
  const api = {
2
3
  endpoint: "samplewsimages",
3
4
  methods: {
4
5
  get: {
5
- init,
6
- request: {
7
- typeId: "GetSampleWSImagesRequest"
8
- },
9
- response: {
10
- typeId: "GetSampleWSImagesResponse"
11
- }
6
+ ...sampleWSImagesPayload,
7
+ init
12
8
  },
13
9
  post: {
14
- alternativeFor: "get",
10
+ ...sampleWSImagesPayload,
15
11
  init
16
12
  }
17
13
  }
@@ -35,8 +31,8 @@ function init({ genomes }) {
35
31
  }
36
32
  };
37
33
  }
38
- function validate_query_getSampleWSImages(ds, genome) {
39
- const q = ds.queries.WSImages;
34
+ function validate_query_getSampleWSImages(ds) {
35
+ const q = ds.queries?.WSImages;
40
36
  if (!q)
41
37
  return;
42
38
  nativeValidateQuery(ds);
package/routes/snp.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { snpPayload } from "#types";
1
2
  import * as utils from "#src/utils.js";
2
3
  const api = {
3
4
  // route endpoint
@@ -7,16 +8,11 @@ const api = {
7
8
  endpoint: "snp",
8
9
  methods: {
9
10
  get: {
10
- init,
11
- request: {
12
- typeId: "any"
13
- },
14
- response: {
15
- typeId: "any"
16
- }
11
+ ...snpPayload,
12
+ init
17
13
  },
18
14
  post: {
19
- alternativeFor: "get",
15
+ ...snpPayload,
20
16
  init
21
17
  }
22
18
  }
@@ -24,10 +20,12 @@ const api = {
24
20
  function init({ genomes }) {
25
21
  return async function handle_snp(req, res) {
26
22
  try {
27
- const n = req.query.genome;
23
+ const q = req.query;
24
+ const n = q.genome;
28
25
  if (!n)
29
26
  throw "no genome";
30
- res.send({ results: await searchSNP(req.query, genomes[n]) });
27
+ const results = await searchSNP(q, genomes[n]);
28
+ res.send({ results });
31
29
  } catch (e) {
32
30
  if (e.stack)
33
31
  console.log(e.stack);
@@ -1,4 +1,5 @@
1
1
  import path from "path";
2
+ import { diffExpPayload } from "#types";
2
3
  import { run_rust } from "@sjcrh/proteinpaint-rust";
3
4
  import { get_ds_tdb } from "../src/termdb.js";
4
5
  import run_R from "../src/run_R.js";
@@ -6,16 +7,13 @@ import serverconfig from "../src/serverconfig.js";
6
7
  const api = {
7
8
  endpoint: "DEanalysis",
8
9
  methods: {
9
- all: {
10
- init,
11
- request: {
12
- typeId: "DERequest"
13
- },
14
- response: {
15
- typeId: "DEResponse"
16
- // will combine this with type checker
17
- //valid: (t) => {}
18
- }
10
+ get: {
11
+ ...diffExpPayload,
12
+ init
13
+ },
14
+ post: {
15
+ ...diffExpPayload,
16
+ init
19
17
  }
20
18
  }
21
19
  };
@@ -1,18 +1,17 @@
1
+ import { boxplotPayload } from "#types";
1
2
  import { getData } from "../src/termdb.matrix.js";
2
3
  import { boxplot_getvalue } from "../src/utils.js";
3
4
  import { sortKey2values } from "../src/termdb.violin.js";
4
5
  const api = {
5
6
  endpoint: "termdb/boxplot",
6
7
  methods: {
7
- all: {
8
- init,
9
- request: {
10
- typeId: "BoxplotRequest"
11
- },
12
- response: {
13
- typeId: "BoxplotResponse"
14
- },
15
- examples: []
8
+ get: {
9
+ ...boxplotPayload,
10
+ init
11
+ },
12
+ post: {
13
+ ...boxplotPayload,
14
+ init
16
15
  }
17
16
  }
18
17
  };
@@ -1,56 +1,15 @@
1
+ import { termdbCategoriesPayload } from "#types";
1
2
  import { getOrderedLabels } from "#src/termdb.barchart.js";
2
3
  import { getData } from "#src/termdb.matrix.js";
3
4
  const api = {
4
5
  endpoint: "termdb/categories",
5
6
  methods: {
6
7
  get: {
7
- init,
8
- request: {
9
- typeId: "getcategoriesRequest"
10
- },
11
- response: {
12
- typeId: "getcategoriesResponse"
13
- },
14
- examples: [
15
- {
16
- request: {
17
- body: {
18
- genome: "hg38-test",
19
- dslabel: "TermdbTest",
20
- embedder: "localhost",
21
- term: { id: "diaggrp" },
22
- filter: {
23
- type: "tvslst",
24
- in: true,
25
- join: "",
26
- lst: [
27
- {
28
- tag: "cohortFilter",
29
- type: "tvs",
30
- tvs: {
31
- term: {
32
- name: "Cohort",
33
- type: "categorical",
34
- values: { ABC: { label: "ABC" }, XYZ: { label: "XYZ" } },
35
- id: "subcohort",
36
- isleaf: false,
37
- groupsetting: { disabled: true }
38
- },
39
- values: [{ key: "ABC", label: "ABC" }]
40
- }
41
- }
42
- ]
43
- }
44
- }
45
- },
46
- response: {
47
- header: { status: 200 }
48
- }
49
- }
50
- ]
8
+ ...termdbCategoriesPayload,
9
+ init
51
10
  },
52
11
  post: {
53
- alternativeFor: "get",
12
+ ...termdbCategoriesPayload,
54
13
  init
55
14
  }
56
15
  }
@@ -1,5 +1,6 @@
1
1
  import path from "path";
2
2
  import run_R from "#src/run_R.js";
3
+ import { termdbClusterPayload } from "#types";
3
4
  import * as utils from "#src/utils.js";
4
5
  import serverconfig from "#src/serverconfig.js";
5
6
  import { gdc_validate_query_geneExpression } from "#src/mds3.gdc.js";
@@ -11,14 +12,13 @@ import { getData } from "#src/termdb.matrix.js";
11
12
  const api = {
12
13
  endpoint: "termdb/cluster",
13
14
  methods: {
14
- all: {
15
- init,
16
- request: {
17
- typeId: "TermdbClusterRequest"
18
- },
19
- response: {
20
- typeId: "TermdbClusterResponse"
21
- }
15
+ get: {
16
+ ...termdbClusterPayload,
17
+ init
18
+ },
19
+ post: {
20
+ ...termdbClusterPayload,
21
+ init
22
22
  }
23
23
  }
24
24
  };
@@ -135,7 +135,7 @@ async function doClustering(data, q, numCases = 1e3) {
135
135
  for (const s of inputData.col_names) {
136
136
  row.push(o[s] || 0);
137
137
  }
138
- inputData.matrix.push(getZscore(row));
138
+ inputData.matrix.push(q.zScoreTransformation ? getZscore(row) : row);
139
139
  }
140
140
  if (inputData.matrix.length == 0)
141
141
  throw "Clustering matrix is empty";