diff --git a/src/rapids_singlecell/pertpy_gpu/__init__.py b/src/rapids_singlecell/pertpy_gpu/__init__.py new file mode 100644 index 00000000..90b92ca7 --- /dev/null +++ b/src/rapids_singlecell/pertpy_gpu/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from ._edistance import pertpy_edistance diff --git a/src/rapids_singlecell/pertpy_gpu/_edistance.py b/src/rapids_singlecell/pertpy_gpu/_edistance.py new file mode 100644 index 00000000..613c2384 --- /dev/null +++ b/src/rapids_singlecell/pertpy_gpu/_edistance.py @@ -0,0 +1,445 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, NamedTuple + +import cupy as cp +import numpy as np +import pandas as pd + +from rapids_singlecell.pertpy_gpu._kernels._edistance import ( + get_compute_group_distances_kernel, +) +from rapids_singlecell.preprocessing._harmony._helper import ( + _create_category_index_mapping, +) +from rapids_singlecell.squidpy_gpu._utils import _assert_categorical_obs + +if TYPE_CHECKING: + from anndata import AnnData + + +class EDistanceResult(NamedTuple): + distances: pd.DataFrame + distances_var: pd.DataFrame | None + + +compute_group_distances_kernel = get_compute_group_distances_kernel() + + +def pertpy_edistance( + adata: AnnData, + groupby: str, + *, + obsm_key: str = "X_pca", + groups: list[str] | None = None, + inplace: bool = False, + bootstrap: bool = False, + n_bootstrap: int = 100, + random_state: int = 0, +) -> pd.DataFrame: + """ + GPU-accelerated pairwise edistance computation with decomposed components. + + Returns d_itself, d_other arrays and final edistance DataFrame where: + df[a,b] = 2*d_other[a,b] - d_itself[a] - d_itself[b] + + Parameters + ---------- + adata : AnnData + Annotated data matrix + groupby : str + Key in adata.obs for grouping + obsm_key : str + Key in adata.obsm for embeddings + groups : list[str] | None + Specific groups to compute (if None, use all) + copy : bool + Whether to return a copy + + Returns + ------- + df : pd.DataFrame + Final edistance matrix + """ + _assert_categorical_obs(adata, key=groupby) + + embedding = cp.array(adata.obsm[obsm_key]).astype(np.float32) + original_groups = adata.obs[groupby] + group_map = {v: i for i, v in enumerate(original_groups.cat.categories.values)} + group_labels = cp.array([group_map[c] for c in original_groups], dtype=cp.int32) + + # Use harmony's category mapping + k = len(group_map) + cat_offsets, cell_indices = _create_category_index_mapping(group_labels, k) + + groups_list = ( + list(original_groups.cat.categories.values) if groups is None else groups + ) + result = None + if not bootstrap: + df = _prepare_edistance_df( + embedding=embedding, + cat_offsets=cat_offsets, + cell_indices=cell_indices, + k=k, + groups_list=groups_list, + groupby=groupby, + ) + result = EDistanceResult(distances=df, distances_var=None) + + else: + df, df_var = _prepare_edistance_df_bootstrap( + embedding=embedding, + cat_offsets=cat_offsets, + cell_indices=cell_indices, + k=k, + groups_list=groups_list, + groupby=groupby, + n_bootstrap=n_bootstrap, + random_state=random_state, + ) + result = EDistanceResult(distances=df, distances_var=df_var) + + if inplace: + adata.uns[f"{groupby}_pairwise_edistance"] = dict(result) + + return result + + +def _pairwise_means( + embedding: cp.ndarray, cat_offsets: cp.ndarray, cell_indices: cp.ndarray, k: int +) -> cp.ndarray: + """ + Compute between-group mean distances for all group pairs. + + Parameters + ---------- + embedding : cp.ndarray + Cell embeddings [n_cells, n_features] + cat_offsets : cp.ndarray + Group start/end indices + cell_indices : cp.ndarray + Sorted cell indices by group + k : int + Number of groups + + Returns + ------- + d_other : cp.ndarray + Between-group mean distances [k, k] + """ + _, n_features = embedding.shape + + pair_left = [] + pair_right = [] + pair_indices = [] + # only upper triangle + for a in range(k): + for b in range(a, k): + pair_left.append(a) + pair_right.append(b) + pair_indices.append(a * k + b) # Flatten matrix index + + pair_left = cp.asarray(pair_left, dtype=cp.int32) + pair_right = cp.asarray(pair_right, dtype=cp.int32) + pair_indices = cp.asarray(pair_indices, dtype=cp.int32) + + num_pairs = len(pair_left) # k * (k-1) pairs instead of k² + + # Allocate output for off-diagonal distances only + d_other_offdiag = cp.zeros(num_pairs, dtype=embedding.dtype) + + # Choose optimal block size + props = cp.cuda.runtime.getDeviceProperties(0) + max_smem = int(props.get("sharedMemPerBlock", 48 * 1024)) + + chosen_threads = None + shared_mem_size = 0 # TODO: think of a better way to do this + for tpb in (1024, 512, 256, 128, 64, 32): + required = tpb * cp.dtype(cp.float32).itemsize + if required <= max_smem: + chosen_threads = tpb + shared_mem_size = required + break + + # Launch kernel - one block per OFF-DIAGONAL group pair only + grid = (num_pairs,) + block = (chosen_threads,) + compute_group_distances_kernel( + grid, + block, + ( + embedding, + cat_offsets, + cell_indices, + pair_left, + pair_right, + d_other_offdiag, + k, + n_features, + ), + shared_mem=shared_mem_size, + ) + + # Build full k x k matrix + pairwise_means = cp.zeros((k, k), dtype=np.float32) + + # Fill the full matrix + for i, idx in enumerate(pair_indices.get()): + a, b = divmod(idx, k) + pairwise_means[a, b] = d_other_offdiag[i] + pairwise_means[b, a] = d_other_offdiag[i] + + return pairwise_means + + +def _generate_bootstrap_indices( + cat_offsets: cp.ndarray, + k: int, + n_bootstrap: int = 100, + random_state: int = 0, +) -> list[list[cp.ndarray]]: + """ + Generate bootstrap indices for all groups and all bootstrap iterations. + This matches the CPU implementation's random sampling logic for reproducibility. + + Parameters + ---------- + cat_offsets : cp.ndarray + Group start/end indices + k : int + Number of groups + n_bootstrap : int + Number of bootstrap samples + random_state : int + Random seed for reproducibility + + Returns + ------- + bootstrap_indices : list[list[cp.ndarray]] + For each bootstrap iteration, list of indices arrays for each group + Shape: [n_bootstrap][k] where each element is cp.ndarray of group_size + """ + import numpy as np + + # Use same RNG logic as CPU code + rng = np.random.default_rng(random_state) + + # Convert to numpy for CPU-based random generation + cat_offsets_np = cat_offsets.get() + + bootstrap_indices = [] + + for bootstrap_iter in range(n_bootstrap): + group_indices = [] + + for group_idx in range(k): + start_idx = cat_offsets_np[group_idx] + end_idx = cat_offsets_np[group_idx + 1] + group_size = end_idx - start_idx + + if group_size > 0: + # Generate bootstrap indices using same logic as CPU code + # rng.choice(a=X.shape[0], size=X.shape[0], replace=True) + bootstrap_group_indices = rng.choice( + group_size, size=group_size, replace=True + ) + # Convert to CuPy array + group_indices.append(cp.array(bootstrap_group_indices, dtype=cp.int32)) + else: + # Empty group + group_indices.append(cp.array([], dtype=cp.int32)) + + bootstrap_indices.append(group_indices) + + return bootstrap_indices + + +def _bootstrap_sample_cells_from_indices( + *, + cat_offsets: cp.ndarray, + cell_indices: cp.ndarray, + k: int, + bootstrap_group_indices: list[cp.ndarray], +) -> tuple[cp.ndarray, cp.ndarray]: + """ + Bootstrap sample cells using pre-generated indices. + + Parameters + ---------- + cat_offsets : cp.ndarray + Group start/end indices + cell_indices : cp.ndarray + Sorted cell indices by group + k : int + Number of groups + bootstrap_group_indices : list[cp.ndarray] + Pre-generated bootstrap indices for each group + + Returns + ------- + new_cat_offsets, new_cell_indices : tuple[cp.ndarray, cp.ndarray] + New category structure with bootstrapped cells + """ + new_cell_indices = [] + new_cat_offsets = cp.zeros(k + 1, dtype=cp.int32) + + for group_idx in range(k): + start_idx = cat_offsets[group_idx] + end_idx = cat_offsets[group_idx + 1] + group_size = end_idx - start_idx + + if group_size > 0: + # Get original cell indices for this group + group_cells = cell_indices[start_idx:end_idx] + + # Use pre-generated bootstrap indices + bootstrap_indices = bootstrap_group_indices[group_idx] + bootstrap_cells = group_cells[bootstrap_indices] + + new_cell_indices.extend(bootstrap_cells.get().tolist()) + + new_cat_offsets[group_idx + 1] = len(new_cell_indices) + + return new_cat_offsets, cp.array(new_cell_indices, dtype=cp.int32) + + +def _pairwise_means_bootstrap( + embedding: cp.ndarray, + *, + cat_offsets: cp.ndarray, + cell_indices: cp.ndarray, + k: int, + n_bootstrap: int = 100, + random_state: int = 0, +) -> tuple[cp.ndarray, cp.ndarray]: + """ + Compute bootstrap statistics for between-group distances. + Uses CPU-compatible random generation for reproducibility. + + Returns: + means: [k, k] matrix of bootstrap means + variances: [k, k] matrix of bootstrap variances + """ + # Generate all bootstrap indices upfront using CPU-compatible logic + bootstrap_indices = _generate_bootstrap_indices( + cat_offsets, k, n_bootstrap, random_state + ) + + bootstrap_results = [] + + for bootstrap_iter in range(n_bootstrap): + # Use pre-generated indices for this bootstrap iteration + boot_cat_offsets, boot_cell_indices = _bootstrap_sample_cells_from_indices( + cat_offsets=cat_offsets, + cell_indices=cell_indices, + k=k, + bootstrap_group_indices=bootstrap_indices[bootstrap_iter], + ) + + # Compute distances with bootstrapped samples + pairwise_means = _pairwise_means( + embedding=embedding, + cat_offsets=boot_cat_offsets, + cell_indices=boot_cell_indices, + k=k, + ) + bootstrap_results.append(pairwise_means.get()) + + # Compute statistics across bootstrap samples + bootstrap_stack = cp.array(bootstrap_results) # [n_bootstrap, k, k] + means = cp.mean(bootstrap_stack, axis=0) + variances = cp.var(bootstrap_stack, axis=0) + + return means, variances + + +def _prepare_edistance_df_bootstrap( + embedding: cp.ndarray, + *, + cat_offsets: cp.ndarray, + cell_indices: cp.ndarray, + k: int, + groups_list: list[str], + groupby: str, + n_bootstrap: int = 100, + random_state: int = 0, +) -> tuple[pd.DataFrame, pd.DataFrame]: + # Bootstrap computation + pairwise_means_boot, pairwise_vars_boot = _pairwise_means_bootstrap( + embedding=embedding, + cat_offsets=cat_offsets, + cell_indices=cell_indices, + k=k, + n_bootstrap=n_bootstrap, + random_state=random_state, + ) + + # Compute final edistance for means and variances + edistance_means = cp.zeros((k, k), dtype=np.float32) + edistance_vars = cp.zeros((k, k), dtype=np.float32) + + for a in range(k): + for b in range(a + 1, k): + # Bootstrap mean edistance + edistance_means[a, b] = ( + 2 * pairwise_means_boot[a, b] + - pairwise_means_boot[a, a] + - pairwise_means_boot[b, b] + ) + edistance_means[b, a] = edistance_means[a, b] + + # Bootstrap variance edistance (using delta method approximation) + # Var(2*X - Y - Z) = 4*Var(X) + Var(Y) + Var(Z) (assuming independence) + edistance_vars[a, b] = ( + 4 * pairwise_vars_boot[a, b] + + pairwise_vars_boot[a, a] + + pairwise_vars_boot[b, b] + ) + edistance_vars[b, a] = edistance_vars[a, b] + + # 5. Create output DataFrames + + df_mean = pd.DataFrame( + edistance_means.get(), index=groups_list, columns=groups_list + ) + df_mean.index.name = groupby + df_mean.columns.name = groupby + df_mean.name = "pairwise edistance" + + df_var = pd.DataFrame(edistance_vars.get(), index=groups_list, columns=groups_list) + df_var.index.name = groupby + df_var.columns.name = groupby + df_var.name = "pairwise edistance variance" + + return df_mean, df_var + + +def _prepare_edistance_df( + embedding: cp.ndarray, + *, + cat_offsets: cp.ndarray, + cell_indices: cp.ndarray, + k: int, + groups_list: list[str], + groupby: str, +) -> pd.DataFrame: + # Compute means + pairwise_means = _pairwise_means(embedding, cat_offsets, cell_indices, k) + + # Compute final edistance: df[a,b] = 2*d_other[a,b] - d_itself[a] - d_itself[b] + edistance_matrix = cp.zeros((k, k), dtype=np.float32) + for a in range(k): + for b in range(a + 1, k): + edistance_matrix[a, b] = ( + 2 * pairwise_means[a, b] - pairwise_means[a, a] - pairwise_means[b, b] + ) + edistance_matrix[b, a] = edistance_matrix[a, b] + + # Create output DataFrame + df = pd.DataFrame(edistance_matrix.get(), index=groups_list, columns=groups_list) + df.index.name = groupby + df.columns.name = groupby + df.name = "pairwise edistance" + + return df diff --git a/src/rapids_singlecell/pertpy_gpu/_kernels/_edistance.py b/src/rapids_singlecell/pertpy_gpu/_kernels/_edistance.py new file mode 100644 index 00000000..d669c2da --- /dev/null +++ b/src/rapids_singlecell/pertpy_gpu/_kernels/_edistance.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from cuml.common.kernel_utils import cuda_kernel_factory + +_compute_group_distances_kernel = r""" +(const float* __restrict__ embedding, + const int* __restrict__ cat_offsets, + const int* __restrict__ cell_indices, + const int* __restrict__ pair_left, + const int* __restrict__ pair_right, + float* __restrict__ d_other, + int k, + int n_features) { + extern __shared__ float shared_sums[]; + + const int thread_id = threadIdx.x; + const int block_id = blockIdx.x; + const int block_size = blockDim.x; + + float local_sum = 0.0f; + + const int a = pair_left[block_id]; + const int b = pair_right[block_id]; + + const int start_a = cat_offsets[a]; + const int end_a = cat_offsets[a + 1]; + const int start_b = cat_offsets[b]; + const int end_b = cat_offsets[b + 1]; + + const int n_a = end_a - start_a; + const int n_b = end_b - start_b; + + for (int ia = start_a + thread_id; ia < end_a; ia += block_size) { + const int idx_i = cell_indices[ia]; + + for (int jb = start_b; jb < end_b; ++jb) { + const int idx_j = cell_indices[jb]; + + double dist_sq = 0.0; + for (int feat = 0; feat < n_features; ++feat) { + double diff = (double)embedding[idx_i * n_features + feat] - + (double)embedding[idx_j * n_features + feat]; + dist_sq += diff * diff; + } + + local_sum += (float)sqrt(dist_sq); + } + } + + shared_sums[thread_id] = local_sum; + __syncthreads(); + + for (int stride = block_size / 2; stride > 0; stride >>= 1) { + if (thread_id < stride) { + shared_sums[thread_id] += shared_sums[thread_id + stride]; + } + __syncthreads(); + } + + if (thread_id == 0) { + d_other[block_id] = shared_sums[0] / (float)(n_a * n_b); + } +} +""" + + +def get_compute_group_distances_kernel(): + return cuda_kernel_factory( + _compute_group_distances_kernel, + (), + "compute_group_distances", + ) diff --git a/src/rapids_singlecell/ptg.py b/src/rapids_singlecell/ptg.py new file mode 100644 index 00000000..2358f441 --- /dev/null +++ b/src/rapids_singlecell/ptg.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from .pertpy_gpu import * diff --git a/tests/pertpy/test_distances.py b/tests/pertpy/test_distances.py new file mode 100644 index 00000000..23a7a557 --- /dev/null +++ b/tests/pertpy/test_distances.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import cupy as cp +import numpy as np +import pandas as pd +import pytest +from anndata import AnnData + +from rapids_singlecell.pertpy_gpu._edistance import EDistanceResult, pertpy_edistance + + +@pytest.fixture +def small_adata() -> AnnData: + rng = np.random.default_rng(0) + n_groups = 3 + cells_per_group = 4 + n_features = 5 + total_cells = n_groups * cells_per_group + + cpu_embedding = rng.normal(size=(total_cells, n_features)).astype(np.float32) + groups = [f"g{idx}" for idx in range(n_groups) for _ in range(cells_per_group)] + obs = pd.DataFrame( + {"group": pd.Categorical(groups, categories=[f"g{i}" for i in range(n_groups)])} + ) + + adata = AnnData(cpu_embedding.copy(), obs=obs) + adata.obsm["X_pca"] = cp.asarray(cpu_embedding, dtype=cp.float32) + return adata + + +def _compute_cpu_reference( + adata: AnnData, obsm_key: str, group_key: str +) -> tuple[np.ndarray, np.ndarray]: + embedding = adata.obsm[obsm_key].get() + group_series = adata.obs[group_key] + categories = list(group_series.cat.categories) + k = len(categories) + + pair_means = np.zeros((k, k), dtype=np.float32) + + for i, gi in enumerate(categories): + idx_i = np.where(group_series == gi)[0] + for j, gj in enumerate(categories[i:], start=i): + idx_j = np.where(group_series == gj)[0] + if len(idx_i) == 0 or len(idx_j) == 0: + mean_distance = 0.0 + else: + distances = [] + for idx in idx_i: + diffs = embedding[idx] - embedding[idx_j] + distances.append(np.sqrt(np.sum(diffs**2, axis=1))) + stacked = np.concatenate(distances) + mean_distance = stacked.mean(dtype=np.float64) + pair_means[i, j] = pair_means[j, i] = np.float32(mean_distance) + + edistance = np.zeros((k, k), dtype=np.float32) + for i in range(k): + for j in range(i + 1, k): + value = 2 * pair_means[i, j] - pair_means[i, i] - pair_means[j, j] + edistance[i, j] = edistance[j, i] = np.float32(value) + + return pair_means, edistance + + +def test_pertpy_edistance_matches_cpu_reference(small_adata: AnnData) -> None: + result = pertpy_edistance(small_adata, groupby="group", obsm_key="X_pca") + + assert isinstance(result, EDistanceResult) + assert result.distances_var is None + + _, cpu_edistance = _compute_cpu_reference(small_adata, "X_pca", "group") + + assert result.distances.shape == cpu_edistance.shape + np.testing.assert_allclose(result.distances.values, cpu_edistance, atol=1e-5) + assert np.allclose(result.distances.values, result.distances.values.T) + + +def test_pertpy_edistance_inplace_populates_uns(small_adata: AnnData) -> None: + key = "group_pairwise_edistance" + result = pertpy_edistance( + small_adata, + groupby="group", + obsm_key="X_pca", + inplace=True, + ) + + assert isinstance(result, EDistanceResult) + assert key in small_adata.uns + stored = small_adata.uns[key] + assert set(stored.keys()) == {"distances", "distances_var"} + np.testing.assert_allclose(stored["distances"].values, result.distances.values) + assert stored["distances_var"] is None + + +def test_pertpy_edistance_bootstrap_returns_variance(small_adata: AnnData) -> None: + result = pertpy_edistance( + small_adata, + groupby="group", + obsm_key="X_pca", + bootstrap=True, + n_bootstrap=8, + random_state=11, + ) + + assert isinstance(result, EDistanceResult) + assert result.distances_var is not None + assert result.distances.shape == result.distances_var.shape + assert np.allclose(result.distances.values, result.distances.values.T) + assert np.allclose(result.distances_var.values, result.distances_var.values.T) + assert np.all(result.distances_var.values >= 0) + + +def test_pertpy_edistance_requires_categorical_obs(small_adata: AnnData) -> None: + bad = small_adata.copy() + bad.obs["group"] = bad.obs["group"].astype(str) + + with pytest.raises(TypeError): + pertpy_edistance(bad, groupby="group", obsm_key="X_pca") + + +@pytest.mark.parametrize("missing_key", ["missing", "other"]) +def test_pertpy_edistance_missing_group_raises( + small_adata: AnnData, missing_key: str +) -> None: + with pytest.raises(KeyError): + pertpy_edistance(small_adata, groupby=missing_key, obsm_key="X_pca") diff --git a/tmp_scripts/a.ipynb b/tmp_scripts/a.ipynb new file mode 100644 index 00000000..a1cb90b9 --- /dev/null +++ b/tmp_scripts/a.ipynb @@ -0,0 +1,1259 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 15, + "id": "e1d4de19", + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "\n", + "import os\n", + "\n", + "import anndata as ad\n", + "import cupy as cp\n", + "import pertpy as pt\n", + "import rmm\n", + "from rmm.allocators.cupy import rmm_cupy_allocator\n", + "\n", + "import rapids_singlecell as rsc\n", + "from rapids_singlecell.ptg import Distance\n", + "\n", + "rmm.reinitialize(\n", + " managed_memory=False, # Allows oversubscription\n", + " pool_allocator=True, # default is False\n", + " devices=0, # GPU device IDs to register. By default registers only GPU 0.\n", + ")\n", + "cp.cuda.set_allocator(rmm_cupy_allocator)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "e9c83bff", + "metadata": {}, + "outputs": [], + "source": [ + "save_dir = os.path.join(\n", + " os.path.expanduser(\"~\"), \"data\", \"adamson_2016_upr_epistasis_pca.h5ad\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "ea369599", + "metadata": {}, + "outputs": [], + "source": [ + "adata = ad.read_h5ad(save_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df4921a8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'IRE1_only_pMJ148',\n", + " '*',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " nan,\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " 'IRE1_only_pMJ148',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " nan,\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " nan,\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " nan,\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_only_pMJ145',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " nan,\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_only_pMJ145',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " nan,\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " nan,\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " 'IRE1_only_pMJ148',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " nan,\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " nan,\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'IRE1_only_pMJ148',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_only_pMJ146',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_only_pMJ145',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_pMJ150',\n", + " '*',\n", + " 'XBP1_pBA578',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'IRE1_only_pMJ148',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " nan,\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'IRE1_only_pMJ148',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " nan,\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'IRE1_only_pMJ148',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_pMJ150',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_only_pMJ146',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_only_pMJ146',\n", + " nan,\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_only_pMJ146',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " nan,\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_only_pMJ145',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_only_pMJ145',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_only_pMJ145',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'IRE1_only_pMJ148',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_PERK_pMJ150',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'PERK_IRE1_pMJ154',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'IRE1_only_pMJ148',\n", + " 'ATF6_IRE1_pMJ152',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'PERK_only_pMJ146',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_IRE1_pMJ154',\n", + " 'IRE1_only_pMJ148',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " '3x_neg_ctrl_pMJ144-2',\n", + " '3x_neg_ctrl_pMJ144-1',\n", + " 'PERK_only_pMJ146',\n", + " 'ATF6_PERK_IRE1_pMJ158',\n", + " 'ATF6_IRE1_pMJ152',\n", + " ...]" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "48beeb95", + "metadata": {}, + "outputs": [], + "source": [ + "rsc.get.anndata_to_GPU(adata, convert_all=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "553326ea", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/shadeform/rapids_singlecell/src/rapids_singlecell/pertpy_gpu/_distances.py:730: RuntimeWarning: Mean of empty slice.\n", + " sigma_Y = P[~idx, :][:, ~idx].mean()\n", + "/home/shadeform/miniforge3/envs/pertpy/lib/python3.13/site-packages/numpy/_core/_methods.py:145: RuntimeWarning: invalid value encountered in divide\n", + " ret = ret.dtype.type(ret / rcount)\n", + "/home/shadeform/rapids_singlecell/src/rapids_singlecell/pertpy_gpu/_distances.py:731: RuntimeWarning: Mean of empty slice.\n", + " delta = P[idx, :][:, ~idx].mean()\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 6.32 s, sys: 1.88 s, total: 8.2 s\n", + "Wall time: 8.19 s\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/shadeform/rapids_singlecell/src/rapids_singlecell/pertpy_gpu/_distances.py:729: RuntimeWarning: Mean of empty slice.\n", + " sigma_X = P[idx, :][:, idx].mean()\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "obs_key = \"perturbation\"\n", + "dist = Distance(obsm_key=\"X_pca\", metric=\"edistance\")\n", + "df = dist.pairwise(adata, groupby=obs_key)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "bd845994", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9423ba54408542468fc5e8eec76894de", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
/home/shadeform/miniforge3/envs/pertpy/lib/python3.13/site-packages/pertpy/tools/_distances/_distances.py:675: \n",
+       "RuntimeWarning: Mean of empty slice.\n",
+       "  sigma_Y = P[~idx, :][:, ~idx].mean()\n",
+       "
\n" + ], + "text/plain": [ + "/home/shadeform/miniforge3/envs/pertpy/lib/python3.13/site-packages/pertpy/tools/_distances/_distances.py:675: \n", + "RuntimeWarning: Mean of empty slice.\n", + " sigma_Y = P[~idx, :][:, ~idx].mean()\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
/home/shadeform/miniforge3/envs/pertpy/lib/python3.13/site-packages/numpy/_core/_methods.py:145: RuntimeWarning: \n",
+       "invalid value encountered in divide\n",
+       "  ret = ret.dtype.type(ret / rcount)\n",
+       "
\n" + ], + "text/plain": [ + "/home/shadeform/miniforge3/envs/pertpy/lib/python3.13/site-packages/numpy/_core/_methods.py:145: RuntimeWarning: \n", + "invalid value encountered in divide\n", + " ret = ret.dtype.type(ret / rcount)\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
/home/shadeform/miniforge3/envs/pertpy/lib/python3.13/site-packages/pertpy/tools/_distances/_distances.py:676: \n",
+       "RuntimeWarning: Mean of empty slice.\n",
+       "  delta = P[idx, :][:, ~idx].mean()\n",
+       "
\n" + ], + "text/plain": [ + "/home/shadeform/miniforge3/envs/pertpy/lib/python3.13/site-packages/pertpy/tools/_distances/_distances.py:676: \n", + "RuntimeWarning: Mean of empty slice.\n", + " delta = P[idx, :][:, ~idx].mean()\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
/home/shadeform/miniforge3/envs/pertpy/lib/python3.13/site-packages/pertpy/tools/_distances/_distances.py:674: \n",
+       "RuntimeWarning: Mean of empty slice.\n",
+       "  sigma_X = P[idx, :][:, idx].mean()\n",
+       "
\n" + ], + "text/plain": [ + "/home/shadeform/miniforge3/envs/pertpy/lib/python3.13/site-packages/pertpy/tools/_distances/_distances.py:674: \n", + "RuntimeWarning: Mean of empty slice.\n", + " sigma_X = P[idx, :][:, idx].mean()\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "dist = pt.tl.Distance(obsm_key=\"X_pca\", metric=\"edistance\")\n",
+    "df = dist.pairwise(adata, groupby=obs_key)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e92bee29",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "pertpy",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.13.7"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/tmp_scripts/compute_edistance.py b/tmp_scripts/compute_edistance.py
new file mode 100644
index 00000000..e325e7a8
--- /dev/null
+++ b/tmp_scripts/compute_edistance.py
@@ -0,0 +1,37 @@
+from __future__ import annotations
+
+import os
+import time
+from pathlib import Path
+
+import anndata as ad
+import cupy as cp
+import rmm
+from rmm.allocators.cupy import rmm_cupy_allocator
+
+import rapids_singlecell as rsc
+from rapids_singlecell.ptg import Distance
+
+rmm.reinitialize(
+    managed_memory=False,  # Allows oversubscription
+    pool_allocator=True,  # default is False
+    devices=0,  # GPU device IDs to register. By default registers only GPU 0.
+)
+cp.cuda.set_allocator(rmm_cupy_allocator)
+
+
+if __name__ == "__main__":
+    obs_key = "perturbation"
+
+    # homedir/data/adamson_2016_upr_epistasis
+    save_dir = os.path.join(
+        os.path.expanduser("~"),
+        "data",
+    )
+    adata = ad.read_h5ad(Path(save_dir) / "adamson_2016_upr_epistasis_pca.h5ad")
+    rsc.get.anndata_to_GPU(adata, convert_all=True)
+    dist = Distance(obsm_key="X_pca", metric="edistance")
+    start_time = time.time()
+    df = dist.pairwise(adata, groupby=obs_key)
+    end_time = time.time()
+    print(f"Time taken: {end_time - start_time} seconds")
diff --git a/tmp_scripts/compute_edistance_cpu.py b/tmp_scripts/compute_edistance_cpu.py
new file mode 100644
index 00000000..694224d1
--- /dev/null
+++ b/tmp_scripts/compute_edistance_cpu.py
@@ -0,0 +1,38 @@
+from __future__ import annotations
+
+import os
+import time
+from argparse import ArgumentParser
+from pathlib import Path
+
+import anndata as ad
+from pertpy.tools import Distance
+
+if __name__ == "__main__":
+    parser = ArgumentParser()
+    parser.add_argument("--bootstrap", action="store_true")
+    args = parser.parse_args()
+    obs_key = "perturbation"
+    bootstrap = args.bootstrap
+
+    # homedir/data/adamson_2016_upr_epistasis
+    save_dir = os.path.join(
+        os.path.expanduser("~"),
+        "data",
+    )
+    adata = ad.read_h5ad(Path(save_dir) / "adamson_2016_upr_epistasis_pca.h5ad")
+    dist = Distance(obsm_key="X_pca", metric="edistance")
+    start_time = time.time()
+    if bootstrap:
+        df, df_var = dist.pairwise(
+            adata, groupby=obs_key, bootstrap=True, n_bootstrap=100
+        )
+    else:
+        df = dist.pairwise(adata, groupby=obs_key)
+    end_time = time.time()
+    print(f"Time taken: {end_time - start_time} seconds")
+    if bootstrap:
+        df_var.to_csv(Path(save_dir) / "df_cpu_bootstrap_var.csv")
+        df.to_csv(Path(save_dir) / "df_cpu_bootstrap.csv")
+    else:
+        df.to_csv(Path(save_dir) / "df_cpu.csv")
diff --git a/tmp_scripts/compute_edistance_standalone.py b/tmp_scripts/compute_edistance_standalone.py
new file mode 100644
index 00000000..ef1d12dd
--- /dev/null
+++ b/tmp_scripts/compute_edistance_standalone.py
@@ -0,0 +1,109 @@
+from __future__ import annotations
+
+import os
+import time
+from argparse import ArgumentParser
+from pathlib import Path
+
+import anndata as ad
+import cupy as cp
+import numpy as np
+import pandas as pd
+import rmm
+from rmm.allocators.cupy import rmm_cupy_allocator
+
+import rapids_singlecell as rsc
+from rapids_singlecell.pertpy_gpu._edistance import pertpy_edistance
+
+rmm.reinitialize(
+    managed_memory=False,  # Allows oversubscription
+    pool_allocator=True,  # default is False
+    devices=0,  # GPU device IDs to register. By default registers only GPU 0.
+)
+cp.cuda.set_allocator(rmm_cupy_allocator)
+
+if __name__ == "__main__":
+    parser = ArgumentParser()
+    parser.add_argument("--bootstrap", action="store_true")
+    args = parser.parse_args()
+    bootstrap = args.bootstrap
+    obs_key = "perturbation"
+    home_dir = Path(os.path.expanduser("~")) / "data"
+
+    # homedir/data/adamson_2016_upr_epistasis
+    save_dir = home_dir / "adamson_2016_upr_epistasis_pca.h5ad"
+    adata = ad.read_h5ad(save_dir)
+    rsc.get.anndata_to_GPU(adata, convert_all=True)
+
+    df_expected = None
+    if bootstrap:
+        df_cpu_bootstrap_var = pd.read_csv(
+            home_dir / "df_cpu_bootstrap_var.csv", index_col=0
+        )
+        df_cpu_bootstrap = pd.read_csv(home_dir / "df_cpu_bootstrap.csv", index_col=0)
+        df_expected = df_cpu_bootstrap
+    else:
+        df_cpu_float64 = pd.read_csv(home_dir / "df_cpu_float64.csv", index_col=0)
+        df_expected = df_cpu_float64
+
+    start_time = time.time()
+    if not bootstrap:
+        res = pertpy_edistance(
+            adata, groupby=obs_key, obsm_key="X_pca", bootstrap=bootstrap
+        )
+        df_gpu = res.distances
+    else:
+        res = pertpy_edistance(
+            adata,
+            groupby=obs_key,
+            obsm_key="X_pca",
+            bootstrap=bootstrap,
+            n_bootstrap=100,
+        )
+        df_gpu = res.distances
+        df_gpu_var = res.distances_var
+    end_time = time.time()
+    print(f"Time taken: {end_time - start_time} seconds")
+
+    is_not_close = []
+    groups = adata.obs[obs_key].unique()
+    k = len(groups)
+    atol = 1e-8 if not bootstrap else 1e-2
+    for idx1 in range(k):
+        for idx2 in range(idx1 + 1, k):
+            group_x = groups[idx1]
+            group_y = groups[idx2]
+            if group_x == group_y:
+                assert df_gpu.loc[group_x, group_y] == 0
+            else:
+                if not np.isclose(
+                    df_gpu.loc[group_x, group_y],
+                    df_expected.loc[group_x, group_y],
+                    atol=atol,
+                ):
+                    is_not_close.append(
+                        (
+                            (group_x, group_y),
+                            df_expected.loc[group_x, group_y],
+                            df_gpu.loc[group_x, group_y],
+                            np.abs(
+                                df_expected.loc[group_x, group_y]
+                                - df_gpu.loc[group_x, group_y]
+                            ),
+                        )
+                    )
+                    print(
+                        f"Group df_gpu: {df_gpu.loc[group_x, group_y]}, Group df: {df_expected.loc[group_x, group_y]}, idx: ({idx1}, {idx2})"
+                    )
+
+    print(
+        "Out of",
+        int(k * (k - 1) / 2),
+        "pairs,",
+        len(is_not_close),
+        "pairs are not close with atol=",
+        atol,
+    )
+    # print(df.equals(df_gpu))
+    # print(df)
+    # print(df_gpu)
diff --git a/tmp_scripts/generate_float64_reference.py b/tmp_scripts/generate_float64_reference.py
new file mode 100644
index 00000000..e3751137
--- /dev/null
+++ b/tmp_scripts/generate_float64_reference.py
@@ -0,0 +1,93 @@
+from __future__ import annotations
+
+import os
+
+import anndata as ad
+import numpy as np
+import pandas as pd
+from sklearn.metrics import pairwise_distances as sklearn_pairwise_distances
+
+
+def compute_edistance_sklearn(X, Y, dtype=np.float64):
+    """Compute edistance using sklearn's pairwise_distances with specified precision"""
+    X = np.array(X, dtype=dtype)
+    Y = np.array(Y, dtype=dtype)
+
+    # Compute pairwise distances using sklearn
+    sigma_X = sklearn_pairwise_distances(X, X, metric="euclidean").mean()
+    sigma_Y = sklearn_pairwise_distances(Y, Y, metric="euclidean").mean()
+    delta = sklearn_pairwise_distances(X, Y, metric="euclidean").mean()
+
+    return 2 * delta - sigma_X - sigma_Y
+
+
+def compute_edistance_pairwise_sklearn(
+    adata, groupby, obsm_key="X_pca", dtype=np.float64
+):
+    """Compute pairwise edistance matrix using sklearn with specified precision"""
+    # Get data and convert to CPU numpy
+    embedding = np.array(adata.obsm[obsm_key], dtype=dtype)
+
+    groups = adata.obs[groupby].cat.categories
+    k = len(groups)
+
+    print(f"Computing edistance for {k} groups with dtype {dtype}...")
+
+    # Build edistance matrix
+    edistance_matrix = np.zeros((k, k), dtype=dtype)
+
+    for i, group_a in enumerate(groups):
+        mask_a = adata.obs[groupby] == group_a
+        X = embedding[mask_a]
+
+        for j, group_b in enumerate(groups):
+            if i == j:
+                edistance_matrix[i, j] = 0.0
+            elif i < j:  # Only compute upper triangle
+                mask_b = adata.obs[groupby] == group_b
+                Y = embedding[mask_b]
+
+                edist = compute_edistance_sklearn(X, Y, dtype=dtype)
+                edistance_matrix[i, j] = edist
+                edistance_matrix[j, i] = edist  # Symmetric
+
+        if (i + 1) % 5 == 0:
+            print(f"  Processed {i + 1}/{k} groups")
+
+    return pd.DataFrame(edistance_matrix, index=groups, columns=groups)
+
+
+if __name__ == "__main__":
+    obs_key = "perturbation"
+
+    # Load the data
+    save_dir = os.path.join(os.path.expanduser("~"), "data")
+    adata_path = os.path.join(save_dir, "adamson_2016_upr_epistasis_pca.h5ad")
+
+    print(f"Loading data from {adata_path}...")
+    adata = ad.read_h5ad(adata_path)
+
+    print(f"Data shape: {adata.shape}")
+    print(f"Groups: {len(adata.obs[obs_key].cat.categories)}")
+
+    # Generate the float64 reference CSV using sklearn
+    print("\nGenerating float64 reference using sklearn...")
+    df_reference = compute_edistance_pairwise_sklearn(
+        adata, obs_key, obsm_key="X_pca", dtype=np.float64
+    )
+
+    # Save the float64 reference
+    output_path = os.path.join(save_dir, "df_cpu_float64.csv")
+    df_reference.to_csv(output_path)
+    print(f"Saved float64 reference to: {output_path}")
+
+    # Show a sample of values
+    print("\nSample values:")
+    groups = df_reference.index[:3]
+    for i, group_a in enumerate(groups):
+        for j, group_b in enumerate(groups):
+            if i < j:
+                val = df_reference.loc[group_a, group_b]
+                print(f"  {group_a} vs {group_b}: {val:.10f}")
+
+    print("Float64 reference generation complete!")
diff --git a/tmp_scripts/prepare_data.py b/tmp_scripts/prepare_data.py
new file mode 100644
index 00000000..2557eeb8
--- /dev/null
+++ b/tmp_scripts/prepare_data.py
@@ -0,0 +1,26 @@
+from __future__ import annotations
+
+import os
+
+import pertpy as pt
+import scanpy as sc
+
+import rapids_singlecell as rsc
+
+if __name__ == "__main__":
+    adata = pt.data.adamson_2016_upr_epistasis()
+    obs_key = "perturbation"
+
+    # remove genes with 0 expression
+    sc.pp.filter_genes(adata, min_counts=1)
+    sc.pp.filter_cells(adata, min_counts=1)
+    # fill na obskeys
+    # set categories first
+    adata.obs[obs_key] = adata.obs[obs_key].cat.add_categories("control")
+    adata.obs[obs_key] = adata.obs[obs_key].fillna("control")
+    rsc.pp.pca(adata, n_comps=50)
+    # save dir as
+    # homedir/data/adamson_2016_upr_epistasis
+    save_dir = os.path.join(os.path.expanduser("~"), "data")
+    os.makedirs(save_dir, exist_ok=True)
+    adata.write(os.path.join(save_dir, "adamson_2016_upr_epistasis_pca.h5ad"))