dir_fan <- path(tempdir(), "naming-fanout")
fan <- Tool1$new(path = system.file("extdata/tool1_outputs", package = "nemo"))
fan$config$get_table_outputs("table7")
#> [1] "table7main" "table7dp"
fan$run(output_dir = dir_fan, format = "parquet", input_id = "input1")
sort(basename(fan$written_files$outpath))
#> [1] "sampleA_2_tool1_table7dp.parquet" "sampleA_2_tool1_table7main.parquet"
#> [3] "sampleA_tool1_table7dp.parquet" "sampleA_tool1_table7main.parquet"Repeat runs of the same sample produce identical file names in different folders. Below: how tidy outputs avoid overwriting each other, and how to keep runs distinguishable after merging.
Anatomy of a tidy filename
{output_dir}/{prefix}_{tool}_{table}.{ext}
-
{output_dir}: flat output directory -
{prefix}: input basename minus the schemapattern(sampleA.tool1.table1.tsv=>sampleA) -
{tool}_{table}: the schema table that matched the file, e.g.tool1_table1; for a file that fans out, one file per declared output instead (see below) -
{ext}: fromformat(parquet, db, tsv, csv or rds)
sampleA.tool1.table1.tsv => sampleA_tool1_table1.parquet.
One file, several tables
A file can fan out into several tables, declared under its schema table’s outputs:. Each output is written as {prefix}_{tool}_{output}: table7 splits into table7main (read counts) and table7dp (long-form depth coverage):
The two table7 files (latest, v1.0.0) share basename sampleA, hence sampleA and sampleA_2, as in the repeated-sample case below.
A repeated sample
Three runs, same input files:
src <- system.file("extdata/tool1/latest", package = "nemo")
dir1 <- path(tempdir(), "naming-demo")
dir_runs <- path(dir1, "runs")
run_ids <- c("run1", "run2", "run3")
for (r in run_ids) {
dest <- dir_create(path(dir_runs, r))
file_copy(dir_ls(src, regexp = "table[12]\\.tsv$"), dest, overwrite = TRUE)
}
dir_tree(dir_runs)
#> /tmp/RtmpHGTPiu/naming-demo/runs
#> ├── run1
#> │ ├── sampleA.tool1.table1.tsv
#> │ └── sampleA.tool1.table2.tsv
#> ├── run2
#> │ ├── sampleA.tool1.table1.tsv
#> │ └── sampleA.tool1.table2.tsv
#> └── run3
#> ├── sampleA.tool1.table1.tsv
#> └── sampleA.tool1.table2.tsvMode A: one recursive tidy
Point Tool1 at the parent folder. Colliding prefixes get _2/_3:
tool <- Tool1$new(path = dir_runs)
tool$list_files() |>
dplyr::select(path, prefix, parser) |>
dplyr::arrange(parser)
#> # A tibble: 6 × 3
#> path prefix parser
#> <chr> <chr> <chr>
#> 1 /tmp/RtmpHGTPiu/naming-demo/runs/run1/sampleA.tool1.table1.tsv sampleA table1
#> 2 /tmp/RtmpHGTPiu/naming-demo/runs/run2/sampleA.tool1.table1.tsv sampleA… table1
#> 3 /tmp/RtmpHGTPiu/naming-demo/runs/run3/sampleA.tool1.table1.tsv sampleA… table1
#> 4 /tmp/RtmpHGTPiu/naming-demo/runs/run1/sampleA.tool1.table2.tsv sampleA table2
#> 5 /tmp/RtmpHGTPiu/naming-demo/runs/run2/sampleA.tool1.table2.tsv sampleA… table2
#> 6 /tmp/RtmpHGTPiu/naming-demo/runs/run3/sampleA.tool1.table2.tsv sampleA… table2No overwrites:
dir_outA <- dir_create(path(dir1, "outA"))
tool$run(
input_id = "input1",
output_dir = dir_outA,
format = "parquet",
prefix_include = TRUE
)
dir_tree(dir_outA)
#> /tmp/RtmpHGTPiu/naming-demo/outA
#> ├── metadata_tool1.parquet
#> ├── sampleA_2_tool1_table1.parquet
#> ├── sampleA_2_tool1_table2.parquet
#> ├── sampleA_3_tool1_table1.parquet
#> ├── sampleA_3_tool1_table2.parquet
#> ├── sampleA_tool1_table1.parquet
#> └── sampleA_tool1_table2.parquetprefix_include = TRUE adds an input_prefix column, so runs stay distinct after stacking:
dir_ls(dir_outA, regexp = "tool1_table1\\.parquet") |>
purrr::map(arrow::read_parquet) |>
purrr::list_rbind()
#> # A tibble: 9 × 8
#> input_id input_prefix sample_id chromosome start end metric_y metric_z
#> <chr> <chr> <chr> <chr> <int> <int> <dbl> <dbl>
#> 1 input1 sampleA_2 sampleA chr1 10 50 0.4 0.7
#> 2 input1 sampleA_2 sampleA chr2 100 500 0.5 0.8
#> 3 input1 sampleA_2 sampleA chr3 1000 5000 0.6 0.9
#> 4 input1 sampleA_3 sampleA chr1 10 50 0.4 0.7
#> 5 input1 sampleA_3 sampleA chr2 100 500 0.5 0.8
#> 6 input1 sampleA_3 sampleA chr3 1000 5000 0.6 0.9
#> 7 input1 sampleA sampleA chr1 10 50 0.4 0.7
#> 8 input1 sampleA sampleA chr2 100 500 0.5 0.8
#> 9 input1 sampleA sampleA chr3 1000 5000 0.6 0.9Limit: one shared input_id; the only per-run key is input_prefix.
Mode B: per-run tidy
Tidy each run separately with its own input_id and output subfolder:
dir_outB <- dir_create(path(dir1, "outB"))
for (r in run_ids) {
Tool1$new(path = path(dir_runs, r))$run(
input_id = r,
output_dir = path(dir_outB, r),
format = "parquet",
prefix_include = TRUE
)
}
dir_tree(dir_outB)
#> /tmp/RtmpHGTPiu/naming-demo/outB
#> ├── run1
#> │ ├── metadata_tool1.parquet
#> │ ├── sampleA_tool1_table1.parquet
#> │ └── sampleA_tool1_table2.parquet
#> ├── run2
#> │ ├── metadata_tool1.parquet
#> │ ├── sampleA_tool1_table1.parquet
#> │ └── sampleA_tool1_table2.parquet
#> └── run3
#> ├── metadata_tool1.parquet
#> ├── sampleA_tool1_table1.parquet
#> └── sampleA_tool1_table2.parquetSame file names per subfolder; input_id separates them once merged:
fs::dir_ls(dir_outB, recurse = TRUE, glob = "*tool1_table1.parquet") |>
purrr::map(arrow::read_parquet) |>
purrr::list_rbind()
#> # A tibble: 9 × 8
#> input_id input_prefix sample_id chromosome start end metric_y metric_z
#> <chr> <chr> <chr> <chr> <int> <int> <dbl> <dbl>
#> 1 run1 sampleA sampleA chr1 10 50 0.4 0.7
#> 2 run1 sampleA sampleA chr2 100 500 0.5 0.8
#> 3 run1 sampleA sampleA chr3 1000 5000 0.6 0.9
#> 4 run2 sampleA sampleA chr1 10 50 0.4 0.7
#> 5 run2 sampleA sampleA chr2 100 500 0.5 0.8
#> 6 run2 sampleA sampleA chr3 1000 5000 0.6 0.9
#> 7 run3 sampleA sampleA chr1 10 50 0.4 0.7
#> 8 run3 sampleA sampleA chr2 100 500 0.5 0.8
#> 9 run3 sampleA sampleA chr3 1000 5000 0.6 0.9For a globally-unique key, set output_id, e.g. to a ULID (CLI: --ulid, via the ulid R package).
Rule of thumb
- On disk:
_2/_3suffixes (Mode A) or per-run folders (Mode B) - After merging:
input_prefix,input_id,output_idcolumns - One recursive tidy is enough =>
prefix_include - Want named runs =>
input_id/output_id
Special cases: semantic prefixes
Germline/somatic variants of a table can strip to the same prefix (sample1 / sample1_2). Linx, Purple and Sage override the private refine_files() hook (runs before disambiguation) to add _germline/_somatic to the prefix. Remaining _2/_3 = repeat runs.
Purple: driver.catalog germline vs. somatic
- Problem:
sample1.purple.driver.catalog.{germline,somatic}.tsvboth go through thedrivercatalogparser and strip to prefixsample1. - Fix: the hook rewrites them by basename to
sample1_germline/sample1_somatic.
Three runs, both driver catalogs + qc:
dir_inP <- path(tempdir(), "purple-runs")
for (r in c("run1", "run2", "run3")) {
dest <- dir_create(path(dir_inP, r))
file_copy(
dir_ls(
path(oa, "purple"),
regexp = "driver\\.catalog\\.(germline|somatic)\\.tsv$|purple\\.qc$"
),
dest,
overwrite = TRUE
)
}
dir_tree(dir_inP)
#> /tmp/RtmpHGTPiu/purple-runs
#> ├── run1
#> │ ├── sample1.purple.driver.catalog.germline.tsv
#> │ ├── sample1.purple.driver.catalog.somatic.tsv
#> │ └── sample1.purple.qc
#> ├── run2
#> │ ├── sample1.purple.driver.catalog.germline.tsv
#> │ ├── sample1.purple.driver.catalog.somatic.tsv
#> │ └── sample1.purple.qc
#> └── run3
#> ├── sample1.purple.driver.catalog.germline.tsv
#> ├── sample1.purple.driver.catalog.somatic.tsv
#> └── sample1.purple.qc
ppl <- Purple$new(path = dir_inP)
dir_outP <- dir_create(path(tempdir(), "purple-out"))
ppl$run(
input_id = "input1",
output_id = "output1",
output_dir = dir_outP,
format = "parquet",
prefix_include = TRUE
)
dir_tree(dir_outP)
#> /tmp/RtmpHGTPiu/purple-out
#> ├── metadata_purple.parquet
#> ├── sample1_2_purple_qc.parquet
#> ├── sample1_3_purple_qc.parquet
#> ├── sample1_germline_2_purple_drivercatalog.parquet
#> ├── sample1_germline_3_purple_drivercatalog.parquet
#> ├── sample1_germline_purple_drivercatalog.parquet
#> ├── sample1_purple_qc.parquet
#> ├── sample1_somatic_2_purple_drivercatalog.parquet
#> ├── sample1_somatic_3_purple_drivercatalog.parquet
#> └── sample1_somatic_purple_drivercatalog.parquetStacked (top 2 rows each):
dir_ls(dir_outP, regexp = "purple_drivercatalog\\.parquet") |>
purrr::map(\(x) arrow::read_parquet(x) |> dplyr::slice_head(n = 2)) |>
purrr::list_rbind() |>
dplyr::select(input_id, input_prefix, output_id, chrom, gene, cn_min)
#> # A tibble: 12 × 6
#> input_id input_prefix output_id chrom gene cn_min
#> <chr> <chr> <chr> <chr> <chr> <dbl>
#> 1 input1 sample1_germline_2 output1 chr6 CRYBG1 0.0153
#> 2 input1 sample1_germline_2 output1 chr1 CSF1 2.02
#> 3 input1 sample1_germline_3 output1 chr6 CRYBG1 0.0153
#> 4 input1 sample1_germline_3 output1 chr1 CSF1 2.02
#> 5 input1 sample1_germline output1 chr6 CRYBG1 0.0153
#> 6 input1 sample1_germline output1 chr1 CSF1 2.02
#> 7 input1 sample1_somatic_2 output1 chr8 TG 6.42
#> 8 input1 sample1_somatic_2 output1 chr20 PLCG1 3.47
#> 9 input1 sample1_somatic_3 output1 chr8 TG 6.42
#> 10 input1 sample1_somatic_3 output1 chr20 PLCG1 3.47
#> 11 input1 sample1_somatic output1 chr8 TG 6.42
#> 12 input1 sample1_somatic output1 chr20 PLCG1 3.47
dir_ls(dir_outP, regexp = "purple_qc\\.parquet") |>
purrr::map(\(x) arrow::read_parquet(x)) |>
purrr::list_rbind() |>
dplyr::select(input_id, input_prefix, output_id, qc_status, purity)
#> # A tibble: 3 × 5
#> input_id input_prefix output_id qc_status purity
#> <chr> <chr> <chr> <chr> <dbl>
#> 1 input1 sample1_2 output1 PASS 1
#> 2 input1 sample1_3 output1 PASS 1
#> 3 input1 sample1 output1 PASS 1Linx: germline vs. somatic annotations
- Problem:
sample1.linx.breakend.tsvandsample1.linx.germline.breakend.tsvboth reduce tosample1. - Fix: the hook tags both sides, but only for parsers with a germline file present; somatic-only tables (
drivers,fusion,vis_*) are untouched.
Three runs, paired tables + fusions:
dir_inL <- path(tempdir(), "linx-runs")
patl <- "\\.(breakend|links|svs|fusion)\\.tsv$"
linx_files <- c(
dir_ls(path(oa, "linx/germline_annotations"), regexp = patl),
dir_ls(path(oa, "linx/somatic_annotations"), regexp = patl)
)
for (r in c("run1", "run2", "run3")) {
dest <- dir_create(path(dir_inL, r))
file_copy(linx_files, dest, overwrite = TRUE)
}
dir_tree(dir_inL)
#> /tmp/RtmpHGTPiu/linx-runs
#> ├── run1
#> │ ├── sample1.linx.breakend.tsv
#> │ ├── sample1.linx.fusion.tsv
#> │ ├── sample1.linx.germline.breakend.tsv
#> │ ├── sample1.linx.germline.links.tsv
#> │ ├── sample1.linx.germline.svs.tsv
#> │ ├── sample1.linx.links.tsv
#> │ └── sample1.linx.svs.tsv
#> ├── run2
#> │ ├── sample1.linx.breakend.tsv
#> │ ├── sample1.linx.fusion.tsv
#> │ ├── sample1.linx.germline.breakend.tsv
#> │ ├── sample1.linx.germline.links.tsv
#> │ ├── sample1.linx.germline.svs.tsv
#> │ ├── sample1.linx.links.tsv
#> │ └── sample1.linx.svs.tsv
#> └── run3
#> ├── sample1.linx.breakend.tsv
#> ├── sample1.linx.fusion.tsv
#> ├── sample1.linx.germline.breakend.tsv
#> ├── sample1.linx.germline.links.tsv
#> ├── sample1.linx.germline.svs.tsv
#> ├── sample1.linx.links.tsv
#> └── sample1.linx.svs.tsv
l <- Linx$new(path = dir_inL)Paired tables split into sample1_germline/sample1_somatic; fusions get _2/_3:
dir_outL <- dir_create(path(tempdir(), "linx-out"))
l$run(
input_id = "input1",
output_id = "output1",
output_dir = dir_outL,
format = "parquet",
prefix_include = TRUE
)
dir_tree(dir_outL)
#> /tmp/RtmpHGTPiu/linx-out
#> ├── metadata_linx.parquet
#> ├── sample1_2_linx_fusions.parquet
#> ├── sample1_3_linx_fusions.parquet
#> ├── sample1_germline_2_linx_breakends.parquet
#> ├── sample1_germline_2_linx_links.parquet
#> ├── sample1_germline_2_linx_svs.parquet
#> ├── sample1_germline_3_linx_breakends.parquet
#> ├── sample1_germline_3_linx_links.parquet
#> ├── sample1_germline_3_linx_svs.parquet
#> ├── sample1_germline_linx_breakends.parquet
#> ├── sample1_germline_linx_links.parquet
#> ├── sample1_germline_linx_svs.parquet
#> ├── sample1_linx_fusions.parquet
#> ├── sample1_somatic_2_linx_breakends.parquet
#> ├── sample1_somatic_2_linx_links.parquet
#> ├── sample1_somatic_2_linx_svs.parquet
#> ├── sample1_somatic_3_linx_breakends.parquet
#> ├── sample1_somatic_3_linx_links.parquet
#> ├── sample1_somatic_3_linx_svs.parquet
#> ├── sample1_somatic_linx_breakends.parquet
#> ├── sample1_somatic_linx_links.parquet
#> └── sample1_somatic_linx_svs.parquetStacked breakends (top 2 rows each):
dir_ls(dir_outL, regexp = "linx_breakends\\.parquet") |>
purrr::map(\(x) arrow::read_parquet(x) |> dplyr::slice_head(n = 2)) |>
purrr::list_rbind() |>
dplyr::select(input_id, input_prefix, output_id, gene, undisrupted_cn)
#> # A tibble: 12 × 5
#> input_id input_prefix output_id gene undisrupted_cn
#> <chr> <chr> <chr> <chr> <dbl>
#> 1 input1 sample1_germline_2 output1 PPP6C 0.286
#> 2 input1 sample1_germline_2 output1 FOXP1 0.281
#> 3 input1 sample1_germline_3 output1 PPP6C 0.286
#> 4 input1 sample1_germline_3 output1 FOXP1 0.281
#> 5 input1 sample1_germline output1 PPP6C 0.286
#> 6 input1 sample1_germline output1 FOXP1 0.281
#> 7 input1 sample1_somatic_2 output1 WRAP73 2.21
#> 8 input1 sample1_somatic_2 output1 TP73 2.21
#> 9 input1 sample1_somatic_3 output1 WRAP73 2.21
#> 10 input1 sample1_somatic_3 output1 TP73 2.21
#> 11 input1 sample1_somatic output1 WRAP73 2.21
#> 12 input1 sample1_somatic output1 TP73 2.21Sage: germline vs. somatic folders
- Problem: older Sage versions write identical basenames (e.g.
sample1.sage.bqr.tsv) into siblinggermline/andsomatic/folders. - Fix: the hook tags by parent folder (
refine_by_variant_folder()).
Three runs, bqr + gene.coverage:
dir_inS <- path(tempdir(), "sage-runs")
for (r in c("run1", "run2", "run3")) {
for (v in c("germline", "somatic")) {
dest <- dir_create(path(dir_inS, r, v))
file_copy(
dir_ls(
path(oa, "sage", v),
regexp = "\\.sage\\.bqr\\.tsv$|\\.sage\\.gene\\.coverage\\.tsv$"
),
dest,
overwrite = TRUE
)
}
}
dir_tree(dir_inS)
#> /tmp/RtmpHGTPiu/sage-runs
#> ├── run1
#> │ ├── germline
#> │ │ ├── sample1.sage.bqr.tsv
#> │ │ ├── sample2.sage.bqr.tsv
#> │ │ └── sample2.sage.gene.coverage.tsv
#> │ └── somatic
#> │ ├── sample1.sage.bqr.tsv
#> │ ├── sample1.sage.gene.coverage.tsv
#> │ └── sample2.sage.bqr.tsv
#> ├── run2
#> │ ├── germline
#> │ │ ├── sample1.sage.bqr.tsv
#> │ │ ├── sample2.sage.bqr.tsv
#> │ │ └── sample2.sage.gene.coverage.tsv
#> │ └── somatic
#> │ ├── sample1.sage.bqr.tsv
#> │ ├── sample1.sage.gene.coverage.tsv
#> │ └── sample2.sage.bqr.tsv
#> └── run3
#> ├── germline
#> │ ├── sample1.sage.bqr.tsv
#> │ ├── sample2.sage.bqr.tsv
#> │ └── sample2.sage.gene.coverage.tsv
#> └── somatic
#> ├── sample1.sage.bqr.tsv
#> ├── sample1.sage.gene.coverage.tsv
#> └── sample2.sage.bqr.tsv
s <- Sage$new(path = dir_inS)
dir_outS <- dir_create(path(tempdir(), "sage-out"))
s$run(
input_id = "input1",
output_id = "output1",
output_dir = dir_outS,
format = "parquet",
prefix_include = TRUE
)
dir_tree(dir_outS)
#> /tmp/RtmpHGTPiu/sage-out
#> ├── metadata_sage.parquet
#> ├── sample1_germline_2_sage_bqrtsv.parquet
#> ├── sample1_germline_3_sage_bqrtsv.parquet
#> ├── sample1_germline_sage_bqrtsv.parquet
#> ├── sample1_somatic_2_sage_bqrtsv.parquet
#> ├── sample1_somatic_2_sage_genecvgcvg.parquet
#> ├── sample1_somatic_2_sage_genecvgmain.parquet
#> ├── sample1_somatic_3_sage_bqrtsv.parquet
#> ├── sample1_somatic_3_sage_genecvgcvg.parquet
#> ├── sample1_somatic_3_sage_genecvgmain.parquet
#> ├── sample1_somatic_sage_bqrtsv.parquet
#> ├── sample1_somatic_sage_genecvgcvg.parquet
#> ├── sample1_somatic_sage_genecvgmain.parquet
#> ├── sample2_germline_2_sage_bqrtsv.parquet
#> ├── sample2_germline_2_sage_genecvgcvg.parquet
#> ├── sample2_germline_2_sage_genecvgmain.parquet
#> ├── sample2_germline_3_sage_bqrtsv.parquet
#> ├── sample2_germline_3_sage_genecvgcvg.parquet
#> ├── sample2_germline_3_sage_genecvgmain.parquet
#> ├── sample2_germline_sage_bqrtsv.parquet
#> ├── sample2_germline_sage_genecvgcvg.parquet
#> ├── sample2_germline_sage_genecvgmain.parquet
#> ├── sample2_somatic_2_sage_bqrtsv.parquet
#> ├── sample2_somatic_3_sage_bqrtsv.parquet
#> └── sample2_somatic_sage_bqrtsv.parquetStacked bqrtsv (one random row each):
dir_ls(dir_outS, regexp = "sage_bqrtsv\\.parquet") |>
purrr::map(\(x) arrow::read_parquet(x) |> dplyr::slice_sample(n = 1)) |>
purrr::list_rbind() |>
dplyr::select(input_id, input_prefix, output_id, dplyr::everything())
#> # A tibble: 12 × 10
#> input_id input_prefix output_id alt ref context read_type count origq
#> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <dbl> <dbl>
#> 1 input1 sample1_germli… output1 C C TCT NONE 3.20e7 37
#> 2 input1 sample1_germli… output1 T T CTG NONE 3.71e7 37
#> 3 input1 sample1_germli… output1 C C ACA NONE 3.24e7 37
#> 4 input1 sample1_somati… output1 A A AAA NONE 5.08e7 37
#> 5 input1 sample1_somati… output1 T T TTT NONE 5.02e7 37
#> 6 input1 sample1_somatic output1 C C ACA NONE 3.24e7 37
#> 7 input1 sample2_germli… output1 T T TTT NONE 4.05e7 37
#> 8 input1 sample2_germli… output1 A A CAG NONE 3.82e7 37
#> 9 input1 sample2_germli… output1 A A AAA NONE 4.10e7 37
#> 10 input1 sample2_somati… output1 C C CCA NONE 3.40e7 37
#> 11 input1 sample2_somati… output1 C C CCA NONE 3.40e7 37
#> 12 input1 sample2_somatic output1 G G GGG NONE 3.18e7 37
#> # ℹ 1 more variable: recalq <dbl>When to reach for the hook
- Prefer schema
patterns that already separate variants - Use
refine_files()when one parser matches files needing a real label, not_2 - Hook can edit any
list_files()column; usuallyprefix
