@@ -35,50 +35,63 @@ def main():
3535
3636 # Input
3737 parser .add_argument ("--input" , "-i" , required = True ,
38- help = "Input UMI count matrix (.csv, .tsv, .txt, or .mtx)" )
38+ help = "[required] Input UMI count matrix (.csv, .tsv, .txt, or .mtx)" )
3939 parser .add_argument ("--genes" , default = None ,
40- help = "Gene names file (required for .mtx input)" )
40+ help = "[optional] Gene names file (required for .mtx input)" )
4141 parser .add_argument ("--barcodes" , default = None ,
42- help = "Barcode names file (required for .mtx input)" )
43-
42+ help = "[optional] Barcode names file (required for .mtx input)" )
43+
4444 # Parameters
4545 parser .add_argument ("--id-type" , default = "S" , choices = ["S" , "E" ],
46- help = "Gene ID type: S=Symbol, E=Ensembl (default: S)" )
46+ help = "[optional] Gene ID type: S=Symbol, E=Ensembl (default: S)" )
4747 parser .add_argument ("--cell-line" , default = "no" , choices = ["yes" , "no" ],
48- help = "Pure cell line mode (default: no)" )
48+ help = "[optional] Pure cell line mode (default: no)" )
4949 parser .add_argument ("--ngene-chr" , type = int , default = 5 ,
50- help = "Min genes per chromosome (default: 5)" )
50+ help = "[optional] Min genes per chromosome (default: 5)" )
5151 parser .add_argument ("--min-genes" , type = int , default = 200 ,
52- help = "Min genes per cell (default: 200)" )
52+ help = "[optional] Min genes per cell (default: 200)" )
5353 parser .add_argument ("--low-dr" , type = float , default = 0.05 ,
54- help = "Min detection rate for smoothing (default: 0.05)" )
54+ help = "[optional] Min detection rate for smoothing (default: 0.05)" )
5555 parser .add_argument ("--up-dr" , type = float , default = 0.1 ,
56- help = "Min detection rate for segmentation (default: 0.1)" )
56+ help = "[optional] Min detection rate for segmentation (default: 0.1)" )
5757 parser .add_argument ("--win-size" , type = int , default = 25 ,
58- help = "Window size for segmentation (default: 25)" )
58+ help = "[optional] Window size for segmentation (default: 25)" )
5959 parser .add_argument ("--norm-cells" , default = "" ,
60- help = "File with known normal cell barcodes (one per line)" )
60+ help = "[optional] File with known normal cell barcodes (one per line)" )
6161 parser .add_argument ("--ks-cut" , type = float , default = 0.1 ,
62- help = "KS test cutoff for breakpoints (default: 0.1)" )
62+ help = "[optional] KS test cutoff for breakpoints (default: 0.1)" )
6363 parser .add_argument ("--sample-name" , default = "" ,
64- help = "Sample name prefix for output files" )
64+ help = "[optional] Sample name prefix for output files" )
6565 parser .add_argument ("--distance" , default = "euclidean" ,
6666 choices = ["euclidean" , "pearson" , "spearman" ],
67- help = "Distance metric for clustering (default: euclidean)" )
67+ help = "[optional] Distance metric for clustering (default: euclidean)" )
6868 parser .add_argument ("--output-seg" , action = "store_true" ,
69- help = "Output .seg file for IGV" )
69+ help = "[optional] Output .seg file for IGV" )
7070 parser .add_argument ("--plot-genes" , dest = "plot_genes" , action = "store_true" , default = True ,
71- help = "Generate the heatmap plot (default: enabled)" )
71+ help = "[optional] Generate the heatmap plot (default: enabled)" )
7272 parser .add_argument ("--no-plot-genes" , dest = "plot_genes" , action = "store_false" ,
73- help = "Skip heatmap plotting for faster runs" )
73+ help = "[optional] Skip heatmap plotting for faster runs" )
7474 parser .add_argument ("--genome" , default = "hg20" , choices = ["hg20" , "mm10" ],
75- help = "Genome assembly (default: hg20)" )
75+ help = "[optional] Genome assembly (default: hg20)" )
7676 parser .add_argument ("--n-cores" , type = int , default = 1 ,
77- help = "Number of CPU cores (default: 1)" )
77+ help = "[optional] Number of CPU cores (default: 1)" )
7878 parser .add_argument ("--pca-components" , type = int , default = None ,
79- help = "Adaptive PCA component cap for large clustering steps (default: automatic by cell count)" )
79+ help = "[optional] Adaptive PCA component cap for large clustering steps (default: automatic by cell count)" )
8080 parser .add_argument ("--output-dir" , "-o" , default = "." ,
81- help = "Output directory (default: current)" )
81+ help = "[optional] Output directory (default: current)" )
82+
83+ # Annotated heatmap options
84+ parser .add_argument ("--meta" , default = None ,
85+ metavar = "CSV" ,
86+ help = "[optional] Per-cell annotation CSV for the annotated heatmap. "
87+ "First column = cell name; remaining columns are drawn "
88+ "as coloured sidebars. Header is auto-detected. "
89+ "When provided, an additional annotated heatmap is saved "
90+ "alongside the standard one." )
91+ parser .add_argument ("--row-split" , default = None ,
92+ metavar = "COLUMN" ,
93+ help = "[optional] Column in --meta used to split and label heatmap rows. "
94+ "Defaults to the second column of the CSV when not given." )
8295
8396 args = parser .parse_args ()
8497
@@ -178,6 +191,26 @@ def main():
178191 pred = result ["prediction" ]["copykat.pred" ].value_counts ()
179192 for k , v in pred .items ():
180193 print (f" { k } : { v } cells" )
194+
195+ if args .meta and args .plot_genes and "CNAmat" in result :
196+ print ("\n Generating annotated heatmap..." )
197+ from copykat_py .plotting import plot_heatmap_annotated
198+ cna_df = result ["CNAmat" ]
199+ ann_mat = cna_df .iloc [:, 3 :].values .astype (np .float32 )
200+ ann_cell_names = cna_df .columns [3 :].tolist ()
201+ ann_chrom_info = cna_df .iloc [:, 0 ].values
202+ ann_output = f"{ args .sample_name } _copykat_annotated_heatmap.png"
203+ plot_heatmap_annotated (
204+ mat = ann_mat ,
205+ cell_names = ann_cell_names ,
206+ chrom_info = ann_chrom_info ,
207+ meta_csv = args .meta ,
208+ row_split_col = args .row_split ,
209+ sample_name = args .sample_name ,
210+ distance = args .distance ,
211+ n_cores = args .n_cores ,
212+ output_path = ann_output ,
213+ )
181214 finally :
182215 print (f"CopyKAT-Py run finished: { time .strftime ('%Y-%m-%d %H:%M:%S' )} " )
183216 print (f"Detailed log saved to: { log_path } " )
@@ -188,5 +221,106 @@ def main():
188221 log_handle .close ()
189222
190223
224+ def plot_main ():
225+ """Entry point for ``copykat-py-plot``: annotated heatmap from CNA results."""
226+ parser = argparse .ArgumentParser (
227+ prog = "copykat-py-plot" ,
228+ description = (
229+ "Draw an annotated CNA heatmap from a copykat-py CNA results file "
230+ "and a per-cell metadata CSV. Rows are split and colour-labelled by "
231+ "a chosen metadata column; cells within each group are ordered by "
232+ "hierarchical or K-means clustering so intra-group CNA structure is "
233+ "preserved."
234+ ),
235+ formatter_class = argparse .RawDescriptionHelpFormatter ,
236+ epilog = """
237+ Examples
238+ --------
239+ # Use second column (leiden_cluster) as row-split (default):
240+ copykat-py-plot \\
241+ --cna sample_copykat_CNA_results.txt \\
242+ --meta meta.csv
243+
244+ # Explicitly split rows by inferred_CellType and also annotate leiden_cluster:
245+ copykat-py-plot \\
246+ --cna sample_copykat_CNA_results.txt \\
247+ --meta xenium_ft_full_meta_celltype_leiden.csv \\
248+ --row-split inferred_CellType \\
249+ --sample-name xenium_all_cells \\
250+ --n-cores 40 \\
251+ --output xenium_annotated_heatmap.png
252+
253+ Meta CSV format
254+ ---------------
255+ First column : cell name (must match column names in the CNA results file)
256+ Remaining : any metadata (cell type, cluster, condition, …)
257+
258+ With header: cell_name,leiden_cluster,inferred_CellType
259+ Without header: aaaajgij-1,5,lumhr (first row treated as data)
260+ """ ,
261+ )
262+
263+ parser .add_argument (
264+ "--cna" , "-c" , required = True ,
265+ help = "[required] CNA results file produced by copykat-py "
266+ "(*_copykat_CNA_results.txt, tab-separated). "
267+ "First three columns must be chrom / chrompos / abspos; "
268+ "remaining columns are cells." ,
269+ )
270+ parser .add_argument (
271+ "--meta" , "-m" , required = True ,
272+ help = "[required] Annotation CSV. First column = cell name; remaining columns are "
273+ "drawn as coloured sidebars. Header row is auto-detected." ,
274+ )
275+ parser .add_argument (
276+ "--row-split" , default = None ,
277+ metavar = "COLUMN" ,
278+ help = "[optional] Metadata column used to split and label rows. "
279+ "Defaults to the second column of the CSV when not supplied." ,
280+ )
281+ parser .add_argument (
282+ "--sample-name" , default = "" ,
283+ help = "[optional] Label shown in the figure title and used as the output filename "
284+ "prefix when --output is not given." ,
285+ )
286+ parser .add_argument (
287+ "--distance" , default = "euclidean" ,
288+ choices = ["euclidean" , "pearson" , "spearman" ],
289+ help = "[optional] Distance metric for within-group cell clustering (default: euclidean)." ,
290+ )
291+ parser .add_argument (
292+ "--n-cores" , type = int , default = 1 ,
293+ help = "[optional] Parallel threads for clustering (default: 1)." ,
294+ )
295+ parser .add_argument (
296+ "--output" , "-o" , default = None ,
297+ metavar = "PATH" ,
298+ help = "[optional] Output PNG path. Defaults to "
299+ "{sample_name}_copykat_annotated_heatmap.png." ,
300+ )
301+
302+ args = parser .parse_args ()
303+
304+ print (f"Loading CNA results: { args .cna } " )
305+ cna_df = pd .read_csv (args .cna , sep = "\t " , index_col = False )
306+ cell_names = cna_df .columns [3 :].tolist ()
307+ chrom_info = cna_df .iloc [:, 0 ].values
308+ mat = cna_df .iloc [:, 3 :].values .astype (np .float32 )
309+ print (f" { mat .shape [1 ]} cells × { mat .shape [0 ]} bins" )
310+
311+ from copykat_py .plotting import plot_heatmap_annotated
312+ plot_heatmap_annotated (
313+ mat = mat ,
314+ cell_names = cell_names ,
315+ chrom_info = chrom_info ,
316+ meta_csv = args .meta ,
317+ row_split_col = args .row_split ,
318+ sample_name = args .sample_name ,
319+ distance = args .distance ,
320+ n_cores = args .n_cores ,
321+ output_path = args .output ,
322+ )
323+
324+
191325if __name__ == "__main__" :
192326 main ()
0 commit comments