sampledisco 0.4.0__tar.gz → 0.4.2__tar.gz

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 (94) hide show
  1. {sampledisco-0.4.0/src/sampledisco.egg-info → sampledisco-0.4.2}/PKG-INFO +2 -1
  2. {sampledisco-0.4.0 → sampledisco-0.4.2}/pyproject.toml +1 -1
  3. {sampledisco-0.4.0 → sampledisco-0.4.2}/requirements.txt +1 -0
  4. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/parameter_selection/autotune.py +16 -9
  5. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_embedding/blocks.py +10 -4
  6. {sampledisco-0.4.0 → sampledisco-0.4.2/src/sampledisco.egg-info}/PKG-INFO +2 -1
  7. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco.egg-info/requires.txt +1 -0
  8. {sampledisco-0.4.0 → sampledisco-0.4.2}/tests/test_sample_embedding_equivalence.py +31 -0
  9. {sampledisco-0.4.0 → sampledisco-0.4.2}/LICENSE +0 -0
  10. {sampledisco-0.4.0 → sampledisco-0.4.2}/README.md +0 -0
  11. {sampledisco-0.4.0 → sampledisco-0.4.2}/setup.cfg +0 -0
  12. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/__init__.py +0 -0
  13. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/cli.py +0 -0
  14. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/config/config_demo.yaml +0 -0
  15. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/gene_activity/ATAC_ArchR.py +0 -0
  16. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/gene_activity/ATAC_RNA_harmony.py +0 -0
  17. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/gene_activity/ATAC_gene_activity.py +0 -0
  18. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/gene_activity/ATAC_peak_annotation.py +0 -0
  19. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/gene_activity/RNA_name_convertor.py +0 -0
  20. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/gene_activity/__init__.py +0 -0
  21. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/gene_activity/pseudo_correlation.py +0 -0
  22. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/gene_activity/test_pseudo_correlation.py +0 -0
  23. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/gene_activity/validation.py +0 -0
  24. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/parameter_selection/__init__.py +0 -0
  25. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/ATAC_cell_type.py +0 -0
  26. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/ATAC_cell_type_gpu.py +0 -0
  27. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/__init__.py +0 -0
  28. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/atac_preprocess_cpu.py +0 -0
  29. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/atac_preprocess_gpu.py +0 -0
  30. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/cell_type_cpu.py +0 -0
  31. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/cell_type_gpu.py +0 -0
  32. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/multi_omics_batch_correction.py +0 -0
  33. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/multi_omics_cell_type_cpu.py +0 -0
  34. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/multi_omics_cell_type_gpu.py +0 -0
  35. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/multi_omics_glue.py +0 -0
  36. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/multi_omics_merge.py +0 -0
  37. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/rna_preprocess_cpu.py +0 -0
  38. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/preparation/rna_preprocess_gpu.py +0 -0
  39. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_association/__init__.py +0 -0
  40. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_association/association.py +0 -0
  41. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_clustering/RAISIN.py +0 -0
  42. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_clustering/RAISIN_TEST.py +0 -0
  43. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_clustering/__init__.py +0 -0
  44. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_clustering/cluster.py +0 -0
  45. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_clustering/cluster_severity_reconcile.py +0 -0
  46. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_clustering/proportion_test.py +0 -0
  47. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_distance/ChiSquare.py +0 -0
  48. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_distance/__init__.py +0 -0
  49. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_distance/distance_test.py +0 -0
  50. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_distance/jensenshannon.py +0 -0
  51. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_distance/sample_distance.py +0 -0
  52. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_embedding/__init__.py +0 -0
  53. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_embedding/sample_embedding.py +0 -0
  54. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_embedding/sample_embedding_gpu.py +0 -0
  55. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_trajectory/CCA.py +0 -0
  56. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_trajectory/CCA_test.py +0 -0
  57. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_trajectory/TSCAN.py +0 -0
  58. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_trajectory/__init__.py +0 -0
  59. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_trajectory/trajectory_DGE_visualization.py +0 -0
  60. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/sample_trajectory/trajectory_diff_gene.py +0 -0
  61. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/Grouping.py +0 -0
  62. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/__init__.py +0 -0
  63. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/batch_regress.py +0 -0
  64. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/embedding_keys.py +0 -0
  65. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/harmony_compat.py +0 -0
  66. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/imbalance_cell_type_handler.py +0 -0
  67. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/inspector.py +0 -0
  68. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/limma.py +0 -0
  69. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/merge_sample_meta.py +0 -0
  70. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/random_seed.py +0 -0
  71. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/safe_save.py +0 -0
  72. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/slim_adata.py +0 -0
  73. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/subsample.py +0 -0
  74. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/subsample_generator.py +0 -0
  75. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/tf_idf.py +0 -0
  76. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/utils/unify_optimal.py +0 -0
  77. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/visualization/ATAC_visualization.py +0 -0
  78. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/visualization/DEG_visualization.py +0 -0
  79. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/visualization/__init__.py +0 -0
  80. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/visualization/multi_omics_visualization.py +0 -0
  81. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/visualization/visualization_embedding.py +0 -0
  82. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/visualization/visualization_helper.py +0 -0
  83. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/visualization/visualization_other.py +0 -0
  84. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/wrapper/__init__.py +0 -0
  85. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/wrapper/atac_wrapper.py +0 -0
  86. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/wrapper/multiomics_wrapper.py +0 -0
  87. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/wrapper/rna_wrapper.py +0 -0
  88. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco/wrapper/wrapper.py +0 -0
  89. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco.egg-info/SOURCES.txt +0 -0
  90. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco.egg-info/dependency_links.txt +0 -0
  91. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco.egg-info/entry_points.txt +0 -0
  92. {sampledisco-0.4.0 → sampledisco-0.4.2}/src/sampledisco.egg-info/top_level.txt +0 -0
  93. {sampledisco-0.4.0 → sampledisco-0.4.2}/tests/test_config_validation.py +0 -0
  94. {sampledisco-0.4.0 → sampledisco-0.4.2}/tests/test_inplace_uns_and_equal_weight.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sampledisco
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Cross-omics, cross-condition sample embedding for single-cell data (RNA + ATAC)
5
5
  Author: Hongkai Ji
6
6
  Author-email: Harry Jiang <hjiang55@jh.edu>
@@ -21,6 +21,7 @@ Requires-Dist: numpy>=1.23
21
21
  Requires-Dist: pandas>=1.5
22
22
  Requires-Dist: scipy>=1.9
23
23
  Requires-Dist: scikit-learn>=1.2
24
+ Requires-Dist: threadpoolctl>=3.1
24
25
  Requires-Dist: scikit-image>=0.19
25
26
  Requires-Dist: scikit-misc>=0.3
26
27
  Requires-Dist: statsmodels>=0.13
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "sampledisco"
12
- version = "0.4.0"
12
+ version = "0.4.2"
13
13
  description = "Cross-omics, cross-condition sample embedding for single-cell data (RNA + ATAC)"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.10"
@@ -7,6 +7,7 @@ numpy>=1.23
7
7
  pandas>=1.5
8
8
  scipy>=1.9
9
9
  scikit-learn>=1.2
10
+ threadpoolctl>=3.1 # imported directly (ThreadpoolController)
10
11
  scikit-image>=0.19 # scrublet-based ATAC doublet detection (doublet_detection=True)
11
12
  scikit-misc>=0.3
12
13
  statsmodels>=0.13
@@ -10,6 +10,9 @@ the data can't support:
10
10
  (CCA, SPS, CV-kNN, pseudotime-Spearman).
11
11
  - If neither → emit a warning and short-circuit with fixed defaults.
12
12
 
13
+ ``scoring="ilisi_label"`` is the two-term variant: one biology term (grouping
14
+ tracking: CCA or categorical PC-R²) and one batch term (iLISI), equally weighted.
15
+
13
16
  Generalized version of `wire_autotune_dualembed_v2.py`. No dataset-specific
14
17
  paths; all data flows through the same `compute_sample_embedding` primitives
15
18
  in `sample_embedding/blocks.py`.
@@ -427,17 +430,20 @@ def make_scorer(name: str, meta: Dict, lam: float = 0.5) -> Callable[[np.ndarray
427
430
  - lam * (_asw_safe(emb, batch) if has_batch else 0.0)
428
431
  )
429
432
 
430
- if name in ("multi_metric_proxy", "auto"):
433
+ if name in ("multi_metric_proxy", "auto", "ilisi_label"):
434
+ two_term = name == "ilisi_label"
431
435
  components: List[Callable[[np.ndarray], float]] = []
432
436
  if has_grouping:
433
437
  tracking_key = "pc_r2_categorical" if grouping_is_categorical else "cca"
434
438
  components.append(
435
439
  lambda emb: _minmax(_grouping_tracking_score(emb),
436
440
  *SCORING_BOUNDS[tracking_key]))
437
- components.append(lambda emb: _minmax(_sps_continuous(emb, grouping), *SCORING_BOUNDS["sps"]))
441
+ if not two_term:
442
+ components.append(lambda emb: _minmax(_sps_continuous(emb, grouping), *SCORING_BOUNDS["sps"]))
438
443
  if has_batch:
439
444
  components.append(lambda emb: _minmax(_ilisi_norm(emb, batch), *SCORING_BOUNDS["ilisi_norm"]))
440
- components.append(lambda emb: _minmax(-_asw_safe(emb, batch), *SCORING_BOUNDS["neg_asw_batch"]))
445
+ if not two_term:
446
+ components.append(lambda emb: _minmax(-_asw_safe(emb, batch), *SCORING_BOUNDS["neg_asw_batch"]))
441
447
  if not components:
442
448
  return lambda emb: 0.0
443
449
 
@@ -760,12 +766,13 @@ _PROXY_DESCRIPTIONS = {
760
766
 
761
767
  def _active_proxies(scoring: str, has_batch: bool, has_grouping: bool):
762
768
  """Return the list of proxy names the scorer actually evaluates."""
763
- if scoring in ("auto", "multi_metric_proxy"):
769
+ if scoring in ("auto", "multi_metric_proxy", "ilisi_label"):
770
+ two_term = scoring == "ilisi_label"
764
771
  names = []
765
772
  if has_grouping:
766
- names += ["cca", "sps"]
773
+ names += ["cca"] if two_term else ["cca", "sps"]
767
774
  if has_batch:
768
- names += ["ilisi_batch", "neg_asw_batch"]
775
+ names += ["ilisi_batch"] if two_term else ["ilisi_batch", "neg_asw_batch"]
769
776
  return names
770
777
  return [scoring] if scoring in _PROXY_DESCRIPTIONS else [scoring]
771
778
 
@@ -818,8 +825,8 @@ def _format_autotune_report(*, best_params, best_score, trace, weights,
818
825
  for name in proxies:
819
826
  kind, desc = _PROXY_DESCRIPTIONS.get(name, ("?", name))
820
827
  lines.append(f" - [{kind:<12s}] {name:<20s} {desc}")
821
- if scoring in ("auto", "multi_metric_proxy"):
822
- lines.append(" ensemble: multi_metric_proxy = mean of the proxies above (each min-max scaled).")
828
+ if scoring in ("auto", "multi_metric_proxy", "ilisi_label"):
829
+ lines.append(f" ensemble: {scoring} = mean of the proxies above (each min-max scaled).")
823
830
  lines.append("")
824
831
  lines.append("Result")
825
832
  lines.append("-" * 68)
@@ -928,7 +935,7 @@ def _finalize(adata, blocks, final_emb, weights, *,
928
935
  tune_on_modality=tune_on_modality, score_n_units=score_n_units,
929
936
  )
930
937
  report_path = os.path.join(out_dir, "autotune_record.txt")
931
- with open(report_path, "w") as f:
938
+ with open(report_path, "w", encoding="utf-8") as f:
932
939
  f.write(report)
933
940
  if verbose:
934
941
  print(f"[autotune] wrote {emb_csv}")
@@ -152,16 +152,22 @@ def kmeans_centers_pair(Z: np.ndarray, K_med: int, K_fine: int, seed: int,
152
152
  """Centres at K_med (seed) and K_fine (seed + 1). With n_threads > 1 the two independent
153
153
  fits run concurrently (each keeps its own RandomState). BLAS (process-wide) is held at one
154
154
  thread because k-means++ issues thousands of tiny GEMMs; the OpenMP thread count is a
155
- per-thread setting, so each worker sets its own share."""
155
+ per-thread setting, so each worker sets its own share.
156
+
157
+ sklearn.cluster is imported here, in the calling thread, so no library is loaded inside the
158
+ worker threads: a library scan (threadpoolctl) in one thread racing a first import of a
159
+ compiled extension in another can deadlock."""
156
160
  if n_threads < 2:
157
161
  return kmeans_centers(Z, K_med, seed), kmeans_centers(Z, K_fine, seed + 1)
158
- from threadpoolctl import threadpool_limits
162
+ from sklearn.cluster import MiniBatchKMeans # noqa: F401 (load sklearn's extensions before the threads start)
163
+ from threadpoolctl import ThreadpoolController
164
+ ctl = ThreadpoolController()
159
165
 
160
166
  def fit(K, sd):
161
- with threadpool_limits(limits=max(1, n_threads // 2), user_api="openmp"):
167
+ with ctl.limit(limits=max(1, n_threads // 2), user_api="openmp"):
162
168
  return kmeans_centers(Z, K, sd)
163
169
 
164
- with threadpool_limits(limits=1, user_api="blas"), ThreadPoolExecutor(2) as ex:
170
+ with ctl.limit(limits=1, user_api="blas"), ThreadPoolExecutor(2) as ex:
165
171
  f_med = ex.submit(fit, K_med, seed)
166
172
  f_fine = ex.submit(fit, K_fine, seed + 1)
167
173
  return f_med.result(), f_fine.result()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sampledisco
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Cross-omics, cross-condition sample embedding for single-cell data (RNA + ATAC)
5
5
  Author: Hongkai Ji
6
6
  Author-email: Harry Jiang <hjiang55@jh.edu>
@@ -21,6 +21,7 @@ Requires-Dist: numpy>=1.23
21
21
  Requires-Dist: pandas>=1.5
22
22
  Requires-Dist: scipy>=1.9
23
23
  Requires-Dist: scikit-learn>=1.2
24
+ Requires-Dist: threadpoolctl>=3.1
24
25
  Requires-Dist: scikit-image>=0.19
25
26
  Requires-Dist: scikit-misc>=0.3
26
27
  Requires-Dist: statsmodels>=0.13
@@ -2,6 +2,7 @@ numpy>=1.23
2
2
  pandas>=1.5
3
3
  scipy>=1.9
4
4
  scikit-learn>=1.2
5
+ threadpoolctl>=3.1
5
6
  scikit-image>=0.19
6
7
  scikit-misc>=0.3
7
8
  statsmodels>=0.13
@@ -2,6 +2,8 @@
2
2
  save_cell_adata switch leaves the cell-level h5ad untouched. Synthetic data, CPU only.
3
3
  Run with `pytest tests/` or `python tests/test_sample_embedding_equivalence.py`."""
4
4
  import os
5
+ import subprocess
6
+ import sys
5
7
  import tempfile
6
8
  import warnings
7
9
 
@@ -93,6 +95,35 @@ def test_soft_composition_bit_identical_odd_and_even():
93
95
  assert np.array_equal(old, soft_composition(Z, C, rows, n_threads=threads)), (n, K, threads)
94
96
 
95
97
 
98
+ def test_kmeans_centers_pair_matches_serial_fits():
99
+ from sampledisco.sample_embedding.blocks import kmeans_centers, kmeans_centers_pair
100
+ Z = np.random.default_rng(3).normal(size=(5000, 8)).astype(np.float32)
101
+ pair = kmeans_centers_pair(Z, 6, 12, 0, n_threads=4)
102
+ assert np.array_equal(pair[0], kmeans_centers(Z, 6, 0)) and np.array_equal(pair[1], kmeans_centers(Z, 12, 1))
103
+
104
+
105
+ _NO_WORKER_IMPORTS = """
106
+ import sys, threading
107
+ import numpy as np
108
+ from sampledisco.sample_embedding.blocks import kmeans_centers_pair
109
+ assert "sklearn.cluster" not in sys.modules
110
+ seen = []
111
+ sys.addaudithook(lambda ev, args: ev == "import" and seen.append((args[0], threading.current_thread())))
112
+ kmeans_centers_pair(np.random.default_rng(3).normal(size=(5000, 8)).astype(np.float32), 6, 12, 0, n_threads=4)
113
+ bad = sorted({m for m, t in seen if t is not threading.main_thread()})
114
+ assert not bad, f"{len(bad)} modules first imported in worker threads, e.g. {bad[:5]}"
115
+ """
116
+
117
+
118
+ def test_kmeans_centers_pair_no_imports_in_workers():
119
+ """A first import inside a worker thread can deadlock against a threadpoolctl scan in the other (0.4.0)."""
120
+ import sampledisco
121
+ env = dict(os.environ, PYTHONPATH=os.pathsep.join(
122
+ [os.path.dirname(os.path.dirname(sampledisco.__file__)), os.environ.get("PYTHONPATH", "")]))
123
+ r = subprocess.run([sys.executable, "-c", _NO_WORKER_IMPORTS], env=env, capture_output=True, text=True, timeout=300)
124
+ assert r.returncode == 0, r.stderr[-1500:]
125
+
126
+
96
127
  def _write_old_cell_h5ad(out):
97
128
  a = make_adata()
98
129
  a.uns["X_DR_sample"] = pd.DataFrame({"PC1": [1.0]}, index=["old"])
File without changes
File without changes
File without changes