1515import cellranger .sgt as cr_sgt
1616import cellranger .stats as cr_stats
1717from cellranger .analysis .diffexp import adjust_pvalue_bh
18- from cellranger .chemistry import CHEMISTRY_DESCRIPTION_FIELD , CHEMISTRY_SC3P_LT
18+ from cellranger .chemistry import (
19+ CHEMISTRY_DESCRIPTION_FIELD ,
20+ CHEMISTRY_SC3P_LT ,
21+ SC3P_V4_CHEMISTRIES ,
22+ SC5P_V3_CHEMISTRIES ,
23+ )
1924
2025# Drop this top fraction of the barcodes when estimating ambient.
2126MAX_OCCUPIED_PARTITIONS_FRAC = 0.5
2227
28+ # Minimum number of UMIs for a barcode to be called as a cell
29+ MIN_GLOBAL_UMIS = 0
30+
31+ # Maximum percentage of mitochondrial reads allowed for a barcode to be called a cell
32+ MAX_MITO_PCT = 100.0
33+
2334# Minimum number of UMIS per barcode to consider after the initial cell calling
2435MIN_UMIS = 500
2536
2637# Default number of background simulations to make
2738NUM_SIMS = 10000
2839
29- # Minimum ratio of UMIs to the median (initial cell call UMI) to consider after the initial cell calling
30- MIN_UMI_FRAC_OF_MEDIAN = 0.01
31-
32- # Maximum adjusted p-value to call a barcode as non-ambient
33- MAX_ADJ_PVALUE = 0.01
34-
3540# Minimum number of UMIS per barcode to consider after the initial cell calling for targeted GEX
3641TARGETED_CC_MIN_UMIS_ADDITIONAL_CELLS = 10
3742
@@ -103,6 +108,15 @@ class NonAmbientBarcodeResult(NamedTuple):
103108 pvalues : np .ndarray # pvalues (n)
104109 pvalues_adj : np .ndarray # B-H adjusted pvalues (n)
105110 is_nonambient : np .ndarray # Boolean nonambient calls (n)
111+ emptydrops_minimum_umis : int # Min UMI threshold for empty drops (1)
112+
113+
114+ def get_empty_drops_fdr (chemistry_description : str ) -> float :
115+ """Gets the maximum adjusted p-value to call a barcode as non-ambient."""
116+ # The chips used with V4 have roughly double the GEMs as the older V3 chips
117+ v4_chemistries = SC3P_V4_CHEMISTRIES + SC5P_V3_CHEMISTRIES
118+ v4_chem_names = [chem [CHEMISTRY_DESCRIPTION_FIELD ] for chem in v4_chemistries ]
119+ return 0.001 if chemistry_description in v4_chem_names else 0.01
106120
107121
108122def get_empty_drops_range (chemistry_description : str , num_probe_bcs : int | None ) -> tuple [int , int ]:
@@ -115,8 +129,13 @@ def get_empty_drops_range(chemistry_description: str, num_probe_bcs: int | None)
115129 low_index:
116130 high_index:
117131 """
132+ # The chips used with V4 have roughly double the GEMs as the older V3 chips
133+ v4_chemistries = SC3P_V4_CHEMISTRIES + SC5P_V3_CHEMISTRIES
134+ v4_chem_names = [chem [CHEMISTRY_DESCRIPTION_FIELD ] for chem in v4_chemistries ]
118135 if chemistry_description == CHEMISTRY_SC3P_LT [CHEMISTRY_DESCRIPTION_FIELD ]:
119136 N_PARTITIONS = 9000
137+ elif chemistry_description in v4_chem_names :
138+ N_PARTITIONS = 80000 * num_probe_bcs if num_probe_bcs and num_probe_bcs > 1 else 160000
120139 else :
121140 N_PARTITIONS = 45000 * num_probe_bcs if num_probe_bcs and num_probe_bcs > 1 else 90000
122141 return (N_PARTITIONS // 2 , N_PARTITIONS )
@@ -128,9 +147,7 @@ def find_nonambient_barcodes(
128147 chemistry_description ,
129148 num_probe_bcs ,
130149 * ,
131- min_umi_frac_of_median = MIN_UMI_FRAC_OF_MEDIAN ,
132150 emptydrops_minimum_umis = MIN_UMIS ,
133- max_adj_pvalue = MAX_ADJ_PVALUE ,
134151 num_sims = NUM_SIMS ,
135152):
136153 """Call barcodes as being sufficiently distinct from the ambient profile.
@@ -148,6 +165,7 @@ def find_nonambient_barcodes(
148165 bc_order = np .argsort (umis_per_bc )
149166
150167 low , high = get_empty_drops_range (chemistry_description , num_probe_bcs )
168+ max_adj_pvalue = get_empty_drops_fdr (chemistry_description )
151169
152170 # Take what we expect to be the barcodes associated w/ empty partitions.
153171 print (f"Range empty barcodes: { low } - { high } " )
@@ -186,14 +204,11 @@ def find_nonambient_barcodes(
186204 eval_bcs = np .ma .array (np .arange (matrix .bcs_dim ))
187205 eval_bcs [orig_cells ] = ma .masked
188206
189- median_initial_umis = np .median (umis_per_bc [orig_cells ])
190- min_umis = int (
191- max (emptydrops_minimum_umis , np .ceil (median_initial_umis * min_umi_frac_of_median ))
192- )
193- print (f"Median UMIs of initial cell calls: { median_initial_umis } " )
194- print (f"Min UMIs: { min_umis } " )
207+ max_background_umis = np .max (umis_per_bc [empty_bcs ], initial = 0 )
208+ emptydrops_minimum_umis = max (emptydrops_minimum_umis , 1 + max_background_umis )
209+ print (f"Max background UMIs: { max_background_umis } " )
195210
196- eval_bcs [umis_per_bc < min_umis ] = ma .masked
211+ eval_bcs [umis_per_bc < emptydrops_minimum_umis ] = ma .masked
197212 n_unmasked_bcs = len (eval_bcs ) - eval_bcs .mask .sum ()
198213
199214 eval_bcs = np .argsort (ma .masked_array (umis_per_bc , mask = eval_bcs .mask ))[0 :n_unmasked_bcs ]
@@ -208,6 +223,7 @@ def find_nonambient_barcodes(
208223 return None
209224
210225 assert not np .any (np .isin (eval_bcs , orig_cells ))
226+ assert not np .any (np .isin (eval_bcs , empty_bcs ))
211227 print (f"Number of candidate bcs: { len (eval_bcs )} " )
212228 print (f"Range candidate bc umis: { umis_per_bc [eval_bcs ].min ()} , { umis_per_bc [eval_bcs ].max ()} " )
213229
@@ -234,6 +250,7 @@ def find_nonambient_barcodes(
234250
235251 pvalues_adj = adjust_pvalue_bh (pvalues )
236252
253+ print (f"Max adjusted P-value: { max_adj_pvalue } " )
237254 is_nonambient = pvalues_adj <= max_adj_pvalue
238255
239256 return NonAmbientBarcodeResult (
@@ -242,4 +259,5 @@ def find_nonambient_barcodes(
242259 pvalues = pvalues ,
243260 pvalues_adj = pvalues_adj ,
244261 is_nonambient = is_nonambient ,
262+ emptydrops_minimum_umis = emptydrops_minimum_umis ,
245263 )
0 commit comments