diff --git a/.Rbuildignore b/.Rbuildignore
new file mode 100644
index 00000000..deb16a00
--- /dev/null
+++ b/.Rbuildignore
@@ -0,0 +1,6 @@
+^\.github$
+^\.lintr$
+^Makefile$
+^docs$
+^gcCorrect_chromosome_coordinates_.*\.txt$
+^LICENSE$
diff --git a/.gitignore b/.gitignore
index ff7e71bf..537d4443 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,7 @@
/doc/
/Meta/
+.RData
+.Rhistory
+.Rprofile
+.DS_Store
+.Rproj.user
diff --git a/.lintr b/.lintr
new file mode 100644
index 00000000..b56db184
--- /dev/null
+++ b/.lintr
@@ -0,0 +1,12 @@
+linters: linters_with_defaults(
+ line_length_linter = line_length_linter(200),
+ object_usage_linter = NULL,
+ object_name_linter = NULL,
+ commented_code_linter = NULL,
+ return_linter = NULL,
+ indentation_linter = NULL,
+ object_length_linter = NULL,
+ pipe_consistency_linter = NULL,
+ T_and_F_symbol_linter = NULL
+ )
+encoding: "UTF-8"
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..d0ff886d
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,101 @@
+# Changelog: Battenberg Refactor v3.0.2
+
+This document outlines the significant mathematical, technical, and feature changes introduced in the refactored Battenberg pipeline compared to the original implementation.
+
+## 1. Architectural Changes: "Pure R" Pipeline
+* **Removal of External Executable Orchestration**:
+ * **Change**: The pipeline no longer internally manages or invokes external binary executables such as `alleleCounter`, `impute2`, or `java -jar beagle.jar`.
+ * **Impact**: Responsibilities such as **Allele Counting** and **Phasing/Imputation** have been factored out. The R package now strictly acts as a consumer of standard genomic file formats (BAM outputs, VCFs) generated by upstream workflow managers (e.g., Nextflow, Snakemake). This makes the package significantly lighter, more portable, and easier to containerize, as it no longer requires a complex "fat" environment with legacy binaries.
+* **VCF Consumption Model**:
+ * **Change**: Instead of wrapping the execution of Beagle 5, the pipeline now includes a native VCF parser (`convert_beagle_to_impute`) that digests the output of modern phasing tools.
+ * **Rationale**: Decouples the statistical copy number calling from the specific version or implementation of the phasing tool.
+
+## 2. Mathematical & Algorithmic Changes
+
+### Geometric Centroid Selection
+* **Change**: When multiple valid copy number solutions (optima) are found during the grid search (rho/psi space), the algorithm now calculates the **geometric centroid** of all valid solutions and selects the specific optimum closest to this center.
+* **Rationale**: The original implementation often defaulted to the solution with the absolute highest goodness-of-fit. Centroid selection ensures a more robust "central" parameter set is chosen when the solution space is flat, reducing outlier artifacts.
+
+### Recalculated Ploidy (Psi_t)
+* **Change**: In `run_clonal_ASCAT`, the ploidy (`psi`) is partially recalculated using *only* high-confidence clonal segments after the initial grid search.
+* **Rationale**: Ensures the final ploidy estimate is not skewed by subclonal noise.
+
+### Optimized Integer Copy Number Fitting
+* **Change**: The C++ implementation (`ascat_distance.cpp`) explicitly optimizes the integer combination of Major/Minor alleles (checking Floor/Ceiling combinations) for every grid point to minimize the BAF squared error.
+* **Rationale**: Provides a rigorously optimized "best fit" for integer copy numbers at every hypothetical grid point.
+
+### LOH/Deletion Constraint Logic
+* **Change**: Applied a heuristic in `calculate_solution_fast` that valid solutions typically require at least some Loss-Of-Heterozygosity (LOH) or deletions (CN=0).
+* **Rationale**: Filters out high-ploidy artifact solutions that lack biological deletion events.
+
+### Winsorization in Segmentation
+* **Change**: Added `copynumber::winsorize` step prior to PCF segmentation (`segmentation.R`).
+* **Rationale**: Prevents single-point outliers from distorting segment means.
+
+## 3. Technical & Performance Optimizations
+
+### C++ Acceleration (`Rcpp`)
+* **Change**: Core bottlenecks—distance calculations (`calculate_ascat_dist_matrix_cpp`) and segmentation (`pcf_core.cpp`)—ported to C++.
+* **Impact**: 10x-100x speedup in grid search and segmentation.
+
+### High-Performance IO & Vectorization
+* **Change**:
+ * **`vroom`**: Used for instant reading/merging of large BAF files (`haplotype.R`).
+ * **`collapse`**: Replaced base R stats with `collapse::fsum`/`fmean`.
+ * **Vectorization**: Grid search logic fully vectorized to remove nested R loops.
+* **Impact**: Massive reduction in I/O overhead and compute time.
+
+### Workflow Checkpoints & Resume
+* **Change**: Added `preprocessed_data_dir` and `phasing_results_dir` arguments to `battenberg()`.
+* **Impact**: Allows skipping expensive upstream steps (allele counting, imputation) when re-running segmentation with new parameters.
+
+### Dynamic Thread Budgeting
+* **Change**: Explicit calculation of total threads (`chromosomes_in_parallel` × `threads_per_chromosome`) and dynamic setting of `OMP_NUM_THREADS` and `MKL_NUM_THREADS`.
+* **Impact**: Prevents system lockups due to thread oversubscription.
+
+### Memory Management
+* **Change**:
+ * **Chunked Back-Transformation**: Processes LRR/BAF vectors in chunks to prevent OOM.
+ * **Smart Downsampling**: Plotting functions downsample data to ~500k points to prevent graphics device hangs.
+
+## 4. Stability & Robustness
+
+### Critical Probe Misalignment Fix
+* **Change**: Explicit logic in `run_ascat_enhanced` to subset vectors using **names** (`lrr[names(baf)]`) rather than position.
+* **Impact**: Fixes data corruption caused by upstream filtering offsets.
+
+### Failsafe Grid Search
+* **Change**: Automatic fallback to **Full Grid Search** or **Top-N Search** if the optimized local minima search yields no results.
+
+## 5. New Features
+
+### Configurable Grid Search
+* **New Arguments**:
+ * `n_neighbors_search`: Limit search to top N closest points.
+ * `psi_step` / `rho_step`: Custom grid resolution.
+ * `local_min_window_size`: Adjustable local minima window.
+
+### Structured Logging
+* **Change**: Full migration to **`logger`** package for timestamped, leveled logs (INFO, DEBUG, ERROR).
+
+### Early Termination
+* **Change**: `early_termination = TRUE` flag to stop grid search once a high-quality solution is found.
+
+## 6. Code Structure & Dependencies
+
+### Modularization
+* **Change**: Split monolithic scripts (`clonal_ascat.R`) into focused modules (`run_clonal_ascat.R`, `clonal_ascat_calc.R`, etc.).
+
+### New Dependencies
+* `Rcpp`, `RcppRoll` (Acceleration)
+* `collapse`, `vroom`, `tictoc`, `fs` (IO/Perf)
+* `logger`, `cli`, `optparse` (Interface)
+* `S4Vectors`, `IRanges` (Genomics)
+
+### License
+* **Change**: Updated to `AGPL-3` since that is what was listed in the LICENSE file.
+
+## 7. Container & CLI Support
+* **Change**: Added `Dockerfile`, `Makefile`, and `cli`/`optparse` support.
+* **Impact**: Facilitates robust command-line usage and reproducible containerized deployment.
+* **Registry**: A Singularity-compatible Docker image is available at [quay.io/ohsu-comp-bio/battenberg](https://quay.io/repository/ohsu-comp-bio/battenberg?tab=tags).
\ No newline at end of file
diff --git a/DESCRIPTION b/DESCRIPTION
index 5bbf8a42..b78cbd90 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,9 +1,9 @@
Package: Battenberg
Maintainer: Stefan Dentro
-License: GPL-3
+License: AGPL-3
Type: Package
Title: Battenberg subclonal copy number caller
-Version: 3.0.0
+Version: 3.0.2
Encoding: UTF-8
Authors@R: c(person("David", "Wedge", role=c("aut"), email="dw9@sanger.ac.uk"),
person("Peter", "Van Loo", role=c("aut")),
@@ -17,15 +17,16 @@ Authors@R: c(person("David", "Wedge", role=c("aut"), email="dw9@sanger.ac.uk"),
person("Mohammed Faizal","Eeman Mootor", role="ctb"),
person("Julio Cesar","Cortes Rios", role="ctb"))
Description: Estimate subclonal copy number from whole genome sequencing or SNP6 data.
-Depends:
- R (>= 4.3.1),
+Depends:
+ R (>= 4.3.0)
+Imports:
stats,
utils,
graphics,
- grDevices
-Imports:
+ grDevices,
+ ASCAT,
+ copynumber,
RColorBrewer,
- ASCAT (>= 3.1.3),
ggplot2,
readr,
gtools,
@@ -36,18 +37,34 @@ Imports:
splines,
GenomicRanges,
VariantAnnotation,
- copynumber,
- data.table
+ data.table,
+ IRanges,
+ S4Vectors,
+ logger,
+ vroom,
+ cli,
+ fs,
+ methods,
+ rlang,
+ SummarizedExperiment,
+ collapse,
+ dplyr,
+ Rcpp,
+ RcppRoll,
+ optparse,
+ tictoc,
+ devtools
Remotes:
- VanLoo-lab/ascat/ASCAT
-URL: https://github.com/Wedge-Oxford/battenberg
+ Crick-CancerGenomics/ascat/ASCAT,
+ igordot/copynumber
+URL: https://github.com/ohsu-comp-bio/battenberg
+LinkingTo: Rcpp
LazyLoad: yes
Suggests:
- testthat,
+ lintr,
+ styler,
knitr,
- rmarkdown,
- ggplot2,
- dplyr
-VignetteBuilder:
+ rmarkdown
+VignetteBuilder:
knitr
-RoxygenNote: 7.3.2
+RoxygenNote: 7.3.3
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..31961308
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,82 @@
+# Stage 1: Build C dependencies
+FROM ubuntu:24.04 AS builder
+ARG DEBIAN_FRONTEND=noninteractive
+RUN apt-get update && apt-get install -y \
+ make git curl gcc g++ bzip2 zlib1g-dev libbz2-dev liblzma-dev libcurl4-gnutls-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN mkdir /tmp/downloads
+# Build htslib
+RUN curl -sSL -o htslib.tar.bz2 https://github.com/samtools/htslib/releases/download/1.7/htslib-1.7.tar.bz2 && \
+ mkdir /tmp/htslib && \
+ tar -C /tmp/htslib --strip-components 1 -xjf htslib.tar.bz2 && \
+ cd /tmp/htslib && \
+ ./configure && \
+ make -j$(nproc) && \
+ make install
+
+# Build alleleCount
+RUN curl -sSL -o allelecount.tar.gz https://github.com/cancerit/alleleCount/archive/v4.0.0.tar.gz && \
+ mkdir /tmp/allelecount && \
+ tar -C /tmp/allelecount --strip-components 1 -zxf allelecount.tar.gz && \
+ cd /tmp/allelecount/c && \
+ mkdir -p bin && \
+ make bin/alleleCounter && \
+ cp bin/alleleCounter /usr/local/bin/
+
+
+# Stage 2: Final image
+FROM ubuntu:24.04
+ARG DEBIAN_FRONTEND=noninteractive
+
+# 1. Install R and System Dependencies
+RUN apt-get update && apt-get install -y \
+ r-base \
+ r-base-dev \
+ openjdk-17-jre-headless \
+ libcurl4-gnutls-dev \
+ libxml2-dev \
+ libssl-dev \
+ libfontconfig1-dev \
+ libharfbuzz-dev \
+ libfribidi-dev \
+ libfreetype6-dev \
+ libpng-dev \
+ libtiff5-dev \
+ libjpeg-dev \
+ make \
+ curl \
+ git \
+ && rm -rf /var/lib/apt/lists/*
+
+# 2. OPTIMIZATION: Configure Posit Binary Repository for Ubuntu Noble
+# We do this AFTER R is installed so the directory exists.
+RUN mkdir -p /usr/lib/R/etc && \
+ echo 'options(repos = c(CRAN = "https://packagemanager.posit.co/cran/__linux__/noble/latest"))' >> /usr/lib/R/etc/Rprofile.site && \
+ echo 'options(HTTPUserAgent = sprintf("R/%s R (%s)", getRversion(), paste(getRversion(), R.version$platform, R.version$arch, R.version$os)))' >> /usr/lib/R/etc/Rprofile.site
+
+# 3. Copy binaries from builder stage
+COPY --from=builder /usr/local/bin/alleleCounter /usr/local/bin/
+# Impute2 (Static x86_64 binary)
+RUN curl -sSL -o tmp.tar.gz https://mathgen.stats.ox.ac.uk/impute/impute_v2.3.2_x86_64_static.tgz && \
+ tar -C /usr/local/bin --strip-components 1 -zxf tmp.tar.gz && \
+ rm tmp.tar.gz
+
+# 4. Install pak (improved installation for Linux)
+RUN Rscript -e "install.packages('pak', repos = 'https://r-lib.github.io/p/pak/stable')"
+
+WORKDIR /opt/battenberg
+
+# 5. OPTIMIZATION: Cache dependency installation layer
+# Copy DESCRIPTION first so that changes to code don't invalidate the dependency cache.
+COPY DESCRIPTION .
+COPY Makefile .
+RUN make deps
+
+# 6. Copy the rest of the code and install the package
+COPY . .
+RUN rm -rf src/*.o src/*.so
+RUN make compile && make docs && make install
+
+WORKDIR /home/ubuntu
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..18ac1823
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,34 @@
+.PHONY: style lint test deps check install docs pak
+
+docs:
+ Rscript -e "roxygen2::roxygenise(clean = TRUE)"
+
+compile:
+ Rscript -e "Rcpp::compileAttributes()"
+
+# Run the auto-formatter (styler)
+style:
+ Rscript -e "styler::style_pkg(transformers = styler::tidyverse_style(strict = TRUE), base_indention = 0)"
+
+# Run the linter
+lint:
+ Rscript -e "lintr::lint_package()"
+
+pak:
+ @echo "Installing pak and core dependencies..."
+ RUN Rscript -e "install.packages('pak', repos = 'https://cran.rstudio.com/')"
+
+deps:
+ @echo "Installing all dependencies listed in DESCRIPTION..."
+ Rscript -e "if (!requireNamespace('pak', quietly = TRUE)) install.packages('pak', repos = 'https://cloud.r-project.org')"
+ Rscript -e "pak::pkg_install(c('Crick-CancerGenomics/ascat/ASCAT', 'igordot/copynumber'))"
+ Rscript -e "pak::repo_add(Bioc = '3.18'); \
+ pak::local_install_deps(upgrade = FALSE, dependencies = TRUE)"
+
+check:
+ Rscript -e "devtools::check(error_on = 'warning')"
+ Rscript -e "devtools::load_all('.'); codetools::checkUsagePackage('Battenberg')"
+
+install:
+ @echo "Installing Battenberg..."
+ Rscript -e "pak::local_install('.', upgrade=TRUE, dependencies=TRUE)"
diff --git a/NAMESPACE b/NAMESPACE
index 2864f1e0..84fde437 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -1,93 +1,68 @@
# Generated by roxygen2: do not edit by hand
-S3method(plot,haplotype.data)
export(GetChromosomeBAFs)
export(GetChromosomeBAFs_SNP6)
export(allele_ratio_plot)
export(battenberg)
+export(battenberg_cli)
export(calc_psi_t)
export(calc_rho_psi_refit)
export(callChrXsubclones)
-export(callSubclones)
export(call_multisample_MSAI)
-export(cel2baf.logr)
+export(call_subclones)
+export(cel2baf_logr)
export(cell_line_baf_logR)
export(cell_line_reconstruct_normal)
export(cnfit_to_refit_suggestions)
-export(combine.baf.files)
-export(combine.impute.output)
-export(convert.impute.input.to.beagle.input)
+export(combine_impute_output)
+export(concatenate_baf_files)
+export(convert_beagle_to_impute)
+export(convert_impute_input_to_beagle_vcf)
export(coverage_plot)
export(find_centroid_of_global_minima)
-export(fit.copy.number)
-export(gc.correct)
-export(gc.correct.wgs)
-export(gc.correct.wgs.germline)
-export(generate.impute.input.snp6)
-export(generate.impute.input.wgs)
-export(generate.impute.input.wgs.germline)
+export(fit_copy_number)
+export(gc_correct)
+export(gc_correct_wgs)
+export(gc_correct_wgs_germline)
+export(generate_impute_input_snp6)
+export(generate_impute_input_wgs)
+export(generate_impute_input_wgs_germline)
export(germline_baf_logR)
export(germline_reconstruct_normal)
-export(get.chrom.names)
-export(getAlleleCounts)
export(getBAFsAndLogRs)
+export(get_chrom_names)
export(get_multisample_phasing)
+export(generate_beagle_input_from_counts)
export(infer_gender_birdseed)
export(input_known_haplotypes)
+export(log_debug)
+export(log_failure)
+export(log_info)
+export(log_setup)
+export(log_warning)
export(make_posthoc_plots)
-export(parse.imputeinfofile)
+export(parse_imputeinfofile)
+export(plot_haplotype_data)
export(prepare_snp6)
export(prepare_wgs)
export(prepare_wgs_cell_line)
export(prepare_wgs_germline)
+export(read_alleleFrequencies)
+export(read_impute_input)
export(read_table_generic)
-export(run.beagle5)
-export(run.impute)
export(runASCAT)
export(run_clonal_ASCAT)
export(run_haplotyping)
export(run_haplotyping_germline)
-export(segment.baf.phased)
-export(segment.baf.phased.legacy)
-export(segment.baf.phased.multisample)
-export(segment.baf.phased.sv)
+export(segment_baf_phased)
+export(segment_baf_phased_multisample)
export(split_input_haplotypes)
-export(squaresplot)
-export(standardiseChrNotation)
-export(standardiseChrNotation_germline)
export(suggest_refit)
export(totalcn_chrom_plot)
export(write_battenberg_phasing)
-export(writebeagle.as.impute)
-export(writevcf.beagle)
-import(ggplot2)
-import(grDevices)
-import(graphics)
-import(stats)
-import(utils)
-importFrom(ASCAT,ascat.plotAscatProfile)
-importFrom(ASCAT,ascat.plotNonRounded)
-importFrom(ASCAT,ascat.plotSunrise)
-importFrom(ASCAT,make_segments)
-importFrom(GenomicRanges,distance)
-importFrom(GenomicRanges,end)
-importFrom(GenomicRanges,findOverlaps)
-importFrom(GenomicRanges,makeGRangesFromDataFrame)
-importFrom(GenomicRanges,mcols)
-importFrom(GenomicRanges,seqinfo)
-importFrom(GenomicRanges,seqnames)
-importFrom(GenomicRanges,start)
-importFrom(GenomicRanges,width)
-importFrom(RColorBrewer,brewer.pal)
-importFrom(doParallel,registerDoParallel)
-importFrom(foreach,"%dopar%")
-importFrom(foreach,foreach)
-importFrom(gridExtra,arrangeGrob)
-importFrom(gridExtra,grid.arrange)
+export(writevcf_beagle)
+export(create_distance_matrix_clonal)
+export(calc_distance_clonal)
+importFrom(data.table,":=")
importFrom(gtools,mixedsort)
-importFrom(parallel,makeCluster)
-importFrom(parallel,stopCluster)
-importFrom(readr,cols)
-importFrom(readr,read_table)
-importFrom(readr,write_tsv)
-importFrom(splines,ns)
+useDynLib(Battenberg, .registration = TRUE)
diff --git a/R/Battenberg-package.R b/R/Battenberg-package.R
deleted file mode 100644
index df664795..00000000
--- a/R/Battenberg-package.R
+++ /dev/null
@@ -1,12 +0,0 @@
-#' @import stats graphics grDevices utils ggplot2
-#' @importFrom RColorBrewer brewer.pal
-#' @importFrom readr read_table write_tsv cols
-#' @importFrom gridExtra grid.arrange arrangeGrob
-#' @importFrom GenomicRanges distance end findOverlaps makeGRangesFromDataFrame mcols seqinfo seqnames start width
-#' @importFrom ASCAT make_segments ascat.plotSunrise ascat.plotAscatProfile ascat.plotNonRounded
-#' @importFrom gtools mixedsort
-#' @importFrom parallel makeCluster stopCluster
-#' @importFrom doParallel registerDoParallel
-#' @importFrom foreach foreach %dopar%
-#' @importFrom splines ns
-NULL
diff --git a/R/RcppExports.R b/R/RcppExports.R
new file mode 100644
index 00000000..01c94531
--- /dev/null
+++ b/R/RcppExports.R
@@ -0,0 +1,51 @@
+# Generated by using Rcpp::compileAttributes() -> do not edit by hand
+# Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393
+
+#' Fast C++ implementation of the ASCAT distance grid calculation (BAF-only distance)
+#' This avoids the memory explosion of creating large matrices in R and the overhead of forking.
+#' @noRd
+calculate_ascat_dist_matrix_cpp <- function(s_b, s_r, s_len, rho_vec, psi_vec, gamma_param) {
+ .Call(`_Battenberg_calculate_ascat_dist_matrix_cpp`, s_b, s_r, s_len, rho_vec, psi_vec, gamma_param)
+}
+
+#' PottsCompact C++ implementation
+#' @param kmin Minimal length of plateau
+#' @param gamma Penalty for discontinuity
+#' @param nr number of values between breakpoints
+#' @param res sum of values between breakpoints
+#' @param sq sum of squares of values between breakpoints
+#' @param yest boolean for estimation
+#' @return List with bestCost and bestSplit
+PottsCompact_cpp <- function(kmin, gamma, nr, res, sq) {
+ .Call(`_Battenberg_PottsCompact_cpp`, kmin, gamma, nr, res, sq)
+}
+
+#' exactPcf C++ implementation
+#' @param y Input vector
+#' @param kmin Minimal length of plateau
+#' @param gamma Penalty
+#' @return List with bestCost, bestAver, bestSplit
+exactPcf_cpp <- function(y, kmin, gamma) {
+ .Call(`_Battenberg_exactPcf_cpp`, y, kmin, gamma)
+}
+
+#' findEst C++ implementation
+#' @param bestSplit vector of best splits from DP
+#' @param N number of compressed points
+#' @param Nr number of original points in each compressed point
+#' @param Sum sum of original values in each compressed point
+#' @param yest boolean for estimation
+#' @return List with segments and optionally yhat
+findEst_cpp <- function(bestSplit, N, Nr, Sum, yest) {
+ .Call(`_Battenberg_findEst_cpp`, bestSplit, N, Nr, Sum, yest)
+}
+
+#' findMarks C++ implementation
+#' @param markSub marks in compressed scale
+#' @param Nr number of observations
+#' @param subsize original scale size
+#' @return LogicalVector of marks in original scale
+findMarks_cpp <- function(markSub, Nr, subsize) {
+ .Call(`_Battenberg_findMarks_cpp`, markSub, Nr, subsize)
+}
+
diff --git a/R/battenberg.R b/R/battenberg.R
index f46dbb85..995840d2 100755
--- a/R/battenberg.R
+++ b/R/battenberg.R
@@ -1,633 +1,858 @@
-
#' Run the Battenberg pipeline
-#'
-#' @param analysis The mode of Battenberg copy number analysis to be undertaken: 'paired' for tumour-normal pair, 'cell_line' for Cell line tumour-only and 'germline' for germline CNV of normal sample (Default: 'paired')
-#' @param samplename Sample identifier (tumour or germline), this is used as a prefix for the output files. If allele counts are supplied separately, they are expected to have this identifier as prefix.
-#' @param normalname Matched normal identifier, this is used as a prefix for the output files. If allele counts are supplied separately, they are expected to have this identifier as prefix.
+#' @param analysis The mode of Battenberg copy number analysis to be undertaken:
+#' 'paired' for tumour-normal pair, 'cell_line' for Cell line tumour-only and
+#' 'germline' for germline CNV of normal sample (Default: 'paired')
+#' @param samplename Sample identifier (tumour or germline), this is used as a
+#' prefix for the output files. If allele counts are supplied separately, they
+#' are expected to have this identifier as prefix.
+#' @param normalname Matched normal identifier, this is used as a prefix for the
+#' output files. If allele counts are supplied separately, they are expected to
+#' have this identifier as prefix.
#' @param sample_data_file A BAM or CEL file for the sample
-#' @param normal_data_file A BAM or CEL file for the normal-pair (paired analysis)
-#' @param imputeinfofile Full path to a Battenberg impute info file with pointers to Impute2 reference data
-#' @param g1000prefix Full prefix path to 1000 Genomes SNP loci data, as part of the Battenberg reference data
-#' @param problemloci Full path to a problem loci file that contains SNP loci that should be filtered out
-#' @param gccorrectprefix Full prefix path to GC content files, as part of the Battenberg reference data, not required for SNP6 data (Default: NULL)
-#' @param repliccorrectprefix Full prefix path to replication timing files, as part of the Battenberg reference data, not required for SNP6 data (Default: NULL)
-#' @param g1000allelesprefix Full prefix path to 1000 Genomes SNP alleles data, as part of the Battenberg reference data, not required for SNP6 data (Default: NA)
-#' @param ismale A boolean set to TRUE if the donor is male, set to FALSE if female, not required for SNP6 data (Default: NA)
-#' @param data_type String that contains either wgs or snp6 depending on the supplied input data (Default: wgs)
-#' @param impute_exe Pointer to the Impute2 executable (Default: impute2, i.e. expected in $PATH)
-#' @param allelecounter_exe Pointer to the alleleCounter executable (Default: alleleCounter, i.e. expected in $PATH)
-#' @param nthreads The number of concurrent processes to use while running the Battenberg pipeline (Default: 8)
-#' @param platform_gamma Platform scaling factor, suggestions are set to 1 for wgs and to 0.55 for snp6 (Default: 1)
-#' @param phasing_gamma Gamma parameter used when correcting phasing mistakes (Default: 1)
-#' @param segmentation_gamma The gamma parameter controls the size of the penalty of starting a new segment during segmentation. It is therefore the key parameter for controlling the number of segments (Default: 10)
-#' @param segmentation_gamma_multisample The gamma parameter controls the size of the penalty of starting a new segment during mutlisample segmentation. It is the key parameter for controlling the number of segments (Default: 10)
-#' @param segmentation_kmin Kmin represents the minimum number of probes/SNPs that a segment should consist of (Default: 3)
-#' @param phasing_kmin Kmin used when correcting for phasing mistakes (Default: 3)
-#' @param clonality_dist_metric Distance metric to use when choosing purity/ploidy combinations (Default: 0)
-#' @param ascat_dist_metric Distance metric to use when choosing purity/ploidy combinations (Default: 1)
+#' @param normal_data_file A BAM or CEL file for the
+#' normal-pair (paired analysis)
+#' @param imputeinfofile Full path to a Battenberg impute info file with
+#' pointers to Impute2 reference data
+#' @param g1000prefix Full prefix path to 1000 Genomes SNP loci data, as part of
+#' the Battenberg reference data
+#' @param problemloci Full path to a problem loci file that contains SNP
+#' loci that should be filtered out
+#' @param gccorrectprefix Full prefix path to GC content files, as part of the
+#' Battenberg reference data, not required for SNP6 data (Default: NULL)
+#' @param repliccorrectprefix Full prefix path to replication timing files,
+#' as part of the Battenberg reference data, not required
+#' for SNP6 data (Default: NULL)
+#' @param g1000allelesprefix Full prefix path to 1000 Genomes SNP alleles data,
+#' as part of the Battenberg reference data, not required for SNP6 data
+#' (Default: NA)
+#' @param ismale A boolean set to TRUE if the donor is male, set to FALSE if
+#' female, not required for SNP6 data (Default: NA)
+#' @param data_type String that contains either wgs or snp6 depending on the
+#' supplied input data (Default: wgs)
+#' @param allele_counts_dir Directory containing the allele counts files (Required for WGS/CellLine/Germline).
+#' @param impute_results_dir Directory containing the imputed haplotype results (Required for phasing).
+#' @param nthreads The number of concurrent processes to use while running the
+#' Battenberg pipeline (Default: 8)
+#' @param platform_gamma Platform scaling factor,
+#' suggestions are set to 1 for wgs and to 0.55 for snp6 (Default: 1)
+#' @param phasing_gamma Gamma parameter used when correcting phasing mistakes
+#' (Default: 1)
+#' @param segmentation_gamma The gamma parameter
+#' controls the size of the penalty
+#' of starting a new segment during segmentation.
+#' It is therefore the key parameter
+#' for controlling the number of segments (Default: 10)
+#' @param segmentation_gamma_multisample The gamma parameter
+#' controls the size of the penalty of starting a new segment
+#' during mutlisample segmentation. It is the
+#' key parameter for controlling the number of segments (Default: 10)
+#' @param segmentation_kmin Kmin represents the minimum number of
+#' probes/SNPs that a segment should consist of (Default: 3)
+#' @param phasing_kmin Kmin used when correcting for phasing mistakes
+#' (Default: 3)
+#' @param clonality_dist_metric Distance metric to use when
+#' choosing purity/ploidy combinations (Default: 0)
+#' @param ascat_dist_metric Distance metric to use when choosing purity/ploidy
+#' combinations (Default: 1)
#' @param min_ploidy Minimum ploidy to be considered (Default: 1.6)
#' @param max_ploidy Maximum ploidy to be considered (Default: 4.8)
#' @param min_rho Minimum purity to be considered (Default: 0.1)
#' @param max_rho Maximum purity to be considered (Default: 1.0)
-#' @param min_goodness Minimum goodness of fit required for a purity/ploidy combination to be accepted as a solution (Default: 0.63)
-#' @param uninformative_BAF_threshold The threshold beyond which BAF becomes uninformative (Default: 0.51)
-#' @param min_normal_depth Minimum depth required in the matched normal for a SNP to be considered as part of the wgs analysis (Default: 10)
-#' @param min_base_qual Minimum base quality required for a read to be counted when allele counting (Default: 20)
-#' @param min_map_qual Minimum mapping quality required for a read to be counted when allele counting (Default: 35)
+#' @param min_goodness Minimum goodness of fit required for a purity/ploidy
+#' combination to be accepted as a solution (Default: 0.63)
+#' @param uninformative_baf_threshold The threshold beyond which BAF becomes
+#' uninformative (Default: 0.51)
+#' @param min_normal_depth Minimum depth required in the matched normal
+#' for a SNP to be considered as part of the wgs analysis (Default: 10)
+#' @param min_base_qual Minimum base quality required for a read to
+#' be counted when allele counting (Default: 20)
+#' @param min_map_qual Minimum mapping quality required for a read to
+#' be counted when allele counting (Default: 35)
#' @param max_allowed_state The maximum CN state allowed (Default 250)
-#' @param cn_upper_limit Maximum number of copy number that can be called (Default 1000)
-#' @param calc_seg_baf_option Sets way to calculate BAF per segment: 1=mean, 2=median, 3=ifelse median==0 | 1, mean, median (Default (paired): 3, cell_line & germline: 1)
-#' @param skip_allele_counting Provide TRUE when allele counting can be skipped (i.e. its already done) (Default: FALSE)
-#' @param skip_preprocessing Provide TRUE when preprocessing is already complete (Default: FALSE)
-#' @param skip_phasing Provide TRUE when phasing is already complete (Default: FALSE)
-#' @param usebeagle Should use beagle5 instead of impute2 Default: FALSE
-#' @param beaglejar Full path to Beagle java jar file Default: NA
-#' @param beagleref.template Full path template to Beagle reference files where the chromosome is replaced by 'CHROMNAME' Default: NA
-#' @param beagleplink.template Full path template to Beagle plink files where the chromosome is replaced by 'CHROMNAME' Default: NA
-#' @param beaglemaxmem Integer Beagle max heap size in Gb Default: 10
-#' @param beaglenthreads Integer number of threads used by beagle5 Default:1
-#' @param beaglewindow Integer size of the genomic window for beagle5 (cM) Default:40
-#' @param beagleoverlap Integer size of the overlap between windows beagle5 Default:4
-#' @param javajre Path to the Java JRE executable, only required for haplotype reconstruction with Beagle (default java, i.e. in $PATH)
-#' @param snp6_reference_info_file Reference files for the SNP6 pipeline only (Default: NA)
-#' @param apt.probeset.genotype.exe Helper tool for extracting data from CEL files, SNP6 pipeline only (Default: apt-probeset-genotype)
-#' @param apt.probeset.summarize.exe Helper tool for extracting data from CEL files, SNP6 pipeline only (Default: apt-probeset-summarize)
-#' @param norm.geno.clust.exe Helper tool for extracting data from CEL files, SNP6 pipeline only (Default: normalize_affy_geno_cluster.pl)
-#' @param birdseed_report_file Sex inference output file, SNP6 pipeline only (Default: birdseed.report.txt)
-#' @param heterozygousFilter Legacy option to set a heterozygous SNP filter, SNP6 pipeline only (Default: "none")
-#' @param prior_breakpoints_file A two column file with prior breakpoints to be used during segmentation (Default: NULL)
-#' @param genomebuild Genome build upon which the 1000G SNP coordinates were obtained (Default: hg19; options: "hg19" or "hg38")
-#' @param externalhaplotypefile Vcf containing externally obtained haplotype blocks (Default: NA)
-#' @param write_battenberg_phasing Write the Battenberg phasing results as vcf to disk, e.g. for multisample cases (Default: TRUE)
-#' @param multisample_maxlag Maximal number of upstream SNPs used in the multisample haplotyping to inform the haplotype at another SNP (Default: 100)
-#' @param multisample_relative_weight_balanced Relative weight to give to haplotype info from a sample without allelic imbalance in the region (Default: 0.25)
-#' @param enhanced_grid_search Should use multi-start, parallelized and multi-approach grid search (Default: FALSE)
+#' @param cn_upper_limit Maximum number of copy number that can be called
+#' (Default 1000)
+#' @param calc_seg_baf_option Sets way to calculate BAF per segment: 1=mean,
+#' 2=median, 3=ifelse median==0 | 1, mean, median (Default (paired): 3,
+#' cell_line & germline: 1)
+#' @param externalhaplotypefile Vcf containing externally
+#' obtained haplotype blocks (Default: NA)
+#' @param write_battenberg_phasing Write the Battenberg phasing results
+#' as vcf to disk, e.g. for multisample cases (Default: TRUE)
+#' @param multisample_maxlag Maximal number of upstream SNPs used in the
+#' multisample haplotyping to inform the haplotype at another SNP (Default: 100)
+#' @param multisample_relative_weight_balanced Relative weight to give to
+#' haplotype info from a sample without allelic imbalance
+#' in the region (Default: 0.25)
+#' @param snp6_reference_info_file Reference info file for SNP6 data (Default: NA)
+#' @param enhanced_grid_search Flag to determine if the grid search should be performed with a higher number of steps (Default: FALSE)
+#' @param beagle_input_dir Directory containing Beagle VCF output files. If provided, 'usebeagle' logic is enabled. (Default: NA)
+#' @param chrom_names Optional vector of chromosome names. If not provided, derived from 'imputeinfofile' or defaults to 1:22. (Default: NULL)
+#' @param n_neighbors_search Number of top grid points to search (integer). Set to Inf for exhaustive search. If NULL, only local minima are searched.
+#' @param logging_path Path to write log files to (Default: ".")
+#'
#' @author sd11, jdemeul, Naser Ansari-Pour, Julio Cesar Cortes Rios
#' @export
-battenberg = function(analysis="paired",
- samplename,
- normalname,
- sample_data_file,
- normal_data_file,
- imputeinfofile,
- g1000prefix,
- problemloci,
- gccorrectprefix=NULL,
- repliccorrectprefix=NULL,
- g1000allelesprefix=NA,
- ismale=NA,
- data_type="wgs",
- impute_exe="impute2",
- allelecounter_exe="alleleCounter",
- nthreads=8,
- platform_gamma=1,
- phasing_gamma=1,
- segmentation_gamma=10,
- segmentation_kmin=3,
- phasing_kmin=1,
- clonality_dist_metric=0,
- ascat_dist_metric=1,
- min_ploidy=1.6,
- max_ploidy=4.8,
- min_rho=0.1,
- max_rho=1.0,
- min_goodness=0.63,
- uninformative_BAF_threshold=0.51,
- min_normal_depth=10,
- min_base_qual=20,
- min_map_qual=35,
- max_allowed_state=250,
- cn_upper_limit=1000,
- calc_seg_baf_option=3,
- skip_allele_counting=F,
- skip_preprocessing=F,
- skip_phasing=F,
- externalhaplotypefile = NA,
- usebeagle=FALSE,
- beaglejar=NA,
- beagleref.template=NA,
- beagleplink.template=NA,
- beaglemaxmem=10,
- beaglenthreads=1,
- beaglewindow=40,
- beagleoverlap=4,
- javajre="java",
- write_battenberg_phasing = T,
- multisample_relative_weight_balanced = 0.25,
- multisample_maxlag = 90,
- segmentation_gamma_multisample = 5,
- snp6_reference_info_file=NA,
- apt.probeset.genotype.exe="apt-probeset-genotype",
- apt.probeset.summarize.exe="apt-probeset-summarize",
- norm.geno.clust.exe="normalize_affy_geno_cluster.pl",
- birdseed_report_file="birdseed.report.txt",
- heterozygousFilter="none",
- prior_breakpoints_file=NULL,
- genomebuild="hg19",
- chrom_coord_file=NULL,
- enhanced_grid_search = F) {
-
- requireNamespace("foreach")
- requireNamespace("doParallel")
- requireNamespace("parallel")
- libs <- .libPaths()
-
- if (analysis == "cell_line"){
- calc_seg_baf_option=1
- phasing_gamma=1
- phasing_kmin=2
- segmentation_gamma=20
- segmentation_kmin=3
- # no matched normal required, but we are generating normal counts which have this name coded
- normalname = paste0(samplename, "_normal")
- # other cell_line specific parameter values
- min_ploidy=min_ploidy
- max_ploidy=max_ploidy
- min_rho=0.99
- max_rho=1.01
- }
- if (analysis == "germline"){
- calc_seg_baf_option=1
- phasing_gamma=3
- phasing_kmin=1
- segmentation_gamma=3
- segmentation_kmin=3
- # no matched normal required, but we are generating normal counts which have this name coded
- normalname = paste0(samplename, "_normal")
- min_ploidy=1.5
- max_ploidy=2.5
- min_rho=0.99
- max_rho=1.01
- }
-
- if (data_type=="wgs" & is.na(ismale)) {
- stop("Please provide a boolean denominator whether this sample represents a male donor")
- }
-
- if (data_type=="wgs" & is.na(g1000allelesprefix)) {
- stop("Please provide a path to 1000 Genomes allele reference files")
- }
-
- if (data_type=="wgs" & is.null(gccorrectprefix)) {
- stop("Please provide a path to GC content reference files")
- }
-
- if (data_type=="wgs" && !file.exists(problemloci)) {
- stop("Please provide a path to a problematic loci file")
- }
-
- if (!file.exists(imputeinfofile)) {
- stop("Please provide a path to an impute info file")
+battenberg <- function(
+ analysis = "paired",
+ samplename,
+ normalname,
+ normal_data_file,
+ sample_data_file,
+ g1000prefix,
+ problemloci,
+ allele_counts_dir,
+ phasing_results_dir = NA,
+ beagle_input_dir = NA,
+ reference_info_file = NA,
+ chrom_names = NULL,
+ gccorrectprefix = NULL,
+ repliccorrectprefix = NULL,
+ g1000allelesprefix = NA,
+ ismale = NA,
+ data_type = "wgs",
+ threads_per_chromosome = 8,
+ chromosomes_in_parallel = 1, # Default to 1 to preserve legacy behavior unless specified
+ platform_gamma = 1,
+ phasing_gamma = 1,
+ segmentation_gamma = 10,
+ segmentation_kmin = 3,
+ phasing_kmin = 1,
+ clonality_dist_metric = 0,
+ ascat_dist_metric = 1,
+ min_ploidy = 1.6,
+ max_ploidy = 4.8,
+ min_rho = 0.1,
+ max_rho = 1.0,
+ min_goodness = 0.63,
+ uninformative_baf_threshold = 0.51,
+ min_normal_depth = 10,
+ min_base_qual = 20,
+ min_map_qual = 35,
+ max_allowed_state = 250,
+ cn_upper_limit = 1000,
+ calc_seg_baf_option = 3,
+ externalhaplotypefile = NA,
+ write_battenberg_phasing = TRUE,
+ multisample_relative_weight_balanced = 0.25,
+ multisample_maxlag = 90,
+ segmentation_gamma_multisample = 5,
+ snp6_reference_info_file = NA,
+ prior_breakpoints_file = NULL,
+ genomebuild = "hg38",
+ chrom_coord_file = NULL,
+ enhanced_grid_search = FALSE,
+ verbose_logging = FALSE,
+ n_neighbors_search = NULL,
+ grid_psi_step = 0.05,
+ grid_rho_step = 0.01,
+ local_min_window_size = 7,
+ beaglejar = NA,
+ beagleref_dir = NA,
+ phasing_engine = "impute2"
+) {
+ # Intelligent inference of phasing engine
+ if (is.na(phasing_engine) || phasing_engine == "impute2") {
+ if (!is.na(beaglejar) && file.exists(beaglejar)) {
+ phasing_engine <- "beagle"
+ } else if (!is.na(beagle_input_dir)) {
+ phasing_engine <- "beagle"
+ }
}
-
- # check whether the impute_info.txt file contains correct paths
- check.imputeinfofile(imputeinfofile = imputeinfofile, is.male = ismale, usebeagle = usebeagle)
-
- # check whether multisample case
- nsamples <- length(samplename)
- if (nsamples > 1) {
- if (length(skip_allele_counting) < nsamples) {
- skip_allele_counting = rep(skip_allele_counting[1], nsamples)
+
+ libs <- .libPaths()
+
+ # Set global thread limits based on user configuration
+ if (requireNamespace("data.table", quietly = TRUE)) {
+ data.table::setDTthreads(threads_per_chromosome)
+ Sys.setenv(OMP_NUM_THREADS = threads_per_chromosome)
+ Sys.setenv(MKL_NUM_THREADS = threads_per_chromosome)
+ Sys.setenv(OPENBLAS_NUM_THREADS = threads_per_chromosome)
+
+ # vroom uses its own threading model; we cap it here to match
+ if (requireNamespace("vroom", quietly = TRUE)) {
+ Sys.setenv(VROOM_THREADS = threads_per_chromosome)
}
- if (length(skip_preprocessing) < nsamples) {
- skip_preprocessing = rep(skip_preprocessing[1], nsamples)
+
+ # Inform the user about the thread configuration
+ log_info(strrep("-", 60))
+ log_info("Battenberg Thread Configuration:")
+ if (threads_per_chromosome == 1 && chromosomes_in_parallel == 1) {
+ log_info(" - MODE: STRICT SEQUENTIAL (1 CPU)")
}
- if (length(skip_phasing) < nsamples) {
- skip_phasing = rep(skip_phasing[1], nsamples)
+ log_info(" - Chromosomes/Samples in parallel: {chromosomes_in_parallel}")
+ log_info(" - Threads per chromosome (Inner): {threads_per_chromosome}")
+ log_info(" - Total max theoretical threads: {chromosomes_in_parallel * threads_per_chromosome}")
+ log_info(" - The pipeline will dynamically allocate these cores between")
+ log_info(" sample-level and logic-level parallelism.")
+ log_info(strrep("-", 60))
+
+ log_info("Starting analysis for {samplename}")
+
+
+ if (analysis == "cell_line") {
+ calc_seg_baf_option <- 1
+ phasing_gamma <- 1
+ phasing_kmin <- 2
+ segmentation_gamma <- 20
+ segmentation_kmin <- 3
+ # no matched normal required, but we are
+ # generating normal counts which have this name coded
+ normalname <- paste0(samplename, "_normal")
+ # other cell_line specific parameter values
+ min_ploidy <- min_ploidy
+ max_ploidy <- max_ploidy
+ min_rho <- 0.99
+ max_rho <- 1.01
}
- }
-
- if (data_type=="wgs" | data_type=="WGS") {
- if (nsamples > 1) {
- print(paste0("Running Battenberg in multisample mode on ", nsamples, " samples: ", paste0(samplename, collapse = ", ")))
+ if (analysis == "germline") {
+ calc_seg_baf_option <- 1
+ phasing_gamma <- 3
+ phasing_kmin <- 1
+ segmentation_gamma <- 3
+ segmentation_kmin <- 3
+ # no matched normal required,
+ # but we are generating normal counts which have this name coded
+ normalname <- paste0(samplename, "_normal")
+ min_ploidy <- 1.5
+ max_ploidy <- 2.5
+ min_rho <- 0.99
+ max_rho <- 1.01
}
- chrom_names = get.chrom.names(imputeinfofile, ismale, analysis=analysis)
- } else if (data_type=="snp6" | data_type=="SNP6") {
- if (nsamples > 1) {
- stop(paste0("Battenberg multisample mode has not been tested with SNP6 data"))
+
+ if (data_type == "wgs" && is.na(ismale)) {
+ log_failure("Please provide a boolean denominator whether \\
+ this sample represents a male donor")
}
- chrom_names = get.chrom.names(imputeinfofile, TRUE)
- logr_file = paste(samplename, "_mutantLogR.tab", sep="")
- allelecounts_file = NULL
- }
- print(chrom_names)
- for (sampleidx in 1:nsamples) {
- if (!skip_preprocessing[sampleidx]) {
- if (data_type=="wgs" | data_type=="WGS") {
- # Setup for parallel computing
- clp = parallel::makeCluster(nthreads,outfile="")
- doParallel::registerDoParallel(clp)
-
- if (analysis == "paired"){
-
- if (is.null(normalname)|is.na(normalname)){
- stop("No normal sample is specified for 'paired analysis' - a normal paired BAM is required")
- }
- prepare_wgs(chrom_names=chrom_names,
- tumourbam=sample_data_file[sampleidx],
- normalbam=normal_data_file,
- tumourname=samplename[sampleidx],
- normalname=normalname,
- g1000allelesprefix=g1000allelesprefix,
- g1000prefix=g1000prefix,
- gccorrectprefix=gccorrectprefix,
- repliccorrectprefix=repliccorrectprefix,
- min_base_qual=min_base_qual,
- min_map_qual=min_map_qual,
- allelecounter_exe=allelecounter_exe,
- min_normal_depth=min_normal_depth,
- nthreads=nthreads,
- skip_allele_counting=skip_allele_counting[sampleidx],
- skip_allele_counting_normal = (sampleidx > 1))
-
+
+ if (data_type == "wgs" && is.na(g1000allelesprefix)) {
+ log_failure("Please provide a path to 1000 Genomes allele reference files")
+ }
+
+ if (data_type == "wgs" && is.null(gccorrectprefix)) {
+ log_failure("Please provide a path to GC content reference files")
+ }
+
+ if (data_type == "wgs" && !file.exists(problemloci)) {
+ log_failure("Please provide a path to a problematic loci file")
+ }
+
+ # check whether the reference_info_file contains correct paths
+ if (!is.na(reference_info_file)) {
+ if (!file.exists(reference_info_file)) {
+ log_failure("reference_info_file provided but does not exist: {reference_info_file}")
+ }
+ check_imputeinfofile(
+ reference_info_file = reference_info_file,
+ is_male = ismale,
+ usebeagle = (phasing_engine == "beagle")
+ )
+ }
+
+ # check whether multisample case
+ nsamples <- length(samplename)
+
+ if (data_type == "wgs" || data_type == "WGS") {
+ if (nsamples > 1) {
+ log_info("Running Battenberg in multisample mode on {nsamples} samples: \\
+ {paste(samplename, collapse = ', ')}")
+ }
+ chrom_names <- get_chrom_names(
+ reference_info_file = reference_info_file,
+ is_male = ismale,
+ analysis = analysis,
+ chrom_names = chrom_names,
+ usebeagle = (phasing_engine == "beagle"),
+ beagleref_dir = beagleref_dir
+ )
+ } else if (data_type == "snp6" || data_type == "SNP6") {
+ if (nsamples > 1) {
+ log_failure("Battenberg multisample mode has \\
+ not been tested with SNP6 data")
+ }
+ chrom_names <- get_chrom_names(reference_info_file, TRUE, chrom_names = chrom_names)
+ }
+ # Global parameter validation
+ if (is.na(allele_counts_dir) || !dir.exists(allele_counts_dir)) {
+ log_failure("allele_counts_dir is missing or invalid: {allele_counts_dir}")
+ }
+ if (is.na(phasing_results_dir) && is.na(beagle_input_dir)) {
+ log_failure("Either phasing_results_dir or beagle_input_dir must be provided.")
+ }
+
+ for (sampleidx in 1:nsamples) {
+ if (data_type == "wgs" || data_type == "WGS") {
+ # Setup for parallel computing using chromosomes_in_parallel
+ if (chromosomes_in_parallel > 1) {
+ # In preprocessing, we run samples sequentially in a for loop.
+ # So each sample uses chromosomes_in_parallel for the parallel map.
+ clp <- parallel::makeCluster(chromosomes_in_parallel, outfile = "")
+ doParallel::registerDoParallel(clp)
+
+ # Export functions to workers for cluster stability
+ vars_to_export <- c("prepare_wgs", "prepare_wgs_cell_line", "prepare_wgs_germline", "libs")
+ parallel::clusterExport(clp, varlist = vars_to_export, envir = environment())
+ }
+
+
+ if (analysis == "paired") {
+ if (is.null(normalname) || is.na(normalname)) {
+ log_failure("No normal sample is specified for \\
+ 'paired analysis' - a normal paired BAM is required")
+ }
+ prepare_wgs(
+ chrom_names = chrom_names,
+ tumourbam = sample_data_file[sampleidx],
+ normalbam = normal_data_file,
+ tumourname = samplename[sampleidx],
+ normalname = normalname,
+ g1000allelesprefix = g1000allelesprefix,
+ g1000prefix = g1000prefix,
+ gccorrectprefix = gccorrectprefix,
+ repliccorrectprefix = repliccorrectprefix,
+ min_base_qual = min_base_qual,
+ min_map_qual = min_map_qual,
+ allele_counts_dir = allele_counts_dir,
+ min_normal_depth = min_normal_depth,
+ nthreads = threads_per_chromosome, # Pass down the inner threads budget (threads per chromosome)
+ libs = libs
+ )
} else if (analysis == "cell_line") {
- prepare_wgs_cell_line(chrom_names=chrom_names,
- chrom_coord=chrom_coord_file,
- tumourbam=sample_data_file,
- tumourname=samplename,
- g1000lociprefix=g1000prefix,
- g1000allelesprefix=g1000allelesprefix,
- gamma_ivd=1e5,
- kmin_ivd=50,
- centromere_noise_seg_size=1e6,
- centromere_dist=5e5,
- min_het_dist=1e5,
- gamma_logr=100,
- length_adjacent=5e4,
- gccorrectprefix=gccorrectprefix,
- repliccorrectprefix=repliccorrectprefix,
- min_base_qual=min_base_qual,
- min_map_qual=min_map_qual,
- allelecounter_exe=allelecounter_exe,
- min_normal_depth=min_normal_depth,
- skip_allele_counting=skip_allele_counting[sampleidx])
- } else if (analysis == "germline"){
-
- prepare_wgs_germline(chrom_names=chrom_names,
- chrom_coord=chrom_coord_file,
- germlinebam=sample_data_file,
- germlinename=samplename,
- g1000lociprefix=g1000prefix,
- g1000allelesprefix=g1000allelesprefix,
- gamma_ivd=1e5,
- kmin_ivd=50,
- centromere_noise_seg_size=1e6,
- centromere_dist=5e5,
- min_het_dist=2e3,
- gamma_logr=100,
- length_adjacent=5e4,
- gccorrectprefix=gccorrectprefix,
- repliccorrectprefix=repliccorrectprefix,
- min_base_qual=min_base_qual,
- min_map_qual=min_map_qual,
- allelecounter_exe=allelecounter_exe,
- min_normal_depth=min_normal_depth,
- skip_allele_counting=skip_allele_counting[sampleidx])
+ prepare_wgs_cell_line(
+ chrom_names = chrom_names,
+ chrom_coord = chrom_coord_file,
+ tumourbam = sample_data_file[sampleidx],
+ tumourname = samplename[sampleidx],
+ g1000lociprefix = g1000prefix,
+ g1000allelesprefix = g1000allelesprefix,
+ gamma_ivd = 1e5,
+ kmin_ivd = 50,
+ centromere_noise_seg_size = 1e6,
+ centromere_dist = 5e5,
+ min_het_dist = 1e5,
+ gamma_logr = 100,
+ length_adjacent = 5e4,
+ gccorrectprefix = gccorrectprefix,
+ repliccorrectprefix = repliccorrectprefix,
+ min_base_qual = min_base_qual,
+ min_map_qual = min_map_qual,
+ allele_counts_dir = allele_counts_dir,
+ min_normal_depth = min_normal_depth,
+ libs = libs
+ )
+ } else if (analysis == "germline") {
+ prepare_wgs_germline(
+ chrom_names = chrom_names,
+ chrom_coord = chrom_coord_file,
+ germlinebam = sample_data_file[sampleidx],
+ germlinename = samplename[sampleidx],
+ g1000lociprefix = g1000prefix,
+ g1000allelesprefix = g1000allelesprefix,
+ gamma_ivd = 1e5,
+ kmin_ivd = 50,
+ centromere_noise_seg_size = 1e6,
+ centromere_dist = 5e5,
+ min_het_dist = 2e3,
+ gamma_logr = 100,
+ length_adjacent = 5e4,
+ gccorrectprefix = gccorrectprefix,
+ repliccorrectprefix = repliccorrectprefix,
+ min_base_qual = min_base_qual,
+ min_map_qual = min_map_qual,
+ allele_counts_dir = allele_counts_dir,
+ min_normal_depth = min_normal_depth,
+ libs = libs
+ )
}
-
-
+
# Kill the threads
- parallel::stopCluster(clp)
-
- } else if (data_type=="snp6" | data_type=="SNP6") {
-
- prepare_snp6(tumour_cel_file=sample_data_file[sampleidx],
- normal_cel_file=normal_data_file,
- tumourname=samplename[sampleidx],
- chrom_names=chrom_names,
- snp6_reference_info_file=snp6_reference_info_file,
- apt.probeset.genotype.exe=apt.probeset.genotype.exe,
- apt.probeset.summarize.exe=apt.probeset.summarize.exe,
- norm.geno.clust.exe=norm.geno.clust.exe,
- birdseed_report_file=birdseed_report_file,
- genomebuild=genomebuild)
-
+ if (chromosomes_in_parallel > 1) {
+ parallel::stopCluster(clp)
+ }
+ # Final GC after preprocessing batch for this sample
+ gc()
+ } else if (data_type == "snp6" || data_type == "SNP6") {
+ prepare_snp6(
+ tumour_cel_file = sample_data_file[sampleidx],
+ normal_cel_file = normal_data_file,
+ tumourname = samplename[sampleidx],
+ chrom_names = chrom_names,
+ snp6_reference_info_file = snp6_reference_info_file,
+ birdseed_report_file = "birdseed.report.txt",
+ genomebuild = genomebuild
+ )
} else {
- print("Unknown data type provided, please provide wgs or snp6")
- q(save="no", status=1)
+ log_failure("Unknown data type provided, please provide wgs or snp6")
+ q(save = "no", status = 1)
}
- }
-
- if (data_type=="snp6" | data_type=="SNP6") {
- # Infer what the gender is - WGS requires it to be specified
- gender = infer_gender_birdseed(birdseed_report_file)
- ismale = gender == "male"
- }
-
-
- if (!skip_phasing[sampleidx]) {
-
+
+ # Removed } else (end of if !skip_preprocessing) as skipping logic is now handled by presence of directories/files inside prepare functions or removed entirely.
+
+
+ if (data_type == "snp6" || data_type == "SNP6") {
+ # Infer what the gender is - WGS requires it to be specified
+ gender <- infer_gender_birdseed("birdseed.report.txt")
+ ismale <- gender == "male"
+ }
+
+
# if external phasing data is provided (as a vcf), split into chromosomes for use in haplotype reconstruction
if (!is.na(externalhaplotypefile) && file.exists(externalhaplotypefile)) {
externalhaplotypeprefix <- paste0(normalname, "_external_haplotypes_chr")
-
+
# if these files exist already, no need to split again
- if (any(!file.exists(paste0(externalhaplotypeprefix, 1:length(chrom_names), ".vcf")))) {
-
- print(paste0("Splitting external phasing data from ", externalhaplotypefile))
- split_input_haplotypes(chrom_names = chrom_names,
- externalhaplotypefile = externalhaplotypefile,
- outprefix = externalhaplotypeprefix)
+ if (any(!file.exists(paste0(externalhaplotypeprefix, seq_along(chrom_names), ".vcf")))) {
+ log_info("Splitting external phasing data from '{externalhaplotypefile}'")
+ split_input_haplotypes(
+ chrom_names = chrom_names,
+ externalhaplotypefile = externalhaplotypefile,
+ outprefix = externalhaplotypeprefix
+ )
} else {
- print("No need to split, external haplotype files per chromosome found")
+ log_info("No need to split, external haplotype files per chromosome found")
}
} else {
externalhaplotypeprefix <- NA
}
-
+
# Setup for parallel computing
- clp = parallel::makeCluster(nthreads,outfile="")
- doParallel::registerDoParallel(clp)
-
+ if (chromosomes_in_parallel > 1) {
+ clp <- parallel::makeCluster(chromosomes_in_parallel, outfile = "")
+ doParallel::registerDoParallel(clp)
+
+ # Export functions to workers for cluster stability
+ vars_to_export <- c("run_haplotyping", "run_haplotyping_germline", "libs")
+ parallel::clusterExport(clp, varlist = vars_to_export, envir = environment())
+ }
+
# Reconstruct haplotypes
- # mclapply(1:length(chrom_names), function(chrom) {
- if (analysis=="germline"){
- foreach::foreach (i=1:length(chrom_names)) %dopar% {
- .libPaths(libs)
- chrom = chrom_names[i]
- print(chrom)
-
- run_haplotyping_germline(chrom=chrom,
- germlinename=samplename,
- normalname=normalname,
- ismale=ismale,
- imputeinfofile=imputeinfofile,
- problemloci=problemloci,
- impute_exe=impute_exe,
- min_normal_depth=min_normal_depth,
- chrom_names=chrom_names,
- externalhaplotypeprefix = NA,
- use_previous_imputation=F,
- snp6_reference_info_file=NA,
- heterozygousFilter=NA,
- usebeagle=usebeagle,
- beaglejar=beaglejar,
- beagleref=gsub("CHROMNAME",chrom,beagleref.template),
- beagleplink=gsub("CHROMNAME",chrom,beagleplink.template),
- beaglemaxmem=beaglemaxmem,
- beaglenthreads=beaglenthreads,
- beaglewindow=beaglewindow,
- beagleoverlap=beagleoverlap)
- }
- } else {
- foreach::foreach (i=1:length(chrom_names)) %dopar% {
+ do_haplotyping <- function(i) {
+ .libPaths(libs)
+ chrom <- chrom_names[i]
+ if (analysis == "germline") {
+ log_info("germline chrom {chrom}")
+ run_haplotyping_germline(
+ chrom = chrom,
+ germlinename = samplename[sampleidx],
+ normalname = normalname,
+ ismale = ismale,
+ problemloci = problemloci,
+ phasing_results_dir = phasing_results_dir,
+ min_normal_depth = min_normal_depth,
+ chrom_names = chrom_names,
+ reference_info_file = reference_info_file,
+ beagle_input_dir = beagle_input_dir,
+ allele_frequencies_dir = allele_counts_dir,
+ chrom_coord_file = chrom_coord_file,
+ beaglejar = beaglejar,
+ beagleref_dir = beagleref_dir,
+ phasing_engine = phasing_engine,
+ threads_per_chromosome = threads_per_chromosome
+ )
+ } else {
.libPaths(libs)
- chrom = chrom_names[i]
- print(chrom)
- run_haplotyping(chrom=chrom,
- tumourname=samplename[sampleidx],
- normalname=normalname,
- ismale=ismale,
- imputeinfofile=imputeinfofile,
- problemloci=problemloci,
- impute_exe=impute_exe,
- min_normal_depth=min_normal_depth,
- chrom_names=chrom_names,
- snp6_reference_info_file=snp6_reference_info_file,
- heterozygousFilter=heterozygousFilter,
- usebeagle=usebeagle,
- beaglejar=beaglejar,
- beagleref=gsub("CHROMNAME", chrom, beagleref.template),
- beagleplink=gsub("CHROMNAME", chrom, beagleplink.template),
- beaglemaxmem=beaglemaxmem,
- beaglenthreads=beaglenthreads,
- beaglewindow=beaglewindow,
- beagleoverlap=beagleoverlap,
- externalhaplotypeprefix=externalhaplotypeprefix,
- use_previous_imputation=(sampleidx > 1))
+ chrom <- chrom_names[i]
+ log_info("chrom {chrom}")
+ run_haplotyping(
+ chrom = chrom,
+ tumourname = samplename[sampleidx],
+ normalname = normalname,
+ ismale = ismale,
+ problemloci = problemloci,
+ phasing_results_dir = phasing_results_dir,
+ min_normal_depth = min_normal_depth,
+ chrom_names = chrom_names,
+ reference_info_file = reference_info_file,
+ beagle_input_dir = beagle_input_dir,
+ allele_frequencies_dir = allele_counts_dir,
+ chrom_coord_file = chrom_coord_file,
+ beaglejar = beaglejar,
+ beagleref_dir = beagleref_dir,
+ phasing_engine = phasing_engine,
+ threads_per_chromosome = threads_per_chromosome
+ )
}
}
-
+ run_with_error_handling(
+ iterator = seq_along(chrom_names),
+ func = do_haplotyping,
+ libs = libs,
+ nthreads = threads_per_chromosome
+ )
+
# Kill the threads as from here its all single core
- parallel::stopCluster(clp)
-
+ if (chromosomes_in_parallel > 1) {
+ parallel::stopCluster(clp)
+ }
+
+ # Trigger GC after phasing completes
+ gc()
+
# Combine all the BAF output into a single file
- combine.baf.files(inputfile.prefix=paste(samplename[sampleidx], "_chr", sep=""),
- inputfile.postfix="_heterozygousMutBAFs_haplotyped.txt",
- outputfile=paste(samplename[sampleidx], "_heterozygousMutBAFs_haplotyped.txt", sep=""),
- chr_names=chrom_names)
+ concatenate_baf_files(
+ input_start = paste(samplename[sampleidx], "_chr", sep = ""),
+ input_end = "_heterozygousMutBAFs_haplotyped.txt",
+ output_file = paste(samplename[sampleidx], "_heterozygousMutBAFs_haplotyped.txt", sep = ""),
+ chr_names = chrom_names
+ )
+
+ # Determine where to look for phasing results
+ phasing_source_dir <- "."
+ segment_baf_phased(
+ samplename = samplename[sampleidx],
+ inputfile = file.path(phasing_source_dir, paste(samplename[sampleidx], "_heterozygousMutBAFs_haplotyped.txt", sep = "")),
+ outputfile = paste(samplename[sampleidx], ".BAFsegmented.txt", sep = ""),
+ prior_breakpoints_file = prior_breakpoints_file,
+ gamma = segmentation_gamma,
+ phasegamma = phasing_gamma,
+ kmin = segmentation_kmin,
+ phasekmin = phasing_kmin,
+ calc_seg_baf_option = calc_seg_baf_option
+ )
+
+ if (nsamples > 1 || write_battenberg_phasing) {
+ # Write the Battenberg phasing information to disk as a vcf
+ write_battenberg_phasing(
+ tumourname = samplename[sampleidx],
+ SNPfiles = file.path(
+ allele_counts_dir,
+ paste0(samplename[sampleidx], "_alleleFrequencies_chr", chrom_names, ".txt")
+ ),
+ imputedHaplotypeFiles = file.path(phasing_source_dir, paste0(
+ samplename[sampleidx],
+ "_impute_output_chr", chrom_names,
+ "_allHaplotypeInfo.txt"
+ )),
+ bafsegmented_file = paste0(samplename[sampleidx], ".BAFsegmented.txt"),
+ outprefix = paste0(samplename[sampleidx], "_Battenberg_phased_chr"),
+ chrom_names = chrom_names,
+ include_homozygous = FALSE
+ )
+ }
}
-
- # Segment the phased and haplotyped BAF data
- segment.baf.phased(samplename=samplename[sampleidx],
- inputfile=paste(samplename[sampleidx], "_heterozygousMutBAFs_haplotyped.txt", sep=""),
- outputfile=paste(samplename[sampleidx], ".BAFsegmented.txt", sep=""),
- prior_breakpoints_file=prior_breakpoints_file,
- gamma=segmentation_gamma,
- phasegamma=phasing_gamma,
- kmin=segmentation_kmin,
- phasekmin=phasing_kmin,
- calc_seg_baf_option=calc_seg_baf_option)
-
- if (nsamples > 1 | write_battenberg_phasing) {
- # Write the Battenberg phasing information to disk as a vcf
- write_battenberg_phasing(tumourname = samplename[sampleidx],
- SNPfiles = paste0(samplename[sampleidx], "_alleleFrequencies_chr", chrom_names, ".txt"),
- imputedHaplotypeFiles = paste0(samplename[sampleidx], "_impute_output_chr", chrom_names, "_allHaplotypeInfo.txt"),
- bafsegmented_file = paste0(samplename[sampleidx], ".BAFsegmented.txt"),
- outprefix = paste0(samplename[sampleidx], "_Battenberg_phased_chr"),
- chrom_names = chrom_names,
- include_homozygous = F)
+
+ # if this is a multisample run, combine the battenberg phasing outputs, incorporate it and resegment
+ if (nsamples > 1) {
+ log_info("Constructing multisample phasing")
+ multisamplehaplotypeprefix <- paste0(normalname, "_multisample_haplotypes_chr")
+
+
+ if (chromosomes_in_parallel > 1) {
+ clp <- parallel::makeCluster(chromosomes_in_parallel, outfile = "")
+ doParallel::registerDoParallel(clp)
+ }
+
+ run_with_error_handling(seq_along(chrom_names), function(i) {
+ chrom <- chrom_names[i]
+ log_info("multisample phasing chrom {chrom}")
+
+ get_multisample_phasing(
+ chrom = chrom,
+ bbphasingprefixes = paste(samplename, "_Battenberg_phased_chr", sep = ""),
+ maxlag = multisample_maxlag,
+ relative_weight_balanced = multisample_relative_weight_balanced,
+ outprefix = multisamplehaplotypeprefix
+ )
+ }, libs, nthreads = threads_per_chromosome)
+
+ # continue over all samples to incorporate the multisample phasing
+ for (sampleidx in 1:nsamples) {
+ # rename the original files without multisample phasing info
+ MutBAFfiles <- paste0(samplename[sampleidx], "_chr", chrom_names, "_heterozygousMutBAFs_haplotyped.txt")
+ heterozygousdatafiles <- paste0(samplename[sampleidx], "_chr", chrom_names, "_heterozygousData.png")
+ raffiles <- paste0(samplename[sampleidx], "_RAFseg_chr", chrom_names, ".png")
+ segfiles <- paste0(samplename[sampleidx], "_segment_chr", chrom_names, ".png")
+ haplotypedandbafsegmentedfiles <- paste0(samplename[sampleidx], c("_heterozygousMutBAFs_haplotyped.txt", ".BAFsegmented.txt"))
+
+ file.copy(
+ from = file.path(phasing_source_dir, MutBAFfiles),
+ to = gsub(
+ pattern = ".txt$", replacement = "_noMulti.txt",
+ x = MutBAFfiles
+ ), overwrite = TRUE
+ )
+ file.copy(
+ from = file.path(phasing_source_dir, heterozygousdatafiles),
+ to = gsub(
+ pattern = ".png$", replacement = "_noMulti.png",
+ x = heterozygousdatafiles
+ ), overwrite = TRUE
+ )
+ file.copy(
+ from = file.path(phasing_source_dir, raffiles),
+ to = gsub(
+ pattern = ".png$", replacement = "_noMulti.png",
+ x = raffiles
+ ), overwrite = TRUE
+ )
+ file.copy(
+ from = file.path(phasing_source_dir, segfiles),
+ to = gsub(
+ pattern = ".png$", replacement = "_noMulti.png",
+ x = segfiles
+ ), overwrite = TRUE
+ )
+ file.copy(
+ from = file.path(phasing_source_dir, haplotypedandbafsegmentedfiles),
+ to = gsub(
+ pattern = ".txt$", replacement = "_noMulti.txt",
+ x = haplotypedandbafsegmentedfiles
+ ), overwrite = TRUE
+ )
+ # done renaming, next sections will overwrite orignals
+
+ run_with_error_handling(seq_along(chrom_names), function(i) {
+ chrom <- chrom_names[i]
+ log_info("sample in nsamples chrom {chrom}")
+
+ # Reconstruct haplotypes from external file
+ input_known_haplotypes(
+ chrom = chrom,
+ chrom_names = chrom_names,
+ imputedHaplotypeFile = file.path(phasing_source_dir, paste(samplename[sampleidx],
+ "_impute_output_chr", chrom,
+ "_allHaplotypeInfo.txt",
+ sep = ""
+ )),
+ externalHaplotypeFile = paste(multisamplehaplotypeprefix, chrom,
+ ".vcf",
+ sep = ""
+ ),
+ oldfilesuffix = "_noMulti.txt"
+ )
+
+ # Get BAFs for the specific chromosome
+ GetChromosomeBAFs(
+ chrom = chrom,
+ SNP_file = file.path(allele_counts_dir, paste(samplename[sampleidx], "_alleleFrequencies_chr",
+ chrom, ".txt",
+ sep = ""
+ )),
+ haplotypeFile = file.path(phasing_source_dir, paste(samplename[sampleidx], "_impute_output_chr",
+ chrom, "_allHaplotypeInfo.txt",
+ sep = ""
+ )),
+ samplename = samplename[sampleidx],
+ outfile = paste(samplename[sampleidx], "_chr", chrom,
+ "_heterozygousMutBAFs_haplotyped.txt",
+ sep = ""
+ ),
+ chr_names = chrom_names,
+ minCounts = min_normal_depth
+ )
+
+ # Plot the intermediate results
+ plot_haplotype_data(
+ haplotyped_baf_file = paste(samplename[sampleidx], "_chr", chrom,
+ "_heterozygousMutBAFs_haplotyped.txt",
+ sep = ""
+ ),
+ image_file_name = paste(samplename[sampleidx], "_chr", chrom,
+ "_heterozygousData.png",
+ sep = ""
+ ),
+ samplename = samplename[sampleidx],
+ chrom = chrom
+ )
+ }, libs, nthreads = threads_per_chromosome)
+ }
+
+ # Kill the threads as from here its single core
+ # Kill the threads as from here its single core
+ if (chromosomes_in_parallel > 1) {
+ parallel::stopCluster(clp)
+ }
+
+ for (sampleidx in 1:nsamples) {
+ # Combine all the BAF output into a single file
+ concatenate_baf_files(
+ input_start = paste0(samplename[sampleidx], "_chr"),
+ input_end = "_heterozygousMutBAFs_haplotyped.txt",
+ output_file = paste0(samplename[sampleidx], "_heterozygousMutBAFs_haplotyped.txt"),
+ chr_names = chrom_names
+ )
+ }
+ # Segment the phased and haplotyped BAF data
+ segment_baf_phased_multisample(
+ samplename = samplename,
+ inputfile = paste(samplename, "_heterozygousMutBAFs_haplotyped.txt", sep = ""),
+ outputfile = paste(samplename, ".BAFsegmented.txt", sep = ""),
+ prior_breakpoints_file = prior_breakpoints_file,
+ gamma = segmentation_gamma_multisample,
+ calc_seg_baf_option = calc_seg_baf_option,
+ GENOMEBUILD = genomebuild
+ )
}
-
- }
-
- # if this is a multisample run, combine the battenberg phasing outputs, incorporate it and resegment
- if (nsamples > 1) {
- print("Constructing multisample phasing")
- multisamplehaplotypeprefix <- paste0(normalname, "_multisample_haplotypes_chr")
-
-
+
+ # Setup for parallel computing
# Setup for parallel computing
- clp = parallel::makeCluster(nthreads,outfile="")
- doParallel::registerDoParallel(clp)
-
- # Reconstruct haplotypes
- .libPaths()
- foreach::foreach (i=1:length(chrom_names)) %dopar% {
- .libPaths(libs)
- .libPaths()
- chrom = chrom_names[i]
- print(chrom)
-
- get_multisample_phasing(chrom = chrom,
- bbphasingprefixes = paste0(samplename, "_Battenberg_phased_chr"),
- maxlag = multisample_maxlag,
- relative_weight_balanced = multisample_relative_weight_balanced,
- outprefix = multisamplehaplotypeprefix)
+ # Setup for parallel computing (Sample-level Parallelism)
+ # Dynamic Budgeting:
+ # Total Cores Needed = (Samples_in_parallel) * (Threads_per_sample)
+ # Here we use nthreads (threads_per_chromosome) as the inner budget per sample,
+ # and chromosomes_in_parallel as the concurrency control (if interpreted as "parallel tasks")
+
+ # For fit_copy_number loop, we parallelize over SAMPLES.
+ # Let's say user wants X concurrent samples.
+ # We will treat 'chromosomes_in_parallel' as the 'max_concurrent_jobs' here for consistency with outer logic.
+
+ if (chromosomes_in_parallel > 1) {
+ num_sample_workers <- min(nsamples, chromosomes_in_parallel)
+ clp <- parallel::makeCluster(num_sample_workers, outfile = "")
+ doParallel::registerDoParallel(clp)
+
+ # Export everything needed to the cluster
+ vars_to_export <- c(
+ "fit_copy_number", "call_subclones", "callChrXsubclones",
+ "make_posthoc_plots", "cnfit_to_refit_suggestions", "libs"
+ )
+ parallel::clusterExport(clp, varlist = vars_to_export, envir = environment())
}
-
- # continue over all samples to incorporate the multisample phasing
- for (sampleidx in 1:nsamples) {
-
- # rename the original files without multisample phasing info
- MutBAFfiles <- paste0(samplename[sampleidx], "_chr", chrom_names, "_heterozygousMutBAFs_haplotyped.txt")
- heterozygousdatafiles <- paste0(samplename[sampleidx], "_chr", chrom_names, "_heterozygousData.png")
- raffiles <- paste0(samplename[sampleidx], "_RAFseg_chr", chrom_names, ".png")
- segfiles <- paste0(samplename[sampleidx], "_segment_chr", chrom_names, ".png")
- haplotypedandbafsegmentedfiles <- paste0(samplename[sampleidx], c("_heterozygousMutBAFs_haplotyped.txt", ".BAFsegmented.txt"))
-
- file.copy(from = MutBAFfiles, to = gsub(pattern = ".txt$", replacement = "_noMulti.txt", x = MutBAFfiles), overwrite = T)
- file.copy(from = heterozygousdatafiles, to = gsub(pattern = ".png$", replacement = "_noMulti.png", x = heterozygousdatafiles), overwrite = T)
- file.copy(from = raffiles, to = gsub(pattern = ".png$", replacement = "_noMulti.png", x = raffiles), overwrite = T)
- file.copy(from = segfiles, to = gsub(pattern = ".png$", replacement = "_noMulti.png", x = segfiles), overwrite = T)
- file.copy(from = haplotypedandbafsegmentedfiles, to = gsub(pattern = ".txt$", replacement = "_noMulti.txt", x = haplotypedandbafsegmentedfiles), overwrite = T)
- # done renaming, next sections will overwrite orignals
-
-
- foreach::foreach (i=1:length(chrom_names)) %dopar% {
- .libPaths(libs)
- chrom = chrom_names[i]
- print(chrom)
-
- input_known_haplotypes(chrom = chrom,
- chrom_names = chrom_names,
- imputedHaplotypeFile = paste0(samplename[sampleidx], "_impute_output_chr", chrom, "_allHaplotypeInfo.txt"),
- externalHaplotypeFile = paste0(multisamplehaplotypeprefix, chrom, ".vcf"),
- oldfilesuffix = "_noMulti.txt")
-
- GetChromosomeBAFs(chrom=chrom,
- SNP_file=paste0(samplename[sampleidx], "_alleleFrequencies_chr", chrom, ".txt"),
- haplotypeFile=paste0(samplename[sampleidx], "_impute_output_chr", chrom, "_allHaplotypeInfo.txt"),
- samplename=samplename[sampleidx],
- outfile=paste0(samplename[sampleidx], "_chr", chrom, "_heterozygousMutBAFs_haplotyped.txt"),
- chr_names=chrom_names,
- minCounts=min_normal_depth)
-
- # Plot what we have until this point
- plot.haplotype.data(haplotyped.baf.file=paste0(samplename[sampleidx], "_chr", chrom, "_heterozygousMutBAFs_haplotyped.txt"),
- imageFileName=paste0(samplename[sampleidx],"_chr",chrom,"_heterozygousData.png"),
- samplename=samplename[sampleidx],
- chrom=chrom,
- chr_names=chrom_names)
+
+ # Use the universal helper to process each sample
+ run_with_error_handling(seq_len(nsamples), function(sampleidx) {
+ # Scoping ensures this function sees 'samplename', 'libs', etc.
+ log_info("Fitting final copy number and calling subclones for sample '{samplename[sampleidx]}'")
+
+ # Determine file paths based on data type and analysis mode
+ if (data_type == "wgs" || data_type == "WGS") {
+ # Combined files (BAF/LogR) are usually in the current directory (results) after preprocessing,
+ # but could optionally be in the allele_counts_dir. We check both to be robust.
+ logr_name <- paste(samplename[sampleidx], "_mutantLogR_gcCorrected.tab", sep = "")
+ logr_file <- if (file.exists(logr_name)) logr_name else file.path(allele_counts_dir, logr_name)
+
+ if (analysis == "paired") {
+ ac_name <- paste(samplename[sampleidx], "_alleleCounts.tab", sep = "")
+ allelecounts_file <- if (file.exists(ac_name)) ac_name else file.path(allele_counts_dir, ac_name)
+ } else {
+ allelecounts_file <- NULL
+ }
}
-
- }
-
- # Kill the threads as from here its single core
- parallel::stopCluster(clp)
-
- for (sampleidx in 1:nsamples) {
-
- # Combine all the BAF output into a single file
- combine.baf.files(inputfile.prefix=paste0(samplename[sampleidx], "_chr"),
- inputfile.postfix="_heterozygousMutBAFs_haplotyped.txt",
- outputfile=paste0(samplename[sampleidx], "_heterozygousMutBAFs_haplotyped.txt"),
- chr_names=chrom_names)
-
- }
- # Segment the phased and haplotyped BAF data
- segment.baf.phased.multisample(samplename=samplename,
- inputfile=paste(samplename, "_heterozygousMutBAFs_haplotyped.txt", sep=""),
- outputfile=paste(samplename, ".BAFsegmented.txt", sep=""),
- prior_breakpoints_file=prior_breakpoints_file,
- gamma=segmentation_gamma_multisample,
- calc_seg_baf_option=calc_seg_baf_option,
- GENOMEBUILD=genomebuild)
-
- }
-
- # Setup for parallel computing
- clp = parallel::makeCluster(min(nthreads, nsamples),outfile="")
- doParallel::registerDoParallel(clp)
- # for (sampleidx in 1:nsamples) {
- foreach::foreach (sampleidx=1:nsamples) %dopar% {
- .libPaths(libs)
- print(paste0("Fitting final copy number and calling subclones for sample ", samplename[sampleidx]))
-
- if (data_type=="wgs" | data_type=="WGS") {
- logr_file = paste(samplename[sampleidx], "_mutantLogR_gcCorrected.tab", sep="")
- if (analysis=="paired") {
- allelecounts_file = paste(samplename[sampleidx], "_alleleCounts.tab", sep="")
- } else {
- # Not produced by a number of analysis and is required for some plots. Setting to NULL makes the pipeline not attempt to create these plots
- allelecounts_file = NULL
+
+ # Calculate safe inner threads to avoid thrashing
+ # Each sample worker gets 'threads_per_chromosome' budget for its internal tasks (like ASCAT grid search)
+ # We trust the user to have set threads_per_chromosome appropriately relative to chromosomes_in_parallel.
+ inner_threads <- threads_per_chromosome
+
+ log_info(
+ "Parallel Execution: concurrent_samples={min(nsamples, chromosomes_in_parallel)}, inner_threads={inner_threads} (per sample)"
+ )
+ # Parallel workers will now report their index and error details if they fail
+ res_fit <- fit_copy_number(
+ samplename = samplename[sampleidx],
+ outputfile_prefix = paste(samplename[sampleidx], "_", sep = ""),
+ inputfile_baf_segmented = paste(samplename[sampleidx], ".BAFsegmented.txt", sep = ""),
+ inputfile_baf = (function(f, d) if (file.exists(f)) f else file.path(d, f))(
+ paste(samplename[sampleidx], "_mutantBAF.tab", sep = ""),
+ allele_counts_dir
+ ),
+ inputfile_logr = logr_file,
+ dist_choice = clonality_dist_metric,
+ ascat_dist_choice = ascat_dist_metric,
+ min_ploidy = min_ploidy,
+ max_ploidy = max_ploidy,
+ min_rho = min_rho,
+ max_rho = max_rho,
+ min_goodness = min_goodness,
+ uninformative_baf_threshold = uninformative_baf_threshold,
+ gamma_param = platform_gamma,
+ use_preset_rho_psi = FALSE,
+ preset_rho = NA,
+ preset_psi = NA,
+ read_depth = 30,
+ analysis = analysis,
+ nthreads = inner_threads,
+ enhanced_grid_search = enhanced_grid_search,
+ n_neighbors_search = n_neighbors_search,
+ grid_psi_step = grid_psi_step,
+ grid_rho_step = grid_rho_step,
+ local_min_window_size = local_min_window_size
+ )
+
+ if (is.null(res_fit)) {
+ log_info("Skipping subclonal analysis for {samplename[sampleidx]} due to fit failure.")
+ return(NULL)
}
+
+ # Fit a second CN state (subclonal)
+ log_info("call_subclones")
+ call_subclones(
+ sample_name = samplename[sampleidx],
+ baf_segmented_file = paste(samplename[sampleidx], ".BAFsegmented.txt", sep = ""),
+ logr_file = logr_file,
+ rho_psi_file = paste(samplename[sampleidx], "_rho_and_psi.txt", sep = ""),
+ output_file = paste(samplename[sampleidx], "_copynumber.txt", sep = ""),
+ output_figures_prefix = paste(samplename[sampleidx], "_subclones_chr",
+ sep = ""
+ ),
+ output_gw_figures_prefix = paste(samplename[sampleidx],
+ "_BattenbergProfile",
+ sep = ""
+ ),
+ masking_output_file = paste(samplename[sampleidx],
+ "_segment_masking_details.txt",
+ sep = ""
+ ),
+ prior_breakpoints_file = prior_breakpoints_file,
+ chr_names = chrom_names,
+ gamma = platform_gamma,
+ segmentation_gamma = NA,
+ siglevel = 0.05,
+ maxdist = 0.01,
+ max_allowed_state = max_allowed_state,
+ nthreads = inner_threads,
+ cn_upper_limit = cn_upper_limit,
+ noperms = 1000,
+ calc_seg_baf_option = calc_seg_baf_option,
+ verbose_logging = verbose_logging
+ )
+
+ # Handle Male ChrX if applicable
+ if (ismale && "X" %in% chrom_names) {
+ log_info("callChrXsubclones")
+ callChrXsubclones(
+ tumourname = samplename[sampleidx],
+ X_gamma = 1000,
+ X_kmin = 100,
+ genomebuild = genomebuild,
+ AR = TRUE,
+ prior_breakpoints_file = prior_breakpoints_file,
+ chrom_names = chrom_names,
+ data_type = data_type
+ )
+ }
+
+ # Cleanup/Post-hoc visualisations
+ log_info("make_posthoc_plots")
+ make_posthoc_plots(
+ samplename = samplename[sampleidx],
+ logr_file = logr_file,
+ bafsegmented_file = paste(samplename[sampleidx], ".BAFsegmented.txt", sep = ""),
+ logrsegmented_file = paste(samplename[sampleidx], ".logRsegmented.txt", sep = ""),
+ allelecounts_file = allelecounts_file
+ )
+
+ # Generate refit suggestions
+ log_info("cnfit_to_refit_suggestions")
+ cnfit_to_refit_suggestions(
+ samplename = samplename[sampleidx],
+ subclones_file = paste(samplename[sampleidx], "_copynumber_extended.txt", sep = ""),
+ rho_psi_file = paste(samplename[sampleidx], "_rho_and_psi.txt", sep = ""),
+ gamma_param = platform_gamma
+ )
+ }, libs, nthreads = threads_per_chromosome)
+
+ # Trigger garbage collection after heavy fitting loop
+ gc()
+
+ if (nsamples > 1) {
+ log_info("Assessing mirrored subclonal allelic imbalance (MSAI)")
+ call_multisample_MSAI(
+ rdsprefix = multisamplehaplotypeprefix,
+ subclonesfiles = paste0(samplename, "_copynumber_extended.txt"),
+ chrom_names = chrom_names,
+ tumournames = samplename,
+ plotting = TRUE
+ )
}
-
- # Fit a clonal copy number profile
- fit.copy.number(samplename=samplename[sampleidx],
- outputfile.prefix=paste(samplename[sampleidx], "_", sep=""),
- inputfile.baf.segmented=paste(samplename[sampleidx], ".BAFsegmented.txt", sep=""),
- inputfile.baf=paste(samplename[sampleidx],"_mutantBAF.tab", sep=""),
- inputfile.logr=logr_file,
- dist_choice=clonality_dist_metric,
- ascat_dist_choice=ascat_dist_metric,
- min.ploidy=min_ploidy,
- max.ploidy=max_ploidy,
- min.rho=min_rho,
- max.rho=max_rho,
- min.goodness=min_goodness,
- uninformative_BAF_threshold=uninformative_BAF_threshold,
- gamma_param=platform_gamma,
- use_preset_rho_psi=F,
- preset_rho=NA,
- preset_psi=NA,
- read_depth=30,
- analysis=analysis,
- nthreads=nthreads,
- enhanced_grid_search=enhanced_grid_search)
-
- # Go over all segments, determine which segements are a mixture of two states and fit a second CN state
- print("callSubclones")
- callSubclones(sample.name=samplename[sampleidx],
- baf.segmented.file=paste(samplename[sampleidx], ".BAFsegmented.txt", sep=""),
- logr.file=logr_file,
- rho.psi.file=paste(samplename[sampleidx], "_rho_and_psi.txt",sep=""),
- output.file=paste(samplename[sampleidx],"_copynumber.txt", sep=""),
- output.figures.prefix=paste(samplename[sampleidx],"_subclones_chr", sep=""),
- output.gw.figures.prefix=paste(samplename[sampleidx],"_BattenbergProfile", sep=""),
- masking_output_file=paste(samplename[sampleidx], "_segment_masking_details.txt", sep=""),
- prior_breakpoints_file=prior_breakpoints_file,
- chr_names=chrom_names,
- gamma=platform_gamma,
- segmentation.gamma=NA,
- siglevel=0.05,
- maxdist=0.01,
- max_allowed_state=max_allowed_state,
- cn_upper_limit=cn_upper_limit,
- noperms=1000,
- calc_seg_baf_option=calc_seg_baf_option)
-
- # If patient is male, get copy number status of ChrX based only on logR segmentation (due to hemizygosity of SNPs)
- # Only do this when X chromosome is included
- if (ismale & "X" %in% chrom_names){
- print("callChrXsubclones")
- callChrXsubclones(tumourname=samplename[sampleidx],
- X_gamma=1000,
- X_kmin=100,
- genomebuild=genomebuild,
- AR=TRUE,
- prior_breakpoints_file=prior_breakpoints_file,
- chrom_names=chrom_names,
- data_type=data_type)
- }
-
- # Make some post-hoc plots
- print("make_posthoc_plots")
- make_posthoc_plots(samplename=samplename[sampleidx],
- logr_file=logr_file,
- bafsegmented_file=paste(samplename[sampleidx], ".BAFsegmented.txt", sep=""),
- logrsegmented_file=paste(samplename[sampleidx], ".logRsegmented.txt", sep=""),
- allelecounts_file=allelecounts_file)
-
- # Save refit suggestions for a future rerun
- print("cnfit_to_refit_suggestions")
- cnfit_to_refit_suggestions(samplename=samplename[sampleidx],
- subclones_file=paste(samplename[sampleidx], "_copynumber_extended.txt", sep=""),
- rho_psi_file=paste(samplename[sampleidx], "_rho_and_psi.txt", sep=""),
- gamma_param=platform_gamma)
- }
-
- # Kill the threads as last part again is single core
- parallel::stopCluster(clp)
-
- if (nsamples > 1) {
- print("Assessing mirrored subclonal allelic imbalance (MSAI)")
- call_multisample_MSAI(rdsprefix = multisamplehaplotypeprefix,
- subclonesfiles = paste0(samplename, "_copynumber_extended.txt"),
- chrom_names = chrom_names,
- tumournames = samplename,
- plotting = T)
}
}
diff --git a/R/cli.R b/R/cli.R
new file mode 100644
index 00000000..baa2cc23
--- /dev/null
+++ b/R/cli.R
@@ -0,0 +1,279 @@
+#' Battenberg Command Line Interface
+#' @description Parses command line arguments and executes the main battenberg function.
+#' @export
+battenberg_cli <- function() {
+ options(error = function() {
+ # Get the raw calls
+ calls <- sys.calls()
+
+ msg <- sprintf("Fatal Error: %s\n\n--- Call Stack ---", geterrmessage())
+ for (i in seq_along(calls)) {
+ msg <- paste(msg, sprintf("[%2d] %s", i, deparse(calls[[i]], width.cutoff = 500)[1]), sep = "\n")
+ }
+ log_failure("{msg}")
+ quit(save = "no", status = 1)
+ })
+ options(show.error.messages = TRUE)
+ options(keep.source = TRUE)
+ options(width = 10000)
+ options(warn = 1) # Print warnings immediately
+
+ option_list <- list(
+ # Core Analysis & Sample Info
+ optparse::make_option(c("-a", "--analysis"),
+ type = "character", default = "paired",
+ help = "Analysis type: paired, cell_line, germline"
+ ),
+ optparse::make_option(c("-t", "--samplename"),
+ type = "character",
+ help = "Tumour/Sample identifier"
+ ),
+ optparse::make_option(c("-n", "--normalname"),
+ type = "character",
+ help = "Matched normal identifier"
+ ),
+ optparse::make_option(c("--sample_data_file"),
+ type = "character",
+ help = "BAM/CEL for sample"
+ ),
+ optparse::make_option(c("--normal_data_file"),
+ type = "character",
+ help = "BAM/CEL for normal"
+ ),
+ optparse::make_option(c("--ismale"),
+ type = "logical",
+ default = NA,
+ help = "TRUE/FALSE for donor sex"
+ ),
+
+ # Reference Paths
+ optparse::make_option(c("--reference_info_file"),
+ type = "character", default = NA,
+ help = "Path to the reference info file (formerly impute_info.txt). Optional if beagle_input_dir and chrom_names are provided."
+ ),
+ optparse::make_option(c("--g1000prefix"),
+ type = "character",
+ help = "Prefix for 1000G SNP loci"
+ ),
+ optparse::make_option(c("--g1000allelesprefix"),
+ type = "character",
+ default = NA,
+ help = "Prefix for 1000G alleles"
+ ),
+ optparse::make_option(c("--gccorrectprefix"),
+ type = "character",
+ default = NULL,
+ help = "Prefix for GC correction"
+ ),
+ optparse::make_option(c("--repliccorrectprefix"),
+ type = "character",
+ default = NULL,
+ help = "Prefix for replication timing"
+ ),
+ optparse::make_option(c("--problemloci"),
+ type = "character",
+ help = "Path to problem loci file"
+ ),
+ optparse::make_option(c("--genomebuild"),
+ type = "character",
+ default = "hg38",
+ help = "hg19 or hg38"
+ ),
+ optparse::make_option(c("--chrom_coord_file"),
+ type = "character",
+ default = NULL
+ ),
+ optparse::make_option(c("--chrom_names"),
+ type = "character", default = NULL,
+ help = "Comma-separated list of chromosomes (e.g., 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,X)"
+ ),
+ optparse::make_option(c("--allele_counts_dir"),
+ type = "character", default = NA,
+ help = "Directory containing pre-calculated allele counts"
+ ),
+ optparse::make_option(c("--phasing_results_dir"),
+ type = "character", default = NA,
+ help = "Directory containing pre-calculated phasing results (Impute2 or Beagle)"
+ ),
+
+ # Executables & Hardware
+ optparse::make_option(c("--threads_per_chromosome"),
+ type = "integer", default = 8,
+ help = "Number of threads to use for each chromosome/sample task (Inner parallelism)"
+ ),
+ optparse::make_option(c("--chromosomes_in_parallel"),
+ type = "integer", default = 1,
+ help = "Number of chromosomes to process in parallel during phasing/haplotyping"
+ ),
+ optparse::make_option(c("--data_type"),
+ type = "character", default = "wgs",
+ help = "Type of data: wgs, cell_line, germline, or snp6"
+ ),
+ optparse::make_option(c("--phasing_engine"),
+ type = "character", default = "impute2",
+ help = "Phasing engine to use: impute2 or beagle (default impute2). Auto-detects beagle if --beaglejar is provided."
+ ),
+
+ # Beagle Specifics
+ optparse::make_option(c("--beagle_input_dir"),
+ type = "character", default = NA,
+ help = "Directory containing pre-calculated Beagle VCF output files"
+ ),
+ optparse::make_option(c("--beaglejar"),
+ type = "character", default = NA,
+ help = "Path to Beagle 5 JAR file. Trigger internal phasing if provided."
+ ),
+ optparse::make_option(c("--beagleref_dir"),
+ type = "character", default = NA,
+ help = "Directory containing Beagle reference VCF files."
+ ),
+
+ # Tuning Parameters (Gamma & Kmin)
+ optparse::make_option(c("--platform_gamma"),
+ type = "double", default = 1
+ ),
+ optparse::make_option(c("--phasing_gamma"),
+ type = "double", default = 1
+ ),
+ optparse::make_option(c("--segmentation_gamma"),
+ type = "double", default = 10
+ ),
+ optparse::make_option(c("--segmentation_gamma_multisample"),
+ type = "double", default = 5
+ ),
+ optparse::make_option(c("--segmentation_kmin"),
+ type = "integer", default = 3
+ ),
+ optparse::make_option(c("--phasing_kmin"),
+ type = "integer", default = 1
+ ),
+
+ # Grid Search / ASCAT Params
+ optparse::make_option(c("--clonality_dist_metric"),
+ type = "integer", default = 0
+ ),
+ optparse::make_option(c("--ascat_dist_metric"),
+ type = "integer", default = 1
+ ),
+ optparse::make_option(c("--min_ploidy"),
+ type = "double", default = 1.6
+ ),
+ optparse::make_option(c("--max_ploidy"),
+ type = "double", default = 4.8
+ ),
+ optparse::make_option(c("--min_rho"),
+ type = "double", default = 0.1
+ ),
+ optparse::make_option(c("--max_rho"),
+ type = "double", default = 1.0
+ ),
+ optparse::make_option(c("--min_goodness"),
+ type = "double", default = 0.63
+ ),
+ optparse::make_option(c("--uninformative_baf_threshold"),
+ type = "double", default = 0.51
+ ),
+ optparse::make_option(c("--enhanced_grid_search"),
+ type = "logical", default = FALSE, action = "store_true"
+ ),
+ optparse::make_option(c("--n_neighbors_search"),
+ type = "numeric", default = NULL,
+ help = "Number of top grid points to search (integer). Set to Inf for exhaustive search. If NULL, only local minima are searched."
+ ),
+ optparse::make_option(c("--grid_psi_step"),
+ type = "double", default = 0.05,
+ help = "Grid spacing for psi (ploidy) dimension, default 0.05"
+ ),
+ optparse::make_option(c("--grid_rho_step"),
+ type = "double", default = 0.01,
+ help = "Grid spacing for rho (cellularity) dimension, default 0.01"
+ ),
+ optparse::make_option(c("--local_min_window_size"),
+ type = "integer", default = 7,
+ help = "Window size for local minimum detection (3, 5, 7, 9, etc.), larger = stricter. Default 7."
+ ),
+
+ # Quality Thresholds
+ optparse::make_option(c("--min_normal_depth"),
+ type = "integer", default = 10
+ ),
+ optparse::make_option(c("--min_base_qual"),
+ type = "integer", default = 20
+ ),
+ optparse::make_option(c("--min_map_qual"),
+ type = "integer", default = 35
+ ),
+ optparse::make_option(c("--max_allowed_state"),
+ type = "integer", default = 250
+ ),
+ optparse::make_option(c("--cn_upper_limit"),
+ type = "integer", default = 1000
+ ),
+ optparse::make_option(c("--calc_seg_baf_option"),
+ type = "integer", default = 3
+ ),
+ optparse::make_option(c("--prior_breakpoints_file"),
+ type = "character", default = NULL
+ ),
+ optparse::make_option(c("--externalhaplotypefile"),
+ type = "character", default = NA
+ ),
+ optparse::make_option(c("--write_battenberg_phasing"),
+ type = "logical", default = TRUE
+ ),
+
+ # Multisample & SNP6 Legacy/Special
+ optparse::make_option(c("--multisample_maxlag"),
+ type = "integer",
+ default = 90
+ ),
+ optparse::make_option(c("--multisample_relative_weight_balanced"),
+ type = "double", default = 0.25
+ ),
+ optparse::make_option(c("--snp6_reference_info_file"),
+ type = "character", default = NA
+ ),
+
+ # Logging & Debug
+ optparse::make_option(c("--verbose_logging"),
+ type = "logical",
+ default = FALSE, action = "store_true"
+ ),
+ optparse::make_option(c("--logging_path"),
+ type = "character", default = "."
+ )
+ )
+
+ # Parse arguments
+ parser <- optparse::OptionParser(option_list = option_list)
+ opt <- optparse::parse_args(parser)
+
+ log_setup(opt$logging_path, opt$verbose_logging)
+
+ # Remove the 'help' flag which optparse adds automatically
+ opt$help <- NULL
+
+ log_info(strrep("=", 120))
+ log_info("BATTENBERG CLI: EXECUTION PARAMETERS")
+ log_info(strrep("=", 120))
+
+ # Sort names so they are easy to find in the log
+ opt_names <- sort(names(opt))
+ for (name in opt_names) {
+ # Cleanly format each argument and its value
+ val <- opt[[name]]
+ log_info(sprintf("%-40s : %s", name, paste(val, collapse = ", ")))
+ }
+ log_info(strrep("=", 120))
+
+ # Split chrom_names if provided as comma-separated string
+ if (!is.null(opt$chrom_names)) {
+ opt$chrom_names <- unlist(strsplit(opt$chrom_names, ","))
+ }
+
+ # Remove CLI-only arguments before calling the main logic
+ opt$logging_path <- NULL
+
+ # Execute main function
+ do.call(battenberg, opt)
+}
diff --git a/R/clonal_ascat.R b/R/clonal_ascat.R
deleted file mode 100755
index ab500fc7..00000000
--- a/R/clonal_ascat.R
+++ /dev/null
@@ -1,1721 +0,0 @@
-
-####################################################################################################
-
-#' A helper function to split the genome into parts
-#' @param SNPpos A data.frame with a row for each SNP. First column is chromosome, second column position
-#' @noRd
-split_genome = function(SNPpos) {
- # look for gaps of more than 1Mb and chromosome borders
- holesOver1Mb = which(diff(SNPpos[,2])>=1000000)+1
- chrBorders = which(diff(as.numeric(factor(SNPpos[,1],levels=unique(SNPpos[,1]))))!=0)+1
- holes = unique(sort(c(holesOver1Mb,chrBorders)))
-
- # find which segments are too small
- joincandidates=which(diff(c(0,holes,dim(SNPpos)[1]))<200)
-
- # if it's the first or last segment, just join to the one next to it, irrespective of chromosome and positions
- while (1 %in% joincandidates) {
- holes=holes[-1]
- joincandidates=which(diff(c(0,holes,dim(SNPpos)[1]))<200)
- }
- while ((length(holes)+1) %in% joincandidates) {
- holes=holes[-length(holes)]
- joincandidates=which(diff(c(0,holes,dim(SNPpos)[1]))<200)
- }
-
- while(length(joincandidates)!=0) {
- # the while loop is because after joining, segments may still be too small..
-
- startseg = c(1,holes)
- endseg = c(holes-1,dim(SNPpos)[1])
-
- # for each segment that is too short, see if it has the same chromosome as the segments before and after
- # the next always works because neither the first or the last segment is in joincandidates now
- previoussamechr = SNPpos[endseg[joincandidates-1],1]==SNPpos[startseg[joincandidates],1]
- nextsamechr = SNPpos[endseg[joincandidates],1]==SNPpos[startseg[joincandidates+1],1]
-
- distanceprevious = SNPpos[startseg[joincandidates],2]-SNPpos[endseg[joincandidates-1],2]
- distancenext = SNPpos[startseg[joincandidates+1],2]-SNPpos[endseg[joincandidates],2]
-
- # if both the same, decide based on distance, otherwise if one the same, take the other, if none, just take one.
- joins = ifelse(previoussamechr&nextsamechr,
- ifelse(distanceprevious>distancenext, joincandidates, joincandidates-1),
- ifelse(nextsamechr, joincandidates, joincandidates-1))
-
- holes=holes[-joins]
-
- joincandidates=which(diff(c(0,holes,dim(SNPpos)[1]))<200)
- }
- # if two neighboring segments are selected, this may make bigger segments then absolutely necessary, but I'm sure this is no problem.
-
- startseg = c(1,holes)
- endseg = c(holes-1,dim(SNPpos)[1])
-
- chr=list()
- for (i in 1:length(startseg)) {
- chr[[i]]=startseg[i]:endseg[i]
- }
-
- return(chr)
-}
-
-####################################################################################################
-#' Helper function that calculates a t-statistic
-#' @noRd
-studentise <-function( sample_size, sample_mean, sample_SD, mu_pop ) # kjd 18-12-2013
-{
- tvar = ( sample_mean - mu_pop ) * sqrt( sample_size ) / sample_SD
-
- return( tvar )
-
-}
-
-####################################################################################################
-#' This function calculates a P-value, for a test where the null hypothesis is that
-#' the sample was drawn from a Gaussian population with the specified mean "mu_pop".
-#' @noRd
-calc_Pvalue_t_twotailed <-function( sample_size, sample_mean, sample_SD, mu_pop, max_dist) # kjd 18-12-2013
-{
- tvar = ( sample_mean - mu_pop ) * sqrt( sample_size ) / sample_SD
-
- if( tvar < 0 )
- {
- lower_tail_prob = pt( tvar , df = sample_size - 1 , lower.tail = TRUE )
-
- }else
- {
- lower_tail_prob = 1 - pt( tvar , df = sample_size - 1 , lower.tail = TRUE )
-
- }
-
- pval = 2 * lower_tail_prob
-
- #DCW 250314
- if(abs(sample_mean - mu_pop) sample_size ){
- sample_count = sample_size
- }
-
- if( pop_proportion < 0 ){
- pop_proportion = 0
- }
-
- if( pop_proportion > 1 ){
- pop_proportion = 1
- }
-
- prob = dbinom( sample_count, sample_size, pop_proportion )
-
- return( prob )
-
-}
-
-####################################################################################################
-#' This function calculates a log likelihood ratio where the two hypotheses are that
-#' the tumour genome segment in question is "clonal".
-#' The first hypothesis is the "best fit" model we can find.
-#' The second hypothesis is the "second best fit" model we can find.
-#' @noRd
-calc_ln_likelihood_ratio <-function( LogR, BAFreq, BAF.length, BAF.size, BAF.mean, read_depth, rho, psi, gamma_param, maxdist_BAF ) # kjd 18-12-2013
-{
- pooled_BAF.size = read_depth * BAF.size
-
- # if we don't have a value for LogR, fill in 0
- if (is.na(LogR)) {
- LogR = 0
- }
- nMajor = (rho-1+BAFreq*psi*2^(LogR/gamma_param))/rho
- nMinor = (rho-1+(1-BAFreq)*psi*2^(LogR/gamma_param))/rho
-
- # to make sure we're always in a positive square:
- #if(nMajor < 0) {
- # nMajor = 0.01
- #}
- #
- #if(nMinor < 0) {
- # nMinor = 0.01
- #}
- #DCW - increase nMajor and nMinor together, to avoid impossible combinations (with negative subclonal fractions)
- if(nMinor<0 | is.na(nMinor)){
- if(BAFreq==1){
- #avoid calling infinite copy number
- nMajor = 1000
- }else{
- nMajor = nMajor + BAFreq * (0.01 - nMinor) / (1-BAFreq)
- if (nMajor<0) nMajor=1000
- }
- nMinor = 0.01
- }
-
- if (!is.finite(nMajor)) {
- nMajor = 0.01
- }
-
- # Check if there is a viable solution
- if (!is.na(BAFreq)) {
- nearest_edge = GetNearestCorners_bestOption( rho, psi, BAFreq, nMajor, nMinor ) # kjd 14-2-2014
- nMaj = nearest_edge$nMaj # kjd 14-2-2014
- nMin = nearest_edge$nMin # kjd 14-2-2014
-
-
- BAF_levels = (1-rho+rho*nMaj)/(2-2*rho+rho*(nMaj+nMin))
-
- index_vect = which( is.finite(BAF_levels) ) # kjd 14-2-2014
- BAF_levels = BAF_levels[ index_vect ] # kjd 14-2-2014
-
- if( length( BAF_levels ) > 1 ) # kjd 14-2-2014
- {
- likelihood_vect = sapply( BAF_levels , function(x){ calc_binomial_prob( BAF.mean, pooled_BAF.size, x ) } )
- likelihood_vect = sort( likelihood_vect, decreasing = TRUE )
-
- if( ( likelihood_vect[1] > 0 ) && ( likelihood_vect[2] > 0 ) )
- {
- ln_lratio = log( likelihood_vect[1] ) - log( likelihood_vect[2] )
-
- }else
- {
- ln_lratio = 0
- }
-
- }else
- {
- ln_lratio = 0
- }
- } else {
- ln_lratio = 0
- }
-
- return( ln_lratio )
-
-}
-
-####################################################################################################
-
-#' Calculate a two tailed binomial p-value
-#' @noRd
-calc_Pvalue_binomial_twotailed <-function( sample_count, sample_size, pop_proportion ) # kjd 27-2-2014
-{
- lower_tail_prob = pbinom( sample_count, sample_size, pop_proportion , lower.tail = TRUE )
-
- if( lower_tail_prob < 0.5 )
- {
- pval = 2 * lower_tail_prob
-
- }else
- {
- pval = 2 * ( 1 - lower_tail_prob )
-
- }
-
- return( pval )
-
-}
-
-####################################################################################################
-#' Helper function that calculates a p-value for a set of BAF values summarised by their mean
-#' TODO: this function is not used in Battenberg
-#' @noRd
-calc_BAF_Pvalue <-function( BAF.mean, pooled_BAF.size, maxdist_BAF, BAF_level ) # kjd 27-2-2014
-{
-
- if( is.finite( BAF_level ) && pooled_BAF.size > 0 )
- {
- sample_size = round( pooled_BAF.size , 0 )
- sample_count = round( BAF.mean * pooled_BAF.size , 0 )
-
- if( sample_count < 0 ){
- sample_count = 0
- }
-
- if( sample_count > sample_size ){
- sample_count = sample_size
- }
-
- pop_proportion = BAF_level
-
- if( BAF_level < 0 ){
- pop_proportion = 0
- }
-
- if( BAF_level > 1 ){
- pop_proportion = 1
- }
-
- pval = calc_Pvalue_binomial_twotailed( sample_count, sample_size, pop_proportion )
-
- if( abs( BAF.mean - BAF_level ) < maxdist_BAF ) {
- pval=1
- }
-
- }else
- {
- pval = 0
-
- }
-
- return( pval )
-
-}
-
-####################################################################################################
-#' Calculate a p-value for a LogR value
-#' TODO: this function is not used in Battenberg
-#' @noRd
-calc_LogR_Pvalue <-function( LogR, maxdist_LogR, LogR_level ) # kjd 27-2-2014
-{
- if( is.finite( LogR_level ) )
- {
- pval = 0
-
- if( abs( LogR - LogR_level ) < maxdist_LogR ) {
- pval=1
- }
-
- }else
- {
- pval = 0
-
- }
-
- return( pval )
-
-}
-
-#' Helper function to estimate rho from a given copy number state and it's BAF. The LogR is not used.
-#' @noRd
-estimate_rho <-function( LogR_value, BAFreq_value, nA_value, nB_value ) # kjd 10-3-2014
-{
- rho_value = (2*BAFreq_value-1)/(2*BAFreq_value-BAFreq_value*(nA_value+nB_value)-1+nA_value)
- return( rho_value )
-
-}
-
-####################################################################################################
-#' Helper function to calculate psi from a copy number fit, BAF, LogR, rho and a platform gamma
-#' @noRd
-estimate_psi <-function( LogR_value, BAFreq_value, nA_value, nB_value, rho_value, gamma_param ) # kjd 10-3-2014
-{
- temp_value = 2^( - LogR_value / gamma_param )
- temp_value = temp_value * ( 2 + ( rho_value * ( nA_value + nB_value - 2 ) ) )
- #return(temp_value) # DCW this returns psi rather than psi_t, i.e. the average ploidy of normal and tumour cells
- temp_value = temp_value - ( 2 * ( 1 - rho_value ) )
- psi_value = temp_value / rho_value
- return( psi_value )
-}
-
-#' Function that calculates rho and psi from a given reference segment, defined by ref_seg, with copy number state nA_ref and nB_ref
-#' @noRd
-get.psi.rho.from.ref.seg <-function( ref_seg, s, nA_ref, nB_ref, gamma_param = 1)
-{
- BAFreq = s[ ref_seg, "b" ]
- LogR = s[ ref_seg, "r" ]
-
- rho = estimate_rho( LogR, BAFreq, nA_ref, nB_ref )
- psi = estimate_psi( LogR, BAFreq, nA_ref, nB_ref, rho, gamma_param )
-
- # ploidy is recalculated based on results, to avoid bias (due to differences in normalization of LogR)
- nA = (rho-1-(s[,"b"]-1)*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
- nB = (rho-1+s[,"b"]*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
- ploidy = sum((nA+nB) * s[,"length"]) / sum(s[,"length"])
-
- # TODO DEBUG
- if (rho > 0) {
- ref_segment_info = list( psi = psi, rho = rho, ploidy = ploidy )
- } else {
- ref_segment_info = list( psi = NA, rho = NA, ploidy = NA )
- }
-
-
-
- return( ref_segment_info )
-}
-
-#' This function decides if a segment is "clonal" (= TRUE) or not (= FALSE).
-#' (The alternative hypothesis is that the tumour genome segment in question exhibits "sub-clonal" variation.)
-#' We test the integer solutions for all 4 corners. Also, along side the hypothesis test for the BAF.
-#' We use a decision rule based on LogR (we could use a hypothesis test which takes account of the variance in LogR, or a fixed “tolerance”).
-#' If the null hypothesis is accepted for at least one corner, then we accept that
-#' the tumour genome segment in question is "clonal".
-#' @noRd
-is.segment.clonal <-function( LogR, BAFreq, BAF.length, BAF.size, BAF.mean, BAF.sd, read_depth, rho, psi, gamma_param, siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR ) # kjd 21-2-2014
-{
- # TODO: read_depth, siglevel_LogR and maxdist_LogR are no longer in use
-
- #270314 no longer used
- #pooled_BAF.size = read_depth * BAF.size
-
- # if we don't have a value for LogR, fill in 0
- if (is.na(LogR)) {
- LogR = 0
- }
-
- nA = (rho-1-(BAFreq-1)*2^(LogR/gamma_param)*((1-rho)*2+rho*psi))/rho
- nB = (rho-1+BAFreq*2^(LogR/gamma_param)*((1-rho)*2+rho*psi))/rho
-
- # if (any(is.na(nA) | is.na(nB)) | any(nA < 0 | nB < 0)) {
- # # Reset any negative copy number to 0
- # index = which(is.na(nA) | is.na(nB) | nA < 0 | nB < 0)
- # print(paste("is.segment.clonal: Found negative copy number for segment", index, "BAF:", BAFreq[index], "logR:", LogR[index], "seg size:", BAF.size[index], "baf.sd:", BAF.sd[index]))
- # nA[nA < 0 | is.na(nA)] = 0
- # nB[nB < 0 | is.na(nB)] = 0
- # }
-
-
- nMajor = max(nA,nB, na.rm=T)
- nMinor = min(nA,nB, na.rm=T)
-
- # check for big shifts in nMajor - if there's a big shift, we shouldn't trust a clonal call
- nMajor.saved = nMajor
- ## to make sure we're always in a positive square:
- #if(nMajor < 0) {
- # nMajor = 0.01
- #}
- #
- #if(nMinor < 0) {
- # nMinor = 0.01
- #}
- #DCW - increase nMajor and nMinor together, to avoid impossible combinations (with negative subclonal fractions)
- if(nMinor<0){
- if(BAFreq==1){
- #avoid calling infinite copy number
- nMajor = 1000
- }else{
- nMajor = nMajor + BAFreq * (0.01 - nMinor) / (1-BAFreq)
- if (nMajor<0) nMajor=1000
- }
- nMinor = 0.01
- }
-
- # note that these are sorted in the order of ascending BAF:
- nMaj = c(floor(nMajor),ceiling(nMajor),floor(nMajor),ceiling(nMajor))
- nMin = c(ceiling(nMinor),ceiling(nMinor),floor(nMinor),floor(nMinor))
- x = floor(nMinor)
- y = floor(nMajor)
-
- # total copy number, to determine priority options
- ntot = nMajor + nMinor
-
- BAF_levels = (1-rho+rho*nMaj)/(2-2*rho+rho*(nMaj+nMin))
- #problem if rho=1 and nMaj=0 and nMin=0
- BAF_levels[nMaj==0 & nMin==0] = 0.5
-
- LogR_levels = gamma_param * log( (2-2*rho+rho*(nMaj+nMin))/(2-2*rho+rho*psi) , 2 ) # kjd 21-2-2014
-
-
- #DCW - just test corners on the nearest edge to determine clonality
- #If the segment is called as subclonal, this is the edge that will be used to determine the subclonal proportions that are reported first
- all.edges = orderEdges(BAF_levels, BAFreq, ntot,x,y)
-
- nMaj.test = all.edges[1,c(1,3)]
- nMin.test = all.edges[1,c(2,4)]
- test.BAF_levels = (1-rho+rho*nMaj.test)/(2-2*rho+rho*(nMaj.test+nMin.test))
- #problem if rho=1 and nMaj=0 and nMin=0
- test.BAF_levels[nMaj.test==0 & nMin.test==0] = 0.5
-
- whichclosestlevel.test = which.min(abs(test.BAF_levels-BAFreq))
-
- #270713 - problem caused by segments with constant BAF (usually 1 or 2)
- if(BAF.sd==0){
- pval=0
- }else{
- #pval[i] = t.test(BAFreq,alternative="two.sided",mu=BAF_levels[whichclosestlevel])$p.value
- #pval = t.test(BAFreq,alternative="two.sided",mu=test.BAF_levels[whichclosestlevel.test])$p.value
- pval = calc_Pvalue_t_twotailed( BAF.size, BAFreq, BAF.sd, test.BAF_levels[whichclosestlevel.test], maxdist_BAF)
- }
- #not necessary, because checked in calc_Pvalue_t_twotailed
- #if(min(abs(l-test.BAF_levels[whichclosestlevel.test])) siglevel_BAF)
- # check for big shifts in nMajor - if there's a big shift, we shouldn't trust a clonal call
- # This is particularly problematic for very high cellularity samples, like some of the ovarian samples
- is.clonal = (pval > siglevel_BAF & nMajor - nMajor.saved <1)
-
- segment_info = list( is.clonal = is.clonal, balanced = balanced, nMaj.test = nMaj.test[whichclosestlevel.test] , nMin.test = nMin.test[whichclosestlevel.test] )
-
- return( segment_info )
-
-}
-
-####################################################################################################
-#' This function calculates a t variate.
-#' @noRd
-calc_standardised_error <-function( LogR, BAFreq, BAF.length, BAF.size, BAF.mean, BAF.sd, rho, psi, gamma_param, maxdist_BAF ) # kjd 31-1-2014
-{
-
- # if we don't have a value for LogR, fill in 0
- if (is.na(LogR)) {
- LogR = 0
- }
- nMajor = (rho-1+BAFreq*psi*2^(LogR/gamma_param))/rho
- nMinor = (rho-1+(1-BAFreq)*psi*2^(LogR/gamma_param))/rho
-
- # to make sure we're always in a positive square:
- if(nMajor < 0 | is.na(nMajor)) {
- nMajor = 0.01
- }
-
- if(nMinor < 0 | is.na(nMinor)) {
- nMinor = 0.01
- }
-
- # note that these are sorted in the order of ascending BAF:
- nMaj = c(floor(nMajor),ceiling(nMajor),floor(nMajor),ceiling(nMajor))
- nMin = c(ceiling(nMinor),ceiling(nMinor),floor(nMinor),floor(nMinor))
- x = floor(nMinor)
- y = floor(nMajor)
-
- # total copy number, to determine priority options
- ntot = nMajor + nMinor
-
- index_vect = which( (2-2*rho+rho*(nMaj+nMin)) != 0 ) # kjd 13-1-2014
- nMaj = nMaj[ index_vect ] # kjd 13-1-2014
- nMin = nMin[ index_vect ] # kjd 13-1-2014
- BAF_levels = (1-rho+rho*nMaj)/(2-2*rho+rho*(nMaj+nMin))
-
- whichclosestlevel = which.min(abs(BAF_levels-BAFreq))
- # if 0.5 and there are multiple options, finetune, because a random option got chosen
- if( length( BAF_levels ) >= 3 ) { # kjd 13-1-2014
- if (BAF_levels[whichclosestlevel]==0.5 && BAF_levels[2]==0.5 && BAF_levels[3]==0.5) {
- whichclosestlevel = ifelse(ntot>x+y+1,2,3)
- }
- } # kjd 13-1-2014
-
- mu=BAF_levels[whichclosestlevel] # kjd 28-1-2014
- included_segment = 0 # kjd 31-1-2014
- if( BAF.size>0 ) { # kjd 13-1-2014
-
- if( BAF.sd==0 | length(mu)==0) {
- # pval=0 # kjd 31-1-2014
- tvar=0 # kjd 31-1-2014
-
- }else{
- # pval = t.test(BAFke,alternative="two.sided",mu=BAF_levels[whichclosestlevel])$p.value
- pval = calc_Pvalue_t_twotailed( BAF.size, BAF.mean, BAF.sd, mu, maxdist_BAF ) # kjd 31-1-2014
-
- tvar = studentise( BAF.size, BAF.mean, BAF.sd, mu ) # kjd 31-1-2014
-
- included_segment = 1 # kjd 31-1-2014
-
- }
- }else{ # kjd 13-1-2014
- # pval = 1 # kjd 13-1-2014 # kjd 31-1-2014
- tvar=0 # kjd 31-1-2014
-
- } # kjd 13-1-2014
-
- standard_error_info = list( included_segment = included_segment , tvar = tvar ) # kjd 31-1-2014
-
- return( standard_error_info )
-
-}
-
-####################################################################################################
-#' This function computes various "distances", which are used as penalties for a copy number solution.
-#' This function is called when searching for a clonal copy number solution.
-#' One such distance is an estimate of the proportion of the tumour genome which is clonal.
-#' For each segment of the genome, we test the null hypothesis is that
-#' the tumour genome segment in question is "clonal". The alternative hypothesis is that
-#' the tumour genome segment in question exhibits "sub-clonal" variation.
-#' @noRd
-calc_distance <-function( segs, dist_choice, rho, psi, gamma_param, uninformative_BAF_threshold=0.51 ) # kjd 10-2-2014
-{
- s = segs
-
- if( dist_choice == 0 ) # original ASCAT distance
- {
- nA = (rho-1-(s[,"b"]-1)*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
- nB = (rho-1+s[,"b"]*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
- # choose the minor allele
- nMinor = NULL
- if (sum(nA,na.rm=T) < sum(nB,na.rm=T)) {
- nMinor = nA
- }
- else {
- nMinor = nB
- }
- #d[i,j] = sum(abs(nMinor - pmax(round(nMinor),0))^2 * s[,"length"] * ifelse(s[,"b"]==0.5,0.05,1), na.rm=T)
- #DCW 180711 - try weighting BAF=0.5 equally with other points
- #dist_value = sum(abs(nMinor - pmax(round(nMinor),0))^2 * s[,"length"], na.rm=T)
- #DCW 310314 - retry weighting
- dist_value = sum(abs(nMinor - pmax(round(nMinor),0))^2 * s[,"length"] * ifelse(s[,"b"]<=uninformative_BAF_threshold,0.05,1), na.rm=T)
-
- minimise = TRUE
-
- }else if( dist_choice == 1 ){ # new similarity measure suggested by DW 7-3-2014
- nA = (rho-1-(s[,"b"]-1)*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
- nB = (rho-1+s[,"b"]*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
- # choose the minor allele
- nMinor = NULL
- if (sum(nA,na.rm=T) < sum(nB,na.rm=T)) {
- nMinor = nA
- }
- else {
- nMinor = nB
- }
- #d[i,j] = sum(abs(nMinor - pmax(round(nMinor),0))^2 * s[,"length"] * ifelse(s[,"b"]==0.5,0.05,1), na.rm=T)
- #DCW 180711 - try weighting BAF=0.5 equally with other points
- # dist_value = sum(abs(nMinor - pmax(round(nMinor),0))^2 * s[,"length"], na.rm=T)
-
- dist_value = sum((0.5-abs(nMinor - pmax(round(nMinor),0)))^2 * s[,"length"], na.rm=T)
-
- minimise = FALSE
-
- }else if( dist_choice == 2 ){ # adapted DW's 7-3-2014 measure by SD 8-8-2014 that takes into account both major and minor alleles
- nA = (rho-1-(s[,"b"]-1)*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
- nB = (rho-1+s[,"b"]*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
- # choose the minor allele
- nMinor = NULL
- nMajor = NULL
- if (sum(nA,na.rm=T) < sum(nB,na.rm=T)) {
- nMinor = nA
- nMajor = nB
- }
- else {
- nMinor = nB
- nMajor = nA
- }
-
- dist_value = 0.5*sum(((0.5-abs(nMinor - pmax(round(nMinor),0)))^2 + (0.5-abs(nMajor - pmax(round(nMajor),0)))^2) * s[,"length"], na.rm=T)
-
- minimise = FALSE
-
- }else if( dist_choice == 3 ){ # adapted DW's 7-3-2014 measure by SD 8-8-2014 that takes into account both major and minor alleles and takes the mean, while it also penalises for the number of homozygous deletions
- nA = (rho-1-(s[,"b"]-1)*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
- nB = (rho-1+s[,"b"]*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
- # choose the minor allele
- nMinor = NULL
- nMajor = NULL
- if (sum(nA,na.rm=T) < sum(nB,na.rm=T)) {
- nMinor = nA
- nMajor = nB
- }
- else {
- nMinor = nB
- nMajor = nA
- }
-
- # Penalise homozygous deletions twice as hard as other segments
- # - the penalty term is increased to make it less likely that hom dels occur
- # - the segment length is increased to penalise harder for longer segments
- segs_penalty = (0.5-abs(nMinor - pmax(round(nMinor),0)))^2 + (0.5-abs(nMajor - pmax(round(nMajor),0)))^2
- hom_del = nMinor<0.5 & nMajor<0.5 & nMinor>=0 & nMajor>=0
- segs_penalty[which(hom_del)] = segs_penalty[which(hom_del)]*4
-
- dist_value = 0.5*sum(segs_penalty * (s[,"length"] * ifelse(hom_del, 2, 1)), na.rm=T)
-
- minimise = FALSE
- }
-
- distance_info = list( distance_value = dist_value , minimise = minimise )
-
- return( distance_info )
-}
-
-####################################################################################################
-#' This function computes various "distances", which are used as penalties for a copy number solution
-#' One such distance is an estimate of the proportion of the tumour genome which is clonal.
-#' For each segment of the genome, we test the null hypothesis is that
-#' the tumour genome segment in question is "clonal". The alternative hypothesis is that
-#' the tumour genome segment in question exhibits "sub-clonal" variation.
-#' @noRd
-calc_distance_clonal <-function( segs, dist_choice, rho, psi, gamma_param, read_depth, siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR, uninformative_BAF_threshold) # kjd 10-2-2014
-{
- s = segs
-
- pval = NULL
-
- # BAFpvals = vector(length=length(BAFseg))
-
- genome_size = 0
- clonal_genome_size = 0
- seg_count = 0 # kjd 24-1-2014
- clonal_seg_count = 0 # kjd 24-1-2014
- n_included_segments = 0 # kjd 31-1-2014
- included_genome_size = 0 # kjd 31-1-2014
- sum1 = 0 # kjd 31-1-2014
- sum2 = 0 # kjd 31-1-2014
- sum3 = 0 # kjd 31-1-2014
- sum_ln_lratio = 0 # kjd 10-2-2014
-
- max_clonal_segment = 0 # There may be no clonal segments, in which case this remains zero.
- max_clonal_segment_size = 0
-
- ref_maj = NA
- ref_min = NA
-
- for(i in 1:nrow(s)) {
-
- BAFreq = s[ i, "b" ] # l = BAFlevels[i]
-
- if( BAFreq > uninformative_BAF_threshold )
- {
- LogR = s[ i, "r" ]
-
- BAF.length = s[ i, "length" ]
- BAF.size = s[ i, "size" ]
- BAF.mean = s[ i, "mean" ]
- BAF.sd = s[ i, "sd" ]
-
- #
- # Calculate P values
- #
-
- segment_info = is.segment.clonal( LogR, BAFreq, BAF.length, BAF.size, BAF.mean, BAF.sd, read_depth, rho, psi, gamma_param, siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR ) # kjd 21-2-2014
- is.clonal = segment_info$is.clonal # kjd 21-2-2014
-
- nMaj = segment_info$nMaj
- nMin = segment_info$nMin
- is.balanced = segment_info$balanced
-
- segment_size = BAF.length # OR segment_size = BAF.size ?
- genome_size = genome_size + segment_size
- seg_count = seg_count + 1 # kjd 24-1-2014
-
- # if( pval[i] > siglevel_BAF ){
- if(is.clonal){ # kjd 21-2-2014
- clonal_genome_size = clonal_genome_size + segment_size
- clonal_seg_count = clonal_seg_count + 1 # kjd 24-1-2014
-
- if( max_clonal_segment_size < segment_size & !is.balanced) #balanced check added by DCW 160314
- {
- max_clonal_segment = i
- max_clonal_segment_size = segment_size
-
- ref_maj = nMaj
- ref_min = nMin
- }
-
- }
-
- #
- # Calculate "standardised error"
- #
-
- standard_error_info = calc_standardised_error( LogR, BAFreq, BAF.length, BAF.size, BAF.mean, BAF.sd, rho, psi, gamma_param, maxdist_BAF ) # kjd 31-1-2014
-
- included_segment = standard_error_info$included_segment # kjd 31-1-2014
- tvar = standard_error_info$tvar # kjd 31-1-2014
-
- n_included_segments = n_included_segments + included_segment # kjd 31-1-2014
- if( included_segment > 0 )
- {
- included_genome_size = included_genome_size + segment_size # kjd 31-1-2014
-
- }
- sum1 = sum1 + tvar^2 # kjd 31-1-2014
-
- sum2 = sum2 + ( BAFreq - BAF.mean )^2
-
- sum3 = sum3 + ( segment_size * ( BAFreq - BAF.mean )^2 )
-
- #
- # Calculate log likelihood ratio
- #
-
- ln_lratio = calc_ln_likelihood_ratio( LogR, BAFreq, BAF.length, BAF.size, BAF.mean, read_depth, rho, psi, gamma_param, maxdist_BAF ) # kjd 10-2-2014
-
- sum_ln_lratio = sum_ln_lratio + ln_lratio
-
- }
-
- }
-
- #
- # Calculate proportion of genome which is "clonal":
- #
-
- clonal_proportion = 0
- if( genome_size > 0 ){
- clonal_proportion = clonal_genome_size / genome_size
-
- }
-
- #
- # Calculate "distances":
- #
-
- dist1 = 0 # kjd 3-2-2014
- if( n_included_segments > 0 ){
- dist1 = sum1 / n_included_segments
-
- } # kjd 3-2-2014
-
- dist2 = 0 # kjd 3-2-2014
- if( seg_count > 0 ){
- dist2 = sum2 / seg_count
-
- } # kjd 3-2-2014
-
- dist3 = 0 # kjd 3-2-2014
- if( genome_size > 0 ){
- dist3 = sum3 / genome_size
-
- } # kjd 3-2-2014
-
-
-
- if( dist_choice == 0 )
- {
- dist_value = clonal_proportion
- minimise = FALSE
- }
-
- if( dist_choice == 1 )
- {
- dist_value = dist1
- minimise = TRUE
- }
-
- if( dist_choice == 2 )
- {
- dist_value = dist2
- minimise = TRUE
- }
-
- if( dist_choice == 3 )
- {
- dist_value = dist3
- minimise = TRUE
- }
-
- if( dist_choice == 4 )
- {
- dist_value = sum_ln_lratio
- minimise = FALSE
- }
-
- distance_info = list( distance_value = dist_value , minimise = minimise , max_clonal_segment = max_clonal_segment, ref_maj = ref_maj, ref_min = ref_min ) # kjd 10-2-2014
-
- # return( clonal_proportion ) # kjd 24-1-2014
-
- return( distance_info ) # kjd 10-2-2014
-
-}
-
-#' Function extends the ASCAT \code{make_segments} function to make segments
-#' of constant BAF and LogR. This function returns a matrix with for each
-#' segment the LogR, BAF, the length of the segment (twice), and the mean and
-#' standard deviation of the BAF values
-#' @noRd
-get_segment_info = function(segLogR , segBAF.table) {
- segBAF = segBAF.table[,5]
-
- names(segBAF) = rownames(segBAF.table)
- names(segLogR) = rownames(segBAF.table)
-
- b = segBAF
- r = segLogR[names(segBAF)]
- pcf_segments = ASCAT::make_segments(r,b)
-
-# m = matrix(ncol = 2, nrow = length(b))
-# m[,1] = r
-# m[,2] = b
-# m = as.matrix(na.omit(m))
-# pcf_segments = matrix(ncol = 3, nrow = dim(m)[1])
-# colnames(pcf_segments) = c("r","b","length");
-# index = 0;
-# previousb = -1;
-# previousr = 1E10;
-# for (i in 1:dim(m)[1]) {
-# if (m[i,2] != previousb || m[i,1] != previousr) {
-# index=index+1;
-# count=1;
-# pcf_segments[index, "r"] = m[i,1];
-# pcf_segments[index, "b"] = m[i,2];
-# }
-# else {
-# count = count + 1;
-# }
-# pcf_segments[index, "length"] = count;
-# previousb = m[i,2];
-# previousr = m[i,1];
-# }
-#
-# # pcf_segments = as.matrix(na.omit(pcf_segments))[,] # kjd 10-1-2014 This version caused bug in R on laptop.
-# pcf_segments = as.matrix(na.omit(pcf_segments)) # kjd 10-1-2014 This version resolved bug in R on laptop. (Problem with installed version of R?)
-#
- segs = matrix(ncol = 6, nrow = nrow(pcf_segments))
- colnames(segs) = c("r","b","length","size", "mean", "sd")
- segs[ , c("r","b","length")] = pcf_segments
-
- for( i in 1:nrow(segs) ) {
- BAFreq = segs[i, "b"] # l = BAFlevels[i]
- index_vect = which( segBAF.table[ , 5] == BAFreq )
- BAFke = segBAF.table[index_vect, 4] # column 4 contains "phased BAF" values; # kjd 6-1-2014
-
- segs[i, "size"] = length(BAFke)
- segs[i, "mean"] = mean(BAFke)
- segs[i, "sd"] = sd(BAFke)
- }
- return(segs);
-}
-
-####################################################################################################
-#' Helper function to find new rho and psi boundaries given a current optimum pair.
-#' @noRd
-get_new_bounds = function( input_optimum_pair, ininitial_bounds ) # kjd 21-2-2014
-{
- psi_optimum = input_optimum_pair$psi
- rho_optimum = input_optimum_pair$rho
-
- psi_min_initial = ininitial_bounds$psi_min
- psi_max_initial = ininitial_bounds$psi_max
- rho_min_initial = ininitial_bounds$rho_min
- rho_max_initial = ininitial_bounds$rho_max
-
- psi_range = 0.1 * ( psi_max_initial - psi_min_initial )
- #rho_range = 0.1 * ( rho_max_initial - rho_min_initial )
- #DCW 170314 - rho range depends on optimum value of rho
- rho_range = 0.1 * rho_optimum
-
- if( (psi_optimum - 0.5 * psi_range) < psi_min_initial )
- {
- psi_min = psi_min_initial
- psi_max = psi_min_initial + psi_range
-
- }else
- {
- if( (psi_optimum + 0.5 * psi_range) > psi_max_initial )
- {
- psi_min = psi_max_initial - psi_range
- psi_max = psi_max_initial
-
- }else
- {
- psi_min = psi_optimum - 0.5 * psi_range
- psi_max = psi_optimum + 0.5 * psi_range
-
- }
- }
-
- if( (rho_optimum - 0.5 * rho_range) < rho_min_initial )
- {
- rho_min = rho_min_initial
- rho_max = rho_min_initial + rho_range
-
- }else
- {
- if( (rho_optimum + 0.5 * rho_range) > rho_max_initial )
- {
- rho_min = rho_max_initial - rho_range
- rho_max = rho_max_initial
-
- }else
- {
- rho_min = rho_optimum - 0.5 * rho_range
- rho_max = rho_optimum + 0.5 * rho_range
-
- }
- }
-
- new_bounds = list( psi_min = psi_min, psi_max = psi_max, rho_min = rho_min, rho_max = rho_max )
-
-
- return( new_bounds )
-
-}
-
-####################################################################################################
-#' function to create the distance matrix (distance for a range of ploidy and tumor percentage values)
-#' input: segmented LRR and BAF and the value for gamma_param
-#' @noRd
-create_distance_matrix = function(s, dist_choice, gamma_param, uninformative_BAF_threshold=0.51, min_rho=0.1, max_rho=1, min_psi=1, max_psi=5.4) {
- psi_pos = seq(min_psi,max_psi,0.05)
- rho_pos = seq(min_rho,max_rho,0.01)
- d = matrix(nrow = length(psi_pos), ncol = length(rho_pos))
- rownames(d) = psi_pos
- colnames(d) = rho_pos
- dmin = 1E20;
- for(i in 1:length(psi_pos)) {
- psi = psi_pos[i]
- for(j in 1:length(rho_pos)) {
- rho = rho_pos[j]
-
- distance_info = calc_distance( s, dist_choice, rho, psi, gamma_param, uninformative_BAF_threshold=uninformative_BAF_threshold ) # kjd 10-2-2014
-
- d[i,j] = distance_info$distance_value
- # minimise = distance_info$minimise
-
- }
- }
-
- minimise = distance_info$minimise
-
- distance_matrix_info = list( distance_matrix = d , minimise = minimise )
-
- # return(d)
- return( distance_matrix_info )
-
-}
-
-#' Helper function to create the clonal distance matrix for a range of
-#' rho and psi values
-#' @noRd
-create_distance_matrix_clonal = function( segs, dist_choice, gamma_param, read_depth, siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR, uninformative_BAF_threshold, new_bounds) # kjd 18-12-2013
-{
- psi_min = new_bounds$psi_min
- psi_max = new_bounds$psi_max
- rho_min = new_bounds$rho_min
- rho_max = new_bounds$rho_max
-
- s = segs
-
- psi_range = psi_max - psi_min
- rho_range = rho_max - rho_min
-
- delta_psi = psi_range / 100
- delta_rho = rho_range / 100
-
- psi_pos = seq( psi_min, psi_max, delta_psi )
- rho_pos = seq( rho_min, rho_max, delta_rho )
-
- # psi_pos = seq(1,5.4,0.05)
- # rho_pos = seq(0.1,1.05,0.01)
-
- ref_seg_matrix = matrix(nrow = length(psi_pos), ncol = length(rho_pos))
- ref_major = matrix(nrow = length(psi_pos), ncol = length(rho_pos))
- ref_minor = matrix(nrow = length(psi_pos), ncol = length(rho_pos))
- rownames(ref_seg_matrix) = psi_pos
- colnames(ref_seg_matrix) = rho_pos
- rownames(ref_major) = psi_pos
- colnames(ref_major) = rho_pos
- rownames(ref_minor) = psi_pos
- colnames(ref_minor) = rho_pos
-
- d = matrix(nrow = length(psi_pos), ncol = length(rho_pos))
- rownames(d) = psi_pos
- colnames(d) = rho_pos
- # dmin = 1E20;
- for(i in 1:length(psi_pos)) {
- psi = psi_pos[i]
- for(j in 1:length(rho_pos)) {
- rho = rho_pos[j]
-
- # clonal_proportion = calc_clonal_proportion( s, LogRvals, BAFvals, segBAF.table, rho, psi, gamma_param, siglevel_BAF, maxdist_BAF ) # kjd 18-12-2013
- distance_info = calc_distance_clonal( s, dist_choice, rho, psi, gamma_param, read_depth, siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR, uninformative_BAF_threshold) # kjd 10-2-2014
-
- distance_value = distance_info$distance_value # kjd 10-2-2014
- # minimise = distance_info$minimise # kjd 10-2-2014
- max_clonal_segment = distance_info$max_clonal_segment
-
- d[i,j] = distance_value # kjd 10-2-2014
- ref_seg_matrix[i,j] = max_clonal_segment
-
- ref_major[i,j] = distance_info$ref_maj
- ref_minor[i,j] = distance_info$ref_min
- }
- }
-
- minimise = distance_info$minimise # kjd 10-2-2014
-
- distance_matrix_info = list( distance_matrix = d , minimise = minimise , ref_seg_matrix = ref_seg_matrix, ref_major = ref_major, ref_minor = ref_minor ) # kjd 10-2-2014
-
- # return(d) # kjd 10-2-2014
- return( distance_matrix_info ) # kjd 10-2-2014
-
-}
-
-####################################################################################################
-#' Helper function to calculate a square distance
-#' @noRd
-calc_square_distance <-function( pt1, pt2 ) # kjd 27-2-2014
-{
- dsqr = ( pt1[1] - pt2[1] )^2 + ( pt1[2] - pt2[2] )^2
-
- return( dsqr )
-
-}
-
-####################################################################################################
-#' This function is an alternative procedure for finding the optimum (psi, rho) pair.
-#' This function first finds all the find all the global optima,
-#' and then finds the centroid of this set of globla optima.
-#' Then we find the global optimum which is nearest to the centroid.
-#' (When the set of global optima is convex, we expect the selected optimum to be at the centroid.)
-#' @param d A distance matrix
-#' @param ref_seg_matrix The corresponding ref seg matrix that belongs to d
-#' @param ref_major The corresponding major allele values with d
-#' @param ref_minor The corresponding minor allele values with d
-#' @param s A segmented BAF/LogR data.frame from \code{get_segment_info}
-#' @param dist_choice Some distance metrics require adaptation of the data (i.e. log transform)
-#' @param minimise Boolean whether we're minimising or maximising
-#' @param new_bounds The rho/psi boundaries between we are searching for a solution. This is a named list with values psi_min, psi_max, rho_min, rho_max
-#' @param distancepng String where the sunrise distance plot will be saved
-#' @param gamma_param The platform gamma
-#' @param siglevel_BAF The level at which BAF becomes significant TODO: this option is no longer used
-#' @param maxdist_BAF TODO: this option is no longer used
-#' @param siglevel_LogR The p-value at which logR becomes significant when establishing whether a segment should be subclonal
-#' @param maxdist_LogR The maximum distance allowed as slack when establishing the significance. This allows for the case when a breakpoint is missed, the segment would then not automatically become subclonal
-#' @param allow100percent Boolean whether to allow for a 100"\%" cellularity solution
-#' @param uninformative_BAF_threshold The threshold above which BAF becomes uninformative
-#' @param read_depth TODO: this option is no longer used
-#' @return A list with fields optima_info_without_ref and optima_info
-#' @export
-find_centroid_of_global_minima <- function( d, ref_seg_matrix, ref_major, ref_minor, s, dist_choice, minimise, new_bounds, distancepng, gamma_param, siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR, allow100percent, uninformative_BAF_threshold, read_depth) # kjd 28-2-2014
-{
-
- #Theoretmaxdist_BAF = sum(rep(0.25,dim(s)[1]) * s[,"length"] * ifelse(s[,"b"]==0.5,0.05,1),na.rm=T)
- #DCW 180711 - try weighting BAF=0.5 equally with other points
- # Theoretmaxdist_BAF = sum(rep(0.25,dim(s)[1]) * s[,"length"],na.rm=T)
-
-
- if( !(minimise) ) # kjd 12-2-2013
- {
- d = - d # This ensures that we "maximise" instead of "minimise"!
- }
-
- # Find height of global minima;
- # (subject to additional conditions: percentzero > 0.01 | perczeroAbb > 0.1)
-
- gmin = max( d )
- for (i in 1:(dim(d)[1])) {
- for (j in 1:(dim(d)[2])) {
- psi = as.numeric(rownames(d)[i])
- rho = as.numeric(colnames(d)[j])
- nA = (rho-1-(s[,"b"]-1)*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
- nB = (rho-1+s[,"b"]*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
-
- # ploidy is recalculated based on results, to avoid bias (due to differences in normalization of LogR)
- ploidy = sum((nA+nB) * s[,"length"]) / sum(s[,"length"]);
-
- percentzero = (sum((round(nA)==0)*s[,"length"])+sum((round(nB)==0)*s[,"length"]))/sum(s[,"length"])
- perczeroAbb = (sum((round(nA)==0)*s[,"length"]*ifelse(s[,"b"]==0.5,0,1))+sum((round(nB)==0)*s[,"length"]*ifelse(s[,"b"]==0.5,0,1)))/sum(s[,"length"]*ifelse(s[,"b"]==0.5,0,1))
- # the next can happen if BAF is a flat line at 0.5
- if (is.na(perczeroAbb)) {
- perczeroAbb = 0
- }
-
- # commented out by kjd 6-3-2014
- #if( percentzero > 0.01 | perczeroAbb > 0.1 ) { # kjd 6-3-2014
-
- if( d[i,j] <= gmin ) {
- gmin = d[i,j]
-
- }
- #}
- }
- }
-
- # Find all global minima;
- # (subject to additional conditions: percentzero > 0.01 | perczeroAbb > 0.1)
-
- nropt = 0
- localmin = NULL
- optima = list()
-
- for (i in 1:(dim(d)[1])) {
- for (j in 1:(dim(d)[2])) {
- if( d[i,j] == gmin ) {
- psi = as.numeric(rownames(d)[i])
- rho = as.numeric(colnames(d)[j])
- nA = (rho-1-(s[,"b"]-1)*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
- nB = (rho-1+s[,"b"]*2^(s[,"r"]/gamma_param)*((1-rho)*2+rho*psi))/rho
-
- # ploidy is recalculated based on results, to avoid bias (due to differences in normalization of LogR)
- ploidy = sum((nA+nB) * s[,"length"]) / sum(s[,"length"]);
-
- percentzero = (sum((round(nA)==0)*s[,"length"])+sum((round(nB)==0)*s[,"length"]))/sum(s[,"length"])
- perczeroAbb = (sum((round(nA)==0)*s[,"length"]*ifelse(s[,"b"]==0.5,0,1))+sum((round(nB)==0)*s[,"length"]*ifelse(s[,"b"]==0.5,0,1)))/sum(s[,"length"]*ifelse(s[,"b"]==0.5,0,1))
- # the next can happen if BAF is a flat line at 0.5
- if (is.na(perczeroAbb)) {
- perczeroAbb = 0
- }
-
- # goodnessOfFit = (1-m/Theoretmaxdist_BAF) * 100
- goodnessOfFit = gmin #DCW 250314 goodnessOfFit is the same as gmin, because the metric is the total amount of the genome that is clonal
- nropt = nropt + 1
- optima[[nropt]] = c(gmin,i,j,ploidy,goodnessOfFit)
- localmin[nropt] = gmin
-
- }
- }
- }
-
- #
- # Find a "centroid" of the set of global minima:
- #
-
- grid_x_vect = unlist( lapply( optima , function(z){ z[2] } ) )
- grid_y_vect = unlist( lapply( optima , function(z){ z[3] } ) )
-
- centre_x = mean( median( grid_x_vect ) )
- centre_y = mean( median( grid_y_vect ) )
-
- centre = c( centre_x, centre_y )
-
- index = 1
- sqrdist_min = (dim(d)[1])^2 + (dim(d)[2])^2
- for (i in 1:length(optima)) {
-
- grid_x = optima[[i]][2] # grid_i = ( psi_opt1 - 1 ) * 20
- grid_y = optima[[i]][3] # grid_j = ( rho_opt1 - 0.1 ) * 100
-
- grid_point = c( grid_x, grid_y )
-
- sqrdist = calc_square_distance( grid_point, centre )
-
- if( sqrdist <= sqrdist_min ) {
- sqrdist_min = sqrdist
- index = i
- }
- }
-
- grid_x = optima[[index]][2] # grid_i = ( psi_opt1 - 1 ) * 20
- grid_y = optima[[index]][3] # grid_j = ( rho_opt1 - 0.1 ) * 100
-
- psi_opt1 = as.numeric(rownames(d)[optima[[index]][2]])
- rho_opt1 = as.numeric(colnames(d)[optima[[index]][3]])
- if(rho_opt1 > 1) {
- rho_opt1 = 1
- }
- ploidy_opt1 = optima[[index]][4]
- goodnessOfFit_opt1 = optima[[index]][5]
-
- ref_seg = ref_seg_matrix[ grid_x, grid_y ]
-
- # store optima for plotting later
- rhos = rho_opt1
- psis = psi_opt1
- #
- # Write to clonal info file:
- #
-
- if( isTRUE(minimise) ) # kjd 12-2-2013
- {
- dist_optima = gmin # when we "minimise";
-
- }else
- {
- dist_optima = - gmin # Recall that when we "maximise", we replace "d" by "-d";
- goodnessOfFit_opt1 = -goodnessOfFit_opt1 #DCW 250314
- }
-
- print(paste("goodnessOfFit from grid=",goodnessOfFit_opt1,sep=""))
- #DCW 140314
- optima_info_without_ref = list( nropt = nropt, psi_opt1 = psi_opt1, rho_opt1 = rho_opt1, ploidy_opt1 = ploidy_opt1, ref_seg = ref_seg, goodnessOfFit_opt1 = goodnessOfFit_opt1 )
-
- #DCW if no ref segment found, there is no tumour present
- if(ref_seg==0){
- psi_opt1 = 2
- rho_opt1 = 1
- ploidy_opt1=2
- goodnessOfFit_opt1 = 1
- }else{
- ref_segment_info = get.psi.rho.from.ref.seg( ref_seg, s, ref_major[ grid_x, grid_y ], ref_minor[ grid_x, grid_y ], gamma_param)
-
- psi_opt1 = ref_segment_info$psi
- rho_opt1 = ref_segment_info$rho
- ploidy_opt1 = ref_segment_info$ploidy
-
- # TODO DEBUG
- if (!is.na(rho_opt1)) {
- #goodness of fit is the same as the distance measure for fraction of genome that is clonal
- distance.info = calc_distance_clonal( s, dist_choice, rho_opt1, psi_opt1, gamma_param, read_depth, siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR, uninformative_BAF_threshold)
- goodnessOfFit_opt1 = distance.info$distance_value
- #goodnessOfFit_opt1 = ref_segment_info$goodnessOfFit_opt1
- } else {
- goodnessOfFit_opt1 = Inf
- }
-
- }
-
- # store optima for plotting later
- rhos = c(rhos, rho_opt1)
- psis = c(psis, psi_opt1)
-
- # separated plotting from logic: create distanceplot here
- if (!is.na(distancepng)) {
- png(filename = distancepng, width = 1000, height = 1000, res = 1000/7, type = "cairo")
- }
- clonal_findcentroid.plot(minimise, dist_choice, -d, psis, rhos, new_bounds)
- if (!is.na(distancepng)) { dev.off() }
-
- optima_info = list( nropt = nropt, psi_opt1 = psi_opt1, rho_opt1 = rho_opt1, ploidy_opt1 = ploidy_opt1, ref_seg = ref_seg, goodnessOfFit_opt1 = goodnessOfFit_opt1 ) # kjd 10-3-2014
-
- return( list(optima_info_without_ref=optima_info_without_ref, optima_info=optima_info) )
-}
-
-#' A modified ASCAT main function to fit Battenberg
-#'
-#' This function returns an initial rho and psi estimate for a clonal copy number fit. It uses an internal distance metric to create a distance matrix.
-#' Using that matrix it will search for a rho and psi combination that yields the least heavy penalty.
-#' @param lrr (unsegmented) log R, in genomic sequence (all probes), with probe IDs
-#' @param baf (unsegmented) B Allele Frequency, in genomic sequence (all probes), with probe IDs
-#' @param lrrsegmented log R, segmented, in genomic sequence (all probes), with probe IDs
-#' @param bafsegmented B Allele Frequency, segmented, in genomic sequence (only probes heterozygous in germline), with probe IDs
-#' @param chromosomes a list containing c vectors, where c is the number of chromosomes and every vector contains all probe numbers per chromosome
-#' @param dist_choice The distance metric to be used internally to penalise a copy number solution
-#' @param distancepng if NA: distance is plotted, if filename is given, the plot is written to a .png file (Default NA)
-#' @param copynumberprofilespng if NA: possible copy number profiles are plotted, if filename is given, the plot is written to a .png file (Default NA)
-#' @param nonroundedprofilepng if NA: copy number profile before rounding is plotted (total copy number as well as the copy number of the minor allele), if filename is given, the plot is written to a .png file (Default NA)
-#' @param cnaStatusFile File where the copy number profile status is written to. This contains either the message "No suitable copy number solution found" or "X copy number solutions found" (Default copynumber_solution_status.txt)
-#' @param gamma technology parameter, compaction of Log R profiles (expected decrease in case of deletion in diploid sample, 100 "\%" aberrant cells; 1 in ideal case, 0.55 of Illumina 109K arrays) (Default 0.55)
-#' @param allow100percent A boolean whether to allow a 100"\%" cellularity solution
-#' @param reliabilityFile String to where fit reliabilty information should be written. This file contains backtransformed BAF and LogR values for segments using the fitted copy number profile (Default NA)
-#' @param min.ploidy The minimum ploidy to consider (Default 1.6)
-#' @param max.ploidy The maximum ploidy to consider (Default 4.8)
-#' @param min.rho The minimum cellularity to consider (Default 0.1)
-#' @param max.rho The maximum cellularity to consider (Default 1.0)
-#' @param min.goodness The minimum goodness of fit for a solution to have to be considered (Default 63)
-#' @param uninformative_BAF_threshold The threshold beyond which BAF becomes uninformative (Default 0.51)
-#' @param chr.names A vector with chromosome names used for plotting
-#' @param analysis A String representing the type of analysis to be run, this determines whether the distance figure is produced (Default paired)
-#' @return A list with fields psi, rho and ploidy
-#' @export
-#the limit on rho is lenient and may lead to spurious solutions
-runASCAT = function(lrr, baf, lrrsegmented, bafsegmented, chromosomes, dist_choice, distancepng = NA, copynumberprofilespng = NA, nonroundedprofilepng = NA, cnaStatusFile = "copynumber_solution_status.txt", gamma = 0.55, allow100percent,reliabilityFile=NA,min.ploidy=1.6,max.ploidy=4.8,min.rho=0.1,max.rho=1.0,min.goodness=63, uninformative_BAF_threshold = 0.51, chr.names, analysis="paired") {
- ch = chromosomes
- b = bafsegmented
- r = lrrsegmented[names(bafsegmented)]
-
- # Adapt the rho/psi boundaries for the local maximum searching below to work
- dist_min_psi = max(min.ploidy-0.6, 0)
- dist_max_psi = max.ploidy+0.6
- dist_min_rho = max(min.rho-0.03, 0.05)
- dist_max_rho = max.rho+0.03
-
- s = ASCAT::make_segments(r,b)
- dist_matrix_info <- create_distance_matrix( s, dist_choice, gamma, uninformative_BAF_threshold=uninformative_BAF_threshold, min_psi=dist_min_psi, max_psi=dist_max_psi, min_rho=dist_min_rho, max_rho=dist_max_rho)
- d = dist_matrix_info$distance_matrix
- minimise = dist_matrix_info$minimise
-
- #TheoretMaxdist = sum(rep(0.25,dim(s)[1]) * s[,"length"] * ifelse(s[,"b"]==0.5,0.05,1),na.rm=T)
- #DCW 180711 - try weighting BAF=0.5 equally with other points
- TheoretMaxdist = sum(rep(0.25,dim(s)[1]) * s[,"length"],na.rm=T)
-
- if( !(minimise) ) # kjd 10-3-2014
- {
- d = - d # This ensures that we "maximise" instead of "minimise"!
- }
-
- nropt = 0
- localmin = NULL
- optima = list()
- for (i in 4:(dim(d)[1]-3)) {
- for (j in 4:(dim(d)[2]-3)) {
- m = d[i,j]
- seld = d[(i-3):(i+3),(j-3):(j+3)]
- seld[4,4] = max(seld)
- if(min(seld) > m) {
- psi = as.numeric(rownames(d)[i])
- rho = as.numeric(colnames(d)[j])
- nA = (rho-1-(s[,"b"]-1)*2^(s[,"r"]/gamma)*((1-rho)*2+rho*psi))/rho
- nB = (rho-1+s[,"b"]*2^(s[,"r"]/gamma)*((1-rho)*2+rho*psi))/rho
-
- # ploidy is recalculated based on results, to avoid bias (due to differences in normalization of LogR)
- ploidy = sum((nA+nB) * s[,"length"]) / sum(s[,"length"]);
- ploidy_opt1 = ploidy
-
- percentzero = (sum((round(nA)==0)*s[,"length"])+sum((round(nB)==0)*s[,"length"]))/sum(s[,"length"])
- perczeroAbb = (sum((round(nA)==0)*s[,"length"]*ifelse(s[,"b"]==0.5,0,1))+sum((round(nB)==0)*s[,"length"]*ifelse(s[,"b"]==0.5,0,1)))/sum(s[,"length"]*ifelse(s[,"b"]==0.5,0,1))
- # the next can happen if BAF is a flat line at 0.5
- if (is.na(perczeroAbb)) {
- perczeroAbb = 0
- }
-
- #goodnessOfFit = (1-m/TheoretMaxdist) * 100
- #140314 - DCW
- if(minimise){
- goodnessOfFit = (1-m/TheoretMaxdist) * 100
- }else{
- goodnessOfFit = -m/TheoretMaxdist * 100 # we have to use minus to reverse d=-d above
- }
-
- print(paste("ploidy=",ploidy,",rho=",rho,",goodness=",goodnessOfFit,",percentzero=",percentzero,", perczerAbb=",perczeroAbb,sep=""))
- if (ploidy >= min.ploidy & ploidy <= max.ploidy & rho >= min.rho & goodnessOfFit >= min.goodness & (percentzero > 0.01 | perczeroAbb > 0.1)) {
- nropt = nropt + 1
- optima[[nropt]] = c(m,i,j,ploidy,goodnessOfFit)
- localmin[nropt] = m
- }
- }
- }
- }
-
- # if solutions with 100 % aberrant cell fraction should be allowed:
- # if there are no solutions, drop the conditions on regions with copy number zero, and include the borders (rho = 1) as well
- # this way, if there is another solution, this is still preferred, but these solutions aren't standardly eliminated
- if (allow100percent & nropt == 0) {
- #first, include borders
- cold = which(as.numeric(colnames(d))>1)
- d[,cold]=1E20
- for (i in 4:(dim(d)[1]-3)) {
- for (j in 4:(dim(d)[2]-3)) {
- m = d[i,j]
- seld = d[(i-3):(i+3),(j-3):(j+3)]
- seld[4,4] = max(seld)
- if(min(seld) > m) {
- psi = as.numeric(rownames(d)[i])
- rho = as.numeric(colnames(d)[j])
- nA = (rho-1-(s[,"b"]-1)*2^(s[,"r"]/gamma)*((1-rho)*2+rho*psi))/rho
- nB = (rho-1+s[,"b"]*2^(s[,"r"]/gamma)*((1-rho)*2+rho*psi))/rho
-
- # ploidy is recalculated based on results, to avoid bias (due to differences in normalization of LogR)
- ploidy = sum((nA+nB) * s[,"length"]) / sum(s[,"length"]);
-
- percentzero = (sum((round(nA)==0)*s[,"length"])+sum((round(nB)==0)*s[,"length"]))/sum(s[,"length"])
- perczeroAbb = (sum((round(nA)==0)*s[,"length"]*ifelse(s[,"b"]==0.5,0,1))+sum((round(nB)==0)*s[,"length"]*ifelse(s[,"b"]==0.5,0,1)))/sum(s[,"length"]*ifelse(s[,"b"]==0.5,0,1))
- # the next can happen if BAF is a flat line at 0.5
- if (is.na(perczeroAbb)) {
- perczeroAbb = 0
- }
-
- #goodnessOfFit = (1-m/TheoretMaxdist) * 100
- #140314 - DCW
- if(minimise){
- goodnessOfFit = (1-m/TheoretMaxdist) * 100
- }else{
- goodnessOfFit = -m/TheoretMaxdist * 100 # we have to use minus to reverse d=-d above
- }
-
- if (ploidy > min.ploidy & ploidy < max.ploidy & rho >= min.rho & goodnessOfFit >= min.goodness) {
- nropt = nropt + 1
- optima[[nropt]] = c(m,i,j,ploidy,goodnessOfFit)
- localmin[nropt] = m
- }
- }
- }
- }
- }
-
- # added for output to plotting
- psi_opt1_plot = vector(mode="numeric")
- rho_opt1_plot = vector(mode="numeric")
-
- if (nropt>0) {
- write.table(paste(nropt, " copy number solutions found", sep=""), file=cnaStatusFile, quote=F, col.names=F, row.names=F)
- optlim = sort(localmin)[1]
- for (i in 1:length(optima)) {
- if(optima[[i]][1] == optlim) {
- psi_opt1 = as.numeric(rownames(d)[optima[[i]][2]])
- rho_opt1 = as.numeric(colnames(d)[optima[[i]][3]])
- if(rho_opt1 > 1) {
- rho_opt1 = 1
- }
- ploidy_opt1 = optima[[i]][4]
- goodnessOfFit_opt1 = optima[[i]][5]
- psi_opt1_plot = c(psi_opt1_plot, psi_opt1)
- rho_opt1_plot = c(rho_opt1_plot, rho_opt1)
- # points((psi_opt1-1)/4.4,(rho_opt1-0.1)/0.95,col="green",pch="X", cex = 2)
- }
- }
- } else {
- write.table(paste("no copy number solutions found", sep=""), file=cnaStatusFile, quote=F, col.names=F, row.names=F)
- print("No suitable copy number solution found")
- psi = NA
- ploidy = NA
- rho = NA
- psi_opt1_plot = -1
- rho_opt1_plot = -1
- }
-
- # NAP: only create this plot for 'paired' analysis mode and not cell_line or germline; it shows strange behaviour and halts execution
- if (analysis=="paired"){
- # separated plotting from logic: create distanceplot here
- if (!is.na(distancepng)) {
- png(filename = distancepng, width = 1000, height = 1000, res = 1000/7, type = "cairo")
- }
- ASCAT::ascat.plotSunrise(-d, psi_opt1_plot, rho_opt1_plot,minimise)
- if (!is.na(distancepng)) { dev.off() }
-}
-
- if(nropt>0) {
-
- rho = rho_opt1
- psi = psi_opt1
- ploidy = ploidy_opt1
-
- nAfull = (rho-1-(b-1)*2^(r/gamma)*((1-rho)*2+rho*psi))/rho
- nBfull = (rho-1+b*2^(r/gamma)*((1-rho)*2+rho*psi))/rho
- nA = pmax(round(nAfull),0)
- nB = pmax(round(nBfull),0)
-
- rBacktransform = gamma*log((rho*(nA+nB)+(1-rho)*2)/((1-rho)*2+rho*psi),2)
- bBacktransform = (1-rho+rho*nB)/(2-2*rho+rho*(nA+nB))
- rConf = ifelse(abs(rBacktransform)>0.15,pmin(100,pmax(0,100*(1-abs(rBacktransform-r)/abs(r)))),NA)
- bConf = ifelse(bBacktransform!=0.5,pmin(100,pmax(0,ifelse(b==0.5,100,100*(1-abs(bBacktransform-b)/abs(b-0.5))))),NA)
- #DCW 150711 - get deviations from expected values
- if(!is.na(reliabilityFile)){
- write.table(data.frame(segmentedBAF=b,backTransformedBAF=bBacktransform,confidenceBAF=bConf,segmentedR=r,backTransformedR=rBacktransform,confidenceR=rConf,nA=nA,nB=nB,nAfull=nAfull,nBfull=nBfull), reliabilityFile,sep=",",row.names=F)
- }
- confidence = ifelse(is.na(rConf),bConf,ifelse(is.na(bConf),rConf,(rConf+bConf)/2))
-
- # Create plot
- if (!is.na(copynumberprofilespng)) {
- png(filename = copynumberprofilespng, width = 2000, height = 500, res = 200, type = "cairo")
- }
- ASCAT::ascat.plotAscatProfile(n1all = nA, n2all = nB, heteroprobes = TRUE, ploidy = ploidy_opt1, rho = rho_opt1, goodnessOfFit = goodnessOfFit_opt1, nonaberrant = FALSE, ch = ch, lrr = lrr, bafsegmented = bafsegmented, chrs=chr.names)
- if (!is.na(copynumberprofilespng)) { dev.off() }
-
- # separated plotting from logic: create nonrounded copy number profile plot here
- if (!is.na(nonroundedprofilepng)) {
- png(filename = nonroundedprofilepng, width = 2000, height = 500, res = 200, type = "cairo")
- }
- # clonal_runascat.plot3(rho_opt1, goodnessOfFit_opt1, ploidy_opt1, nAfull, nBfull, ch, lrr, bafsegmented)
- ASCAT::ascat.plotNonRounded(ploidy = ploidy_opt1, rho = rho_opt1, goodnessOfFit = goodnessOfFit_opt1, nonaberrant = FALSE, nAfull = nAfull, nBfull = nBfull, bafsegmented = bafsegmented, ch = ch, lrr = lrr, chrs=chr.names)
- if (!is.na(nonroundedprofilepng)) { dev.off() }
-
- }
- output_optimum_pair = list(psi = psi, rho = rho, ploidy = ploidy)
- return( output_optimum_pair ) # kjd 20-2-2014
-}
-
-####################################################################################################
-#' ASCAT like function to obtain a clonal copy number profile
-#'
-#' This function takes an initial optimum rho/psi pair and uses
-#' an internal distance metric to calculate a score for each rho/psi pair allowed.
-#' The solution with the best score is then taken to obtain a global copy number
-#' profile. This function performs both a grid search and tries to find a reference
-#' segment, but the grid search result is always used for now.
-#' @param lrr (unsegmented) log R, in genomic sequence (all probes), with probe IDs
-#' @param baf (unsegmented) B Allele Frequency, in genomic sequence (all probes), with probe IDs
-#' @param lrrsegmented log R, segmented, in genomic sequence (all probes), with probe IDs
-#' @param bafsegmented B Allele Frequency, segmented, in genomic sequence (only probes heterozygous in germline), with probe IDs
-#' @param chromosomes a list containing c vectors, where c is the number of chromosomes and every vector contains all probe numbers per chromosome
-#' @param segBAF.table Segmented BAF data.frame from \code{get_segment_info}
-#' @param input_optimum_pair A list containing fields for rho, psi and ploidy, as is output from \code{runASCAT}
-#' @param dist_choice The distance metric to be used internally to penalise a copy number solution
-#' @param distancepng if NA: distance is plotted, if filename is given, the plot is written to a .png file (Default NA)
-#' @param copynumberprofilespng if NA: possible copy number profiles are plotted, if filename is given, the plot is written to a .png file (Default NA)
-#' @param nonroundedprofilepng if NA: copy number profile before rounding is plotted (total copy number as well as the copy number of the minor allele), if filename is given, the plot is written to a .png file (Default NA)
-#' @param gamma_param technology parameter, compaction of Log R profiles (expected decrease in case of deletion in diploid sample, 100 "\%" aberrant cells; 1 in ideal case, 0.55 of Illumina 109K arrays) (Default 0.55)
-#' @param read_depth TODO: unused parameter that should be removed
-#' @param uninformative_BAF_threshold The threshold beyond which BAF becomes uninformative
-#' @param allow100percent A boolean whether to allow a 100"\%" cellularity solution
-#' @param reliabilityFile String to where fit reliabilty information should be written. This file contains backtransformed BAF and LogR values for segments using the fitted copy number profile (Default NA)
-#' @param psi_min_initial Minimum psi value to be considered (Default: 1.0)
-#' @param psi_max_initial Maximum psi value to be considered (Default: 5.4)
-#' @param rho_min_initial Minimum rho value to be considered (Default: 0.1)
-#' @param rho_max_initial Maximum rho value to be considered (Default: 1.05)
-#' @param chr.names A vector with chromosome names used for plotting
-#' @return A list with fields output_optimum_pair, output_optimum_pair_without_ref, distance, distance_without_ref, minimise and is.ref.better
-#' @export
-run_clonal_ASCAT = function(lrr, baf, lrrsegmented, bafsegmented, chromosomes, segBAF.table, input_optimum_pair, dist_choice, distancepng = NA, copynumberprofilespng = NA, nonroundedprofilepng = NA, gamma_param, read_depth, uninformative_BAF_threshold, allow100percent, reliabilityFile=NA, psi_min_initial=1.0, psi_max_initial=5.4, rho_min_initial=0.1, rho_max_initial=1.05, chr.names) # kjd 10-1-2014
-{
- siglevel_BAF = 0.05 # kjd 21-2-2014
- # siglevel_BAF = 0.005 # kjd 21-2-2014
-
- maxdist_BAF = 0.01 # kjd 21-2-2014
- # maxdist_BAF = 0.005 # kjd 21-2-2014
- # maxdist_BAF = 0.001 # kjd 21-2-2014
-
- #siglevel_LogR = 0.05 # kjd 21-2-2014
- #maxdist_LogR = 0.1 # kjd 21-2-2014
-
- #DCW 160314 - much more lenient logR thresholds (allow anything!)
- siglevel_LogR = -0.01 # TODO: This parameter is pushed down to is.segment.clonal but not used there (maybe not used at all?)
- maxdist_LogR = 1 # TODO: This parameter is pushed down to is.segment.clonal but not used there (maybe not used at all?)
-
-
-# psi_min_initial = 1.0
-# psi_max_initial = 5.4
-# rho_min_initial = 0.1
-# rho_max_initial = 1.05
-
- ininitial_bounds = list( psi_min = psi_min_initial, psi_max = psi_max_initial, rho_min = rho_min_initial, rho_max = rho_max_initial )
-
- new_bounds = get_new_bounds( input_optimum_pair, ininitial_bounds ) # kjd 21-2-2014
-
-
- ch = chromosomes
- b = bafsegmented
- r = lrrsegmented[names(bafsegmented)]
-
- s = get_segment_info(lrrsegmented,segBAF.table)
- # Make sure no segment of length 1 remains - TODO: this should not occur and needs to be prevented upstream
- s = s[s[,3] > 1,]
- dist_matrix_info <- create_distance_matrix_clonal( s, dist_choice, gamma_param, read_depth, siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR, uninformative_BAF_threshold, new_bounds)# kjd 10-2-2013
-
- d = dist_matrix_info$distance_matrix # kjd 10-2-2013
- minimise = dist_matrix_info$minimise # kjd 10-2-2013
-
- #DCW 210314
- if(minimise){
- best.distance = min(d)
- }else{
- best.distance = max(d)
- }
-
- ref_seg_matrix = dist_matrix_info$ref_seg_matrix
-
- ref_major = dist_matrix_info$ref_major
- ref_minor = dist_matrix_info$ref_minor
-
- #########################################################
-
- ret = find_centroid_of_global_minima( d, ref_seg_matrix, ref_major, ref_minor, s, dist_choice, minimise, new_bounds, distancepng, gamma_param, siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR, allow100percent, uninformative_BAF_threshold, read_depth) # kjd 28-2-2014
- optima_info_without_ref = ret$optima_info_without_ref
- optima_info = ret$optima_info
-
- nropt = optima_info$nropt
- psi_opt1 = optima_info$psi_opt1
- rho_opt1 = optima_info$rho_opt1
- ploidy_opt1 = optima_info$ploidy_opt1
- goodnessOfFit_opt1 = optima_info$goodnessOfFit_opt1
-
- distance.from.ref.seg = goodnessOfFit_opt1
-
- is.ref.better = F
- if (is.na(rho_opt1)) {
- print("reference segment did not provide a possible solution")
- } else if(psi_opt1>= psi_min_initial & psi_opt1<= psi_max_initial & rho_opt1>= rho_min_initial & rho_opt1<= rho_max_initial & ((minimise & distance.from.ref.segbest.distance))){
- is.ref.better = T
- print("reference segment gives better results than grid search")
- } else {
- print("reference segment gives no better results than grid search. Reverting to grid search solution")
- }
-
- psi_without_ref = optima_info_without_ref$psi_opt1
- rho_without_ref = optima_info_without_ref$rho_opt1
- ploidy_without_ref = optima_info_without_ref$ploidy_opt1
- goodnessOfFit_without_ref = optima_info_without_ref$goodnessOfFit_opt1
-
- #########################################################
-
- if(nropt>0) {
-
- #310314 DCW - always use grid search solution, because ref segment sometimes gives strange results
- #if(is.ref.better){
- # rho = rho_opt1
- # psi = psi_opt1
- # ploidy = ploidy_opt1
- # goodnessOfFit = goodnessOfFit_opt1
- # print("ref segment gives best solution. Using this solution for plotting")
- #}else{
- rho = rho_without_ref
- psi = psi_without_ref
- ploidy = ploidy_without_ref
- goodnessOfFit = goodnessOfFit_without_ref*100
- #print("grid search gives best solution. Using this solution for plotting")
- #}
-
- nAfull = (rho-1-(b-1)*2^(r/gamma_param)*((1-rho)*2+rho*psi))/rho
- nBfull = (rho-1+b*2^(r/gamma_param)*((1-rho)*2+rho*psi))/rho
- nA = pmax(round(nAfull),0)
- nB = pmax(round(nBfull),0)
-
- rBacktransform = gamma_param*log((rho*(nA+nB)+(1-rho)*2)/((1-rho)*2+rho*psi),2)
- bBacktransform = (1-rho+rho*nB)/(2-2*rho+rho*(nA+nB))
- rConf = ifelse(abs(rBacktransform)>0.15,pmin(100,pmax(0,100*(1-abs(rBacktransform-r)/abs(r)))),NA)
- bConf = ifelse(bBacktransform!=0.5,pmin(100,pmax(0,ifelse(b==0.5,100,100*(1-abs(bBacktransform-b)/abs(b-0.5))))),NA)
- #DCW 150711 - get deviations from expected values
- if(!is.na(reliabilityFile)){
- write.table(data.frame(segmentedBAF=b,backTransformedBAF=bBacktransform,confidenceBAF=bConf,segmentedR=r,backTransformedR=rBacktransform,confidenceR=rConf,nA=nA,nB=nB,nAfull=nAfull,nBfull=nBfull), reliabilityFile,sep=",",row.names=F)
- }
- confidence = ifelse(is.na(rConf),bConf,ifelse(is.na(bConf),rConf,(rConf+bConf)/2))
-
-
- # Make plots
- if (!is.na(copynumberprofilespng)) { png(filename = copynumberprofilespng, width = 2000, height = 500, res = 200, type = "cairo") }
- ASCAT::ascat.plotAscatProfile(n1all = nA, n2all = nB, heteroprobes = TRUE, ploidy = ploidy, rho = rho, goodnessOfFit = goodnessOfFit, nonaberrant = FALSE, ch = ch, lrr = lrr, bafsegmented = bafsegmented, chrs=chr.names)
- if (!is.na(copynumberprofilespng)) { dev.off() }
-
- # separated plotting from logic: create nonrounded copy number profile plot here
- if (!is.na(nonroundedprofilepng)) { png(filename = nonroundedprofilepng, width = 2000, height = 500, res = 200, type = "cairo") }
- ASCAT::ascat.plotNonRounded(ploidy = ploidy, rho = rho, goodnessOfFit = goodnessOfFit, nonaberrant = FALSE, nAfull = nAfull, nBfull = nBfull, bafsegmented = bafsegmented, ch = ch, lrr = lrr, chrs=chr.names)
- if (!is.na(nonroundedprofilepng)) { dev.off() }
- }
-
- # Recalculate the psi_t for this rho using only clonal segments
- psi_t = recalc_psi_t(psi_without_ref, rho_without_ref, gamma_param, lrrsegmented, segBAF.table, siglevel_BAF, maxdist_BAF, include_subcl_segments=F)
-
- # If there aren't any clonally fit segments, the above yields NA. In this case, revert to the original grid search psi_t
- if (is.na(psi_t)) {
- print("Recalculated psi_t was NA, reverting to grid search solution. This occurs when no segment could be fit with a clonal state, check sample for contamination")
- psi_t = psi_without_ref
- }
-
- output_optimum_pair = list(psi = psi_opt1, rho = rho_opt1, ploidy = ploidy_opt1)
- #output_optimum_pair_without_ref = list(psi = psi_without_ref, rho = rho_without_ref, ploidy = ploidy_without_ref)
- # Use the recalculated psi_t from the clonal segments as our final estimate of psi_t which is data driven with rho fixed
- output_optimum_pair_without_ref = list(psi = psi_t, rho = rho_without_ref, ploidy = ploidy_without_ref)
- return(list(output_optimum_pair=output_optimum_pair, output_optimum_pair_without_ref=output_optimum_pair_without_ref, distance = distance.from.ref.seg, distance_without_ref = best.distance, minimise = minimise, is.ref.better = is.ref.better)) # kjd 20-2-2014, adapted by DCW 140314
-}
-
-#' Recalculate psi_t based on rho and the available data
-#'
-#' @param psi A psi estimate
-#' @param rho A rho estimate
-#' @param platform_gamma The platform specific LogR scaling parameter
-#' @param lrrsegmented Segmented LogR, a vector with just the values
-#' @param segBAF.table Segmented BAF, the full table
-#' @param siglevel_BAF Significance level when testing wether a segment is clonal or subclonal given a rho/psi combination, parameter is used in \code{is.segment.clonal}
-#' @param maxdist_BAF Max distance BAF is allowed to be away from the copy number solution before we don't trust the value and overrule a p-value, parameter required when determining the clonal status of a segment in \code{is.segment.clonal}
-#' @param include_subcl_segments Boolean flag, supply TRUE if subclonal segments should be included when calculating psi_t, supply FALSE if only clonal segments should be included (default: TRUE)
-#' @noRd
-recalc_psi_t = function(psi, rho, gamma_param, lrrsegmented, segBAF.table, siglevel_BAF, maxdist_BAF, include_subcl_segments=T) {
- # Create segments of constant BAF/LogR
- s = get_segment_info(lrrsegmented[rownames(segBAF.table)], segBAF.table)
- # Make sure no segment of length 1 remains - TODO: this should not occur and needs to be prevented upstream
- s = s[s[,3] > 1,]
-
- # Fetch all segments, if required check which ones are clonal with this rho/psi configuration
- segs = list()
- for (i in 1:nrow(s)) {
- read_depth = NA # Unused parameter
- maxdist_LogR = NA # Unused parameter
- siglevel_LogR = NA # Unused parameter
- segment_info = is.segment.clonal(LogR=s[i, "r"],
- BAFreq=s[i, "b"],
- BAF.length=s[i, "length"],
- BAF.size=s[i, "size"],
- BAF.mean=s[i, "mean"],
- BAF.sd=s[i, "sd"],
- read_depth=read_depth,
- rho=rho,
- psi=psi,
- gamma_param=gamma_param,
- siglevel_BAF=siglevel_BAF,
- maxdist_BAF=maxdist_BAF,
- siglevel_LogR=siglevel_LogR,
- maxdist_LogR=maxdist_LogR)
- # Include this segment if we want to include all segments, or if we don't want subclonal segments include it only if its clonal
- if (include_subcl_segments | segment_info$is.clonal) {
- nMaj = segment_info$nMaj.test
- nMin = segment_info$nMin.test
- psi_t = calc_psi_t(nMaj+nMin, s[i, "r"], rho, gamma_param)
- segs[[length(segs)+1]] = data.frame(nMaj=nMaj, nMin=nMin, length=s[i, "length"], psi_t=psi_t)
- }
- }
- segs = do.call(rbind, segs)
-
- # Calculate psi_t as the weighted average copy number across all segments
- psi_t = sum(segs$psi_t * segs$length, na.rm=T) / sum(segs$length, na.rm=T)
- return(psi_t)
-}
-
-
-#' Calculate psi based on a reference segment and its associated logr
-#'
-#' @param total_cn Integer representing the total clonal copynumber (i.e. nMajor+nMinor)
-#' @param r The LogR of the segment with the total_cn copy number
-#' @param rho A cellularity estimate
-#' @param gamma_param Platform gamma parameter
-#' @author sd11
-#' @export
-calc_psi_t = function(total_cn, r, rho, gamma_param) {
- psi = (rho*(total_cn)+2-2*rho)/(2^(r/gamma_param))
- psi_t = (psi-2*(1-rho))/rho
- return(psi_t)
-}
diff --git a/R/clonal_ascat_calc.R b/R/clonal_ascat_calc.R
new file mode 100644
index 00000000..a21fd6fb
--- /dev/null
+++ b/R/clonal_ascat_calc.R
@@ -0,0 +1,399 @@
+####################################################################################################
+#' This function calculates a P-value, for a test where the null hypothesis is that
+#' the sample was drawn from a Gaussian population with the specified mean "mu_pop".
+#' @noRd
+calc_Pvalue_t_twotailed <- function(
+ sample_size,
+ sample_mean,
+ sample_SD,
+ mu_pop,
+ max_dist
+) {
+ tvar <- (sample_mean - mu_pop) * sqrt(sample_size) / sample_SD
+
+ # Guard against df <= 0 (sample_size <= 1)
+ pval <- rep(0, length(tvar))
+ valid <- !is.na(tvar) & (sample_size > 1)
+
+ if (any(valid)) {
+ pval[valid] <- 2 * stats::pt(abs(tvar[valid]), df = sample_size[valid] - 1, lower.tail = FALSE)
+ }
+
+ # Apply maxdist override
+ pval[is.na(pval)] <- 0
+ pval[abs(sample_mean - mu_pop) < max_dist] <- 1
+ return(pval)
+}
+
+####################################################################################################
+#' Helper function that calculates a binomial probability
+#' @noRd
+calc_binomial_prob <- function(sample_proportion, sample_size, pop_proportion) {
+ p <- pmax(0, pmin(1, pop_proportion))
+ x <- round(sample_proportion * sample_size)
+ x <- pmax(0, pmin(sample_size, x))
+
+ return(stats::dbinom(x, size = sample_size, prob = p))
+}
+
+####################################################################################################
+#' This function calculates a log likelihood ratio where the two hypotheses are that
+#' the tumour genome segment in question is "clonal".
+#' The first hypothesis is the "best fit" model we can find.
+#' The second hypothesis is the "second best fit" model we can find.
+#' @noRd
+calc_ln_likelihood_ratio <- function(LogR, BAF_req, BAF_length, BAF_size, BAF_mean, read_depth, rho, psi, gamma_param, maxdist_BAF) {
+ pooled_BAF_size <- read_depth * BAF_size
+
+ # if we don't have a value for LogR, fill in 0
+ if (is.na(LogR)) {
+ LogR <- 0
+ }
+ nMajor <- (rho - 1 + BAF_req * psi * 2^(LogR / gamma_param)) / rho
+ nMinor <- (rho - 1 + (1 - BAF_req) * psi * 2^(LogR / gamma_param)) / rho
+
+
+ # DCW - increase nMajor and nMinor together, to avoid impossible combinations (with negative subclonal fractions)
+ if (nMinor < 0 || is.na(nMinor)) {
+ if (BAF_req == 1) {
+ # avoid calling infinite copy number
+ nMajor <- 1000
+ } else {
+ nMajor <- nMajor + BAF_req * (0.01 - nMinor) / (1 - BAF_req)
+ if (nMajor < 0) nMajor <- 1000
+ }
+ nMinor <- 0.01
+ }
+
+ if (!is.finite(nMajor)) {
+ nMajor <- 0.01
+ }
+
+ # Check if there is a viable solution
+ if (!is.na(BAF_req)) {
+ nearest_edge <- prioritizeCopyNumbers(
+ rho = rho,
+ psi = psi,
+ BAF_req = BAF_req,
+ nMajor = nMajor,
+ nMinor = nMinor,
+ full = FALSE
+ )
+ nMaj <- nearest_edge$nMaj
+ nMin <- nearest_edge$nMin
+ BAF_levels <- (1 - rho + rho * nMaj) / (2 - 2 * rho + rho * (nMaj + nMin))
+ index_vect <- which(is.finite(BAF_levels))
+ BAF_levels <- BAF_levels[index_vect]
+
+ if (length(BAF_levels) > 1) {
+ likelihood_vect <- sapply(BAF_levels, function(x) {
+ calc_binomial_prob(BAF_mean, pooled_BAF_size, x)
+ })
+ likelihood_vect <- sort(likelihood_vect, decreasing = TRUE)
+
+ if ((likelihood_vect[1] > 0) && (likelihood_vect[2] > 0)) {
+ ln_lratio <- log(likelihood_vect[1]) - log(likelihood_vect[2])
+ } else {
+ ln_lratio <- 0
+ }
+ } else {
+ ln_lratio <- 0
+ }
+ } else {
+ ln_lratio <- 0
+ }
+
+ return(ln_lratio)
+}
+
+
+#' Helper function to estimate rho from a given copy number state and it's BAF. The LogR is not used.
+#' @noRd
+estimate_rho <- function(LogR_value, BAF_req_value, nA_value, nB_value) {
+ rho_value <- (2 * BAF_req_value - 1) / (2 * BAF_req_value - BAF_req_value * (nA_value + nB_value) - 1 + nA_value)
+ return(rho_value)
+}
+
+####################################################################################################
+#' Helper function to calculate psi from a copy number fit, BAF, LogR, rho and a platform gamma
+#' @noRd
+estimate_psi <- function(LogR_value, BAF_req_value, nA_value, nB_value, rho_value, gamma_param) {
+ temp_value <- 2^(-LogR_value / gamma_param)
+ temp_value <- temp_value * (2 + (rho_value * (nA_value + nB_value - 2)))
+ # DCW this returns psi rather than psi_t, i.e. the average ploidy of normal and tumour cells
+ temp_value <- temp_value - (2 * (1 - rho_value))
+ psi_value <- temp_value / rho_value
+ return(psi_value)
+}
+
+
+#' Function that calculates rho and psi from a given reference segment, defined by ref_seg, with copy number state nA_ref and nB_ref
+#' @noRd
+get_psi_rho_from_ref_seg <- function(ref_seg, s, nA_ref, nB_ref, gamma_param = 1) {
+ BAF_req <- s[ref_seg, "b"]
+ LogR <- s[ref_seg, "r"]
+
+ rho <- estimate_rho(LogR, BAF_req, nA_ref, nB_ref)
+ psi <- estimate_psi(LogR, BAF_req, nA_ref, nB_ref, rho, gamma_param)
+
+ # ploidy is recalculated based on results, to avoid bias (due to differences in normalization of LogR)
+ nA <- (rho - 1 - (s[, "b"] - 1) * 2^(s[, "r"] / gamma_param) * ((1 - rho) * 2 + rho * psi)) / rho
+ nB <- (rho - 1 + s[, "b"] * 2^(s[, "r"] / gamma_param) * ((1 - rho) * 2 + rho * psi)) / rho
+ ploidy <- sum((nA + nB) * s[, "length"]) / sum(s[, "length"])
+
+ # TODO DEBUG
+ if (rho > 0) {
+ ref_segment_info <- list(psi = psi, rho = rho, ploidy = ploidy)
+ } else {
+ ref_segment_info <- list(psi = NA, rho = NA, ploidy = NA)
+ }
+
+ return(ref_segment_info)
+}
+
+
+####################################################################################################
+#' This function calculates a t variate.
+#' @noRd
+calc_standardised_error <- function(
+ LogR, BAF_req, BAF_length, BAF_size, BAF_mean, BAF_sd,
+ rho, psi, gamma_param, maxdist_BAF
+) {
+ # if we don't have a value for LogR, fill in 0
+ if (is.na(LogR)) {
+ LogR <- 0
+ }
+
+ # Pre-calculate shared terms
+ factor <- 2^(LogR / gamma_param)
+ term_psi <- ((1 - rho) * 2 + rho * psi)
+
+ nMajor <- (rho - 1 + BAF_req * factor * term_psi) / rho
+ nMinor <- (rho - 1 + (1 - BAF_req) * factor * term_psi) / rho
+
+ # to make sure we're always in a positive square:
+ nMajor <- if (is.na(nMajor) || nMajor < 0) 0.01 else nMajor
+ nMinor <- if (is.na(nMinor) || nMinor < 0) 0.01 else nMinor
+
+ # note that these are sorted in the order of ascending BAF:
+ nMaj_opts <- c(floor(nMajor), ceiling(nMajor), floor(nMajor), ceiling(nMajor))
+ nMin_opts <- c(ceiling(nMinor), ceiling(nMinor), floor(nMinor), floor(nMinor))
+ x <- floor(nMinor)
+ y <- floor(nMajor)
+ ntot <- nMajor + nMinor
+
+ # Calculate BAF levels and handle division by zero
+ denom <- (2 - 2 * rho + rho * (nMaj_opts + nMin_opts))
+ index_vect <- which(denom != 0)
+
+ nMaj_opts <- nMaj_opts[index_vect]
+ nMin_opts <- nMin_opts[index_vect]
+ BAF_levels <- (1 - rho + rho * nMaj_opts) / denom[index_vect]
+
+ whichclosestlevel <- which.min(abs(BAF_levels - BAF_req))
+
+ # if 0.5 and there are multiple options, finetune
+ if (length(BAF_levels) >= 3) {
+ if (abs(BAF_levels[whichclosestlevel] - 0.5) < 1e-10 &&
+ abs(BAF_levels[2] - 0.5) < 1e-10 &&
+ abs(BAF_levels[3] - 0.5) < 1e-10) {
+ whichclosestlevel <- if (ntot > x + y + 1) 2 else 3
+ }
+ }
+
+ mu <- BAF_levels[whichclosestlevel]
+ included_segment <- 0
+ tvar <- 0
+
+ if (BAF_size > 0) {
+ if (BAF_sd != 0 && length(mu) > 0) {
+ # Use the provided calc_Pvalue_t_twotailed logic if needed,
+ # but original used studentise
+ tvar <- studentise(BAF_size, BAF_mean, BAF_sd, mu)
+ included_segment <- 1
+ }
+ }
+
+ return(list(included_segment = included_segment, tvar = tvar))
+}
+
+#' Helper function to calculate a studentised t-variate
+#' @noRd
+studentise <- function(sample_size, sample_mean, sample_sd, mu) {
+ return((sample_mean - mu) * sqrt(sample_size) / sample_sd)
+}
+
+
+#' Recalculate psi_t based on rho and the available data
+#'
+#' @param psi A psi estimate
+#' @param rho A rho estimate
+#' @param platform_gamma The platform specific LogR scaling parameter
+#' @param lrrsegmented Segmented LogR, a vector with just the values
+#' @param segBAF_table Segmented BAF, the full table
+#' @param siglevel_BAF Significance level when testing wether a segment is clonal or subclonal given a rho/psi combination, parameter is used in \code{is_segment_clonal}
+#' @param maxdist_BAF Max distance BAF is allowed to be away from the copy number solution before we don't trust the value and overrule a p-value, parameter required when determining the clonal status of a segment in \code{is_segment_clonal}
+#' @param include_subcl_segments Boolean flag, supply TRUE if subclonal segments should be included when calculating psi_t, supply FALSE if only clonal segments should be included (default: TRUE)
+#' @noRd
+recalc_psi_t <- function(psi, rho, gamma_param, lrrsegmented, segBAF_table, siglevel_BAF, maxdist_BAF, include_subcl_segments = TRUE) {
+ # Create segments of constant BAF/LogR
+ # Align lrrsegmented with segBAF_table using names if available
+ lrr_aligned <- if (!is.null(names(lrrsegmented)) && !is.null(rownames(segBAF_table))) {
+ lrrsegmented[rownames(segBAF_table)]
+ } else {
+ lrrsegmented
+ }
+
+ s <- get_segment_info(lrr_aligned, segBAF_table)
+ # Make sure no segment of length 1 remains
+ s <- s[!is.na(s[, 3]) & s[, 3] > 1, , drop = FALSE]
+
+ if (nrow(s) == 0) {
+ return(NA)
+ }
+
+ # Check which segments are clonal with this rho/psi configuration
+ segment_info <- is_segment_clonal(
+ LogR = s[, "r"],
+ BAF_req = s[, "b"],
+ BAF_length = s[, "length"],
+ BAF_size = s[, "size"],
+ BAF_mean = s[, "mean"],
+ BAF_sd = s[, "sd"],
+ read_depth = NA, # Unused legacy param
+ rho = rho,
+ psi = psi,
+ gamma_param = gamma_param,
+ siglevel_BAF = siglevel_BAF,
+ maxdist_BAF = maxdist_BAF,
+ siglevel_LogR = NA, # Unused legacy param
+ maxdist_LogR = NA # Unused legacy param
+ )
+
+ # Include this segment if we want to include all segments,
+ # or if we don't want subclonal segments include it only if its clonal
+ keep_mask <- if (include_subcl_segments) rep(TRUE, nrow(s)) else segment_info$is_clonal
+
+ if (!any(keep_mask)) {
+ return(NA)
+ }
+
+ nMaj <- segment_info$nMaj[keep_mask]
+ nMin <- segment_info$nMin[keep_mask]
+ s_r <- s[keep_mask, "r"]
+ s_len <- s[keep_mask, "length"]
+
+ # Calculate psi_t for each segment and then the weighted average
+ psi_t_vec <- calc_psi_t(nMaj + nMin, s_r, rho, gamma_param)
+ psi_t <- collapse::fsum(psi_t_vec * s_len) / collapse::fsum(s_len)
+
+ return(psi_t)
+}
+
+#' Calculate psi based on a reference segment and its associated logr
+#'
+#' @param total_cn Integer representing the total clonal copynumber (i.e. nMajor+nMinor)
+#' @param r The LogR of the segment with the total_cn copy number
+#' @param rho A cellularity estimate
+#' @param gamma_param Platform gamma parameter
+#' @author sd11
+#' @export
+calc_psi_t <- function(total_cn, r, rho, gamma_param) {
+ psi <- (rho * (total_cn) + 2 - 2 * rho) / (2^(r / gamma_param))
+ psi_t <- (psi - 2 * (1 - rho)) / rho
+ return(psi_t)
+}
+
+
+# Optimized Batch version of the t-test logic
+calc_batch_standardised_errors <- function(s, rho, psi, gamma_param) {
+ # s contains columns: r (LogR), b (BAF_req), length, size, mean, sd
+
+ scale <- psi * 2^(s[, "r"] / gamma_param)
+ nMajor_raw <- (rho - 1 + s[, "b"] * scale) / rho
+ nMinor_raw <- (rho - 1 + (1 - s[, "b"]) * scale) / rho
+
+ # Vectorized floor at 0.01
+ nMajor <- pmax(0.01, nMajor_raw)
+ nMinor <- pmax(0.01, nMinor_raw)
+
+ # Instead of a 4-item list per segment, we do 4 separate vector calculations
+ # This is where the massive speedup happens
+ nMaj_opts <- list(floor(nMajor), ceiling(nMajor), floor(nMajor), ceiling(nMajor))
+ nMin_opts <- list(ceiling(nMinor), ceiling(nMinor), floor(nMinor), floor(nMinor))
+
+ # Calculate BAF levels for all 4 possibilities across all segments simultaneously
+ BAF_levels <- lapply(1:4, function(k) {
+ denom <- (2 - 2 * rho + rho * (nMaj_opts[[k]] + nMin_opts[[k]]))
+ (1 - rho + rho * nMaj_opts[[k]]) / denom
+ })
+
+ # Vectorized "which.min(abs(BAF_levels - BAF_req))"
+ # We find the distance for all 4 options
+ diffs <- cbind(
+ abs(BAF_levels[[1]] - s[, "b"]),
+ abs(BAF_levels[[2]] - s[, "b"]),
+ abs(BAF_levels[[3]] - s[, "b"]),
+ abs(BAF_levels[[4]] - s[, "b"])
+ )
+
+ # Pick the best index for every segment at once
+ best_idx <- max.col(-diffs) # max of negative is min
+
+ # Map the best mu values
+ mu <- mapply(function(row, col) BAF_levels[[col]][row], seq_len(nrow(s)), best_idx)
+
+ # Final t-variable calculation
+ is_valid <- s[, "size"] > 0 & s[, "sd"] != 0
+ tvar <- ifelse(is_valid, (s[, "mean"] - mu) * sqrt(s[, "size"]) / s[, "sd"], 0)
+
+ return(tvar)
+}
+
+# Optimized batch version of log likelihood ratio
+#' @export
+calc_batch_ln_likelihood_ratios <- function(s, read_depth, rho, psi, gamma_param) {
+ # s contains columns: r (LogR), b (BAF_req), length, size, mean, sd
+ pooled_BAF_size <- read_depth * s[, "size"]
+ LogR <- s[, "r"]
+ LogR[is.na(LogR)] <- 0
+
+ # Pre-calculate shared terms
+ factor <- 2^(LogR / gamma_param)
+ term_psi <- ((1 - rho) * 2 + rho * psi)
+
+ nMajor_raw <- (rho - 1 + s[, "b"] * factor * term_psi) / rho
+ nMinor_raw <- (rho - 1 + (1 - s[, "b"]) * factor * term_psi) / rho
+
+ nMajor <- pmax(0.01, nMajor_raw)
+ nMinor <- pmax(0.01, nMinor_raw)
+
+ # Get nearest edges (best option only for likelihood)
+ nearest_edges <- prioritizeCopyNumbers(
+ rho = rho, psi = psi, BAF_req = s[, "b"],
+ nMajor = nMajor, nMinor = nMinor, full = FALSE
+ )
+
+ # corners 1 and 2
+ nMaj_opts <- nearest_edges$nMaj
+ nMin_opts <- nearest_edges$nMin
+
+ # Calculate BAF levels for both corners
+ calc_lev <- function(nM, nm) {
+ den <- (2 - 2 * rho + rho * (nM + nm))
+ ifelse(den != 0, (1 - rho + rho * nM) / den, 0.5)
+ }
+
+ lev1 <- calc_lev(nMaj_opts[, 1], nMin_opts[, 1])
+ lev2 <- calc_lev(nMaj_opts[, 2], nMin_opts[, 2])
+
+ # Calculate likelihoods for both
+ L1 <- calc_binomial_prob(s[, "mean"], pooled_BAF_size, lev1)
+ L2 <- calc_binomial_prob(s[, "mean"], pooled_BAF_size, lev2)
+
+ L_best <- pmax(L1, L2)
+ L_second <- pmin(L1, L2)
+
+ ln_lratio <- ifelse(L_best > 0 & L_second > 0, log(L_best) - log(L_second), 0)
+ return(ln_lratio)
+}
diff --git a/R/clonal_ascat_centroid.R b/R/clonal_ascat_centroid.R
new file mode 100644
index 00000000..367df267
--- /dev/null
+++ b/R/clonal_ascat_centroid.R
@@ -0,0 +1,177 @@
+####################################################################################################
+#' This function is an alternative procedure for finding the optimum (psi, rho) pair.
+#' This function first finds all the find all the global optima,
+#' and then finds the centroid of this set of globla optima.
+#' Then we find the global optimum which is nearest to the centroid.
+#' (When the set of global optima is convex, we expect the selected optimum to be at the centroid.)
+#' @param d A distance matrix
+#' @param ref_seg_matrix The corresponding ref seg matrix that belongs to d
+#' @param ref_major The corresponding major allele values with d
+#' @param ref_minor The corresponding minor allele values with d
+#' @param s A segmented BAF/LogR data.frame from \code{get_segment_info}
+#' @param dist_choice Some distance metrics require adaptation of the data (i.e. log transform)
+#' @param minimise Boolean whether we're minimising or maximising
+#' @param new_bounds The rho/psi boundaries between we are searching for a solution. This is a named list with values psi_min, psi_max, rho_min, rho_max
+#' @param distancepng String where the sunrise distance plot will be saved
+#' @param gamma_param The platform gamma
+#' @param siglevel_BAF The level at which BAF becomes significant TODO: this option is no longer used
+#' @param maxdist_BAF TODO: this option is no longer used
+#' @param siglevel_LogR The p-value at which logR becomes significant when establishing whether a segment should be subclonal
+#' @param maxdist_LogR The maximum distance allowed as slack when establishing the significance. This allows for the case when a breakpoint is missed, the segment would then not automatically become subclonal
+#' @param allow100percent Boolean whether to allow for a 100"\%" cellularity solution
+#' @param uninformative_baf_threshold The threshold above which BAF becomes uninformative
+#' @param read_depth TODO: this option is no longer used
+#' @return A list with fields optima_info_without_ref and optima_info
+#' @export
+find_centroid_of_global_minima <- function(
+ d, ref_seg_matrix, ref_major, ref_minor, s, dist_choice, minimise,
+ new_bounds, distancepng, gamma_param, siglevel_BAF, maxdist_BAF,
+ siglevel_LogR, maxdist_LogR, allow100percent, uninformative_baf_threshold,
+ read_depth
+) {
+ if (!minimise) {
+ d <- -d # This ensures that we "maximise" instead of "minimise"!
+ }
+
+ # Find height of global minima
+ gmin <- min(d, na.rm = TRUE)
+
+ # Find all global minima
+ nropt <- 0
+ optima <- list()
+
+ # Pre-extract psi/rho values from grid
+ psi_grid <- as.numeric(rownames(d))
+ rho_grid <- as.numeric(colnames(d))
+
+ for (i in seq_len(nrow(d))) {
+ for (j in seq_len(ncol(d))) {
+ if (!is.na(d[i, j]) && d[i, j] == gmin) {
+ psi <- psi_grid[i]
+ rho <- rho_grid[j]
+
+ # Calculate ploidy
+ term_base <- (rho - 1)
+ term_psi <- ((1 - rho) * 2 + rho * psi)
+ factor <- 2^(s[, "r"] / gamma_param)
+
+ nA <- (term_base - (s[, "b"] - 1) * factor * term_psi) / rho
+ nB <- (term_base + s[, "b"] * factor * term_psi) / rho
+ ploidy <- sum((nA + nB) * s[, "length"]) / sum(s[, "length"])
+
+ goodnessOfFit <- if (dist_choice == 0) {
+ # If we are already using the clonal proportion metric, gof is gmin
+ gmin
+ } else {
+ # If metric is squared error, we need to calculate clonal proportion separately for the plotter title
+ g_info <- calc_distance_clonal(
+ s, 0, rho, psi, gamma_param,
+ read_depth = NA,
+ siglevel_BAF = 0.05, maxdist_BAF = 0.01, siglevel_LogR = -0.01,
+ maxdist_LogR = 1, uninformative_baf_threshold = uninformative_baf_threshold
+ )
+ g_info$distance_value
+ }
+
+ nropt <- nropt + 1
+ optima[[nropt]] <- list(gmin = gmin, i = i, j = j, ploidy = ploidy, gof = goodnessOfFit)
+ }
+ }
+ }
+
+ # Find a "centroid" of the set of global minima
+ grid_x_vect <- sapply(optima, function(z) z$i)
+ grid_y_vect <- sapply(optima, function(z) z$j)
+
+ centre_x <- median(grid_x_vect)
+ centre_y <- median(grid_y_vect)
+ centre <- c(centre_x, centre_y)
+
+ index <- 1
+ sqrdist_min <- Inf
+ for (i in seq_along(optima)) {
+ grid_point <- c(optima[[i]]$i, optima[[i]]$j)
+ sqrdist <- (grid_point[1] - centre[1])^2 + (grid_point[2] - centre[2])^2
+
+ if (sqrdist <= sqrdist_min) {
+ sqrdist_min <- sqrdist
+ index <- i
+ }
+ }
+
+ grid_x <- optima[[index]]$i
+ grid_y <- optima[[index]]$j
+
+ psi_opt1 <- psi_grid[grid_x]
+ rho_opt1 <- min(rho_grid[grid_y], 1)
+ ploidy_opt1 <- optima[[index]]$ploidy
+ goodness_of_fit_opt1 <- optima[[index]]$gof
+
+ ref_seg <- ref_seg_matrix[grid_x, grid_y]
+
+ if (!minimise && dist_choice == 0) {
+ goodness_of_fit_opt1 <- -goodness_of_fit_opt1
+ }
+
+ # First optima set (without reference segment override)
+ optima_info_without_ref <- list(
+ nropt = nropt, psi_opt1 = psi_opt1, rho_opt1 = rho_opt1,
+ ploidy_opt1 = ploidy_opt1, ref_seg = ref_seg,
+ goodness_of_fit_opt1 = goodness_of_fit_opt1
+ )
+
+ # Logic for reference segment override
+ if (ref_seg == 0) {
+ psi_opt1 <- 2
+ rho_opt1 <- 1
+ ploidy_opt1 <- 2
+ goodness_of_fit_opt1 <- 1
+ } else {
+ ref_segment_info <- get_psi_rho_from_ref_seg(
+ ref_seg, s, ref_major[grid_x, grid_y], ref_minor[grid_x, grid_y], gamma_param
+ )
+
+ psi_opt1 <- ref_segment_info$psi
+ rho_opt1 <- ref_segment_info$rho
+ ploidy_opt1 <- ref_segment_info$ploidy
+
+ if (!is.na(rho_opt1)) {
+ distance_info <- calc_distance_clonal(
+ s, dist_choice, rho_opt1, psi_opt1, gamma_param, read_depth,
+ siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR, uninformative_baf_threshold
+ )
+ # Store the optimization distance separately if needed, but for now we follow the existing pattern
+ # but ensure we also have the goodness of fit (percentage)
+ if (dist_choice == 0) {
+ goodness_of_fit_opt1 <- distance_info$distance_value
+ } else {
+ g_info <- calc_distance_clonal(
+ s, 0, rho_opt1, psi_opt1, gamma_param, read_depth,
+ siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR, uninformative_baf_threshold
+ )
+ goodness_of_fit_opt1 <- g_info$distance_value
+ }
+ } else {
+ goodness_of_fit_opt1 <- Inf
+ }
+ }
+
+ # Final optima set
+ optima_info <- list(
+ nropt = nropt, psi_opt1 = psi_opt1, rho_opt1 = rho_opt1,
+ ploidy_opt1 = ploidy_opt1, ref_seg = ref_seg,
+ goodness_of_fit_opt1 = goodness_of_fit_opt1
+ )
+
+ # Plotting
+ if (!is.na(distancepng)) {
+ rhos <- c(optima_info_without_ref$rho_opt1, rho_opt1)
+ psis <- c(optima_info_without_ref$psi_opt1, psi_opt1)
+
+ grDevices::png(filename = distancepng, width = 1000, height = 1000, res = 1000 / 7, type = "cairo")
+ clonal_findcentroid_plot(minimise, dist_choice, -d, psis, rhos, new_bounds)
+ grDevices::dev.off()
+ }
+
+ return(list(optima_info_without_ref = optima_info_without_ref, optima_info = optima_info))
+}
diff --git a/R/clonal_ascat_distance.R b/R/clonal_ascat_distance.R
new file mode 100644
index 00000000..e2b7eb14
--- /dev/null
+++ b/R/clonal_ascat_distance.R
@@ -0,0 +1,343 @@
+#' This function computes various "distances", which are used as penalties for a copy number solution.
+#' This function is called when searching for a clonal copy number solution.
+#' @noRd
+calc_distance <- function(segs, dist_choice, rho, psi, gamma_param, uninformative_baf_threshold = 0.51) {
+ s <- segs
+
+ # common nA/nB logic
+ mult <- 2^(s[, "r"] / gamma_param) * ((1 - rho) * 2 + rho * psi)
+ nA <- (rho - 1 - (s[, "b"] - 1) * mult) / rho
+ nB <- (rho - 1 + s[, "b"] * mult) / rho
+
+
+ if (dist_choice == 0) { # original ASCAT distance
+ sum_nA <- sum(nA, na.rm = TRUE)
+ sum_nB <- sum(nB, na.rm = TRUE)
+ if (sum_nA < sum_nB) {
+ nMinor <- nA
+ } else {
+ nMinor <- nB
+ }
+
+ # Correctly identify uninformative BAF (near 0.5)
+ # Original logic using <= threshold is dangerous for unmirrored BAF (0..1)
+ # We want to downweight ONLY values close to 0.5
+ # Fallback to a tight window (0.49-0.51) if threshold is weird, or just trust the threshold logic
+ # Assuming uninformative_baf_threshold is e.g. 0.51 (meaning deviations < 0.01 from 0.5 are noisy)
+ # Let's use a robust check: uninformative if distance to 0.5 is small
+ # Correctly identify uninformative BAF (near 0.5)
+ # Original logic: weight <- ifelse(s[, "b"] <= uninformative_baf_threshold, 0.05, 1)
+ # NOTE: Since inputs are Minor Allele (<0.5) and threshold is 0.51, this effectively weights ALL segments as 0.05.
+ # While potentially counter-intuitive, this matches the Original Battenberg behavior exactly.
+ weight <- ifelse(s[, "b"] <= uninformative_baf_threshold, 0.05, 1)
+
+ dist_value <- sum(abs(nMinor - pmax(round(nMinor), 0))^2 * s[, "length"] * weight, na.rm = TRUE)
+ minimise <- TRUE
+ } else if (dist_choice == 1) { # new similarity measure suggested by DW 7-3-2014
+ sum_nA <- sum(nA, na.rm = TRUE)
+ sum_nB <- sum(nB, na.rm = TRUE)
+ if (sum_nA < sum_nB) {
+ nMinor <- nA
+ } else {
+ nMinor <- nB
+ }
+ dist_value <- sum((pmax(0, 0.5 - abs(nMinor - pmax(round(nMinor), 0))))^2 * s[, "length"], na.rm = TRUE)
+ minimise <- FALSE
+ } else if (dist_choice == 2) { # adapted DW's 7-3-2014 measure by SD 8-8-2014
+ sum_nA <- sum(nA, na.rm = TRUE)
+ sum_nB <- sum(nB, na.rm = TRUE)
+ if (sum_nA < sum_nB) {
+ nMinor <- nA
+ nMajor <- nB
+ } else {
+ nMinor <- nB
+ nMajor <- nA
+ }
+
+ dist_value <- 0.5 * sum((pmax(0, 0.5 - abs(nMinor - pmax(round(nMinor), 0)))^2 + (pmax(0, 0.5 - abs(nMajor - pmax(round(nMajor), 0)))^2)) * s[, "length"], na.rm = TRUE)
+ minimise <- FALSE
+ } else if (dist_choice == 3) { # adapted DW's 7-3-2014 measure by SD 8-8-2014 with homozygous deletion penalty
+ sum_nA <- sum(nA, na.rm = TRUE)
+ sum_nB <- sum(nB, na.rm = TRUE)
+ if (sum_nA < sum_nB) {
+ nMinor <- nA
+ nMajor <- nB
+ } else {
+ nMinor <- nB
+ nMajor <- nA
+ }
+
+ segs_penalty <- (pmax(0, 0.5 - abs(nMinor - pmax(round(nMinor), 0))))^2 + (pmax(0, 0.5 - abs(nMajor - pmax(round(nMajor), 0))))^2
+ hom_del <- nMinor < 0.5 & nMajor < 0.5 & nMinor >= 0 & nMajor >= 0
+ segs_penalty[which(hom_del)] <- segs_penalty[which(hom_del)] * 4
+ dist_value <- 0.5 * sum(segs_penalty * (s[, "length"] * ifelse(hom_del, 2, 1)), na.rm = TRUE)
+ minimise <- FALSE
+ }
+
+ return(list(distance_value = dist_value, minimise = minimise))
+}
+
+#' Internal optimized grid search distance matrix calculator
+#' @noRd
+create_distance_matrix <- function(s, dist_choice, gamma_param, uninformative_baf_threshold = 0.51,
+ min_rho = 0.1, max_rho = 1, min_psi = 1, max_psi = 5.4, nthreads = 1) {
+ psi_pos <- seq(min_psi, max_psi, 0.05)
+ rho_pos <- seq(min_rho, max_rho, 0.01)
+
+ d <- matrix(nrow = length(psi_pos), ncol = length(rho_pos))
+ rownames(d) <- psi_pos
+ colnames(d) <- rho_pos
+
+ if (nthreads > 1 && .Platform$OS.type != "windows") {
+ grid <- expand.grid(psi_idx = seq_along(psi_pos), rho_idx = seq_along(rho_pos))
+ results <- parallel::mclapply(seq_len(nrow(grid)), function(idx) {
+ tryCatch(
+ {
+ i <- grid$psi_idx[idx]
+ j <- grid$rho_idx[idx]
+ distance_info <- calc_distance(s, dist_choice, rho_pos[j], psi_pos[i], gamma_param, uninformative_baf_threshold = uninformative_baf_threshold)
+ return(list(i = i, j = j, val = distance_info$distance_value, minimise = distance_info$minimise))
+ },
+ error = function(e) {
+ return(e)
+ }
+ )
+ }, mc.cores = nthreads)
+
+ valid_results <- results[sapply(results, function(x) is.list(x) && !inherits(x, "error"))]
+
+ if (length(valid_results) < length(results)) {
+ warning("Some parallel distance calculations failed.")
+ }
+
+ for (res in valid_results) {
+ d[res$i, res$j] <- res$val
+ }
+
+ if (length(valid_results) > 0) {
+ minimise <- valid_results[[1]]$minimise
+ } else {
+ # Fallback if all failed or empty grid (unlikely)
+ # Calculate once synchronously to determine minimise or catch error
+ minimise <- TRUE
+ }
+ } else {
+ for (i in seq_along(psi_pos)) {
+ psi <- psi_pos[i]
+ for (j in seq_along(rho_pos)) {
+ rho <- rho_pos[j]
+ distance_info <- calc_distance(s, dist_choice, rho, psi, gamma_param, uninformative_baf_threshold = uninformative_baf_threshold)
+ d[i, j] <- distance_info$distance_value
+ }
+ }
+ minimise <- distance_info$minimise
+ }
+ return(list(distance_matrix = d, minimise = minimise))
+}
+
+#' Calculate distance matrix for clonal ASCAT
+#' @export
+create_distance_matrix_clonal <- function(
+ s, dist_choice, gamma_param, read_depth, siglevel_BAF, maxdist_BAF,
+ siglevel_LogR, maxdist_LogR, uninformative_baf_threshold, new_bounds,
+ nthreads = 1
+) {
+ psi_min <- new_bounds$psi_min
+ psi_max <- new_bounds$psi_max
+ rho_min <- new_bounds$rho_min
+ rho_max <- new_bounds$rho_max
+
+ psi_range <- psi_max - psi_min
+ rho_range <- rho_max - rho_min
+
+ delta_psi <- psi_range / 100
+ delta_rho <- rho_range / 100
+
+ psi_pos <- seq(psi_min, psi_max, delta_psi)
+ rho_pos <- seq(rho_min, rho_max, delta_rho)
+
+ grid <- expand.grid(psi = psi_pos, rho = rho_pos)
+
+ # Pre-calculate informative segments once for the entire grid search
+ lenient_threshold <- pmin(uninformative_baf_threshold, 0.505)
+ informative_idx <- which(!is.na(s[, "b"]) & pmax(s[, "b"], 1 - s[, "b"]) > lenient_threshold)
+ log_debug("Clonal distance check: {length(informative_idx)}/{nrow(s)} segments informative at >{lenient_threshold} threshold")
+
+ run_grid_point <- function(idx) {
+ psi <- grid$psi[idx]
+ rho <- grid$rho[idx]
+
+ res <- calc_distance_clonal(
+ s, dist_choice, rho, psi, gamma_param, read_depth,
+ siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR,
+ uninformative_baf_threshold,
+ informative_idx = informative_idx
+ )
+ return(res)
+ }
+
+ if (nthreads > 1 && .Platform$OS.type != "windows") {
+ results <- parallel::mclapply(seq_len(nrow(grid)), run_grid_point, mc.cores = nthreads)
+ } else {
+ results <- lapply(seq_len(nrow(grid)), run_grid_point)
+ }
+
+ # Extract values
+ # Handle both list and atomic vector results from mclapply (e.g. error strings)
+ get_val <- function(res, field) {
+ if (is.list(res) && field %in% names(res)) res[[field]] else NA
+ }
+
+ d_vals <- sapply(results, get_val, "distance_value")
+ ref_vals <- sapply(results, get_val, "max_clonal_segment")
+ maj_vals <- sapply(results, get_val, "ref_maj")
+ min_vals <- sapply(results, get_val, "ref_min")
+
+ dist_mat <- matrix(d_vals, nrow = length(psi_pos), ncol = length(rho_pos))
+ ref_seg_mat <- matrix(ref_vals, nrow = length(psi_pos), ncol = length(rho_pos))
+ ref_major_mat <- matrix(maj_vals, nrow = length(psi_pos), ncol = length(rho_pos))
+ ref_minor_mat <- matrix(min_vals, nrow = length(psi_pos), ncol = length(rho_pos))
+
+ rownames(dist_mat) <- psi_pos
+ colnames(dist_mat) <- rho_pos
+
+ # Safety check: If mclapply failed, provide a fallback for 'minimise'
+ minimise <- if (length(results) > 0 && is.list(results[[1]])) results[[1]]$minimise else TRUE
+
+ return(list(
+ distance_matrix = dist_mat,
+ minimise = minimise,
+ ref_seg_matrix = ref_seg_mat,
+ ref_major = ref_major_mat,
+ ref_minor = ref_minor_mat
+ ))
+}
+
+#' Internal function to calculate distance for a single rho/psi
+#' @noRd
+calc_distance_clonal <- function(
+ s, dist_choice, rho, psi, gamma_param, read_depth,
+ siglevel_BAF, maxdist_BAF, siglevel_LogR, maxdist_LogR,
+ uninformative_baf_threshold,
+ informative_idx = NULL
+) {
+ # Initialize accumulators
+ genome_size <- 0
+ clonal_genome_size <- 0
+ seg_count <- 0
+ n_included_segments <- 0
+ sum1 <- 0
+ sum2 <- 0
+ sum3 <- 0
+ sum_ln_lratio <- 0
+
+ max_clonal_segment <- 0
+ ref_maj <- NA
+ ref_min <- NA
+
+ if (is.null(informative_idx)) {
+ lenient_threshold <- pmin(uninformative_baf_threshold, 0.505)
+ informative_idx <- which(!is.na(s[, "b"]) & pmax(s[, "b"], 1 - s[, "b"]) > lenient_threshold)
+ }
+
+ if (length(informative_idx) == 0) {
+ # If no segments informative, we still need to return a structure
+ return(list(distance_value = 0, minimise = FALSE, max_clonal_segment = 0, ref_maj = NA, ref_min = NA))
+ }
+
+ # Vectorized calculation over informative segments
+ subset_s <- s[informative_idx, , drop = FALSE]
+ segment_info <- is_segment_clonal(
+ LogR = subset_s[, "r"],
+ BAF_req = subset_s[, "b"],
+ BAF_length = subset_s[, "length"],
+ BAF_size = subset_s[, "size"],
+ BAF_mean = subset_s[, "mean"],
+ BAF_sd = subset_s[, "sd"],
+ read_depth = read_depth,
+ rho = rho,
+ psi = psi,
+ gamma_param = gamma_param,
+ siglevel_BAF = siglevel_BAF,
+ maxdist_BAF = maxdist_BAF,
+ siglevel_LogR = siglevel_LogR,
+ maxdist_LogR = maxdist_LogR
+ )
+
+ is_clonal <- segment_info$is_clonal
+ nMaj <- segment_info$nMaj
+ nMin <- segment_info$nMin
+ is_balanced <- segment_info$balanced
+
+ # Genomic stats (Vectorized)
+ segment_sizes <- subset_s[, "length"]
+ genome_size <- sum(segment_sizes)
+ seg_count <- length(segment_sizes)
+ clonal_genome_size <- sum(segment_sizes[is_clonal])
+
+ # Reference segment selection (Vectorized)
+ max_clonal_segment <- 0
+ ref_maj <- NA
+ ref_min <- NA
+ is_ref_candidate <- is_clonal & !is_balanced
+
+ if (any(is_ref_candidate)) {
+ candidates_sizes <- segment_sizes[is_ref_candidate]
+ idx_in_candidates <- which.max(candidates_sizes)
+ # Map back to original indices
+ max_clonal_segment <- informative_idx[is_ref_candidate][idx_in_candidates]
+ ref_maj <- nMaj[is_ref_candidate][idx_in_candidates]
+ ref_min <- nMin[is_ref_candidate][idx_in_candidates]
+ }
+
+ # Standard error (Batch operation)
+ tvars <- calc_batch_standardised_errors(subset_s, rho, psi, gamma_param)
+ # Standard errors include segments where size > 0 and sd != 0
+ is_valid_se <- subset_s[, "size"] > 0 & !is.na(subset_s[, "sd"]) & subset_s[, "sd"] != 0
+ n_included_segments <- sum(is_valid_se)
+ sum1 <- sum(tvars[is_valid_se]^2)
+
+ # Distance sums (Vectorized)
+ baf_diff_sq <- (subset_s[, "b"] - subset_s[, "mean"])^2
+ sum2 <- sum(baf_diff_sq)
+ sum3 <- sum(subset_s[, "length"] * baf_diff_sq)
+
+ # Log Likelihood Ratio (Batch operation)
+ ln_lratios <- calc_batch_ln_likelihood_ratios(subset_s, read_depth, rho, psi, gamma_param)
+ sum_ln_lratio <- sum(ln_lratios)
+
+ # Calculate final distance values
+ clonal_proportion <- if (genome_size > 0) clonal_genome_size / genome_size else 0
+ dist1 <- if (n_included_segments > 0) sum1 / n_included_segments else 0
+ dist2 <- if (seg_count > 0) sum2 / seg_count else 0
+ dist3 <- if (genome_size > 0) sum3 / genome_size else 0
+
+ if (dist_choice == 0) {
+ dist_value <- clonal_proportion
+ minimise <- FALSE
+ } else if (dist_choice == 1) {
+ dist_value <- dist1
+ minimise <- TRUE
+ } else if (dist_choice == 2) {
+ dist_value <- dist2
+ minimise <- TRUE
+ } else if (dist_choice == 3) {
+ dist_value <- dist3
+ minimise <- TRUE
+ } else if (dist_choice == 4) {
+ dist_value <- sum_ln_lratio
+ minimise <- FALSE
+ } else {
+ # Default fallback
+ dist_value <- clonal_proportion
+ minimise <- FALSE
+ }
+
+ return(list(
+ distance_value = dist_value,
+ minimise = minimise,
+ max_clonal_segment = max_clonal_segment,
+ ref_maj = ref_maj,
+ ref_min = ref_min
+ ))
+}
diff --git a/R/clonal_segment.R b/R/clonal_segment.R
new file mode 100644
index 00000000..6023fcde
--- /dev/null
+++ b/R/clonal_segment.R
@@ -0,0 +1,157 @@
+#' This function decides if a segment is "clonal" (= TRUE) or not (= FALSE).
+#' (The alternative hypothesis is that the tumour genome segment in question exhibits "sub-clonal" variation.)
+#' We test the integer solutions for all 4 corners. Also, along side the hypothesis test for the BAF.
+#' We use a decision rule based on LogR (we could use a hypothesis test which takes account of the variance in LogR, or a fixed “tolerance”).
+#' If the null hypothesis is accepted for at least one corner, then we accept that
+#' the tumour genome segment in question is "clonal".
+#' @noRd
+is_segment_clonal <- function(
+ LogR, BAF_req, BAF_length, BAF_size, BAF_mean, BAF_sd,
+ read_depth, rho, psi, gamma_param, siglevel_BAF, maxdist_BAF,
+ siglevel_LogR, maxdist_LogR
+) {
+ # Handle NAs in LogR efficiently
+ # If LogR is a vector, we modify it in place
+ LogR[is.na(LogR)] <- 0
+
+ # Pre-calculate shared terms
+ factor <- 2^(LogR / gamma_param)
+ term_base <- (rho - 1)
+ term_psi <- ((1 - rho) * 2 + rho * psi)
+
+ nA <- (term_base - (BAF_req - 1) * factor * term_psi) / rho
+ nB <- (term_base + BAF_req * factor * term_psi) / rho
+
+ nMajor <- pmax(nA, nB, na.rm = TRUE)
+ nMinor <- pmin(nA, nB, na.rm = TRUE)
+
+ # Check validation logic (Vectorized)
+ nMajor.saved <- nMajor
+
+ # Validation logic for negative nMinor
+ neg_idx <- which(nMinor < 0)
+ if (length(neg_idx) > 0) {
+ b_req_sub <- BAF_req[neg_idx]
+
+ # Identify which ones are BAF_req == 1
+ is_one <- abs(b_req_sub - 1) < 1e-9
+
+ # Case 1: BAF == 1 -> Major = 1000
+ nMajor[neg_idx[is_one]] <- 1000
+
+ # Case 2: BAF != 1 -> Recalculate Major
+ not_one <- neg_idx[!is_one]
+ if (length(not_one) > 0) {
+ val <- nMajor[not_one] + BAF_req[not_one] * (0.01 - nMinor[not_one]) / (1 - BAF_req[not_one])
+ # Clamp to 1000 if negative
+ val[val < 0] <- 1000
+ nMajor[not_one] <- val
+ }
+
+ nMinor[neg_idx] <- 0.01
+ }
+
+ # prioritizeCopyNumbers is now vectorized (assumed - we will update it next)
+ all.edges <- prioritizeCopyNumbers(
+ rho = rho, psi = psi, BAF_req = BAF_req,
+ nMajor = nMajor, nMinor = nMinor, full = TRUE
+ )
+
+ # Columns follow the list format: nMaj[, 1] and nMaj[, 2] are the two states of the best edge
+ nMaj.test <- all.edges$nMaj
+ nMin.test <- all.edges$nMin
+
+ # Calculate levels for both options (Option 1 and Option 2)
+ calc_baf <- function(nM, nm) {
+ num <- 1 - rho + rho * nM
+ den <- 2 - 2 * rho + rho * (nM + nm)
+ lev <- num / den
+ lev[nM == 0 & nm == 0] <- 0.5
+ lev
+ }
+
+ lev1 <- calc_baf(nMaj.test[, 1], nMin.test[, 1])
+ lev2 <- calc_baf(nMaj.test[, 2], nMin.test[, 2])
+
+ dist1 <- abs(lev1 - BAF_req)
+ dist2 <- abs(lev2 - BAF_req)
+
+ # Vectorized choice of best index
+ choose_2 <- dist2 < dist1
+
+ best_nMaj <- ifelse(choose_2, nMaj.test[, 2], nMaj.test[, 1])
+ best_nMin <- ifelse(choose_2, nMin.test[, 2], nMin.test[, 1])
+ best_level <- ifelse(choose_2, lev2, lev1)
+
+ # P-value calculation
+ # Handle BAF_sd == 0 case
+ pval <- numeric(length(BAF_req))
+ valid_sd <- !is.na(BAF_sd) & BAF_sd > 0
+
+ if (any(valid_sd)) {
+ # Test segmented BAF value against theoretical copy number level
+ pval[valid_sd] <- calc_Pvalue_t_twotailed(
+ BAF_size[valid_sd], BAF_req[valid_sd],
+ BAF_sd[valid_sd], best_level[valid_sd], maxdist_BAF
+ )
+ }
+ # SD == 0 stays 0
+
+ balanced <- (best_nMaj == best_nMin)
+
+ # Clonal decision
+ # Explicitly handle NAs in pval to avoid propagating NAs to the is_clonal vector
+ is_clonal <- isTRUE(pval > siglevel_BAF)
+ # result of isTRUE is never NA. But if pval is vector?
+ # Need vectorized version of isTRUE
+ is_clonal <- !is.na(pval) & pval > siglevel_BAF
+
+ # Stability check (Vectorized)
+ unstable <- (nMajor - nMajor.saved) >= 1
+ # Handle NAs in unstable check just in case
+ unstable[is.na(unstable)] <- TRUE
+ is_clonal[unstable] <- FALSE
+
+ return(list(
+ is_clonal = is_clonal,
+ balanced = balanced,
+ nMaj = best_nMaj,
+ nMin = best_nMin
+ ))
+}
+
+
+#' Helper function to find new rho and psi boundaries given a current optimum pair.
+#' @noRd
+get_new_bounds <- function(input_optimum_pair, initial_bounds) {
+ # Define the window sizes (half-ranges)
+ psi_half <- 0.05 * (initial_bounds$psi_max - initial_bounds$psi_min)
+ rho_half <- 0.05 * input_optimum_pair$rho
+
+ # Calculate raw windows
+ psi_bounds <- c(input_optimum_pair$psi - psi_half, input_optimum_pair$psi + psi_half)
+ rho_bounds <- c(input_optimum_pair$rho - rho_half, input_optimum_pair$rho + rho_half)
+
+ # Clamp the windows to ensure they stay within initial boundaries
+ # If the window hits the bottom, shift it up; if it hits the top, shift it down.
+ adjust_bounds <- function(bounds, start, end) {
+ range_val <- bounds[2] - bounds[1]
+ low <- max(start, min(end - range_val, bounds[1]))
+ high <- min(end, max(start + range_val, bounds[2]))
+ return(c(low, high))
+ }
+
+ psi_final <- adjust_bounds(
+ psi_bounds, initial_bounds$psi_min,
+ initial_bounds$psi_max
+ )
+ rho_final <- adjust_bounds(
+ rho_bounds, initial_bounds$rho_min,
+ initial_bounds$rho_max
+ )
+
+ return(list(
+ psi_min = psi_final[1], psi_max = psi_final[2],
+ rho_min = rho_final[1], rho_max = rho_final[2]
+ ))
+}
diff --git a/R/concatenate.R b/R/concatenate.R
new file mode 100644
index 00000000..3011fe8d
--- /dev/null
+++ b/R/concatenate.R
@@ -0,0 +1,128 @@
+########################################################################################
+# Concatenate files
+########################################################################################
+#' Function to concatenate Impute output
+#' @noRd
+concatenateImputeFiles <- function(inputStart, boundaries) {
+ # Generate the list of potential filenames
+ # Using paste0 and vectorized division for a bit more speed
+ infiles <- paste0(inputStart, "_", boundaries[, 1] / 1000, "K_", boundaries[, 2] / 1000, "K.txt_haps")
+
+ # Filter for existing files with data
+ existing_files <- infiles[file.exists(infiles) & file.info(infiles)$size > 0]
+
+ if (length(existing_files) == 0) {
+ return(NULL)
+ }
+ # Impute files (.haps) have no headers
+ result <- vroom::vroom(
+ existing_files,
+ delim = " ",
+ col_names = FALSE,
+ show_col_types = FALSE
+ )
+ return(data.table::as.data.table(result))
+}
+
+#' Function to concatenate allele counter output
+#' @noRd
+concatenateAlleleCountFiles <- function(inputStart, inputEnd, chr_names) {
+ # Robust filename resolution: try both '1' and 'chr1'
+ find_file <- function(prefix, chrom, suffix) {
+ f1 <- paste0(prefix, chrom, suffix)
+ if (file.exists(f1)) {
+ return(f1)
+ }
+ # Try with/without 'chr'
+ if (grepl("^chr", chrom, ignore.case = TRUE)) {
+ f2 <- paste0(prefix, gsub("^chr", "", chrom, ignore.case = TRUE), suffix)
+ } else {
+ f2 <- paste0(prefix, "chr", chrom, suffix)
+ }
+ if (file.exists(f2)) {
+ return(f2)
+ }
+ return(NULL)
+ }
+
+ infiles <- character(0)
+ for (cn in chr_names) {
+ f <- find_file(inputStart, cn, inputEnd)
+ if (!is.null(f) && file.info(f)$size > 0) {
+ infiles <- c(infiles, f)
+ }
+ }
+
+ if (length(infiles) == 0) {
+ return(data.frame())
+ }
+ log_info("Using {length(infiles)} infiles in concatenateAlleleCountFiles. Example: {infiles[1]}")
+
+ # Bulk read using vroom. We remove delim="\t" to allow guessing,
+ # which handles both space and tab delimited counts.
+ combined <- vroom::vroom(
+ infiles,
+ col_names = c("CHR", "POS", "Count_A", "Count_C", "Count_G", "Count_T", "Good_depth"),
+ col_types = "ciiiiii",
+ comment = "#",
+ show_col_types = FALSE
+ )
+ data.table::setDF(combined)
+ return(combined)
+}
+
+#' Function to concatenate 1000 Genomes SNP reference files
+#' @noRd
+concatenateG1000SnpFiles <- function(inputStart, inputEnd, chr_names) {
+ # Robust filename resolution
+ find_file <- function(prefix, chrom, suffix) {
+ f1 <- paste0(prefix, chrom, suffix)
+ if (file.exists(f1)) {
+ return(f1)
+ }
+ if (grepl("^chr", chrom, ignore.case = TRUE)) {
+ f2 <- paste0(prefix, gsub("^chr", "", chrom, ignore.case = TRUE), suffix)
+ } else {
+ f2 <- paste0(prefix, "chr", chrom, suffix)
+ }
+ if (file.exists(f2)) {
+ return(f2)
+ }
+ return(NULL)
+ }
+
+ existing_files <- character(0)
+ for (cn in chr_names) {
+ f <- find_file(inputStart, cn, inputEnd)
+ if (!is.null(f) && file.info(f)$size > 0) {
+ existing_files[cn] <- f
+ }
+ }
+
+ if (length(existing_files) == 0) {
+ return(data.frame())
+ }
+
+ # Read files individually to inject chromosome if missing (common in some bundles)
+ # using data.table::fread for multi-delimiter robustness
+ datalist <- lapply(names(existing_files), function(cn) {
+ f <- existing_files[cn]
+
+ # Force colClasses to character for initial read to prevent parsing issues
+ d <- data.table::fread(f, sep = "auto", header = "auto", colClasses = "character", data.table = FALSE)
+
+ if (ncol(d) == 3) {
+ # File has (POS, A0, A1), we prepend the CHR from filename
+ d <- cbind(CHR = cn, d)
+ }
+
+ # Ensure consistent column naming to prevent binding issues
+ colnames(d)[1:4] <- c("CHR", "POS", "A0", "A1")
+
+ # Standardise structure to exactly 4 columns: CHR, POS, A0, A1
+ return(d[, 1:4])
+ })
+
+ combined <- data.table::as.data.table(data.table::rbindlist(datalist, use.names = TRUE))
+ return(combined)
+}
diff --git a/R/fastPCF.R b/R/fastPCF.R
deleted file mode 100755
index 8b7158ae..00000000
--- a/R/fastPCF.R
+++ /dev/null
@@ -1,519 +0,0 @@
-#PCF-ALGORITHM (KL):
-### EXACT version
-exactPcf <- function(y, kmin=5, gamma, yest) {
-## Implementaion of exact PCF by Potts-filtering
- ## x: input array of (log2) copy numbers
- ## kmin: Mininal length of plateaus
- ## gamma: penalty for each discontinuity
- N <- length(y)
- yhat <- rep(0,N);
- if (N < 2*kmin) {
- if (yest) {
- return(list(Lengde = N, sta = 1, mean = mean(y), nIntervals=1, yhat=rep(mean(y),N)))
- } else {
- return(list(Lengde = N, sta = 1, mean = mean(y), nIntervals=1))
- }
- }
- initSum <- sum(y[1:kmin])
- initKvad <- sum(y[1:kmin]^2)
- initAve <- initSum/kmin;
- bestCost <- rep(0,N)
- bestCost[kmin] <- initKvad - initSum*initAve
- bestSplit <- rep(0,N)
- bestAver <- rep(0,N)
- bestAver[kmin] <- initAve
- Sum <- rep(0,N)
- Kvad <- rep(0,N)
- Aver <- rep(0,N)
- Cost <- rep(0,N)
- kminP1=kmin+1
- for (k in (kminP1):(2*kmin-1)) {
- Sum[kminP1:k]<-Sum[kminP1:k]+y[k]
- Aver[kminP1:k] <- Sum[kminP1:k]/((k-kmin):1)
- Kvad[kminP1:k] <- Kvad[kminP1:k]+y[k]^2
- bestAver[k] <- (initSum+Sum[kminP1])/k
- bestCost[k] <- (initKvad+Kvad[kminP1])-k*bestAver[k]^2
- }
- for (n in (2*kmin):N) {
- yn <- y[n]
- yn2 <- yn^2
- Sum[kminP1:n] <- Sum[kminP1:n]+yn
- Aver[kminP1:n] <- Sum[kminP1:n]/((n-kmin):1)
- Kvad[kminP1:n] <- Kvad[kminP1:n]+yn2
- nMkminP1=n-kmin+1
- Cost[kminP1:nMkminP1] <- bestCost[kmin:(n-kmin)]+Kvad[kminP1:nMkminP1]-Sum[kminP1:nMkminP1]*Aver[kminP1:nMkminP1]+gamma
- Pos <- which.min(Cost[kminP1:nMkminP1])+kmin
- cost <- Cost[Pos]
- aver <- Aver[Pos]
- totAver <- (Sum[kminP1]+initSum)/n
- totCost <- (Kvad[kminP1]+initKvad) - n*totAver*totAver
- if (totCost < cost) {
- Pos <- 1
- cost <- totCost
- aver <- totAver
- }
- bestCost[n] <- cost
- bestAver[n] <- aver
- bestSplit[n] <- Pos-1
- }
- n <- N
- antInt <- 0
- if(yest){
- while (n > 0) {
- yhat[(bestSplit[n]+1):n] <- bestAver[n]
- n <- bestSplit[n]
- antInt <- antInt+1
- }
- } else {
- while (n > 0) {
- n <- bestSplit[n]
- antInt <- antInt+1
- }
- }
- n <- N #nProbes Spr Knut, fant ikke nProbes noe sted..
- lengde <- rep(0,antInt)
- start <- rep(0,antInt)
- verdi <- rep(0,antInt)
- oldSplit <- n
- antall <- antInt
- while (n > 0) {
- start[antall] <- bestSplit[n]+1
- lengde[antall] <- oldSplit-bestSplit[n]
- verdi[antall] <- bestAver[n]
- n <- bestSplit[n]
- oldSplit <- n
- antall <- antall-1
- }
- if (yest) {
- return(list(Lengde = lengde, sta = start, mean = verdi, nIntervals=antInt, yhat=yhat))
- } else {
- return(list(Lengde = lengde, sta = start, mean = verdi, nIntervals=antInt))
- }
-}
-
-
-
-selectFastPcf <- function(x,kmin,gamma,yest){
- xLength <- length(x)
- if (xLength< 1000) {
- result<-runFastPcf(x,kmin,gamma,0.15,0.15,yest)
- } else {
- if (xLength < 15000){
- result<-runFastPcf(x,kmin,gamma,0.12,0.05,yest)
- } else {
- result<-runPcfSubset(x,kmin,gamma,0.12,0.05,yest)
- }
- }
- return(result)
-}
-
-
-runFastPcf <- function(x,kmin,gamma,frac1,frac2,yest){
- antGen <- length(x)
- mark<-filterMarkS4(x,kmin,8,1,frac1,frac2,0.02,0.9)
- mark[antGen]=TRUE
- dense <- compact(x,mark)
- #print(dense$Nr)
- #print(frac2)
- result<-PottsCompact(kmin,gamma,dense$Nr,dense$Sum,dense$Sq,yest)
- return(result)
-}
-
-runPcfSubset <- function(x,kmin,gamma,frac1,frac2,yest){
- SUBSIZE <- 5000
- antGen <- length(x)
- mark<-filterMarkS4(x,kmin,8,1,frac1,frac2,0.02,0.9)
- markInit<-c(mark[1:(SUBSIZE-1)],TRUE)
- compX<-compact(x[1:SUBSIZE],markInit)
- mark2 <- rep(FALSE,antGen)
- mark2[1:SUBSIZE] <- markWithPotts(kmin,gamma,compX$Nr,compX$Sum,compX$Sq,SUBSIZE)
- mark2[4*SUBSIZE/5]<-TRUE
- start <- 4*SUBSIZE/5+1
- while(start + SUBSIZE < antGen){
- slutt<-start+SUBSIZE-1
- markSub<-c(mark2[1:(start-1)],mark[start:slutt])
- markSub[slutt] <- TRUE
- compX<-compact(x[1:slutt],markSub)
- mark2[1:slutt] <- markWithPotts(kmin,gamma,compX$Nr,compX$Sum,compX$Sq,slutt)
- start <- start+4*SUBSIZE/5
- mark2[start-1]<-TRUE
- }
- markSub<-c(mark2[1:(start-1)],mark[start:antGen])
- compX<-compact(x,markSub)
- result <- PottsCompact(kmin,gamma,compX$Nr,compX$Sum,compX$Sq,yest)
- return(result)
-}
-
-PottsCompact <- function(kmin, gamma, nr, res, sq, yest) {
-## Potts filtering on compact array;
- ## kmin: minimal length of plateau
- ## gamma: penalty for discontinuity
- ## nr: number of values between breakpoints
- ## res: sum of values between breakpoints
- ## sq: sum of squares of values between breakpoints
-
- N <- length(nr)
- Ant <- rep(0,N)
- Sum <- rep(0,N)
- Kvad <- rep(0,N)
- Cost <- rep(0,N)
- if (sum(nr) < 2*kmin){
- estim <- sum(res)/sum(nr)
- return(estim)
- }
- initAnt <- nr[1]
- initSum <- res[1]
- initKvad <- sq[1]
- initAve <- initSum/initAnt
- bestCost <- rep(0,N)
- bestCost[1] <- initKvad - initSum*initAve
- bestSplit <- rep(0,N)
- k <- 2
- while(sum(nr[1:k]) < 2*kmin) {
- Ant[2:k] <- Ant[2:k]+nr[k]
- Sum[2:k]<-Sum[2:k]+res[k]
- Kvad[2:k] <- Kvad[2:k]+sq[k]
- bestCost[k] <- (initKvad+Kvad[2])-(initSum+Sum[2])^2/(initAnt+Ant[2])
- k <- k+1
- }
- for (n in k:N) {
- Ant[2:n] <- Ant[2:n]+nr[n]
- Sum[2:n] <- Sum[2:n]+res[n]
- Kvad[2:n] <- Kvad[2:n]+sq[n]
- limit <- n
- while(limit > 2 & Ant[limit] < kmin) {limit <- limit-1}
- Cost[2:limit] <- bestCost[1:limit-1]+Kvad[2:limit]-Sum[2:limit]^2/Ant[2:limit]
- Pos <- which.min(Cost[2:limit])+ 1
- cost <- Cost[Pos]+gamma
- totCost <- (Kvad[2]+initKvad) - (Sum[2]+initSum)^2/(Ant[2]+initAnt)
- if (totCost < cost) {
- Pos <- 1
- cost <- totCost
- }
- bestCost[n] <- cost
- bestSplit[n] <- Pos-1
- }
- if (yest) {
- yhat<-rep(0,N)
- res<-findEst(bestSplit,N,nr,res,TRUE)
- } else {
- res<-findEst(bestSplit,N,nr,res,FALSE)
- }
- return(res)
-}
-
-compact <- function(y,mark){
- ## accumulates numbers of observations, sums and
- ## sums of squares between potential breakpoints
- return(list(
- Nr = diff(append(0, which(mark))),
- Sum = diff(append(0, cumsum(y)[mark])),
- Sq = diff(append(0, cumsum(y ^ 2)[mark]))))
-}
-
-findEst <- function(bestSplit,N,Nr,Sum,yest){
- n<-N
- lengde<-rep(0,N)
- antInt<-0
- while (n>0){
- antInt<-antInt+1
- lengde[antInt] <- n-bestSplit[n]
- n<-bestSplit[n]
- }
- lengde<-lengde[antInt:1]
- lengdeOrig<-rep(0,antInt)
- startOrig<-rep(1,antInt+1)
- verdi<-rep(0,antInt)
- start<-rep(1,antInt+1)
- for(i in 1:antInt){
- start[i+1] <- start[i]+lengde[i]
- lengdeOrig[i] <- sum(Nr[start[i]:(start[i+1]-1)])
- startOrig[i+1] <- startOrig[i]+lengdeOrig[i]
- verdi[i]<-sum(Sum[start[i]:(start[i+1]-1)])/lengdeOrig[i]
- }
-
- if(yest){
- yhat<-rep(0,startOrig[antInt+1]-1)
- for (i in 1:antInt){
- yhat[startOrig[i]:(startOrig[i+1]-1)]<-verdi[i]
- }
- startOrig<-startOrig[1:antInt]
- return(list(Lengde=lengdeOrig,sta=startOrig,mean=verdi,nIntervals=antInt,yhat=yhat))
- } else {
- startOrig<-startOrig[1:antInt]
- return(list(Lengde=lengdeOrig,sta=startOrig,mean=verdi,nIntervals=antInt))
- }
-
-}
-
-
-markWithPotts <- function(kmin, gamma, nr, res, sq, subsize) {
-## Potts filtering on compact array;
- ## kmin: minimal length of plateau
- ## gamma: penalty for discontinuity
- ## nr: number of values between breakpoints
- ## res: sum of values between breakpoints
- ## sq: sum of squares of values between breakpoints
-
- N <- length(nr)
- Ant <- rep(0,N)
- Sum <- rep(0,N)
- Kvad <- rep(0,N)
- Cost <- rep(0,N)
- markSub <- rep(FALSE,N)
- initAnt <- nr[1]
- initSum <- res[1]
- initKvad <- sq[1]
- initAve <- initSum/initAnt
- bestCost <- rep(0,N)
- bestCost[1] <- initKvad - initSum*initAve
- bestSplit <- rep(0,N)
- k <- 2
- while(sum(nr[1:k]) < 2*kmin) {
- Ant[2:k] <- Ant[2:k]+nr[k]
- Sum[2:k]<-Sum[2:k]+res[k]
- Kvad[2:k] <- Kvad[2:k]+sq[k]
- bestCost[k] <- (initKvad+Kvad[2])-(initSum+Sum[2])^2/(initAnt+Ant[2])
- k <- k+1
- }
- for (n in k:N) {
- Ant[2:n] <- Ant[2:n]+nr[n]
- Sum[2:n] <- Sum[2:n]+res[n]
- Kvad[2:n] <- Kvad[2:n]+sq[n]
- limit <- n
- while(limit > 2 & Ant[limit] < kmin) {limit <- limit-1}
- Cost[2:limit] <- bestCost[1:limit-1]+Kvad[2:limit]-Sum[2:limit]^2/Ant[2:limit]
- Pos <- which.min(Cost[2:limit])+ 1
- cost <- Cost[Pos]+gamma
- totCost <- (Kvad[2]+initKvad) - (Sum[2]+initSum)^2/(Ant[2]+initAnt)
- if (totCost < cost) {
- Pos <- 1
- cost <- totCost
- }
- bestCost[n] <- cost
- bestSplit[n] <- Pos-1
- markSub[Pos-1] <- TRUE
- }
- help<-findMarks(markSub,nr,subsize)
- return(help=help)
-}
-
-
-findMarks <- function(markSub,Nr,subsize){
- ## markSub: marks in compressed scale
- ## NR: number of observations between potenstial breakpoints
- mark<-rep(FALSE,subsize) ## marks in original scale
- if(sum(markSub)<1) {return(mark)} else {
- N<-length(markSub)
- ant <- seq(1:N)
- help <- ant[markSub]
- lengdeHelp<-length(help)
- help0 <- c(0,help[1:(lengdeHelp-1)])
- lengde <- help-help0
- start<-1
- oldStart<-1
- startOrig<-1
- for(i in 1:lengdeHelp){
- start <- start+lengde[i]
- lengdeOrig <- sum(Nr[oldStart:(start-1)])
- startOrig <- startOrig+lengdeOrig
- mark[startOrig-1]<-TRUE
- oldStart<-start
- }
- return(mark)
- }
-
-}
-
-
-compact <- function(y,mark){
-## accumulates numbers of observations, sums and
-## sums of squares between potential breakpoints
-## y: array to be compacted
-## mark: logical array of potential breakpoints
- tell<-seq(1:length(y))
- cCTell<-tell[mark]
- Ncomp<-length(cCTell)
- lowTell<-c(0,cCTell[1:(Ncomp-1)])
- ant<-cCTell-lowTell
- cy<-cumsum(y)
- cCcy<-cy[mark]
- lowcy<-c(0,cCcy[1:(Ncomp-1)])
- sum<-cCcy-lowcy
- cy2<-cumsum(y^2)
- cCcy2<-cy2[mark]
- lowcy2<-c(0,cCcy2[1:(Ncomp-1)])
- sq<-cCcy2-lowcy2
- return(list(Nr=ant,Sum=sum,Sq=sq))
-}
-
-filterMarkS4 <- function(x,kmin,L,L2,frac1,frac2,frac3,thres){
-## marks potential breakpoints, partially by a two 6*L and 6*L2 highpass
-## filters (L>L2), then by a filter seaching for potential kmin long segments
- lengdeArr <- length(x)
- xc<-cumsum(x)
- xc<-c(0,xc)
- ind11<-1:(lengdeArr-6*L+1)
- ind12<-ind11+L
- ind13<-ind11+3*L
- ind14<-ind11+5*L
- ind15<-ind11+6*L
- cost1<-abs(4*xc[ind13]-xc[ind11]-xc[ind12]-xc[ind14]-xc[ind15])
- cost1<-c(rep(0,3*L-1),cost1,rep(0,3*L))
- ##mark shortening in here
- in1<-1:(lengdeArr-6)
- in2<-in1+1
- in3<-in1+2
- in4<-in1+3
- in5<-in1+4
- in6<-in1+5
- in7<-in1+6
- test<-pmax(cost1[in1],cost1[in2],cost1[in3],cost1[in4],cost1[in5],cost1[in6],cost1[in7])
- test<-c(rep(0,3),test,rep(0,3))
- cost1B<-cost1[cost1>=thres*test]
- frac1B<-min(0.8,frac1*length(cost1)/length(cost1B))
- limit <- quantile(cost1B,(1-frac1B),names=FALSE)
- mark<-(cost1>limit)&(cost1>0.9*test)
-
-
- ind21<-1:(lengdeArr-6*L2+1)
- ind22<-ind21+L2
- ind23<-ind21+3*L2
- ind24<-ind21+5*L2
- ind25<-ind21+6*L2
- cost2<-abs(4*xc[ind23]-xc[ind21]-xc[ind22]-xc[ind24]-xc[ind25])
- limit2 <- quantile(cost2,(1-frac2),names=FALSE)
- mark2<-(cost2>limit2)
- mark2<-c(rep(0,3*L2-1),mark2,rep(0,3*L2))
- if(3*L>kmin){
- mark[kmin:(3*L-1)]<-TRUE
- mark[(lengdeArr-3*L+1):(lengdeArr-kmin)]<-TRUE
- }
- else
- {
- mark[kmin]<- TRUE
- mark[lengdeArr-kmin]<-TRUE
- }
-
- if(kmin>1){
- ind1<-1:(lengdeArr-3*kmin+1)
- ind2<-ind1+3*kmin
- ind3<-ind1+kmin
- ind4<-ind1+2*kmin
- shortAb <- abs(3*(xc[ind4]-xc[ind3])-(xc[ind2]-xc[ind1]))
- in1<-1:(length(shortAb)-6)
- in2<-in1+1
- in3<-in1+2
- in4<-in1+3
- in5<-in1+4
- in6<-in1+5
- in7<-in1+6
- test<-pmax(shortAb[in1],shortAb[in2],shortAb[in3],shortAb[in4],shortAb[in5],shortAb[in6],shortAb[in7])
- test<-c(rep(0,3),test,rep(0,3))
- cost1C<-shortAb[shortAb>=thres*test]
- frac1C<-min(0.8,frac3*length(shortAb)/length(cost1C))
- limit3 <- quantile(cost1C,(1-frac1C),names=FALSE)
- markH1<-(shortAb>limit3)&(shortAb>thres*test)
- markH2<-c(rep(FALSE,(kmin-1)),markH1,rep(FALSE,2*kmin))
- markH3<-c(rep(FALSE,(2*kmin-1)),markH1,rep(FALSE,kmin))
- mark<-mark|mark2|markH2|markH3
- } else {
- mark<-mark|mark2
- }
-
- if(3*L>kmin){
- mark[1:(kmin-1)]<-FALSE
- mark[kmin:(3*L-1)]<-TRUE
- mark[(lengdeArr-3*L+1):(lengdeArr-kmin)]<-TRUE
- mark[(lengdeArr-kmin+1):(lengdeArr-1)]<-FALSE
- mark[lengdeArr]<-TRUE
- }
- else
- {
- mark[1:(kmin-1)]<-FALSE
- mark[(lengdeArr-kmin+1):(lengdeArr-1)]<-FALSE
- mark[lengdeArr]<-TRUE
- mark[kmin]<- TRUE
- mark[lengdeArr-kmin]<-TRUE
- }
-
- return(mark)
-}
-
-#Get mad SD-estimate
-
-##Input:
-### x: vector of observations for which mad Sd is to be calculated
-### k: window size to be used in median filtering
-
-##Output:
-### SD: mad sd estimate
-
-##Required by:
-### multiPcf
-### fastPcf
-### pcf
-### aspcf
-
-
-##Requires:
-### medianFilter
-
-
-
-
-getMad <- function(x,k=25){
-
- #Remove observations that are equal to zero; are likely to be imputed, should not contribute to sd:
- x <- x[x!=0]
-
- #Calculate runMedian
- runMedian <- medianFilter(x,k)
-
- dif <- x-runMedian
- SD <- mad(dif)
-
- return(SD)
-}
-
-
-#########################################################################
-# Function to calculate running median for a given a window size
-#########################################################################
-
-##Input:
-### x: vector of numeric values
-### k: window size to be used for the sliding window (actually half-window size)
-
-## Output:
-### runMedian : the running median corresponding to each observation
-
-##Required by:
-### getMad
-### medianFilter
-
-
-##Requires:
-### none
-
-medianFilter <- function(x,k){
- n <- length(x)
- filtWidth <- 2*k + 1
-
- #Make sure filtWidth does not exceed n
- if(filtWidth > n){
- if(n==0){
- filtWidth <- 1
- }else if(n%%2 == 0){
- #runmed requires filtWidth to be odd, ensure this:
- filtWidth <- n - 1
- }else{
- filtWidth <- n
- }
- }
-
- runMedian <- runmed(x,k=filtWidth,endrule="median")
-
- return(runMedian)
-
-}
diff --git a/R/fast_PCF.R b/R/fast_PCF.R
new file mode 100755
index 00000000..e588e3a8
--- /dev/null
+++ b/R/fast_PCF.R
@@ -0,0 +1,330 @@
+# PCF-ALGORITHM (KL):
+### EXACT version
+exactPcf <- function(y, kmin = 5, gamma, yest) {
+ ## Implementaion of exact PCF by Potts-filtering
+ ## x: input array of (log2) copy numbers
+ ## kmin: Mininal length of plateaus
+ ## gamma: penalty for each discontinuity
+ N <- length(y)
+ yhat <- rep(0, N)
+ if (N < 2 * kmin) {
+ if (yest) {
+ return(list(Lengde = N, sta = 1, mean = mean(y), nIntervals = 1, yhat = rep(mean(y), N)))
+ } else {
+ return(list(Lengde = N, sta = 1, mean = mean(y), nIntervals = 1))
+ }
+ }
+ initSum <- sum(y[1:kmin])
+ initKvad <- sum(y[1:kmin]^2)
+ initAve <- initSum / kmin
+ bestCost <- rep(0, N)
+ bestCost[kmin] <- initKvad - initSum * initAve
+ bestSplit <- rep(0, N)
+ bestAver <- rep(0, N)
+ bestAver[kmin] <- initAve
+ Sum <- rep(0, N)
+ Kvad <- rep(0, N)
+ Aver <- rep(0, N)
+ kminP1 <- kmin + 1
+ for (k in (kminP1):(2 * kmin - 1)) {
+ Sum[kminP1:k] <- Sum[kminP1:k] + y[k]
+ Aver[kminP1:k] <- Sum[kminP1:k] / ((k - kmin):1)
+ Kvad[kminP1:k] <- Kvad[kminP1:k] + y[k]^2
+ bestAver[k] <- (initSum + Sum[kminP1]) / k
+ bestCost[k] <- (initKvad + Kvad[kminP1]) - k * bestAver[k]^2
+ }
+ # Call C++ core for the O(N^2) dynamic programming
+ cpp_res <- exactPcf_cpp(y, kmin, gamma)
+ bestSplit <- cpp_res$bestSplit
+ bestAver <- cpp_res$bestAver
+ n <- N
+ antInt <- 0
+ if (yest) {
+ while (n > 0) {
+ yhat[(bestSplit[n] + 1):n] <- bestAver[n]
+ n <- bestSplit[n]
+ antInt <- antInt + 1
+ }
+ } else {
+ while (n > 0) {
+ n <- bestSplit[n]
+ antInt <- antInt + 1
+ }
+ }
+ n <- N # nProbes Spr Knut, fant ikke nProbes noe sted..
+ lengde <- rep(0, antInt)
+ start <- rep(0, antInt)
+ verdi <- rep(0, antInt)
+ oldSplit <- n
+ antall <- antInt
+ while (n > 0) {
+ start[antall] <- bestSplit[n] + 1
+ lengde[antall] <- oldSplit - bestSplit[n]
+ verdi[antall] <- bestAver[n]
+ n <- bestSplit[n]
+ oldSplit <- n
+ antall <- antall - 1
+ }
+ if (yest) {
+ return(list(Lengde = lengde, sta = start, mean = verdi, nIntervals = antInt, yhat = yhat))
+ } else {
+ return(list(Lengde = lengde, sta = start, mean = verdi, nIntervals = antInt))
+ }
+}
+
+
+selectFastPcf <- function(x, kmin, gamma, yest) {
+ xLength <- length(x)
+ if (xLength < 1000) {
+ result <- runFastPcf(x, kmin, gamma, 0.15, 0.15, yest)
+ } else {
+ if (xLength < 15000) {
+ result <- runFastPcf(x, kmin, gamma, 0.12, 0.05, yest)
+ } else {
+ result <- runPcfSubset(x, kmin, gamma, 0.12, 0.05, yest)
+ }
+ }
+ return(result)
+}
+
+
+runFastPcf <- function(x, kmin, gamma, frac1, frac2, yest) {
+ antGen <- length(x)
+ mark <- filterMarkS4(x, kmin, 8, 1, frac1, frac2, 0.02, 0.9)
+ mark[antGen] <- TRUE
+ dense <- compact(x, mark)
+ result <- PottsCompact(kmin, gamma, dense$Nr, dense$Sum, dense$Sq, yest)
+ return(result)
+}
+
+runPcfSubset <- function(x, kmin, gamma, frac1, frac2, yest) {
+ SUBSIZE <- 5000
+ antGen <- length(x)
+ mark <- filterMarkS4(x, kmin, 8, 1, frac1, frac2, 0.02, 0.9)
+ markInit <- c(mark[1:(SUBSIZE - 1)], TRUE)
+ compX <- compact(x[1:SUBSIZE], markInit)
+ mark2 <- rep(FALSE, antGen)
+ mark2[1:SUBSIZE] <- markWithPotts(kmin, gamma, compX$Nr, compX$Sum, compX$Sq, SUBSIZE)
+ mark2[4 * SUBSIZE / 5] <- TRUE
+ start <- 4 * SUBSIZE / 5 + 1
+ while (start + SUBSIZE < antGen) {
+ slutt <- start + SUBSIZE - 1
+ markSub <- c(mark2[1:(start - 1)], mark[start:slutt])
+ markSub[slutt] <- TRUE
+ compX <- compact(x[1:slutt], markSub)
+ mark2[1:slutt] <- markWithPotts(kmin, gamma, compX$Nr, compX$Sum, compX$Sq, slutt)
+ start <- start + 4 * SUBSIZE / 5
+ mark2[start - 1] <- TRUE
+ }
+ markSub <- c(mark2[1:(start - 1)], mark[start:antGen])
+ compX <- compact(x, markSub)
+ result <- PottsCompact(kmin, gamma, compX$Nr, compX$Sum, compX$Sq, yest)
+ return(result)
+}
+
+PottsCompact <- function(kmin, gamma, nr, res, sq, yest) {
+ ## Potts filtering on compact array;
+ ## kmin: minimal length of plateau
+ ## gamma: penalty for discontinuity
+ ## nr: number of values between breakpoints
+ ## res: sum of values between breakpoints
+ ## sq: sum of squares of values between breakpoints
+
+ N <- length(nr)
+ Ant <- rep(0, N)
+ Sum <- rep(0, N)
+ Kvad <- rep(0, N)
+ if (sum(nr) < 2 * kmin) {
+ estim <- sum(res) / sum(nr)
+ return(estim)
+ }
+ initAnt <- nr[1]
+ initSum <- res[1]
+ initKvad <- sq[1]
+ initAve <- initSum / initAnt
+ bestCost <- rep(0, N)
+ bestCost[1] <- initKvad - initSum * initAve
+ k <- 2
+ while (sum(nr[1:k]) < 2 * kmin) {
+ Ant[2:k] <- Ant[2:k] + nr[k]
+ Sum[2:k] <- Sum[2:k] + res[k]
+ Kvad[2:k] <- Kvad[2:k] + sq[k]
+ bestCost[k] <- (initKvad + Kvad[2]) - (initSum + Sum[2])^2 / (initAnt + Ant[2])
+ k <- k + 1
+ }
+ # Call C++ core for the O(N^2) dynamic programming
+ cpp_res <- PottsCompact_cpp(kmin, gamma, nr, res, sq)
+ # Optimize the back-tracking and state expansion in C++
+ res <- findEst_cpp(cpp_res$bestSplit, N, nr, res, yest)
+ return(res)
+}
+
+compact <- function(y, mark) {
+ ## accumulates numbers of observations, sums and
+ ## sums of squares between potential breakpoints
+ return(list(
+ Nr = diff(append(0, which(mark))),
+ Sum = diff(append(0, cumsum(y)[mark])),
+ Sq = diff(append(0, cumsum(y^2)[mark]))
+ ))
+}
+
+
+markWithPotts <- function(kmin, gamma, nr, res, sq, subsize) {
+ ## Potts filtering on compact array;
+ ## kmin: minimal length of plateau
+ ## gamma: penalty for discontinuity
+ ## nr: number of values between breakpoints
+ ## res: sum of values between breakpoints
+ ## sq: sum of squares of values between breakpoints
+
+ N <- length(nr)
+ Ant <- rep(0, N)
+ Sum <- rep(0, N)
+ Kvad <- rep(0, N)
+ markSub <- rep(FALSE, N)
+ initAnt <- nr[1]
+ initSum <- res[1]
+ initKvad <- sq[1]
+ initAve <- initSum / initAnt
+ bestCost <- rep(0, N)
+ bestCost[1] <- initKvad - initSum * initAve
+ bestSplit <- rep(0, N)
+ k <- 2
+ while (sum(nr[1:k]) < 2 * kmin) {
+ Ant[2:k] <- Ant[2:k] + nr[k]
+ Sum[2:k] <- Sum[2:k] + res[k]
+ Kvad[2:k] <- Kvad[2:k] + sq[k]
+ bestCost[k] <- (initKvad + Kvad[2]) - (initSum + Sum[2])^2 / (initAnt + Ant[2])
+ k <- k + 1
+ }
+ # Call C++ core for the O(N^2) dynamic programming
+ cpp_res <- PottsCompact_cpp(kmin, gamma, nr, res, sq)
+ bestSplit <- cpp_res$bestSplit
+
+ # Reproduce markSub logic: mark the best split position for EVERY n
+ markSub[bestSplit[bestSplit > 0]] <- TRUE
+
+ # Optimize the mark expansion in C++
+ help <- findMarks_cpp(markSub, nr, subsize)
+ return(help = help)
+}
+
+
+filterMarkS4 <- function(x, kmin, L, L2, frac1, frac2, frac3, thres) {
+ lengdeArr <- length(x)
+ xc <- c(0, cumsum(x)) # Lead with 0 so xc[1] is 0
+
+ # --- Cost 1 Calculation (Window L) ---
+ ind11 <- 1:(lengdeArr - 6 * L + 1)
+ # The formula: 4*xc[ind13] - xc[ind11] - xc[ind12] - xc[ind14] - xc[ind15]
+ cost1 <- abs(4 * xc[ind11 + 3 * L] - xc[ind11] - xc[ind11 + L] - xc[ind11 + 5 * L] - xc[ind11 + 6 * L])
+ cost1_full <- c(numeric(3 * L - 1), cost1, numeric(3 * L))
+
+ # --- Rolling Max Parity ---
+ # Use original pmax approach for exact equivalence
+ in1 <- 1:(lengdeArr - 6)
+ test1_core <- pmax(
+ cost1_full[in1], cost1_full[in1 + 1], cost1_full[in1 + 2],
+ cost1_full[in1 + 3], cost1_full[in1 + 4], cost1_full[in1 + 5], cost1_full[in1 + 6]
+ )
+ test1 <- c(rep(0, 3), test1_core, rep(0, 3))
+
+ cost1B <- cost1_full[cost1_full >= thres * test1]
+ frac1B <- min(0.8, frac1 * length(cost1_full) / length(cost1B))
+ limit1 <- quantile(cost1B, (1 - frac1B), names = FALSE)
+ mark <- (cost1_full > limit1) & (cost1_full > 0.9 * test1)
+
+ # --- Cost 2 Calculation (Window L2) ---
+ ind21 <- 1:(lengdeArr - 6 * L2 + 1)
+ cost2 <- abs(4 * xc[ind21 + 3 * L2] - xc[ind21] - xc[ind21 + L2] - xc[ind21 + 5 * L2] - xc[ind21 + 6 * L2])
+ limit2 <- quantile(cost2, (1 - frac2), names = FALSE)
+
+ mark2_core <- (cost2 > limit2)
+ mark2 <- c(numeric(3 * L2 - 1), mark2_core, numeric(3 * L2))
+
+ # --- Edge Case Overrides ---
+ if (3 * L > kmin) {
+ mark[kmin:(3 * L - 1)] <- TRUE
+ mark[(lengdeArr - 3 * L + 1):(lengdeArr - kmin)] <- TRUE
+ } else {
+ mark[kmin] <- TRUE
+ mark[lengdeArr - kmin] <- TRUE
+ }
+
+ # --- Short Segment Detection (kmin) ---
+ if (kmin > 1) {
+ i_s <- 1:(lengdeArr - 3 * kmin + 1)
+ shortAb <- abs(3 * (xc[i_s + 2 * kmin] - xc[i_s + kmin]) - (xc[i_s + 3 * kmin] - xc[i_s]))
+
+ in1_s <- 1:(length(shortAb) - 6)
+ test_s_core <- pmax(
+ shortAb[in1_s], shortAb[in1_s + 1], shortAb[in1_s + 2],
+ shortAb[in1_s + 3], shortAb[in1_s + 4], shortAb[in1_s + 5], shortAb[in1_s + 6]
+ )
+ test_s <- c(rep(0, 3), test_s_core, rep(0, 3))
+
+ cost1C <- shortAb[shortAb >= thres * test_s]
+ frac1C <- min(0.8, frac3 * length(shortAb) / length(cost1C))
+ limit3 <- quantile(cost1C, (1 - frac1C), names = FALSE)
+
+ markH1 <- (shortAb > limit3) & (shortAb > thres * test_s)
+
+ # Pixel-perfect shift reproduction
+ markH2 <- c(logical(kmin - 1), markH1, logical(2 * kmin))
+ markH3 <- c(logical(2 * kmin - 1), markH1, logical(kmin))
+ mark <- mark | mark2 | markH2 | markH3
+ } else {
+ mark <- mark | mark2
+ }
+
+ # --- Final Boundary Cleanup ---
+ # Re-applying the final mark overrides exactly as the original function
+ if (3 * L > kmin) {
+ mark[1:(kmin - 1)] <- FALSE
+ mark[kmin:(3 * L - 1)] <- TRUE
+ mark[(lengdeArr - 3 * L + 1):(lengdeArr - kmin)] <- TRUE
+ mark[(lengdeArr - kmin + 1):(lengdeArr - 1)] <- FALSE
+ } else {
+ mark[1:(kmin - 1)] <- FALSE
+ mark[(lengdeArr - kmin + 1):(lengdeArr - 1)] <- FALSE
+ mark[kmin] <- TRUE
+ mark[lengdeArr - kmin] <- TRUE
+ }
+ mark[lengdeArr] <- TRUE
+
+ return(mark)
+}
+
+# Optimized function to calculate the Median Absolute Deviation of a signal
+# after removing a running median trend.
+get_mad <- function(x, k = 25) {
+ # Use collapse for fast, memory-efficient subsetting
+ # Removes zeros which often represent missing/imputed data in genomics
+ x_filtered <- collapse::fsubset(x, x != 0)
+
+ # Use rlang to safely check for empty input after filtering
+ if (length(x_filtered) == 0) {
+ return(NA)
+ }
+
+ # Calculate running median parameters
+ n <- length(x_filtered)
+ filt_width <- 2 * k + 1
+
+ # Ensure filt_width is odd and does not exceed n to satisfy runmed requirements
+ if (filt_width > n) {
+ filt_width <- if (n %% 2 == 0) max(1, n - 1) else max(1, n)
+ }
+
+ # Calculate the running median using the C-based engine
+ # endrule = "median" ensures we don't get NAs at the start/end of the vector
+ run_median <- stats::runmed(x_filtered, k = filt_width, endrule = "median")
+
+ # Calculate the difference and the MAD
+ # collapse::fmad is significantly faster than stats::mad
+ residual_signal <- x_filtered - run_median
+ SD <- stats::mad(residual_signal)
+
+ return(SD)
+}
diff --git a/R/fit_copy_number.R b/R/fit_copy_number.R
new file mode 100644
index 00000000..75b73cc6
--- /dev/null
+++ b/R/fit_copy_number.R
@@ -0,0 +1,1511 @@
+#' Fit copy number
+#'
+#' Function that will fit a clonal copy number profile to segmented data. It
+#' first matches the raw LogR with the segmented BAF to create segmented LogR.
+#' Then ASCAT is run to obtain a clonal copy number profile. Beyond logRsegmented
+#' it produces the rho_and_psi file and the cellularity_ploidy file.
+#' @param samplename Samplename used to name the segmented logr output file
+#' @param outputfile_prefix Prefix used for all output file names, except
+#' logRsegmented
+#' @param inputfile_baf_segmented Filename that points to the BAF segmented data
+#' @param inputfile_baf Filename that points to the raw BAF data
+#' @param inputfile_logr Filename that points to the raw LogR data
+#' @param dist_choice The distance metric that is used internally to rank clonal
+#' copy number solutions
+#' @param ascat_dist_choice The distance metric used to obtain an initial
+#' cellularity and ploidy estimate
+#' @param min_ploidy The minimum ploidy to consider (Default 1.6)
+#' @param max_ploidy The maximum ploidy to consider (Default 4.8)
+#' @param min_rho The minimum cellularity to consider (Default 0.1)
+#' @param max_rho The maximum cellularity to consider (Default 1.0)
+#' @param min_goodness The minimum goodness of fit for a solution to have to be
+#' considered (Default 63)
+#' @param uninformative_baf_threshold The threshold beyond which BAF becomes
+#' uninformative (Default 0.51)
+#' @param gamma_param Technology parameter, compaction of Log R profiles.
+#' Expected decrease in case of deletion in diploid sample, 100 "\%" aberrant
+#' cells; 1 in ideal case, 0.55 of Illumina 109K arrays (Default 1)
+#' @param use_preset_rho_psi Boolean whether to use user specified rho and psi
+#' values (Default FALSE)
+#' @param preset_rho A user specified rho to fit a copy number profile to
+#' (Default NA)
+#' @param preset_psi A user specified psi to fit a copy number profile to
+#' (Default NA)
+#' @param read_depth Legacy parameter that is no longer used (Default 30)
+#' @param analysis A String representing the type of analysis to be run, this
+#' determines whether the distance figure is produced (Default paired)
+#' @param nthreads The number of paralel processes to run
+#' @param enhanced_grid_search Flag to determine if the grid search should be performed with a higher number of steps (Default: FALSE)
+#' @param n_neighbors_search Number of top grid points to search (integer). Set to Inf for exhaustive search. If NULL, only local minima are searched.
+#' @author dw9, sd11
+#' @export
+fit_copy_number <- function(
+ samplename,
+ outputfile_prefix,
+ inputfile_baf_segmented,
+ inputfile_baf,
+ inputfile_logr,
+ dist_choice,
+ ascat_dist_choice,
+ min_ploidy = 1.6,
+ max_ploidy = 4.8,
+ min_rho = 0.1,
+ max_rho = 1.0,
+ min_goodness = 0.63,
+ uninformative_baf_threshold = 0.51,
+ gamma_param = 1,
+ use_preset_rho_psi = FALSE,
+ preset_rho = NA,
+ preset_psi = NA,
+ read_depth = 30,
+ analysis = "paired",
+ nthreads = 1,
+ enhanced_grid_search = FALSE,
+ n_neighbors_search = NULL,
+ grid_psi_step = 0.05,
+ grid_rho_step = 0.01,
+ local_min_window_size = 7
+) {
+ options(warn = 1) # Force immediate warning printing
+ assert_file_exists(inputfile_baf_segmented)
+ assert_file_exists(inputfile_baf)
+ assert_file_exists(inputfile_logr)
+
+ if ((max_ploidy - min_ploidy) < 0.05) {
+ log_failure("Supplied ploidy range must be larger than 0.05: \\
+ {min_ploidy}-{max_ploidy}")
+ }
+
+ # Read in the required data
+ segmented.BAF.data <- read_bafsegmented(inputfile_baf_segmented)
+ # removed setDF to keep as data.table
+
+ raw.BAF.data <- read_baf_as_data_frame(inputfile_baf)
+ data.table::setDT(raw.BAF.data)
+ names(raw.BAF.data)[3] <- "RawBAF"
+
+ raw.logR.data <- read_baf_as_data_frame(inputfile_logr)
+ data.table::setDT(raw.logR.data)
+ names(raw.logR.data)[3] <- "RawLogR"
+
+ # Remove duplicates and set keys (Fast data.table deduplication)
+ segmented.BAF.data[, identifier := paste(Chromosome, Position, sep = "_")]
+ if (anyDuplicated(segmented.BAF.data, by = "identifier")) {
+ segmented.BAF.data <- unique(segmented.BAF.data, by = "identifier")
+ }
+ # We don't need rownames on data.table, but we can keep identifier column if needed
+
+ # Drop NAs
+ raw.BAF.data <- raw.BAF.data[!is.na(RawBAF)]
+ raw.logR.data <- raw.logR.data[!is.na(RawLogR)]
+
+ BAF.data <- list()
+ logR.data <- list()
+ segmented.logR.data <- list()
+ matched.segmented.BAF.data <- list()
+
+ gsubchr <- function(chr) gsub("chr", "", as.character(chr))
+ chr_names <- gsubchr(unique(segmented.BAF.data$Chromosome))
+
+ # Fast update of headers (by reference)
+ segmented.BAF.data[, Chromosome := gsubchr(Chromosome)]
+ raw.BAF.data[, Chromosome := gsubchr(Chromosome)]
+ raw.logR.data[, Chromosome := gsubchr(Chromosome)]
+
+ # Efficient Key Setting
+ # Efficient Key Setting
+ data.table::setkey(segmented.BAF.data, Chromosome, Position)
+ data.table::setkey(raw.BAF.data, Chromosome, Position)
+ data.table::setkey(raw.logR.data, Chromosome, Position)
+
+ # Inner Join: Only keep positions present in BOTH segmented and raw BAF data
+ log_info("Merging BAF data (Intersection)...")
+ matched.segmented.BAF.data <- merge(segmented.BAF.data, raw.BAF.data, by = c("Chromosome", "Position"), all = FALSE)
+
+ # Inner Join: Only keep positions present in LogR data
+ log_info("Merging LogR data (Intersection)...")
+ master_data <- merge(matched.segmented.BAF.data, raw.logR.data, by = c("Chromosome", "Position"), all = FALSE)
+
+ # Calculate Segmented LogR
+ # We perform this by Chromosome to ensure segments don't bleed across chromosomes
+ log_info("Calculating Segmented LogR...")
+
+ # Ensure key is set for faster grouping
+ data.table::setkey(master_data, Chromosome, Position)
+
+ # Original logic uses mean. fmean handles NAs by default.
+ master_data[, SegmentedLogR := {
+ if (all(is.na(BAFseg))) {
+ NA_real_
+ } else {
+ seg_ids <- data.table::rleid(BAFseg)
+ collapse::fmean(RawLogR, g = seg_ids, TRA = "replace")
+ }
+ }, by = Chromosome]
+
+ log_info("Final data synchronization check: {nrow(master_data)} loci.")
+ if (nrow(master_data) < 100) {
+ log_failure("Too few SNPs ({nrow(master_data)}) remain. Data is likely unusable.")
+ }
+
+ # Prepare vectors for ASCAT
+ if (!"BAFseg" %in% names(master_data)) log_failure("Missing BAFseg column in merged data")
+
+ # Extract final vectors and set Names for runASCAT alignment
+ names_vec <- paste(master_data$Chromosome, master_data$Position, sep = "_")
+
+ segBAF <- 1 - master_data$BAFseg
+ names(segBAF) <- names_vec
+
+ segLogR <- master_data$SegmentedLogR
+ names(segLogR) <- names_vec
+
+ logR <- master_data$RawLogR
+ names(logR) <- names_vec
+
+ if (!is.numeric(segLogR)) {
+ segLogR <- as.numeric(segLogR)
+ }
+
+ # Crucial: Use rownames to allow ASCAT to map segments to probes
+ row_ids <- paste(master_data$Chromosome, master_data$Position, sep = "_")
+ names(segBAF) <- row_ids
+ names(segLogR) <- row_ids
+ names(logR) <- row_ids
+
+ # Calculate chromosome indices for the combined vectors
+ # Using split is efficient enough here
+ chr_segs <- split(seq_len(nrow(master_data)), master_data$Chromosome)
+ # Re-order chr_segs to match chr_names order explicitly
+ chr_segs <- chr_segs[chr_names]
+ chr_segs <- chr_segs[!sapply(chr_segs, is.null)]
+
+ # write out the segmented logR data
+ data.table::fwrite(
+ master_data[, .(Chromosome, Position, SegmentedLogR)],
+ paste0(samplename, ".logRsegmented.txt"),
+ sep = "\t", col.names = FALSE, row.names = FALSE, quote = FALSE
+ )
+
+ # Compatibility: Ensure matched.segmented.BAF.data is the full object expected by run_clonal_ASCAT
+ # Original code expects column 5 to be named after the samplename
+ # master_data columns: Chromosome (1), Position (2), BAF (3), BAFphased (4), BAFseg (5), RawBAF (6), RawLogR (7), SegmentedLogR (8)
+ matched.segmented.BAF.data <- master_data
+ names(matched.segmented.BAF.data)[5] <- samplename
+ # run_clonal_ASCAT uses 1 - matched.segmented.BAF.data[[5]]
+ # With data.table merge, column order depends on inputs.
+ # segmented.BAF.data: Chromosome, Position, BAF, BAFphased, BAFseg
+ # raw.BAF.data: Chromosome, Position, RawBAF
+ # merge puts 'by' first (Chr, Pos). Then cols from x (BAF, BAFphased, BAFseg), then y (RawBAF).
+ # So BAFseg is indeed col 5. But accessing by name is safer if code allows.
+ # But existing run_clonal_ASCAT might function call with positional args or subsetting?
+ # The original code passed 'matched.segmented.BAF.data' to run_clonal_ASCAT (line 283).
+ # Let's check run_clonal_ASCAT signature if possible, but assuming it uses column names or similar structure is safe enough
+ # given we kept the structure 'master_data'.
+
+ # Also BAF.data[[2]] is used. In original list, it was Position, RawBAF.
+ # So [[2]] is RawBAF.
+ # We need to construct the expected arguments for runASCAT calls below.
+ # runASCAT(logR, 1 - BAF.data[[2]], ...)
+ # Here BAF.data[[2]] means strict column 2 access?
+ # If BAF.data was a data.frame Position, RawBAF, then [[2]] is RawBAF vector.
+ # So we pass 'master_data$RawBAF'.
+
+ # Run ASCAT Grid Search
+ if (use_preset_rho_psi) {
+ log_info("Using preset rho ({preset_rho}) and psi ({preset_psi}). Skipping grid search.")
+ ascat_optimum_pair <- list(rho = preset_rho, psi = preset_psi, ploidy = preset_psi)
+ } else {
+ log_info("Starting ASCAT Grid Search (this may take several minutes)...")
+ distance_outfile <- paste0(outputfile_prefix, "distance.png")
+ copynumberprofile_outfile <- paste0(
+ outputfile_prefix,
+ "copynumberprofile.png"
+ )
+ nonroundedprofile_outfile <- paste0(
+ outputfile_prefix,
+ "nonroundedprofile.png"
+ )
+ cnaStatusFile <- paste0(
+ outputfile_prefix,
+ "copynumber_solution_status.txt"
+ )
+
+ if (enhanced_grid_search) {
+ log_info("Running ENHANCED grid search...")
+ ascat_optimum_pair <- runASCAT_enhanced(
+ logR, 1 - master_data$RawBAF, segLogR, segBAF,
+ chr_segs, ascat_dist_choice, distance_outfile,
+ copynumberprofile_outfile, nonroundedprofile_outfile,
+ cnaStatusFile = cnaStatusFile, gamma = gamma_param,
+ allow100percent = TRUE, min_ploidy = min_ploidy,
+ max_ploidy = max_ploidy, min_rho = min_rho, max_rho = max_rho,
+ min_goodness = min_goodness,
+ chr_names = chr_names,
+ analysis = analysis,
+ uninformative_baf_threshold = uninformative_baf_threshold,
+ early_termination = FALSE,
+ n_neighbors_search = n_neighbors_search,
+ psi_step = grid_psi_step,
+ rho_step = grid_rho_step,
+ local_min_window_size = local_min_window_size,
+ nthreads = nthreads
+ )
+ } else {
+ log_info("Running STANDARD grid search...")
+ ascat_optimum_pair <- runASCAT(
+ logR, 1 - master_data$RawBAF, segLogR, segBAF,
+ chr_segs, ascat_dist_choice,
+ distancepng = distance_outfile,
+ copynumberprofilespng = copynumberprofile_outfile,
+ nonroundedprofilepng = nonroundedprofile_outfile,
+ cnaStatusFile = cnaStatusFile,
+ gamma = gamma_param, allow100percent = TRUE,
+ min_ploidy = min_ploidy, max_ploidy = max_ploidy,
+ min_rho = min_rho, max_rho = max_rho,
+ min_goodness = min_goodness, chr_names = chr_names, analysis = analysis,
+ uninformative_baf_threshold = uninformative_baf_threshold,
+ local_min_window_size = local_min_window_size,
+ n_neighbors_search = n_neighbors_search,
+ nthreads = nthreads
+ )
+ }
+ log_info("Grid Search complete. Optimum found: \\
+ Rho={ascat_optimum_pair$rho}, Psi={ascat_optimum_pair$psi}")
+
+ # guard rail - check for valid solution
+ if (is.na(ascat_optimum_pair$rho) || is.na(ascat_optimum_pair$psi)) {
+ log_info("Grid search failed to find a valid purity/ploidy solution for {samplename}. Data might be too noisy or parameters too restrictive.")
+ return(invisible(NULL))
+ }
+ }
+
+ log_info("Running final clonal ASCAT model fit...")
+ # Final clonal ASCAT run
+ out <- run_clonal_ASCAT(
+ logR, 1 - master_data$RawBAF, segLogR, segBAF, chr_segs,
+ matched.segmented.BAF.data, ascat_optimum_pair, dist_choice,
+ paste0(outputfile_prefix, "second_distance.png"),
+ paste0(outputfile_prefix, "second_copynumberprofile.png"),
+ paste0(outputfile_prefix, "second_nonroundedprofile.png"),
+ gamma_param = gamma_param, read_depth, uninformative_baf_threshold,
+ allow100percent = TRUE, psi_min_initial = min_ploidy,
+ psi_max_initial = max_ploidy, rho_min_initial = min_rho,
+ rho_max_initial = max_rho, chr_names = chr_names,
+ nthreads = nthreads
+ )
+
+ if (is.na(out$output_optimum_pair$rho) || is.na(out$output_optimum_pair$psi)) {
+ log_info("Final clonal model fit failed to identify a valid purity/ploidy solution for {samplename}.")
+ return(invisible(NULL))
+ }
+ d <- out$dist_matrix_info$distance_matrix
+ if (all(is.na(d)) || all(is.infinite(d))) {
+ log_info("Distance matrix is entirely NA or Inf for {samplename}. No valid copy number solution possible.")
+ return(invisible(NULL))
+ }
+ log_info("ASCAT modeling complete for {samplename}. Writing output files.")
+ # Save results
+ rho_psi_output <- data.frame(
+ rho = c(ascat_optimum_pair$rho, out$output_optimum_pair_without_ref$rho, out$output_optimum_pair$rho),
+ psi = c(ascat_optimum_pair$psi, out$output_optimum_pair_without_ref$psi, out$output_optimum_pair$psi),
+ ploidy = c(ascat_optimum_pair$ploidy, out$output_optimum_pair_without_ref$ploidy, out$output_optimum_pair$ploidy),
+ distance = c(NA, out$distance_without_ref, out$distance),
+ is_best = c(FALSE, !out$is_ref_better, out$is_ref_better),
+ row.names = c("ASCAT", "FRAC_GENOME", "REF_SEG")
+ )
+ # Write with row.names = TRUE to match original Battenberg format
+ write.table(rho_psi_output,
+ paste0(outputfile_prefix, "rho_and_psi.txt"),
+ sep = "\t", quote = FALSE, row.names = TRUE, col.names = NA
+ )
+ return(ascat_optimum_pair)
+}
+
+#' Fit subclonal copy number
+#'
+#' This function fits a subclonal copy number profile where a clonal profile is unlikely.
+#' It goes over each segment of a clonal copy number profile and does a simple t-test. If the
+#' test is significant it is unlikely that the data can be explained by a single copy number
+#' state. We therefore fit a second state, i.e. there are two cellular populations with each
+#' a different state: Subclonal copy number.
+#' @param sample_name Name of the sample, used in figures
+#' @param baf_segmented_file String that points to a file with segmented BAF output
+#' @param logr_file String that points to the raw LogR file to be used in the
+#' subclonal copy number figures
+#' @param rho_psi_file String pointing to the rho_and_psi file generated by
+#' \code{fit_copy_number}
+#' @param output_file Filename of the file where the final copy number fit will be
+#' written to
+#' @param output_figures_prefix Prefix of the filenames for the chromosome specific
+#' copy number figures
+#' @param output_gw_figures_prefix Prefix of the filenames for the genome wide copy
+#' number figures
+#' @param chr_names Vector of allowed chromosome names
+#' @param masking_output_file Filename of where the masking details need to be
+#' written. Masking is performed to remove very high copy number state segments
+#' @param max_allowed_state The maximum CN state allowed (Default 250)
+#' @param cn_upper_limit The maximum CN that can be called (Default 1000)
+#' @param prior_breakpoints_file A two column file with prior breakpoints (e.g. from SVs). Must contain a header with columns 'chromosome' and 'position' (header case-insensitive)
+#' from structural variants. This file must contain two columns: chromosome and
+#' position. These are used when making the figures
+#' @param gamma Technology specific scaling parameter for LogR (Default 1)
+#' @param segmentation_gamma Legacy parameter that is no longer used (Default NA)
+#' @param siglevel Threshold under which a p-value becomes significant. When it is
+#' significant a second copy number state will be fitted (Default 0.05)
+#' @param maxdist Slack in BAF space to allow a segment to be off it's optimum
+#' before becoming significant. A segment becomes significant very quickly when a
+#' breakpoint is missed, this parameter alleviates the effect (Default 0.01)
+#' @param noperms The number of permutations to be run when bootstrapping the
+#' confidence intervals on the copy number state of each segment (Default 1000)
+#' @param seed Seed to set when performing bootstrapping (Default: Current time)
+#' @param calc_seg_baf_option Various options to recalculate the BAF of a segment.
+#' Options are: 1 - median, 2 - mean, 3 - ifelse median==0|1, mean, median.
+#' (Default: 3)
+#' @param verbose_logging Print out more information during the run (Default: FALSE)
+#' @param nthreads The number of paralel processes to run
+#' @author dw9, sd11
+#' @export
+call_subclones <- function(
+ sample_name, baf_segmented_file,
+ logr_file, rho_psi_file, output_file,
+ output_figures_prefix, output_gw_figures_prefix,
+ chr_names, masking_output_file,
+ max_allowed_state = 250, cn_upper_limit = 1000,
+ prior_breakpoints_file = NULL, gamma = 1,
+ segmentation_gamma = NA, siglevel = 0.05,
+ maxdist = 0.01, noperms = 1000, seed = as.integer(Sys.time()),
+ calc_seg_baf_option = 3, verbose_logging = FALSE,
+ nthreads = 1
+) {
+ set.seed(seed)
+
+ # Load and calculate initial rho/psi metrics
+ res <- load_rho_psi_file(rho_psi_file)
+ rho <- res$rho
+ psit <- res$psit
+ psi <- (rho * psit) + (2 * (1 - rho))
+ goodness <- res$goodness
+
+ # Load BAF data and handle possible row-name artifacts ("X")
+ BAFvals <- read_bafsegmented(baf_segmented_file)
+ if ("X" %in% colnames(BAFvals)) {
+ BAFvals <- BAFvals[, -1, with = FALSE]
+ }
+
+ # Positional indexing for generalizability: Col 3 = BAF, Col 5 = BAFseg
+ BAF <- BAFvals[[3]]
+ BAFseg <- BAFvals[[5]]
+ SNPpos <- BAFvals[, c(1, 2), with = FALSE]
+
+ # Load LogR data and handle row-name artifacts
+ LogRvals <- read_logr(logr_file)
+ if (identical(colnames(LogRvals)[1], "X")) {
+ LogRvals <- LogRvals[, -1, drop = FALSE]
+ }
+
+ ctrans <- ctrans_logR <- stats::setNames(seq_along(chr_names), chr_names)
+
+ # First Pass: Determine Copy Number and Merge Segments
+ res_cn <- determine_copynumber(
+ BAFvals, LogRvals, rho, psi, gamma,
+ ctrans, ctrans_logR, maxdist, siglevel, noperms, cn_upper_limit
+ )
+
+ # Refine via merging
+ merge_res <- merge_segments(
+ res_cn$subcloneres, BAFvals, LogRvals,
+ rho, psi, gamma, calc_seg_baf_option, TRUE
+ )
+ BAFvals <- merge_res$bafsegmented
+
+ # Second Pass: Final Copy Number Determination
+ res_final <- determine_copynumber(
+ BAFvals, LogRvals, rho, psi, gamma,
+ ctrans, ctrans_logR, maxdist, siglevel,
+ noperms, cn_upper_limit
+ )
+ subcloneres <- res_final$subcloneres
+ BAFpvals <- res_final$BAFpvals
+
+ # Mask high CN artifacts
+ mask_res <- mask_high_cn_segments(subcloneres, BAFvals, max_allowed_state)
+ subcloneres <- mask_res$subclones
+
+ data.table::fwrite(
+ list(
+ samplename = sample_name,
+ masked_count = mask_res$masked_count,
+ masked_size = mask_res$masked_size,
+ max_allowed_state = max_allowed_state
+ ),
+ file = masking_output_file,
+ quote = FALSE,
+ sep = "\t",
+ )
+
+ # Generate output paths
+ base_out <- tools::file_path_sans_ext(output_file)
+ ext_out <- tools::file_ext(output_file)
+
+ data.table::fwrite(
+ subcloneres[, c(1:3, 8:13)], output_file,
+ quote = FALSE, sep = "\t"
+ )
+ data.table::fwrite(
+ subcloneres, paste0(base_out, "_extended.", ext_out),
+ quote = FALSE, sep = "\t"
+ )
+
+ subcloneres$length <- subcloneres$endpos - subcloneres$startpos
+
+ # Behavior: Identical, but using which() ensures integer indexing for safe exclusion
+ diploid_idx <- which(subcloneres$nMaj1_A == 1 & subcloneres$nMin1_A == 1 & subcloneres$frac1_A == 1)
+
+ # Behavior: Identical. The if-statement handles the integer(0) case safely
+ cna <- if (length(diploid_idx) > 0) subcloneres[-diploid_idx, ] else subcloneres
+
+ # Use fsubset for subclonal filtering
+ # Behavior: Identical. fsubset handles 0-row matches more cleanly than base [,]
+ subcloneres_subclonal <- collapse::fsubset(
+ subcloneres, subcloneres$frac1_A < 1
+ )
+
+ # Pre-calculate sums using collapse::fsum
+ cna_total_len <- collapse::fsum(cna$length, na.rm = TRUE)
+
+ # Logic Gate
+ # Behavior: Identical. Checks for 0 rows or 0 total length
+ if (nrow(cna) == 0 || cna_total_len == 0 || nrow(subcloneres_subclonal) == 0) {
+ goodness <- 1.0
+ } else {
+ # Updated goodness calculation to match Battenberg logic:
+ # Goodness here represents the Fraction of the Genome that is Clonal (1 - subclonal_fraction)
+ # But specifically on the ABERRANT genome (excluding diploid)
+
+ # Calculate total genome length
+ total_genome_len <- collapse::fsum(subcloneres$length, na.rm = TRUE)
+
+ # Calculate length of segments that are NOT clonal (i.e. subclonal)
+ # definition: frac1_A < 1
+ subclonal_len <- collapse::fsum(subcloneres$length[subcloneres$frac1_A < 1], na.rm = TRUE)
+
+ # Calculate goodness as the % of genome that is clonal
+ goodness <- 1 - (subclonal_len / total_genome_len)
+
+ # Ensure goodness is valid and finite
+ if (is.na(goodness) || is.infinite(goodness)) {
+ goodness <- 1.0
+ } else {
+ goodness <- max(0, min(1, goodness))
+ }
+ }
+
+ log_info("PGA.is.clonal = {sprintf('%2.1f%%', goodness * 100)}")
+
+ # Visualization
+ segment_breakpoints <- collapse_bafsegmented_to_segments(BAFvals)
+ has_prior <- !is.null(prior_breakpoints_file) &&
+ !is.na(prior_breakpoints_file) &&
+ prior_breakpoints_file != "NA"
+
+ if (has_prior) {
+ svs <- data.table::fread(prior_breakpoints_file, data.table = FALSE)
+ }
+
+ # Pre-split data into chunks to avoid memory contention and parallel overhead
+ log_info("Preparing chromosome data chunks for plotting...")
+ b_chr_vec <- as.character(.subset2(BAFvals, 1))
+ baf_by_chr <- split(BAF, b_chr_vec)
+ bafseg_by_chr <- split(BAFseg, b_chr_vec)
+ bafpvals_by_chr <- split(BAFpvals, b_chr_vec)
+ pos_by_chr <- split(.subset2(SNPpos, 2), b_chr_vec)
+
+ l_chr_vec <- as.character(.subset2(LogRvals, 1))
+ l_pos <- .subset2(LogRvals, 2)
+ l_val <- .subset2(LogRvals, 3)
+ logr_pos_by_chr <- split(l_pos, l_chr_vec)
+ logr_val_by_chr <- split(l_val, l_chr_vec)
+
+ log_info("Executing chromosomal plotting (sequentially for safety)...")
+ lapply(chr_names, function(chr) {
+ # Extract only the data for this chromosome
+ pos <- pos_by_chr[[chr]]
+ if (is.null(pos) || length(pos) == 0) {
+ log_info("PLOTTING: Skipping chromosome {chr} (no BAF data found for this name).")
+ return(NULL)
+ }
+
+ # Optional prior breakpoints
+ svs_pos <- if (has_prior) {
+ chr_svs <- svs[svs[[1]] == chr, ]
+ if (nrow(chr_svs) > 0) chr_svs[[2]] / 1e6 else NULL
+ } else {
+ NULL
+ }
+
+ bp_chr <- segment_breakpoints[segment_breakpoints[[1]] == chr, ]
+ breakpoints_pos <- if (nrow(bp_chr) > 0) sort(unique(c(bp_chr[[2]], bp_chr[[3]]) / 1e6)) else NULL
+
+ logr_pos <- logr_pos_by_chr[[chr]]
+ logr_val <- logr_val_by_chr[[chr]]
+ baf_val <- baf_by_chr[[chr]]
+ baf_seg <- bafseg_by_chr[[chr]]
+ baf_pval <- bafpvals_by_chr[[chr]]
+
+ if (is.null(logr_pos)) {
+ log_info("PLOTTING: Warning - no LogR data found for chromosome {chr}. Plot may be incomplete.")
+ }
+
+ # Smart Downsampling Per Chromosome (Target: 15,000 points per plot)
+ max_points <- 15000
+ if (length(pos) > max_points) {
+ idx_sample <- bt_downsample_indices(pos, max_points)
+ pos <- pos[idx_sample]
+ baf_val <- baf_val[idx_sample]
+ baf_seg <- baf_seg[idx_sample]
+ baf_pval <- baf_pval[idx_sample]
+ }
+ if (!is.null(logr_pos) && length(logr_pos) > max_points) {
+ idx_sample_logr <- bt_downsample_indices(logr_val, max_points)
+ logr_pos <- logr_pos[idx_sample_logr]
+ logr_val <- logr_val[idx_sample_logr]
+ }
+
+ grDevices::png(
+ filename = paste0(output_figures_prefix, chr, ".png"),
+ width = 2000, height = 2000, res = 200, type = "cairo"
+ )
+ create_subclonal_cn_plot(
+ chrom = chr, chrom_position = pos / 1e6, LogRposke = logr_pos, LogRchr = logr_val,
+ BAFchr = baf_val, BAFsegchr = baf_seg, BAFpvalschr = baf_pval,
+ subcloneres = subcloneres, siglevel = siglevel,
+ x_min = min(pos) / 1e6, x_max = max(pos) / 1e6,
+ title = paste(sample_name, ", chromosome ", chr),
+ xlab = "Position (Mb)", ylab_logr = "LogR", ylab_baf = "BAF (phased)",
+ breakpoints_pos = breakpoints_pos, svs_pos = svs_pos
+ )
+ grDevices::dev.off()
+ return(NULL)
+ })
+
+ # Manual GC to prevent container shared memory buildup
+ gc(verbose = FALSE)
+
+ # Clean up and calculate Ploidy
+ subclones <- as.data.frame(subcloneres)
+ seg_len <- floor((subcloneres$endpos - subcloneres$startpos) / 1000)
+
+ # Calculate weighted states for min/maj
+ calc_state <- function(n1, n2, f1, f2) {
+ is_sub <- abs(n1 - n2) > 0
+ is_sub[is.na(is_sub)] <- FALSE
+ ifelse(is_sub, (n1 * f1) + (n2 * f2), n1)
+ }
+
+ state_min <- calc_state(subclones$nMin1_A, subclones$nMin2_A, subclones$frac1_A, subclones$frac2_A)
+ state_maj <- calc_state(subclones$nMaj1_A, subclones$nMaj2_A, subclones$frac1_A, subclones$frac2_A)
+
+ total_len <- sum(seg_len, na.rm = TRUE)
+ ploidy <- if (total_len > 0) sum((state_min + state_maj) * seg_len, na.rm = TRUE) / total_len else 2.0
+
+ if (is.na(ploidy) || ploidy <= 0) ploidy <- 2.0
+
+ # Final Outputs - Downsample BAFvals for genome-wide plot performance
+ log_info("Downsampling BAFvals for genome-wide plotting...")
+ target_gw <- 500000
+ if (nrow(BAFvals) > target_gw) {
+ gw_idx <- bt_downsample_indices(BAFvals$Position, target_gw)
+ BAFvals_ds <- BAFvals[gw_idx, ]
+ } else {
+ BAFvals_ds <- BAFvals
+ }
+
+ plot_gw_subclonal_cn(subclones, BAFvals_ds, rho, ploidy, goodness, output_gw_figures_prefix, chr_names, sample_name)
+
+ cp_out <- data.frame(purity = rho, ploidy = ploidy, psi = psit)
+ log_info("Writing purity/ploidy for {sample_name}: rho={rho}, ploidy={ploidy}, psit={psit}")
+ data.table::fwrite(cp_out, paste0(sample_name, "_purity_ploidy.txt"), quote = FALSE, sep = "\t", row.names = FALSE)
+}
+
+#' Given all the determined values make a copy number call for each segment
+#'
+#' @param BAFvals BAFsegmented data.frame with 5 columns
+#' @param LogRvals Raw logR values in data.frame with 3 columns
+#' @param rho Optimal rho value, the choosen cellularity
+#' @param psi Optimal psi value, the choosen ploidy
+#' @param gamma Platform gamma parameter
+#' @param ctrans Named vector of chromosome names
+#' @param ctrans_logR Named vector of chromosome names
+#' @param maxdist Max distance a segment is tolerated to be not considered for subclonal copy number
+#' @param siglevel Level at which a segment can become significantly different from the nearest clonal state
+#' @param noperms Number of bootstrap permutations
+#' @param cn_upper_limit Maximum number of CN that can be called
+#' @return A data.frame with copy number determined for each segment
+#' @author dw9
+#' @noRd
+determine_copynumber <- function(BAFvals, LogRvals, rho, psi, gamma, ctrans,
+ ctrans.logR, maxdist, siglevel, noperms,
+ cn_upper_limit) {
+ # Standardizing inputs - use .subset2 to extract columns as vectors from data.table
+ BAFphased <- as.numeric(.subset2(BAFvals, 4))
+ BAFseg <- as.numeric(.subset2(BAFvals, 5))
+ BAFchr <- as.character(.subset2(BAFvals, 1))
+ BAFposition <- as.numeric(.subset2(BAFvals, 2))
+ BAFpos <- ctrans[BAFchr] * 1e9 + BAFposition
+
+ LogRchr <- as.character(.subset2(LogRvals, 1))
+ LogRposition <- as.numeric(.subset2(LogRvals, 2))
+ LogRpos <- ctrans.logR[LogRchr] * 1e9 + LogRposition
+
+ # Boundary logic - now BAFchr is already extracted as a vector
+ switchpoints <- c(0, which(BAFseg[-1] != BAFseg[-length(BAFseg)] | BAFchr[-1] != BAFchr[-length(BAFchr)]), length(BAFseg))
+ BAFlevels <- BAFseg[switchpoints[-1]]
+
+ res_list <- vector(mode = "list", length = length(BAFlevels))
+ BAFpvals <- vector(length = length(BAFseg))
+
+ # 1. Fast LogR averaging using collapse
+ # Map each LogR probe to a segment index
+ # LogRpos and segment boundaries (startpos/endpos) are both sorted globally
+ # We can find which segment each LogR probe falls into.
+
+ # Get all segment boundaries
+ seg_starts <- BAFpos[switchpoints[-length(switchpoints)] + 1]
+ seg_ends <- BAFpos[switchpoints[-1]]
+
+ # findInterval returns index i such that seg_starts[i] <= LogRpos < seg_starts[i+1]
+ # We need to ensure LogRpos <= seg_ends[i] as well (handling gaps)
+ seg_ids <- findInterval(LogRpos, seg_starts)
+
+ # Filter LogR probes that are within the matched segment's end and not infinite
+ # Use .subset2 to extract column as vector from data.table (avoids list return)
+ logr_col3 <- as.numeric(.subset2(LogRvals, 3))
+ valid_ids <- pmax(1, seg_ids)
+ valid_logr <- which(seg_ids > 0 & LogRpos <= seg_ends[valid_ids] & !is.infinite(logr_col3) & !is.na(logr_col3))
+
+ # Calculate mean LogR per segment ID
+ # We use collapse::fmean with the assigned group IDs
+ seg_logr_means <- as.numeric(collapse::fmean(logr_col3[valid_logr], g = seg_ids[valid_logr]))
+
+ # Map back to the BAFlevels (some segments might be missing LogR data)
+ LogR_vec <- numeric(length(BAFlevels))
+ LogR_vec[sort(unique(seg_ids[valid_logr]))] <- seg_logr_means
+
+ # 2. Clonal Copy Number Expectations (Pixel Perfect arithmetic)
+ # Basic physical floor for Rho to prevent Inf results
+ rho_floor <- max(0.01, rho, na.rm = TRUE)
+ logr_factor <- 2^(LogR_vec / gamma)
+ l_vec <- BAFlevels
+
+ nMajor_vec <- (rho_floor - 1 + l_vec * psi * logr_factor) / rho_floor
+ nMinor_vec <- (rho_floor - 1 + (1 - l_vec) * psi * logr_factor) / rho_floor
+
+ # Handle physical impossibility (Negative nMinor)
+ neg_minor <- nMinor_vec < 0 & !is.na(nMinor_vec)
+ if (any(neg_minor)) {
+ is_one <- l_vec == 1
+ nMajor_vec[neg_minor & is_one] <- cn_upper_limit
+ nMajor_vec[neg_minor & !is_one] <- nMajor_vec[neg_minor & !is_one] +
+ l_vec[neg_minor & !is_one] * (0.01 - nMinor_vec[neg_minor & !is_one]) / (1 - l_vec[neg_minor & !is_one])
+ nMinor_vec[neg_minor] <- 0.01
+ }
+
+ # 3. Vectorized is_segment_clonal-style testing
+ # We need BAF_size, BAF_sd for each segment for the p-value
+ # We can get these from the BAFphased data using the switchpoints
+ baf_groups <- rep(seq_along(BAFlevels), diff(switchpoints))
+ BAF_stats <- data.frame(
+ mean = as.numeric(collapse::fmean(BAFphased, g = baf_groups)),
+ sd = as.numeric(collapse::fsd(BAFphased, g = baf_groups)),
+ size = as.numeric(collapse::fnobs(BAFphased, g = baf_groups))
+ )
+ BAF_stats$sd[is.na(BAF_stats$sd)] <- 0
+
+ # Call is_segment_clonal in one vectorized go
+ # is_segment_clonal is already vectorized and returns best_nMaj, best_nMin, is_clonal
+ # We need to ensure we have all required parameters
+ best_clonal_res <- is_segment_clonal(
+ LogR = LogR_vec,
+ BAF_req = l_vec,
+ BAF_length = BAF_stats$size, # approximating length with size
+ BAF_size = BAF_stats$size,
+ BAF_mean = BAF_stats$mean,
+ BAF_sd = BAF_stats$sd,
+ rho = rho,
+ psi = psi,
+ gamma_param = gamma,
+ siglevel_BAF = siglevel,
+ maxdist_BAF = maxdist
+ )
+
+ # Map p-values back to SNP-level BAFpvals
+ # Note: is_segment_clonal (vectorized version) doesn't return pval currently,
+ # but it sets is_clonal based on pval > siglevel.
+ # We actually need the p-value ourselves to fill BAFpvals.
+ # Let's extract that logic or re-calculate here.
+
+ # Re-calculate best_level for p-value (Option 1 vs 2)
+ # This matches the prioritized testing in determine_copynumber
+ calc_baf_lev <- function(nM, nm) {
+ num <- 1 - rho + rho * nM
+ den <- 2 - 2 * rho + rho * (nM + nm)
+ lev <- num / den
+ lev[nM == 0 & nm == 0] <- 0.5
+ lev
+ }
+
+ best_levels <- calc_baf_lev(best_clonal_res$nMaj, best_clonal_res$nMin)
+
+ # Vectorized p-value calculation
+ p_vals <- numeric(length(BAFlevels))
+ valid_stats <- BAF_stats$size > 1 & BAF_stats$sd > 0
+ if (any(valid_stats)) {
+ p_vals[valid_stats] <- calc_Pvalue_t_twotailed(
+ sample_size = BAF_stats$size[valid_stats],
+ sample_mean = BAF_stats$mean[valid_stats],
+ sample_SD = BAF_stats$sd[valid_stats],
+ mu_pop = best_levels[valid_stats],
+ max_dist = maxdist
+ )
+ }
+
+ # Fill BAFpvals (SNP level)
+ BAFpvals <- p_vals[baf_groups]
+
+ # 4. Process Subclonal Segments (Only for those where p_vals <= siglevel)
+ # This part is harder to vectorize fully due to the bootstrap loop,
+ # but we only do it for the subclonal subset.
+ subclonal_idx <- which(p_vals <= siglevel)
+
+ for (i in seq_along(BAFlevels)) {
+ l <- l_vec[i]
+ LogR <- LogR_vec[i]
+ ntot <- nMajor_vec[i] + nMinor_vec[i]
+
+ start_idx <- switchpoints[i] + 1
+ end_idx <- switchpoints[i + 1]
+
+ curr_start <- seg_starts[i] %% 1e9
+ curr_end <- seg_ends[i] %% 1e9
+
+ if (i %in% subclonal_idx) {
+ # SUBCLONAL
+ BAFke <- BAFphased[start_idx:end_idx]
+ n_ke <- length(BAFke)
+ sd_BAFke <- BAF_stats$sd[i]
+
+ # Need all edges for subclonal optimization
+ all_edges <- prioritizeCopyNumbers(
+ rho = rho, psi = psi, BAF_req = l,
+ nMajor = nMajor_vec[i], nMinor = nMinor_vec[i], full = TRUE
+ )
+
+ all_edges_res <- all_edges
+ all_edges <- cbind(
+ as.vector(all_edges_res$nMaj1), as.vector(all_edges_res$nMin1),
+ as.vector(all_edges_res$nMaj2), as.vector(all_edges_res$nMin2)
+ )
+
+ na_idx <- which(is.na(rowSums(all_edges)))
+ if (length(na_idx) > 0) all_edges <- rbind(all_edges[-na_idx, ], all_edges[na_idx, ])
+
+ nM1 <- all_edges[, 1]
+ nmi1 <- all_edges[, 2]
+ nM2 <- all_edges[, 3]
+ nmi2 <- all_edges[, 4]
+
+ # Vectorized math for tau across all 6 options
+ denom_tau <- (l * rho * (nmi1 + nM1) - l * rho * (nmi2 + nM2) - rho * nM1 + rho * nM2)
+ tau <- (1 - rho + rho * nM2 - 2 * l * (1 - rho) - l * rho * (nmi2 + nM2)) / denom_tau
+
+ # Clip tau to [0, 1] and handle NAs/Infs
+ tau[is.na(tau) | is.infinite(tau)] <- 0
+ tau <- pmax(0, pmin(1, tau))
+
+ sdl <- sd_BAFke / sqrt(n_ke)
+
+ # Optimized Delta method for sdtau
+ calc_sdtau <- function(curr_l) {
+ d <- (curr_l * rho * (nmi1 + nM1) - curr_l * rho * (nmi2 + nM2) - rho * nM1 + rho * nM2)
+ v <- (1 - rho + rho * nM2 - 2 * curr_l * (1 - rho) - curr_l * rho * (nmi2 + nM2)) / d
+ v[is.na(v) | is.infinite(v)] <- 0
+ pmax(0, pmin(1, v))
+ }
+ sdtau <- (abs(calc_sdtau(l + sdl) - tau) + abs(calc_sdtau(l - sdl) - tau)) / 2
+
+ # Optimized Bootstrap (Vectorized)
+ # We generate all samples at once
+ boot_means <- colMeans(matrix(sample(BAFke, n_ke * noperms, replace = TRUE), nrow = n_ke))
+
+ opt_data <- vector("list", 6)
+ for (opt in seq_along(tau)) {
+ # Vectorized pFrac calculation with safety
+ denom <- (boot_means * rho * (nM1[opt] + nmi1[opt]) - boot_means * rho * (nM2[opt] + nmi2[opt]) - rho * nM1[opt] + rho * nM2[opt])
+
+ pFrac <- (1 - rho + rho * nM2[opt] - 2 * boot_means * (1 - rho) - boot_means * rho * (nmi2[opt] + nM2[opt])) / denom
+
+ # Clip pFrac to [0, 1] and handle NAs/Infs
+ pFrac[is.na(pFrac) | is.infinite(pFrac)] <- 0
+ pFrac <- pmax(0, pmin(1, pFrac))
+
+ o_frac <- sort(pFrac)
+ opt_data[[opt]] <- c(
+ nM1[opt], nmi1[opt], tau[opt], nM2[opt], nmi2[opt], 1 - tau[opt],
+ sdtau[opt], collapse::fsd(pFrac), o_frac[round(0.025 * noperms)], o_frac[round(0.975 * noperms)]
+ )
+ }
+
+ res_list[[i]] <- c(BAFvals$Chromosome[start_idx], curr_start, curr_end, l, p_vals[i], LogR, ntot, unlist(opt_data))
+ } else {
+ # CLONAL
+ res_list[[i]] <- c(
+ BAFvals$Chromosome[start_idx], curr_start, curr_end, l, p_vals[i], LogR, ntot,
+ best_clonal_res$nMaj[i], best_clonal_res$nMin[i], 1, rep(NA, 57)
+ )
+ }
+ }
+
+
+ # Generate dynamic column names
+ base_names <- c("nMaj1", "nMin1", "frac1", "nMaj2", "nMin2", "frac2", "SDfrac", "SDfrac_boot", "frac1_0.025", "frac1_0.975")
+ dynamic_names <- paste0(rep(base_names, 6), "_", rep(LETTERS[1:6], each = 10))
+
+ # Final formatting
+ subcloneres <- as.data.frame(do.call(rbind, res_list))
+ colnames(subcloneres) <- c("chr", "startpos", "endpos", "BAF", "pval", "LogR", "ntot", dynamic_names)
+
+
+ # Modern fast type conversion
+ subcloneres[-1] <- lapply(subcloneres[-1], function(x) as.numeric(as.character(x)))
+
+ return(list(subcloneres = subcloneres, BAFpvals = BAFpvals))
+}
+
+
+#' Plot the copy number genome wide in two different ways. This creates the
+#' Battenberg average profile where subclonal copy number is represented as a
+#' mixture of two different states and the Battenberg subclones profile where
+#' subclonal copy number is plotted as two different separate states. The thickness
+#' of the line represents the fraction of tumour cells carying the particular state
+#' @noRd
+plot_gw_subclonal_cn <- function(subclones, BAFvals, rho, ploidy, goodness,
+ output_gw_figures_prefix, chr_names,
+ tumourname) {
+ # Robust chromosome normalization to ensure consistent indexing
+ gsubchr <- function(x) gsub("chr", "", as.character(x), ignore.case = TRUE)
+ BAFvals$Chromosome <- gsubchr(BAFvals$Chromosome)
+ subclones$chr <- gsubchr(subclones$chr)
+ chr_names <- gsubchr(chr_names)
+
+ for (chr in unique(subclones$chr)) {
+ baf_idx <- which(BAFvals$Chromosome == chr)
+ if (length(baf_idx) == 0) next
+
+ sub_idx <- which(subclones$chr == chr)
+ curr_sub <- subclones[sub_idx, ]
+
+ # Map each SNP to a segment index using findInterval
+ snp_to_seg <- findInterval(BAFvals$Position[baf_idx], curr_sub$startpos)
+
+ # Validate SNPs are within the assigned segment's endpos
+ valid_mask <- snp_to_seg > 0
+ in_seg_mask <- valid_mask & BAFvals$Position[baf_idx] <= curr_sub$endpos[pmax(1, snp_to_seg)]
+
+ if (any(in_seg_mask)) {
+ seg_ids_found <- snp_to_seg[in_seg_mask]
+ abs_snp_indices <- baf_idx[in_seg_mask]
+
+ # Find min/max SNP index for each segment found
+ pos_min[sub_idx[unique(seg_ids_found)]] <- collapse::fmin(abs_snp_indices, g = seg_ids_found)
+ pos_max[sub_idx[unique(seg_ids_found)]] <- collapse::fmax(abs_snp_indices, g = seg_ids_found)
+ }
+ }
+
+ # For those segments that are subclonal, we can now just subset the pre-calculated boundaries.
+ is_subclonal <- which(subclones$frac1_A < 1)
+ subcl_min <- pos_min[is_subclonal]
+ subcl_max <- pos_max[is_subclonal]
+
+ # Determine whether it's the major or the minor allele that is represented by two states
+ is_subclonal_maj <- abs(subclones$nMaj1_A - subclones$nMaj2_A) > 0
+ is_subclonal_min <- abs(subclones$nMin1_A - subclones$nMin2_A) > 0
+ is_subclonal_maj[is.na(is_subclonal_maj)] <- FALSE
+ is_subclonal_min[is.na(is_subclonal_min)] <- FALSE
+
+ segment_states_min <- subclones$nMin1_A * ifelse(is_subclonal_min,
+ subclones$frac1_A, 1
+ ) +
+ ifelse(is_subclonal_min, subclones$nMin2_A, 0) *
+ ifelse(is_subclonal_min, subclones$frac2_A, 0)
+ segment_states_maj <- subclones$nMaj1_A * ifelse(is_subclonal_maj,
+ subclones$frac1_A, 1
+ ) +
+ ifelse(is_subclonal_maj, subclones$nMaj2_A, 0) *
+ ifelse(is_subclonal_maj, subclones$frac2_A, 0)
+ segment_states_tot <- segment_states_maj + segment_states_min
+
+ # Determine which SNPs are on which chromosome, to be used as a proxy for chromosome size in the plots
+ # BAFvals$Chromosome and chr_names are already normalized above
+ chr_segs <- lapply(seq_along(chr_names), function(ch) {
+ which(BAFvals$Chromosome == chr_names[ch])
+ })
+
+ # Plot subclonal copy number as mixtures of two states
+ # Use explicit calls to refactored plotting functions
+ grDevices::png(
+ filename = paste(output_gw_figures_prefix, "_average.png", sep = ""),
+ width = 2000, height = 500, res = 200, type = "cairo"
+ )
+ create_bb_plot_average(
+ bafsegmented = BAFvals,
+ ploidy = ploidy,
+ rho = rho,
+ goodness_of_fit = goodness,
+ pos_min = pos_min,
+ pos_max = pos_max,
+ segment_states_min = segment_states_min,
+ segment_states_tot = segment_states_tot,
+ chr_segs = chr_segs,
+ chr_names = chr_names,
+ tumourname = tumourname
+ )
+ grDevices::dev.off()
+
+ # Plot subclonal copy number as two separate states
+ grDevices::png(
+ filename = paste(output_gw_figures_prefix, "_subclones.png", sep = ""),
+ width = 2000, height = 500, res = 200, type = "cairo"
+ )
+ create_bb_plot_subclones(
+ bafsegmented = BAFvals,
+ subclones = subclones,
+ ploidy = ploidy,
+ rho = rho,
+ goodness_of_fit = goodness,
+ pos_min = pos_min,
+ pos_max = pos_max,
+ subcl_min = subcl_min,
+ subcl_max = subcl_max,
+ is_subclonal = is_subclonal,
+ is_subclonal_maj = is_subclonal_maj,
+ is_subclonal_min = is_subclonal_min,
+ chr_segs = chr_segs,
+ chr_names = chr_names,
+ tumourname = tumourname
+ )
+ grDevices::dev.off()
+}
+
+#' Collapse a BAFsegmented file into segment start and end points
+#'
+#' This function looks through the BAFsegmented for stretches of equal
+#' BAFseg and records the start and end coordinates in a data.frame
+#' @param bafsegmented The BAFsegmented output from segmentation
+#' @return A data.frame with columns chromosome, start and end
+#' @author sd11
+#' @noRd
+collapse_bafsegmented_to_segments <- function(bafsegmented) {
+ # Fast validation
+ req_cols <- c("Chromosome", "Position", "BAFseg")
+ if (!all(req_cols %in% colnames(bafsegmented))) {
+ stop("Missing required columns in BAFsegmented data")
+ }
+
+ # Use data.table logic for extremely fast segment collapsing
+ # We group by Chromosome and then by the 'rleid' of the BAFseg value to identify blocks
+ # rleid identifies contiguous identical values which is exactly what a segment is.
+ dt <- if (data.table::is.data.table(bafsegmented)) bafsegmented else data.table::as.data.table(bafsegmented)
+
+ segments <- dt[, .(
+ start = .subset2(Position, 1),
+ end = .subset2(Position, .N)
+ ), by = .(Chromosome, seg_id = data.table::rleid(Chromosome, BAFseg))]
+
+ return(as.data.frame(segments[, .(chromosome = Chromosome, start, end)]))
+}
+
+#' Function to make additional figures
+#'
+#' @param samplename Name of the sample for the plot title
+#' @param logr_file File containing all logR data
+#' @param bafsegmented_file File containing the BAFsegmented data
+#' @param logrsegmented_file File with the logRsegmented data
+#' @param allelecounts_file Optional file with raw allele counts (Default: NULL)
+#' @author sd11
+#' @export
+make_posthoc_plots <- function(samplename, logr_file, bafsegmented_file, logrsegmented_file, allelecounts_file = NULL) {
+ # Make some post-hoc plots
+ logr <- read_table_generic(logr_file)
+ bafsegmented <- as.data.frame(read_table_generic(bafsegmented_file))
+ logrsegmented <- as.data.frame(read_table_generic(logrsegmented_file, header = FALSE))
+ colnames(logrsegmented) <- c("Chromosome", "Position", "logRseg")
+ outputfile <- paste0(samplename, "_alleleratio.png")
+ allele_ratio_plot(
+ samplename = samplename, logr = logr,
+ bafsegmented = bafsegmented, logrsegmented = logrsegmented,
+ outputfile = outputfile, max.plot.cn = 8
+ )
+
+ if (!is.null(allelecounts_file)) {
+ allelecounts <- as.data.frame(read_table_generic(allelecounts_file))
+ outputfile <- paste0(samplename, "_coverage.png")
+ coverage_plot(samplename, allelecounts, outputfile)
+ }
+}
+
+
+#' Fit ChrX subclonal copy number (male only)
+#'
+#' Function to call ChrX copy number based on LogR (suitable for male samples).
+#' Copy number cannot be called for the non-PAR region of ChrX due to the
+#' hemizygosity of all 1000G SNPs. This function enables calling subclonal copy
+#' number for the non-PAR region by segmenting LogR. A number of correction steps
+#' are undertaken to account for the noisy nature of LogR. This function
+#' requires the following libraries: copynumber, data.table and ggplot2. It reads
+#' in three files generated by previous steps of Battenberg, namely
+#' samplename_mutantLogR_gcCorrected.tab, samplename_purity_ploidy.txt
+#' and samplename_copynumber_extended.txt.
+#' This function will also update the Battenberg genome-wide profile plots
+#' (average.png and subclones.png) to include the chrX profile by also
+#' reading in the samplename.BAFsegmented.txt and samplename_rho_psi.txt files
+#' @param tumourname The sample name used for Battenberg (i.e. the tumour BAM
+#' file name without the .bam extension)
+#' @param X_gamma The PCF gamma value for segmentation of 1000G SNP LogR values
+#' (Default 1000)
+#' @param X_kmin The min number of SNPs to support a segment in PCF of LogR values
+#' (Default 100)
+#' @param genomebuild The genome build used in running Battenberg (hg19 or hg38)
+#' @param AR Should the segment carrying the androgen receptor (AR) locus to be
+#' visually distinguished in average plot? (Default TRUE)
+#' @param prior_breakpoints_file A two column text file with prior genome-wide
+#' breakpoints, possibly from structural variants. This file must contain two
+#' columns with headers "chr" and "pos" representing chromosome and position.
+#' @param chrom_names A vector containing the names of chromosomes to be included
+#' in the final genome-wide Battenberg copy number plot with chrX
+#' @author naser.ansari-pour
+#' @export
+callChrXsubclones <- function(
+ tumourname, X_gamma = 1000,
+ X_kmin = 100, genomebuild,
+ AR = TRUE, prior_breakpoints_file = NULL,
+ chrom_names, data_type = "wgs"
+) {
+ log_info("Processing sample: {tumourname}")
+
+ # Set genome-specific coordinates
+ if (genomebuild == "hg19") {
+ par_regions <- c(2699520, 155260560)
+ x_centromere <- c(58632012, 61632012)
+ ar_locus <- data.frame(startpos = 66763874, endpos = 66950461)
+ } else if (genomebuild == "hg38") {
+ par_regions <- c(2781479, 156030895)
+ x_centromere <- c(58605580, 62412542)
+ ar_locus <- data.frame(startpos = 67544021, endpos = 67730619)
+ } else {
+ log_failure("Genomebuild not supported for callChrXsubclones")
+ }
+
+ # Load LogR data
+ suffix <- if (tolower(data_type) == "wgs") "_mutantLogR_gcCorrected.tab" else "_mutantLogR.tab"
+ pcf_input_raw <- read_table_generic(paste0(tumourname, suffix)) |> as.data.frame()
+
+ # Identify chromosome notation and filter for non-PAR X regions
+ chr_x_name <- unique(pcf_input_raw$Chromosome[pcf_input_raw$Chromosome %in% c("X", "chrX")])[1]
+ pcf_input <- pcf_input_raw[pcf_input_raw$Chromosome == chr_x_name &
+ pcf_input_raw$Position > par_regions[1] &
+ pcf_input_raw$Position < par_regions[2], ]
+ colnames(pcf_input)[3] <- tumourname
+ log_info("Number of chrX nonPAR SNPs = {nrow(pcf_input)}")
+
+ # Segmentation with optional prior breakpoints
+ if (!is.null(prior_breakpoints_file)) {
+ sv_data <- data.table::fread(prior_breakpoints_file, data.table = FALSE)
+ colnames(sv_data) <- tolower(colnames(sv_data))
+ colnames(sv_data)[colnames(sv_data) %in% c("chromosome")] <- "chr"
+ colnames(sv_data)[colnames(sv_data) %in% c("position")] <- "pos"
+
+ if (!all(c("chr", "pos") %in% colnames(sv_data))) {
+ log_failure("Prior breakpoints file for ChrX must contain 'chromosome'/'chr' and 'position'/'pos' columns. Found: {paste(colnames(sv_data), collapse=', ')}")
+ }
+
+ sv_x <- sv_data[sv_data$chr %in% c("X", "chrX"), ]
+
+ if (nrow(sv_x) > 0) {
+ # Filter breakpoints within the valid LogR range
+ valid_sv_pos <- sv_x$pos[sv_x$pos > min(pcf_input$Position) & sv_x$pos < max(pcf_input$Position)]
+ breaks <- sort(unique(c(min(pcf_input$Position), valid_sv_pos, max(pcf_input$Position))))
+
+ pcf_results <- list()
+ for (j in 1:(length(breaks) - 1)) {
+ subset_input <- pcf_input[pcf_input$Position >= breaks[j] & pcf_input$Position < breaks[j + 1], ]
+ if (nrow(subset_input) > 0) {
+ pcf_results[[length(pcf_results) + 1]] <- copynumber::pcf(subset_input, gamma = X_gamma, kmin = X_kmin)
+ }
+ }
+ pcf_df <- do.call(rbind, pcf_results)
+ }
+ } else {
+ pcf_df <- copynumber::pcf(pcf_input, gamma = X_gamma, kmin = X_kmin)
+ }
+ log_info("PCF complete: found {nrow(pcf_df)} segments on chrX.")
+
+ data.table::fwrite(pcf_df, paste0(tumourname, "_PCF_gamma_", X_gamma, "_chrX.txt"), sep = "\t", quote = FALSE, row.names = FALSE)
+
+ # Load purity, ploidy and autosomal segments
+ pupl <- data.table::fread(paste0(tumourname, "_purity_ploidy.txt"), data.table = FALSE)
+ rho <- pupl$purity[1]
+ psi_sample <- pupl$ploidy[1]
+ log_info("Loaded autosomal metrics for {tumourname}: rho={rho}, ploidy={psi_sample}")
+ bb_data <- data.table::fread(paste0(tumourname, "_copynumber_extended.txt"), data.table = FALSE)
+
+ # Calculate LogR correction based on autosomal diploid regions
+ bb_dip <- bb_data[which(bb_data$nMaj1_A == 1 & bb_data$nMin1_A == 1 & bb_data$frac1_A == 1), ]
+ bb_corr <- if (nrow(bb_dip) > 0) {
+ -mean(bb_dip$LogR, na.rm = TRUE)
+ } else {
+ # WGD Fallback logic
+ cnloh <- bb_data[which(bb_data$nMaj1_A == 2 & bb_data$nMin1_A == 0 & bb_data$frac1_A == 1), ]
+ if (nrow(cnloh) > 0) -mean(cnloh$LogR, na.rm = TRUE) else -log2(2 / max(psi_sample, 0.1, na.rm = TRUE))
+ }
+ log_info("LogR correction (bb_corr): {bb_corr}")
+
+ # Estimate LogR Standard Deviation (Pixel Perfect SD logic)
+ bb_g1 <- bb_data[which(bb_data$nMaj1_A == 2 & bb_data$nMin1_A == 1 & bb_data$frac1_A == 1), ]
+ bb_g2 <- bb_data[which(bb_data$nMaj1_A == 3 & bb_data$nMin1_A == 1 & bb_data$frac1_A == 1), ]
+ bb_g3 <- bb_data[which(bb_data$nMaj1_A == 4 & bb_data$nMin1_A == 1 & bb_data$frac1_A == 1), ]
+ bb_sd_max <- max(c(
+ collapse::fsd(bb_dip$LogR),
+ collapse::fsd(bb_g1$LogR),
+ collapse::fsd(bb_g2$LogR),
+ collapse::fsd(bb_g3$LogR),
+ 0.05
+ ), na.rm = TRUE)
+ log_info("Estimated LogR SD (bb_sd_max): {bb_sd_max}")
+
+ # Expected LogR values for Male ChrX
+ exp_logr_gain <- sapply(2:10000, function(x) log2((rho * x + (1 - rho)) / 1))
+ exp_logr_loss <- max(log2((1 - rho)), log2(0.01))
+
+ # Process each segment for Copy Number and CCF
+ bb_loh_ref <- bb_data[bb_data$nMin1_A == 0 & bb_data$frac1_A == 1, ]
+ loh_sd <- if (nrow(bb_loh_ref) > 1) {
+ collapse::fsd(bb_loh_ref$LogR)
+ } else {
+ bb_sd_max
+ }
+ log_info("LOH LogR SD (loh_sd): {loh_sd}")
+
+ process_seg <- function(seg_row) {
+ seg <- as.list(seg_row)
+ seg$mean <- as.numeric(seg$mean) + bb_corr
+ seg$type <- if (isTRUE(seg$mean < 0)) "loss" else "gain"
+
+ # Check if CNA is significant
+ seg$CNA <- if (isTRUE(seg$type == "gain")) {
+ if (isTRUE(seg$mean > (1.96 * bb_sd_max))) "yes" else "no"
+ } else {
+ if (isTRUE(seg$mean < (-1.96 * bb_sd_max))) "yes" else "no"
+ }
+
+ if (isTRUE(seg$CNA == "yes")) {
+ if (isTRUE(seg$type == "gain")) {
+ # Determine CN by ranking against expectations
+ rank_val <- which(sort(c(exp_logr_gain, seg$mean)) == seg$mean)[1]
+ seg$CN <- rank_val + 1
+
+ # Clonality test
+ if (rank_val == 1) {
+ is_clonal <- isTRUE(round(exp_logr_gain[rank_val] - seg$mean, 2) <= round(bb_sd_max / exp_logr_gain[rank_val], 2))
+ seg$clonal <- if (is_clonal) "yes" else "no"
+ } else if (rank_val >= 5) {
+ # Closest check for high CN
+ if (isTRUE(abs(seg$mean - exp_logr_gain[rank_val - 1]) < abs(seg$mean - exp_logr_gain[rank_val]))) seg$CN <- seg$CN - 1
+ seg$clonal <- "yes"
+ } else {
+ if (isTRUE(abs(seg$mean - exp_logr_gain[rank_val - 1]) < abs(seg$mean - exp_logr_gain[rank_val]))) {
+ is_clonal <- isTRUE(round(seg$mean - exp_logr_gain[rank_val - 1], 2) <= round(bb_sd_max / exp_logr_gain[rank_val - 1], 2))
+ if (is_clonal) seg$CN <- seg$CN - 1
+ seg$clonal <- if (is_clonal) "yes" else "no"
+ } else {
+ is_clonal <- isTRUE(round(exp_logr_gain[rank_val] - seg$mean, 2) < round(bb_sd_max / exp_logr_gain[rank_val], 2))
+ seg$clonal <- if (is_clonal) "yes" else "no"
+ }
+ }
+ # CCF Gain
+ seg$CCF <- if (isTRUE(seg$clonal == "no")) (2^seg$mean - (rho * (seg$CN - 1) + (1 - rho))) / rho else 1
+ } else {
+ # Loss Logic
+ seg$CN <- 0
+ seg$clonal <- if (isTRUE(round(abs(exp_logr_loss - seg$mean), 2) < round(abs(loh_sd / exp_logr_loss), 2))) "yes" else "no"
+ # CCF Loss
+ seg$CCF <- if (isTRUE(seg$clonal == "no")) (1 - 2^seg$mean) / rho else 1
+ if (isTRUE(seg$CCF >= 0.95)) {
+ seg$CCF <- 1
+ seg$clonal <- "yes"
+ }
+ }
+ } else {
+ seg$CN <- 1
+ seg$clonal <- NA
+ seg$CCF <- 1
+ }
+ return(as.data.frame(seg))
+ }
+
+ seg_list <- lapply(seq_len(nrow(pcf_df)), function(i) process_seg(pcf_df[i, ]))
+ seg_df_all <- do.call(rbind, seg_list)
+
+ # Deep diagnostics
+ log_info("Diagnostics - rho: {rho}")
+ log_info("Diagnostics - seg_df_all columns: {paste(colnames(seg_df_all), collapse=', ')}")
+ log_info("Diagnostics - first row CN: {seg_df_all$CN[1]}, CNA: {seg_df_all$CNA[1]}, type: {seg_df_all$type[1]}, clonal: {seg_df_all$clonal[1]}, CCF: {seg_df_all$CCF[1]}")
+
+ log_info("Processed {nrow(seg_df_all)} segments before centromere filtering.")
+
+ # Centromere Noise Filtering
+ # Safely handle missing columns to prevent logical(0) wiping out the data frame
+ has_cols <- "arm" %in% colnames(seg_df_all) && "end.pos" %in% colnames(seg_df_all) &&
+ "CNA" %in% colnames(seg_df_all) && "start.pos" %in% colnames(seg_df_all)
+
+ if (!has_cols) {
+ log_warn("Centromere filtering columns missing (arm, end.pos, CNA, or start.pos). Skipping noise filter.")
+ }
+
+ if (has_cols) {
+ is_noise <- (seg_df_all$arm == "p" & seg_df_all$end.pos > (x_centromere[1] - 1e6) & seg_df_all$CNA == "yes" & seg_df_all$end.pos < (seg_df_all$start.pos + 1e6)) |
+ (seg_df_all$arm == "q" & seg_df_all$end.pos < (x_centromere[2] + 1e6) & seg_df_all$CNA == "yes" & seg_df_all$end.pos < (seg_df_all$start.pos + 1e6))
+ is_noise[is.na(is_noise)] <- FALSE
+ } else {
+ is_noise <- rep(FALSE, nrow(seg_df_all))
+ }
+
+ seg_filtered <- seg_df_all[!is_noise, ]
+ log_info("{nrow(seg_filtered)} segments remaining after centromere noise filtering.")
+
+ # Map to nMaj/nMin structure (Pixel Perfect mapping)
+ final_rows <- list()
+ for (i in seq_len(nrow(seg_filtered))) {
+ s <- seg_filtered[i, ]
+ if (isTRUE(s$CNA == "no")) {
+ s$nMaj1 <- 1
+ s$nMin1 <- 0
+ s$frac1 <- 1
+ s$nMaj2 <- 0
+ s$nMin2 <- 0
+ s$frac2 <- 0
+ } else {
+ if (isTRUE(s$type == "gain")) {
+ if (isTRUE(s$clonal == "yes")) {
+ s$nMaj1 <- s$CN
+ s$nMin1 <- 0
+ s$frac1 <- 1
+ s$nMaj2 <- 0
+ s$nMin2 <- 0
+ s$frac2 <- 0
+ } else {
+ main_clone <- if (isTRUE(s$CCF > 0.5)) s$CN else s$CN - 1
+ sec_clone <- if (isTRUE(s$CCF > 0.5)) s$CN - 1 else s$CN
+ s$nMaj1 <- main_clone
+ s$nMin1 <- 0
+ s$frac1 <- if (isTRUE(s$CCF > 0.5)) s$CCF else 1 - s$CCF
+ s$nMaj2 <- sec_clone
+ s$nMin2 <- 0
+ s$frac2 <- 1 - s$frac1
+ }
+ } else {
+ # Loss
+ if (isTRUE(s$clonal == "yes")) {
+ s$nMaj1 <- s$CN
+ s$nMin1 <- 0
+ s$frac1 <- 1
+ s$nMaj2 <- 0
+ s$nMin2 <- 0
+ s$frac2 <- 0
+ } else {
+ s$nMaj1 <- if (isTRUE(s$CCF > 0.5)) 0 else 1
+ s$nMaj2 <- if (isTRUE(s$CCF > 0.5)) 1 else 0
+ s$frac1 <- if (isTRUE(s$CCF > 0.5)) s$CCF else 1 - s$CCF
+ s$frac2 <- 1 - s$frac1
+ s$nMin1 <- 0
+ s$nMin2 <- 0
+ }
+ }
+ }
+ final_rows[[i]] <- s
+ }
+ subclones_full <- do.call(rbind, final_rows)
+ subclones_full$subclonalCN <- (as.numeric(subclones_full$nMaj1) + as.numeric(subclones_full$nMin1)) * as.numeric(subclones_full$frac1) +
+ (as.numeric(subclones_full$nMaj2) + as.numeric(subclones_full$nMin2)) * as.numeric(subclones_full$frac2)
+
+ # Ensure no NAs in subclonalCN
+ subclones_full$subclonalCN[is.na(subclones_full$subclonalCN)] <- 0
+ log_info("subclonalCN calculated. Range: {min(subclones_full$subclonalCN)} to {max(subclones_full$subclonalCN)}")
+
+ # Reformat and Merge Adjacent Segments
+ out_df <- data.frame(
+ chrom = subclones_full$chrom, arm = subclones_full$arm, startpos = subclones_full$start.pos,
+ endpos = subclones_full$end.pos, nSNPs = subclones_full$n.probes, LogR = subclones_full$mean,
+ type = ifelse(subclones_full$type == "gain", "+ve", "-ve"), CNA = subclones_full$CNA,
+ CN = subclones_full$CN, clonal = subclones_full$clonal, nMaj1 = subclones_full$nMaj1,
+ nMin1 = subclones_full$nMin1, frac1 = subclones_full$frac1, nMaj2 = subclones_full$nMaj2,
+ nMin2 = subclones_full$nMin2, frac2 = subclones_full$frac2, subclonalCN = subclones_full$subclonalCN,
+ stringsAsFactors = FALSE
+ )
+
+ # Group and Merge logic (Consecutive segments with same CN state)
+ out_df$orig_rank <- seq_len(nrow(out_df))
+ sorted_df <- out_df[order(out_df$subclonalCN), ]
+ groups <- split(sorted_df$orig_rank, cumsum(c(1, diff(sorted_df$orig_rank) != 1)))
+
+ merged_list <- list()
+ for (grp in groups) {
+ sub_grp <- out_df[out_df$orig_rank %in% grp, ]
+ if (nrow(sub_grp) > 1 && length(unique(sub_grp$arm)) == 1 && isTRUE(collapse::fsd(sub_grp$subclonalCN) <= 0.01)) {
+ m_seg <- sub_grp[1, ]
+ m_seg$endpos <- sub_grp$endpos[nrow(sub_grp)]
+ m_seg$nSNPs <- sum(sub_grp$nSNPs)
+ m_seg$LogR <- collapse::fmean(sub_grp$LogR, w = sub_grp$nSNPs, na.rm = TRUE)
+ merged_list[[length(merged_list) + 1]] <- m_seg
+ } else {
+ # Handle specific arm-based sub-merging as per original messy logic
+ merged_list[[length(merged_list) + 1]] <- sub_grp
+ }
+ }
+ merged_df <- do.call(rbind, merged_list) |> (\(x) x[order(x$startpos), ])()
+ log_info("Number of rows merged = {nrow(out_df) - nrow(merged_df)}")
+
+ # Update File Outputs
+ autosomal_only <- bb_data[!bb_data$chr %in% c("X", "chrX"), ]
+
+ # Standard copynumber.txt update
+ x_new <- data.frame(
+ chr = merged_df$chrom, startpos = merged_df$startpos, endpos = merged_df$endpos,
+ nMaj1_A = merged_df$nMaj1, nMin1_A = merged_df$nMin1, frac1_A = merged_df$frac1,
+ nMaj2_A = merged_df$nMaj2, nMin2_A = merged_df$nMin2, frac2_A = merged_df$frac2
+ )
+ data.table::fwrite(rbind(autosomal_only[, c(1:3, 8:13)], x_new), paste0(tumourname, "_copynumber.txt"), sep = "\t", quote = FALSE, row.names = FALSE)
+
+ # Standard copynumber_extended.txt update
+ x_new_extended <- data.frame(
+ chr = merged_df$chrom, startpos = merged_df$startpos, endpos = merged_df$endpos,
+ BAF = NA, pval = NA, LogR = merged_df$LogR, ntot = NA,
+ nMaj1_A = merged_df$nMaj1, nMin1_A = merged_df$nMin1, frac1_A = merged_df$frac1,
+ nMaj2_A = merged_df$nMaj2, nMin2_A = merged_df$nMin2, frac2_A = merged_df$frac2,
+ stringsAsFactors = FALSE
+ )
+ if (ncol(bb_data) > 13) {
+ extra_cols <- as.data.frame(matrix(NA, nrow = nrow(x_new_extended), ncol = ncol(bb_data) - 13))
+ colnames(extra_cols) <- colnames(bb_data)[14:ncol(bb_data)]
+ x_new_extended <- cbind(x_new_extended, extra_cols)
+ }
+ data.table::fwrite(rbind(autosomal_only, x_new_extended), paste0(tumourname, "_copynumber_extended.txt"), sep = "\t", quote = FALSE, row.names = FALSE)
+
+ # Average Ploidy Plot
+ pga_val <- if (any(merged_df$CNA == "yes", na.rm = TRUE)) {
+ clonal_yes <- !is.na(merged_df$clonal) & merged_df$clonal == "yes"
+ sum(merged_df$endpos[clonal_yes] - merged_df$startpos[clonal_yes], na.rm = TRUE) /
+ sum(merged_df$endpos[!is.na(merged_df$clonal)] - merged_df$startpos[!is.na(merged_df$clonal)], na.rm = TRUE)
+ } else {
+ "NA"
+ }
+
+ plot_title <- paste0(
+ tumourname, " , Ploidy: ", round(psi_sample, 3), " , Purity: ", round(rho * 100, 0), "%, chrX PGA.is.clonal: ",
+ if (pga_val == "NA") "NA" else paste0(round(as.numeric(pga_val) * 100, 1), "%")
+ )
+
+ if (nrow(merged_df) > 0) {
+ avg_plot <- ggplot2::ggplot(merged_df) +
+ ggplot2::geom_hline(
+ yintercept = 0:ceiling(max(merged_df$subclonalCN, na.rm = TRUE)),
+ linetype = "longdash", col = "grey", linewidth = 0.2
+ ) +
+ ggplot2::geom_rect(
+ ggplot2::aes(
+ xmin = startpos, xmax = endpos,
+ ymin = subclonalCN - 0.02, ymax = subclonalCN + 0.02
+ )
+ ) +
+ ggplot2::geom_vline(
+ xintercept = x_centromere, linetype = "longdash", col = "green"
+ ) +
+ ggplot2::labs(
+ x = "ChrX coordinate (bp)",
+ y = "Average Ploidy",
+ title = plot_title
+ ) +
+ ggplot2::theme_minimal() +
+ ggplot2::theme(plot.title = ggplot2::element_text(hjust = 0.5))
+
+ if (AR) {
+ # Highlight AR locus
+ seg_ar <- merged_df[!is.na(merged_df$startpos) & !is.na(merged_df$endpos) &
+ merged_df$startpos < ar_locus$endpos & merged_df$endpos > ar_locus$startpos, ]
+ if (nrow(seg_ar) > 0) {
+ avg_plot <- avg_plot + ggplot2::geom_rect(
+ data = seg_ar,
+ ggplot2::aes(
+ xmin = startpos,
+ xmax = endpos,
+ ymin = subclonalCN - 0.02,
+ ymax = subclonalCN + 0.02
+ ),
+ fill = "red"
+ )
+ }
+ }
+
+ grDevices::pdf(paste0(tumourname, "_chrX_average_ploidy.pdf"))
+ print(avg_plot)
+ log_info("Average ploidy plot generated for chrX.")
+ grDevices::dev.off()
+ } else {
+ log_info("No segments found for chrX. Skipping average ploidy plot.")
+ }
+
+ # Final Genome-wide Plot Update
+ temp_dt <- data.table::fread(paste0(tumourname, "_rho_and_psi.txt"), data.table = FALSE)
+ goodness_val <- temp_dt[temp_dt$is_best %in% TRUE, "distance"][1]
+ log_info("Retrieved goodness_val for plot: {goodness_val}")
+ baf_raw <- read_bafsegmented(
+ paste0(tumourname, ".BAFsegmented.txt")
+ ) |> as.data.frame()
+
+ # Simulate ChrX BAF for plot (Male sample)
+ sim_len <- round(nrow(baf_raw) * 0.05)
+ baf_sim_x <- data.frame(
+ Chromosome = "X", Position = sort(sample(1:155e6, sim_len)),
+ BAF = sample(0:1, sim_len, replace = TRUE), BAFphased = 1, BAFseg = 1
+ )
+ baf_updated <- rbind(baf_raw[!baf_raw$Chromosome %in% c("X", "chrX"), ], baf_sim_x)
+
+ plot_gw_subclonal_cn(
+ subclones = rbind(autosomal_only[, c(1:3, 8:13)], x_new), BAFvals = baf_updated, rho = rho, ploidy = psi_sample,
+ goodness = goodness_val, output_gw_figures_prefix = paste0(tumourname, "_BattenbergProfile"),
+ chr_names = chrom_names, tumourname = tumourname
+ )
+}
+
+fast_p <- function(x, y) {
+ n1 <- length(x)
+ n2 <- length(y)
+ if (n1 < 2 || n2 < 2) {
+ return(1)
+ }
+
+ m1 <- mean(x)
+ m2 <- mean(y)
+ v1 <- stats::var(x)
+ v2 <- stats::var(y)
+
+ se <- sqrt(v1 / n1 + v2 / n2)
+ if (se == 0) {
+ return(1)
+ }
+
+ t_stat <- (m1 - m2) / se
+ df <- (v1 / n1 + v2 / n2)^2 / ((v1 / n1)^2 / (n1 - 1) + (v2 / n2)^2 / (n2 - 1))
+
+ 2 * stats::pt(-abs(t_stat), df)
+}
diff --git a/R/fit_merge_segments.R b/R/fit_merge_segments.R
new file mode 100644
index 00000000..c3c405a2
--- /dev/null
+++ b/R/fit_merge_segments.R
@@ -0,0 +1,377 @@
+#' Merge copy number segments
+#'
+#' Merges segments if there is not enough evidence for them to be separate. Two adjacent segments are merged
+#' when they are either fit with the same clonal copy number state or when their BAF is not significantly different
+#' and their logR puts them in the same square.
+#' @param subclones A completely fit copy number profile in Battenberg output format
+#' @param bafsegmented A BAFsegmented data.frame with the 5 columns that corresponds to the subclones file
+#' @param logR The raw logR data
+#' @param rho The rho estimate that the profile was fit with
+#' @param psi the psi estimate that the profile was fit with
+#' @param platform_gamma The gamma parameter for this platform
+#' @param calc_seg_baf_option Various options to recalculate the BAF of a segment. Options are: 1 - median, 2 - mean, 3 - ifelse median== 0|1, mean, median. (Default: 3)
+#' @param verbose A boolean to show merging operations (Default: FALSE)
+#' @return A list with two fields: bafsegmented and subclones. The subclones field contains a data.frame in
+#' Battenberg output format with the merged segments. The bafsegmented field contains the BAFsegmented data
+#' corresponding to the provided subclones data.frame.
+#' @author sd11, tl
+#' @noRd
+merge_segments <- function(
+ subclones,
+ bafsegmented,
+ logR,
+ rho,
+ psi,
+ platform_gamma,
+ calc_seg_baf_option = 3,
+ verbose_logging = FALSE
+) {
+ calc_nmin <- function(rho, psi, baf, logr, platform_gamma) {
+ return((rho - 1 - (baf - 1) * 2^(logr / platform_gamma) * ((1 - rho) * 2 + rho * psi)) / rho)
+ }
+ calc_nmaj <- function(rho, psi, baf, logr, platform_gamma) {
+ return((rho - 1 + baf * 2^(logr / platform_gamma) * ((1 - rho) * 2 + rho * psi)) / rho)
+ }
+ # Convert DF into GRanges objects
+ df2gr <- function(DF, chr, pos1, pos2) {
+ return(GenomicRanges::makeGRangesFromDataFrame(
+ df = DF,
+ keep.extra.columns = TRUE,
+ ignore.strand = TRUE,
+ seqinfo = NULL,
+ seqnames.field = chr,
+ start.field = pos1,
+ end.field = pos2,
+ starts.in.df.are.0based = FALSE
+ ))
+ }
+ # Function called when two segments have not been merged so there is no need to recheck those again
+ update_neighbour <- function(subclones, INDEX, INDEX_N) {
+ if (INDEX_N > INDEX) {
+ subclones$next_checked[INDEX] <- TRUE
+ subclones$prev_checked[INDEX_N] <- TRUE
+ } else {
+ subclones$prev_checked[INDEX] <- TRUE
+ subclones$next_checked[INDEX_N] <- TRUE
+ }
+ return(subclones)
+ }
+ # Function called when two segments have been merged so we need to recheck its two neighbours
+ updateAround <- function(subclones, INDEX) {
+ if (INDEX > 1) {
+ subclones$prev_checked[INDEX] <- FALSE
+ subclones$next_checked[INDEX - 1] <- FALSE
+ } else {
+ subclones$prev_checked[INDEX] <- TRUE
+ }
+ if (INDEX < length(subclones)) {
+ subclones$next_checked[INDEX] <- FALSE
+ subclones$prev_checked[INDEX + 1] <- FALSE
+ } else {
+ subclones$next_checked[INDEX] <- TRUE
+ }
+ return(subclones)
+ }
+ # Function called to test whether two segments must be checked
+ check_status <- function(subclones, INDEX, INDEX_N) {
+ if (INDEX_N > INDEX) {
+ # Largest segment (INDEX_N) is after smallest one (INDEX)
+ stopifnot(subclones$next_checked[INDEX] == subclones$prev_checked[INDEX_N])
+ if (subclones$next_checked[INDEX] && subclones$prev_checked[INDEX_N]) {
+ return(TRUE)
+ } else {
+ return(FALSE)
+ }
+ } else {
+ # Largest segment (INDEX_N) is before smallest one (INDEX)
+ stopifnot(subclones$prev_checked[INDEX] == subclones$next_checked[INDEX_N])
+ if (subclones$prev_checked[INDEX] && subclones$next_checked[INDEX_N]) {
+ return(TRUE)
+ } else {
+ return(FALSE)
+ }
+ }
+ }
+
+ # Function to merge two segments
+ merge_seg <- function(
+ subclones, bafsegmented,
+ logR, INDEX, INDEX_N,
+ calc_seg_baf_option
+ ) {
+ # Standard GenomicRanges coordinate updates
+ if (INDEX_N < INDEX) {
+ GenomicRanges::end(
+ subclones[INDEX_N]
+ ) <- GenomicRanges::end(subclones[INDEX])
+ } else {
+ GenomicRanges::start(
+ subclones[INDEX_N]
+ ) <- GenomicRanges::start(subclones[INDEX])
+ }
+
+ # Remove the merged-from segment
+ subclones <- subclones[-INDEX]
+ if (INDEX_N < INDEX) INDEX <- INDEX - 1
+
+ # Trigger local neighbor update logic
+ subclones <- updateAround(subclones, INDEX)
+
+ # Efficient overlap extraction
+ # subjectHits is the linter-safe version of @to
+ baf_idx <- S4Vectors::subjectHits(
+ GenomicRanges::findOverlaps(subclones[INDEX], bafsegmented)
+ )
+ baf_vals <- bafsegmented$BAFphased[baf_idx]
+
+ # Modernized BAF calculation with safety for NA values
+ if (calc_seg_baf_option == 1) {
+ NEW_BAF <- collapse::fmedian(baf_vals, na.rm = TRUE)
+ } else if (calc_seg_baf_option == 2) {
+ NEW_BAF <- collapse::fmean(baf_vals, na.rm = TRUE)
+ } else if (calc_seg_baf_option == 3) {
+ # Calculate both using high-performance C++ bindings
+ m_baf <- collapse::fmedian(baf_vals, na.rm = TRUE)
+
+ # Robust Logic: Only use the median if it's not NA
+ # This avoids the "missing value where TRUE/FALSE needed" error
+ if (!is.na(m_baf) && m_baf != 0 && m_baf != 1) {
+ NEW_BAF <- m_baf
+ } else {
+ NEW_BAF <- collapse::fmean(baf_vals, na.rm = TRUE)
+ }
+ }
+
+ # LogR update with safety for empty segments
+ logr_idx <- S4Vectors::subjectHits(
+ GenomicRanges::findOverlaps(subclones[INDEX], logR)
+ )
+
+ if (length(logr_idx) == 0) {
+ subclones[INDEX]$LogR <- 0
+ } else {
+ subclones[INDEX]$LogR <- collapse::fmean(
+ logR$logR[logr_idx],
+ na.rm = TRUE
+ )
+ }
+
+ # Update metadata on the S4 objects
+ subclones[INDEX]$BAF <- NEW_BAF
+ bafsegmented$BAFseg[baf_idx] <- NEW_BAF
+
+ # Standard Evaluation sequence generation
+ subclones$ID <- seq_along(subclones)
+
+ list(subclones = subclones, bafsegmented = bafsegmented)
+ }
+
+ log_debug("Converting DFs into GRanges objects")
+
+ subclones <- subclones |>
+ df2gr("chr", "startpos", "endpos") |>
+ GenomicRanges::sort()
+
+ bafsegmented <- bafsegmented |>
+ df2gr("Chromosome", "Position", "Position") |>
+ GenomicRanges::sort()
+
+ logR <- logR |>
+ df2gr("Chromosome", "Position", "Position") |>
+ GenomicRanges::sort()
+ names(GenomicRanges::mcols(logR)) <- "logR"
+
+ # Get unique chromosomes
+ chr_names <- unique(as.character(GenomicRanges::seqnames(bafsegmented)))
+
+ # Split by chromosome
+ subclones <- split(subclones, GenomicRanges::seqnames(subclones))
+ bafsegmented <- split(bafsegmented, GenomicRanges::seqnames(bafsegmented))
+ logR <- split(logR, GenomicRanges::seqnames(logR))
+
+ if (!all(chr_names %in% names(subclones)) || !all(chr_names %in% names(bafsegmented)) || !all(chr_names %in% names(logR))) {
+ log_failure("Missing data for some chromosomes in one or more inputs")
+ }
+
+ # Process each chromosome
+ for (CHR in chr_names) {
+ log_debug("Merging segments within: {CHR}")
+
+ subclones_chr <- subclones[[CHR]]
+ bafsegmented_chr <- bafsegmented[[CHR]]
+ logR_chr <- logR[[CHR]]
+
+ # Initialize tracking columns
+ subclones_chr$ID <- seq_along(subclones_chr)
+ subclones_chr$prev_checked <- FALSE
+ subclones_chr$next_checked <- FALSE
+ subclones_chr$prev_checked[1] <- TRUE
+ subclones_chr$next_checked[length(subclones_chr)] <- TRUE
+
+ while (TRUE) {
+ # Find segments needing checks
+ unchecked <- which(!subclones_chr$prev_checked | !subclones_chr$next_checked)
+ if (length(unchecked) == 0) break
+
+ # Select smallest unchecked segment
+ widths <- GenomicRanges::width(subclones_chr[unchecked])
+ index <- unchecked[which.min(widths)]
+
+ log_debug("Working on segment: {index} ({subclones_chr[index]})")
+
+ # Determine possible neighbors
+ n <- length(subclones_chr)
+ neighbors <- integer(0)
+ if (index > 1) neighbors <- c(neighbors, index - 1)
+ if (index < n) neighbors <- c(neighbors, index + 1)
+
+ if (length(neighbors) == 0) next
+
+ # Sort neighbors by distance (closest first)
+ dists <- GenomicRanges::distance(subclones_chr[index], subclones_chr[neighbors])
+ sorted_neighbors <- neighbors[order(dists)]
+
+ merged <- FALSE
+ for (index_n in sorted_neighbors) {
+ log_debug("Checking neighbour: {index_n} ({subclones_chr[index_n]}; distance={dists[which(neighbors == index_n)]})")
+
+ # Skip if already checked
+ if (check_status(subclones_chr, index, index_n)) {
+ log_debug("Already checked")
+ next
+ }
+
+ # Check distance threshold
+ if (GenomicRanges::distance(subclones_chr[index], subclones_chr[index_n]) > 3e6) {
+ log_debug("Distance > 3Mb - do not merge")
+ subclones_chr <- update_neighbour(subclones_chr, index, index_n)
+ next
+ }
+
+ # Check for identical clonal CN
+ if (subclones_chr$nMaj1_A[index] == subclones_chr$nMaj1_A[index_n] &&
+ subclones_chr$nMin1_A[index] == subclones_chr$nMin1_A[index_n] &&
+ subclones_chr$frac1_A[index] == 1 &&
+ subclones_chr$frac1_A[index_n] == 1) {
+ log_debug("Same clonal CN solution - merge")
+ res <- merge_seg(subclones_chr, bafsegmented_chr, logR_chr, index, index_n, calc_seg_baf_option)
+ subclones_chr <- res$subclones
+ bafsegmented_chr <- res$bafsegmented
+ merged <- TRUE
+ break
+ }
+
+ # Check for compatible CN via stats
+ log_debug("Different CN solutions: check BAF and logR")
+ nmin_curr <- round(calc_nmin(rho, psi, subclones_chr$BAF[index], subclones_chr$LogR[index], platform_gamma))
+ nmaj_curr <- round(calc_nmaj(rho, psi, subclones_chr$BAF[index], subclones_chr$LogR[index], platform_gamma))
+ nmin_other <- round(calc_nmin(rho, psi, subclones_chr$BAF[index_n], subclones_chr$LogR[index_n], platform_gamma))
+ nmaj_other <- round(calc_nmaj(rho, psi, subclones_chr$BAF[index_n], subclones_chr$LogR[index_n], platform_gamma))
+
+ if (nmin_curr == nmin_other || nmaj_curr == nmaj_other) {
+ # Check sufficient data points
+ logr_curr <- logR_chr$logR[S4Vectors::subjectHits(GenomicRanges::findOverlaps(subclones_chr[index], logR_chr))]
+ logr_other <- logR_chr$logR[S4Vectors::subjectHits(GenomicRanges::findOverlaps(subclones_chr[index_n], logR_chr))]
+ baf_curr <- bafsegmented_chr$BAFphased[S4Vectors::subjectHits(GenomicRanges::findOverlaps(subclones_chr[index], bafsegmented_chr))]
+ baf_other <- bafsegmented_chr$BAFphased[S4Vectors::subjectHits(GenomicRanges::findOverlaps(subclones_chr[index_n], bafsegmented_chr))]
+
+ if (sum(!is.na(logr_curr)) > 10 && sum(!is.na(logr_other)) > 10 &&
+ sum(!is.na(baf_curr)) > 10 && sum(!is.na(baf_other)) > 10) {
+ logr_p <- fast_p(logr_curr, logr_other)
+ baf_p <- fast_p(baf_curr, baf_other)
+ if (logr_p >= 0.05 && baf_p >= 0.05) {
+ log_debug("No significant difference - merge")
+ res <- merge_seg(subclones_chr, bafsegmented_chr, logR_chr, index, index_n, calc_seg_baf_option)
+ subclones_chr <- res$subclones
+ bafsegmented_chr <- res$bafsegmented
+ merged <- TRUE
+ break
+ } else {
+ log_debug("Significant difference - do not merge")
+ subclones_chr <- update_neighbour(subclones_chr, index, index_n)
+ }
+ } else {
+ log_debug("Too few values - do not merge")
+ subclones_chr <- update_neighbour(subclones_chr, index, index_n)
+ }
+ } else {
+ log_debug("Different squares - do not merge")
+ subclones_chr <- update_neighbour(subclones_chr, index, index_n)
+ }
+ }
+ if (merged) next # Continue while loop after merge
+ }
+
+ # Store back processed data
+ subclones[[CHR]] <- subclones_chr
+ bafsegmented[[CHR]] <- bafsegmented_chr
+ }
+
+ log_debug("Convert GRanges objects into DFs")
+
+ # Combine and convert to data frames
+ bafsegmented <- data.frame(Reduce(c, bafsegmented), stringsAsFactors = FALSE)[, -c(3:5)]
+ bafsegmented$seqnames <- as.character(bafsegmented$seqnames)
+ colnames(bafsegmented)[1:2] <- c("Chromosome", "Position")
+
+ subclones <- data.frame(Reduce(c, subclones), stringsAsFactors = FALSE)[, -c(4:5)]
+ subclones$seqnames <- as.character(subclones$seqnames)
+ colnames(subclones)[1:3] <- c("chr", "startpos", "endpos")
+ subclones$ID <- NULL
+ subclones$prev_checked <- NULL
+ subclones$next_checked <- NULL
+
+ return(list(bafsegmented = bafsegmented, subclones = subclones))
+}
+
+#' Mask segments that have a too high CN state
+#' @param subclones Subclones output data
+#' @param bafsegmented BAFsegmented data
+#' @param max_allowed_state The maximum state allowed before overruling takes place
+#' @return A list with the masked subclones, bafsegmented and the number of segments masked and their total genome size
+#' @author sd11
+mask_high_cn_segments <- function(subclones, bafsegmented, max_allowed_state) {
+ to_mask_idx <- which(subclones$nMaj1_A > max_allowed_state | subclones$nMin1_A > max_allowed_state)
+
+ if (length(to_mask_idx) == 0) {
+ return(list(
+ subclones = subclones,
+ bafsegmented = bafsegmented,
+ masked_count = 0,
+ masked_size = 0
+ ))
+ }
+
+ count <- length(to_mask_idx)
+ masked_size <- sum(subclones$endpos[to_mask_idx] - subclones$startpos[to_mask_idx])
+
+ # Identify segments to mask in the BAFsegmented file
+ # Use GenomicRanges for O(N+M) overlap detection instead of the O(N*M) loop
+ segs_to_mask <- subclones[to_mask_idx, ]
+ gr_segs <- GenomicRanges::GRanges(
+ seqnames = segs_to_mask$chr,
+ # Original logic: startpos < Position <= endpos
+ ranges = IRanges::IRanges(start = segs_to_mask$startpos + 1, end = segs_to_mask$endpos)
+ )
+
+ gr_snps <- GenomicRanges::GRanges(
+ seqnames = bafsegmented$Chromosome,
+ ranges = IRanges::IRanges(start = bafsegmented$Position, end = bafsegmented$Position)
+ )
+
+ # Find SNPs that fall within any masked segment
+ overlaps <- GenomicRanges::findOverlaps(gr_snps, gr_segs)
+ if (length(overlaps) > 0) {
+ bafsegmented$BAFseg[unique(S4Vectors::queryHits(overlaps))] <- NA
+ }
+
+ # Now mask the subclones table
+ subclones[to_mask_idx, c("nMaj1_A", "nMin1_A", "nMaj2_A", "nMin2_A")] <- NA
+
+ return(list(
+ subclones = subclones,
+ bafsegmented = bafsegmented,
+ masked_count = count,
+ masked_size = masked_size
+ ))
+}
diff --git a/R/fitcopynumber.R b/R/fitcopynumber.R
deleted file mode 100644
index 003a5d65..00000000
--- a/R/fitcopynumber.R
+++ /dev/null
@@ -1,1407 +0,0 @@
-#' Fit copy number
-#'
-#' Function that will fit a clonal copy number profile to segmented data. It first
-#' matches the raw LogR with the segmented BAF to create segmented LogR. Then ASCAT
-#' is run to obtain a clonal copy number profile. Beyond logRsegmented it produces
-#' the rho_and_psi file and the cellularity_ploidy file.
-#' @param samplename Samplename used to name the segmented logr output file
-#' @param outputfile.prefix Prefix used for all output file names, except logRsegmented
-#' @param inputfile.baf.segmented Filename that points to the BAF segmented data
-#' @param inputfile.baf Filename that points to the raw BAF data
-#' @param inputfile.logr Filename that points to the raw LogR data
-#' @param dist_choice The distance metric that is used internally to rank clonal copy number solutions
-#' @param ascat_dist_choice The distance metric used to obtain an initial cellularity and ploidy estimate
-#' @param min.ploidy The minimum ploidy to consider (Default 1.6)
-#' @param max.ploidy The maximum ploidy to consider (Default 4.8)
-#' @param min.rho The minimum cellularity to consider (Default 0.1)
-#' @param max.rho The maximum cellularity to consider (Default 1.0)
-#' @param min.goodness The minimum goodness of fit for a solution to have to be considered (Default 63)
-#' @param uninformative_BAF_threshold The threshold beyond which BAF becomes uninformative (Default 0.51)
-#' @param gamma_param Technology parameter, compaction of Log R profiles. Expected decrease in case of deletion in diploid sample, 100 "\%" aberrant cells; 1 in ideal case, 0.55 of Illumina 109K arrays (Default 1)
-#' @param use_preset_rho_psi Boolean whether to use user specified rho and psi values (Default F)
-#' @param preset_rho A user specified rho to fit a copy number profile to (Default NA)
-#' @param preset_psi A user specified psi to fit a copy number profile to (Default NA)
-#' @param read_depth Legacy parameter that is no longer used (Default 30)
-#' @param analysis A String representing the type of analysis to be run, this determines whether the distance figure is produced (Default paired)
-#' @author dw9, sd11
-#' @export
-fit.copy.number = function(samplename, outputfile.prefix, inputfile.baf.segmented, inputfile.baf, inputfile.logr, dist_choice, ascat_dist_choice, min.ploidy=1.6, max.ploidy=4.8, min.rho=0.1, max.rho=1.0, min.goodness=63, uninformative_BAF_threshold=0.51, gamma_param=1, use_preset_rho_psi=F, preset_rho=NA, preset_psi=NA, read_depth=30, analysis="paired", nthreads, enhanced_grid_search=F) {
-
- assert.file.exists(inputfile.baf.segmented)
- assert.file.exists(inputfile.baf)
- assert.file.exists(inputfile.logr)
- # Check for enough options supplied for rho and psi
- if ((max.ploidy - min.ploidy) < 0.05) {
- stop(paste("Supplied ploidy range must be larger than 0.05: ", min.ploidy, "-", max.ploidy, sep=""))
- }
- if ((max.rho - min.rho) < 0.01) {
- stop(paste("Supplied rho range must be larger than 0.01: ", min.rho, "-", max.rho, sep=""))
- }
-
- # Read in the required data
- segmented.BAF.data = as.data.frame(read_bafsegmented(inputfile.baf.segmented))
- raw.BAF.data = as.data.frame(read_baf(inputfile.baf))
- raw.logR.data = as.data.frame(read_logr(inputfile.logr))
-
- # Assign rownames as those are required by various clonal_ascat.R functions
- # If there are duplicates (possible with old versions of BB) then remove those
- identifiers = paste(segmented.BAF.data[,1], segmented.BAF.data[,2], sep="_")
- dups = which(duplicated(identifiers))
- if (length(dups) > 0) {
- segmented.BAF.data = segmented.BAF.data[-dups,]
- identifiers = identifiers[-dups]
- }
- rownames(segmented.BAF.data) = identifiers
-
- # Drop NAs
- raw.BAF.data = raw.BAF.data[!is.na(raw.BAF.data[,3]),]
- raw.logR.data = raw.logR.data[!is.na(raw.logR.data[,3]),]
-
- ## Chromosome names are sometimes 'chr1', etc.
- #if(length(grep("chr",raw.BAF.data[1,1]))>0){
- # raw.BAF.data[,1] = gsub("chr","",raw.BAF.data[,1])
- #}
- #if(length(grep("chr",raw.logR.data[1,1]))>0){
- # raw.logR.data[,1] = gsub("chr","",raw.logR.data[,1])
- #}
-
- BAF.data = list()
- logR.data = list()
- segmented.logR.data = list()
- matched.segmented.BAF.data = list()
- gsubchr = function(chr) gsub("chr","",as.character(chr))
-
- chr.names = gsubchr(unique(segmented.BAF.data[,1]))
-
- segmented.BAF.data$Chromosome = gsubchr(segmented.BAF.data$Chromosome)
- raw.BAF.data$Chromosome = gsubchr(raw.BAF.data$Chromosome)
- raw.logR.data$Chromosome =gsubchr(raw.logR.data$Chromosome)
-
- baf_segmented_split = split(segmented.BAF.data, f=segmented.BAF.data$Chromosome)
- baf_split = split(raw.BAF.data, f=raw.BAF.data$Chromosome)
- logr_split = split(raw.logR.data, f=raw.logR.data$Chromosome)
-
- # For each chromosome
- for(chr in chr.names){
- chr.BAF.data = baf_split[[chr]]
-
- # Skip the rest if there is no data for this chromosome
- if(is.null(chr.BAF.data) || nrow(chr.BAF.data)==0){ next }
- # Match segments with chromosome position
- chr.segmented.BAF.data = baf_segmented_split[[chr]]
- indices = match(chr.segmented.BAF.data[,2],chr.BAF.data$Position )
-
- if (sum(is.na(indices))==length(indices) | length(indices)==0) {
- next
- }
-
- # Drop NAs here too
- chr.segmented.BAF.data = chr.segmented.BAF.data[!is.na(indices),]
-
- # Append the segmented data
- matched.segmented.BAF.data[[chr]] = chr.segmented.BAF.data
- BAF.data[[chr]] = chr.BAF.data[indices[!is.na(indices)],]
-
- # Append raw LogR
- chr.logR.data = logr_split[[chr]]
- indices = match(chr.segmented.BAF.data[,2],chr.logR.data$Position)
- logR.data[[chr]] = chr.logR.data[indices[!is.na(indices)],]
- chr.segmented.logR.data = chr.logR.data[indices[!is.na(indices)],]
-
- # Append segmented LogR
- segs = rle(chr.segmented.BAF.data[,5])$lengths
- cum.segs = c(0,cumsum(segs))
- for(s in 1:length(segs)){
- chr.segmented.logR.data[(cum.segs[s]+1):cum.segs[s+1],3] = mean(chr.segmented.logR.data[(cum.segs[s]+1):cum.segs[s+1],3], na.rm=T)
- }
- segmented.logR.data[[chr]] = chr.segmented.logR.data
- }
-
- # Sync the dataframes
- selection = c()
- for (chrom in chr.names) {
- matched.segmented.BAF.data.chr = matched.segmented.BAF.data[[chrom]] #matched.segmented.BAF.data[matched.segmented.BAF.data[,1]==chrom,]
- logR.data.chr = logR.data[[chrom]] #logR.data[logR.data[,1]==chrom,]
-
- selection = matched.segmented.BAF.data.chr[,2] %in% logR.data.chr[,2]
- matched.segmented.BAF.data[[chrom]] = matched.segmented.BAF.data.chr[selection,]
- segmented.logR.data[[chrom]] = segmented.logR.data[[chrom]][selection,]
- }
-
- # Combine the split data frames into a single for the subsequent steps
- matched.segmented.BAF.data = do.call(rbind, matched.segmented.BAF.data)
- segmented.logR.data = do.call(rbind, segmented.logR.data)
- BAF.data = do.call(rbind, BAF.data)
- logR.data = do.call(rbind, logR.data)
- names(matched.segmented.BAF.data)[5] = samplename
-
- # write out the segmented logR data
- row.names(segmented.logR.data) = row.names(matched.segmented.BAF.data)
- row.names(logR.data) = row.names(matched.segmented.BAF.data)
- write.table(segmented.logR.data,paste(samplename,".logRsegmented.txt",sep=""),sep="\t",quote=F,col.names=F,row.names=F)
-
- # Prepare the data for going into the runASCAT functions
- segBAF = 1-matched.segmented.BAF.data[,5]
- segLogR = segmented.logR.data[,3]
- logR = logR.data[,3]
- names(segBAF) = rownames(matched.segmented.BAF.data)
- names(segLogR) = rownames(matched.segmented.BAF.data)
- names(logR) = rownames(matched.segmented.BAF.data)
-
- chr.segs = NULL
- for(ch in 1:length(chr.names)){
- chr.segs[[ch]] = which(logR.data[,1]==chr.names[ch])
- }
-
- if(use_preset_rho_psi){
- ascat_optimum_pair = list(rho=preset_rho, psi = preset_psi, ploidy = preset_psi)
- }else{
- distance.outfile=paste(outputfile.prefix, "distance.png", sep="", collapse="") # kjd 20-2-2014
- copynumberprofile.outfile=paste(outputfile.prefix, "copynumberprofile.png", sep="", collapse="") # kjd 20-2-2014
- nonroundedprofile.outfile=paste(outputfile.prefix, "nonroundedprofile.png", sep="", collapse="") # kjd 20-2-2014
- cnaStatusFile = paste(outputfile.prefix, "copynumber_solution_status.txt", sep="", collapse="")
-
- if(enhanced_grid_search) {
- ascat_optimum_pair = runASCAT_enhanced(logR, 1-BAF.data[,3], segLogR, segBAF, chr.segs, ascat_dist_choice,distance.outfile, copynumberprofile.outfile, nonroundedprofile.outfile, cnaStatusFile=cnaStatusFile, gamma=gamma_param, allow100percent=T, reliabilityFile=NA, min.ploidy=min.ploidy, max.ploidy=max.ploidy, min.rho=min.rho, max.rho=max.rho, min.goodness, chr.names=chr.names, analysis=analysis, uninformative_BAF_threshold=uninformative_BAF_threshold, verbose=TRUE)
- } else {
- ascat_optimum_pair = runASCAT(logR, 1-BAF.data[,3], segLogR, segBAF, chr.segs, ascat_dist_choice,distance.outfile, copynumberprofile.outfile, nonroundedprofile.outfile, cnaStatusFile=cnaStatusFile, gamma=gamma_param, allow100percent=T, reliabilityFile=NA, min.ploidy=min.ploidy, max.ploidy=max.ploidy, min.rho=min.rho, max.rho=max.rho, min.goodness, chr.names=chr.names, analysis=analysis) # kjd 4-2-2014
- }
- }
-
- distance.outfile=paste(outputfile.prefix,"second_distance.png",sep="",collapse="") # kjd 20-2-2014
- copynumberprofile.outfile=paste(outputfile.prefix,"second_copynumberprofile.png",sep="",collapse="") # kjd 20-2-2014
- nonroundedprofile.outfile=paste(outputfile.prefix,"second_nonroundedprofile.png",sep="",collapse="") # kjd 20-2-2014
-
- # All is set up, now run ASCAT to obtain a clonal copynumber profile
- out = run_clonal_ASCAT( logR, 1-BAF.data[,3], segLogR, segBAF, chr.segs, matched.segmented.BAF.data, ascat_optimum_pair, dist_choice, distance.outfile, copynumberprofile.outfile, nonroundedprofile.outfile, gamma_param=gamma_param, read_depth, uninformative_BAF_threshold, allow100percent=T, reliabilityFile=NA, psi_min_initial=min.ploidy, psi_max_initial=max.ploidy, rho_min_initial=min.rho, rho_max_initial=max.rho, chr.names=chr.names) # kjd 21-2-2014
-
- ascat_optimum_pair_fraction_of_genome = out$output_optimum_pair_without_ref
- ascat_optimum_pair_ref_seg = out$output_optimum_pair
- is.ref.better = out$is.ref.better
-
- # Save rho, psi and ploidy for future reference
- rho_psi_output = data.frame(rho = c(ascat_optimum_pair$rho,ascat_optimum_pair_fraction_of_genome$rho,ascat_optimum_pair_ref_seg$rho),psi = c(ascat_optimum_pair$psi,ascat_optimum_pair_fraction_of_genome$psi,ascat_optimum_pair_ref_seg$psi), ploidy = c(ascat_optimum_pair$ploidy,ascat_optimum_pair_fraction_of_genome$ploidy,ascat_optimum_pair_ref_seg$ploidy), distance = c(NA,out$distance_without_ref,out$distance), is.best = c(NA,!is.ref.better,is.ref.better),row.names=c("ASCAT","FRAC_GENOME","REF_SEG"))
- write.table(rho_psi_output,paste(outputfile.prefix,"rho_and_psi.txt",sep=""),quote=F,sep="\t")
-}
-
-#' Fit subclonal copy number
-#'
-#' This function fits a subclonal copy number profile where a clonal profile is unlikely.
-#' It goes over each segment of a clonal copy number profile and does a simple t-test. If the
-#' test is significant it is unlikely that the data can be explained by a single copy number
-#' state. We therefore fit a second state, i.e. there are two cellular populations with each
-#' a different state: Subclonal copy number.
-#' @param sample.name Name of the sample, used in figures
-#' @param baf.segmented.file String that points to a file with segmented BAF output
-#' @param logr.file String that points to the raw LogR file to be used in the subclonal copy number figures
-#' @param rho.psi.file String pointing to the rho_and_psi file generated by \code{fit.copy.number}
-#' @param output.file Filename of the file where the final copy number fit will be written to
-#' @param output.figures.prefix Prefix of the filenames for the chromosome specific copy number figures
-#' @param output.gw.figures.prefix Prefix of the filenames for the genome wide copy number figures
-#' @param chr_names Vector of allowed chromosome names
-#' @param masking_output_file Filename of where the masking details need to be written. Masking is performed to remove very high copy number state segments
-#' @param max_allowed_state The maximum CN state allowed (Default 250)
-#' @param cn_upper_limit The maximum CN that can be called (Default 1000)
-#' @param prior_breakpoints_file A two column file with prior breakpoints, possibly from structural variants. This file must contain two columns: chromosome and position. These are used when making the figures
-#' @param gamma Technology specific scaling parameter for LogR (Default 1)
-#' @param segmentation.gamma Legacy parameter that is no longer used (Default NA)
-#' @param siglevel Threshold under which a p-value becomes significant. When it is significant a second copy number state will be fitted (Default 0.05)
-#' @param maxdist Slack in BAF space to allow a segment to be off it's optimum before becoming significant. A segment becomes significant very quickly when a breakpoint is missed, this parameter alleviates the effect (Default 0.01)
-#' @param noperms The number of permutations to be run when bootstrapping the confidence intervals on the copy number state of each segment (Default 1000)
-#' @param seed Seed to set when performing bootstrapping (Default: Current time)
-#' @param calc_seg_baf_option Various options to recalculate the BAF of a segment. Options are: 1 - median, 2 - mean, 3 - ifelse median==0|1, mean, median. (Default: 3)
-#' @author dw9, sd11
-#' @export
-
-callSubclones = function(sample.name, baf.segmented.file, logr.file, rho.psi.file, output.file, output.figures.prefix, output.gw.figures.prefix, chr_names, masking_output_file, max_allowed_state=250, cn_upper_limit=1000, prior_breakpoints_file=NULL, gamma=1, segmentation.gamma=NA, siglevel=0.05, maxdist=0.01, noperms=1000, seed=as.integer(Sys.time()), calc_seg_baf_option=3) {
-
- set.seed(seed)
- # Load rho/psi/goodness of fit
- res = load.rho.psi.file(rho.psi.file)
- rho = res$rho
- psit = res$psit
- psi = rho*psit + 2 * (1-rho) # psi of all cells
- goodness = res$goodness
-
- # Load the BAF segmented data
- BAFvals = as.data.frame(read_bafsegmented(baf.segmented.file))
- if (colnames(BAFvals)[1] == "X") {
- # If there were rownames, then delete this column. Should not be an issue with new BB runs
- BAFvals = BAFvals[,-1]
- }
-
- BAF = BAFvals[,3]
- BAFphased = BAFvals[,4]
- BAFseg = BAFvals[,5]
-
- # Save SNP positions separately
- SNPpos = BAFvals[,c(1,2)]
-
- # Load the raw LogR data
- LogRvals = as.data.frame(read_logr(logr.file))
- if (colnames(LogRvals)[1] == "X") {
- # If there were rownames, then delete this column. Should not be an issue with new BB runs
- LogRvals = LogRvals[,-1]
- }
-
- # Chromosome names are sometimes 'chr1', etc.
- #if(length(grep("chr",LogRvals[1,1]))>0){
- # LogRvals[,1] = gsub("chr","",LogRvals[,1])
- #}
-
- ctrans = c(1:length(chr_names))
- names(ctrans) = chr_names
- ctrans.logR = c(1:length(chr_names))
- names(ctrans.logR) = chr_names
-
- # = as.vector(ctrans.logR[as.vector(LogRvals[,1])]*1000000000+LogRvals[,2])
- BAFpos = as.vector(ctrans[as.vector(BAFvals[,1])]*1000000000+BAFvals[,2])
-
- ################################################################################################
- # Determine copy number for each segment
- ################################################################################################
- res = determine_copynumber(BAFvals, LogRvals, rho, psi, gamma, ctrans, ctrans.logR, maxdist, siglevel, noperms, cn_upper_limit)
- subcloneres = res$subcloneres
- #write.table(subcloneres, gsub(".txt", "_1.txt", output.file), quote=F, col.names=T, row.names=F, sep="\t")
- write.table(subcloneres, paste0(tools::file_path_sans_ext(output.file),"_1.",tools::file_ext(output.file),sep=""), quote=F, col.names=T, row.names=F, sep="\t")
- # Scan the segments for cases that should be merged
- res = merge_segments(subcloneres, BAFvals, LogRvals, rho, psi, gamma, calc_seg_baf_option)
- BAFvals = res$bafsegmented
-
- res = determine_copynumber(BAFvals, LogRvals, rho, psi, gamma, ctrans, ctrans.logR, maxdist, siglevel, noperms, cn_upper_limit)
- subcloneres = res$subcloneres
- BAFpvals = res$BAFpvals
-
- # Scan for very high copy number segments and set those to NA - This is in part an artifact of small segments
- res = mask_high_cn_segments(subcloneres, BAFvals, max_allowed_state)
- subcloneres = res$subclones
- # No longer writing out the BAFsegmented data after masking
- #BAFvals = res$bafsegmented
- #write.table(BAFvals, file=baf.segmented.file, sep="\t", row.names=F, col.names=T, quote=F)
- # Write the masking details to file
- masking_details = data.frame(samplename=sample.name, masked_count=res$masked_count, masked_size=res$masked_size, max_allowed_state=max_allowed_state)
- write.table(masking_details, file=masking_output_file, quote=F, col.names=T, row.names=F, sep="\t")
-
- # Write the final copy number profile
- # NAP: generating two output files: first reporting solution A and the second reporting alternative solutions (B to F)
- write.table(subcloneres[,c(1:3,8:13)], output.file, quote=F, col.names=T, row.names=F, sep="\t")
-
- #write.table(subcloneres, gsub(".txt","_extended.txt",output.file), quote=F, col.names=T, row.names=F, sep="\t")
- write.table(subcloneres, paste0(tools::file_path_sans_ext(output.file),"_extended.",tools::file_ext(output.file),sep=""), quote=F, col.names=T, row.names=F, sep="\t")
-
- # NAP - November 2023
- # Recalculate PGA.is.clonal to match the final copy number profile in copynumber.txt file (previously subclones.txt file)
- subcloneres$length = subcloneres$endpos-subcloneres$startpos
- subcloneres_subclonal = subcloneres[which(subcloneres$frac1_A<1),]
- diploid = which(subcloneres$nMaj1_A==1 & subcloneres$nMin1_A==1 & subcloneres$frac1_A==1)
- # NAP - June 2025
- # Check 'diploid' length for rare edge cases
- if (length(diploid) > 0) {
- cna = subcloneres[-diploid,]
- } else {
- cna = subcloneres
- print("No diploid region found in copy number profile - likely due to WGD or error in fitting copy number in rare cases")
- }
-
- if(nrow(cna) == 0 || sum(cna$length) == 0) {
- # No copy number alterations found
- goodness <- 1.0 # 100% clonal (no CNAs to be subclonal)
- print("No copy number alterations detected - setting PGA.is.clonal to 100%\n")
- } else if(nrow(subcloneres_subclonal) == 0) {
- # No subclonal segments
- goodness <- 1.0 # 100% clonal
- print("No subclonal segments detected - setting PGA.is.clonal to 100%\n")
- } else {
- subclonal_fraction <- sum(subcloneres_subclonal$length) / sum(cna$length)
- goodness <- 1 - subclonal_fraction
-
- # Ensure goodness is within valid range [0,1]
- goodness <- max(0, min(1, goodness))
- }
- print(paste0("PGA.is.clonal = ",sprintf("%2.1f",goodness*100),"%"))
-
- ################################################################################################
- # Make a plot per chromosome
- ################################################################################################
- # Collapse the BAFsegmented into breakpoints to be used in plotting
- segment_breakpoints = collapse_bafsegmented_to_segments(BAFvals)
- if (!is.null(prior_breakpoints_file) & !ifelse(is.null(prior_breakpoints_file), TRUE, prior_breakpoints_file=="NA") & !ifelse(is.null(prior_breakpoints_file), TRUE, is.na(prior_breakpoints_file))) {
- svs = read.table(prior_breakpoints_file, header=T, stringsAsFactors=F)
- }
-
- # Create a plot per chromosome that shows the segments with their CN state in text
- for (chr in chr_names) {
- pos = SNPpos[SNPpos[,1]==chr, 2]
- #if no points to plot, skip
- if (length(pos)==0) { next }
-
- if (!is.null(prior_breakpoints_file) & !ifelse(is.null(prior_breakpoints_file), TRUE, prior_breakpoints_file=="NA") & !ifelse(is.null(prior_breakpoints_file), TRUE, is.na(prior_breakpoints_file))) {
- svs_pos = svs[svs$chromosome==chr,]$position / 1000000
- } else {
- svs_pos = NULL
- }
-
- breakpoints_pos = segment_breakpoints[segment_breakpoints$chromosome==chr,]
- breakpoints_pos = sort(unique(c(breakpoints_pos$start, breakpoints_pos$end) / 1000000))
-
- png(filename = paste(output.figures.prefix, chr,".png",sep=""), width = 2000, height = 2000, res = 200, type = "cairo")
- create.subclonal.cn.plot(chrom=chr,
- chrom.position=pos/1000000,
- LogRposke=LogRvals[LogRvals[,1]==chr,2],
- LogRchr=LogRvals[LogRvals[,1]==chr,3],
- BAFchr=BAF[SNPpos[,1]==chr],
- BAFsegchr=BAFseg[SNPpos[,1]==chr],
- BAFpvalschr=BAFpvals[SNPpos[,1]==chr],
- subcloneres=subcloneres,
- breakpoints_pos=breakpoints_pos,
- svs_pos=svs_pos,
- siglevel=siglevel,
- x.min=min(pos)/1000000,
- x.max=max(pos)/1000000,
- title=paste(sample.name,", chromosome ", chr, sep=""),
- xlab="Position (Mb)",
- ylab.logr="LogR",
- ylab.baf="BAF (phased)")
- dev.off()
- }
-
- # Cast columns back to numeric
- subclones = as.data.frame(subcloneres)
- subclones[,2:ncol(subclones)] = sapply(2:ncol(subclones), function(x) { as.numeric(as.character(subclones[,x])) })
-
- # Recalculate the ploidy based on the actual fit
- seg_length = floor((subclones$endpos-subclones$startpos)/1000)
- is_subclonal_maj = abs(subclones$nMaj1_A - subclones$nMaj2_A) > 0
- is_subclonal_min = abs(subclones$nMin1_A - subclones$nMin2_A) > 0
- is_subclonal_maj[is.na(is_subclonal_maj)] = F
- is_subclonal_min[is.na(is_subclonal_min)] = F
- segment_states_min = subclones$nMin1_A * ifelse(is_subclonal_min, subclones$frac1_A, 1) + ifelse(is_subclonal_min, subclones$nMin2_A, 0) * ifelse(is_subclonal_min, subclones$frac2_A, 0)
- segment_states_maj = subclones$nMaj1_A * ifelse(is_subclonal_maj, subclones$frac1_A, 1) + ifelse(is_subclonal_maj, subclones$nMaj2_A, 0) * ifelse(is_subclonal_maj, subclones$frac2_A, 0)
- ploidy = sum((segment_states_min+segment_states_maj) * seg_length, na.rm=T) / sum(seg_length, na.rm=T)
-
- # Plot genome wide figures
- plot.gw.subclonal.cn(subclones=subclones, BAFvals=BAFvals, rho=rho, ploidy=ploidy, goodness=goodness, output.gw.figures.prefix=output.gw.figures.prefix, chr.names=chr_names, tumourname=sample.name)
-
- # Create user friendly cellularity and ploidy output file
- cellularity_ploidy_output = data.frame(purity = c(rho), ploidy = c(ploidy), psi = c(psit))
-
- # cellularity_file = gsub("_.+\\.txt$", "_purity_ploidy.txt", output.file) # NAP: updated the name of the output file, consistent with new title (and added flexibility with what output.file is named)
- cellularity_file = paste0(sample.name,"_purity_ploidy.txt")
-
- write.table(cellularity_ploidy_output, cellularity_file, quote=F, sep="\t", row.names=F)
-}
-
-
-#' Given all the determined values make a copy number call for each segment
-#'
-#' @param BAFvals BAFsegmented data.frame with 5 columns
-#' @param LogRvals Raw logR values in data.frame with 3 columns
-#' @param rho Optimal rho value, the choosen cellularity
-#' @param psi Optimal psi value, the choosen ploidy
-#' @param gamma Platform gamma parameter
-#' @param ctrans Named vector of chromosome names
-#' @param ctrans.logR Named vector of chromosome names
-#' @param maxdist Max distance a segment is tolerated to be not considered for subclonal copy number
-#' @param siglevel Level at which a segment can become significantly different from the nearest clonal state
-#' @param noperms Number of bootstrap permutations
-#' @param cn_upper_limit Maximum number of CN that can be called
-#' @return A data.frame with copy number determined for each segment
-#' @author dw9
-#' @noRd
-determine_copynumber = function(BAFvals, LogRvals, rho, psi, gamma, ctrans, ctrans.logR, maxdist, siglevel, noperms, cn_upper_limit) {
- BAFphased = BAFvals[,4]
- BAFseg = BAFvals[,5]
- BAFpos = as.vector(ctrans[as.vector(BAFvals[,1])]*1000000000+BAFvals[,2])
- LogRpos = as.vector(ctrans.logR[as.vector(LogRvals[,1])]*1000000000+LogRvals[,2])
-
- #DCW 240314
- switchpoints = c(0,which(BAFseg[-1] != BAFseg[-(length(BAFseg))] | BAFvals[-1,1] != BAFvals[-nrow(BAFvals),1]),length(BAFseg))
- BAFlevels = BAFseg[switchpoints[-1]]
-
- pval = NULL
- BAFpvals = vector(length=length(BAFseg))
- subcloneres = NULL
-
- for (i in 1:length(BAFlevels)) {
- # subcloneres = rbind(subcloneres, fit_segment(BAFpos, LogRpos, BAFlevels, BAFphased, LogRvals, switchpoints, rho, psi, gamma, i))
- l = BAFlevels[i]
-
- # Make sure that BAF>=0.5, otherwise nMajor and nMinor may be the wrong way around
- l = max(l,1-l)
-
- BAFke = BAFphased[(switchpoints[i]+1):switchpoints[i+1]]
-
- #startpos = min(BAFpos[names(BAFke)])
- #endpos = max(BAFpos[names(BAFke)])
- startpos = min(BAFpos[(switchpoints[i]+1):switchpoints[i+1]])
- endpos = max(BAFpos[(switchpoints[i]+1):switchpoints[i+1]])
- #chrom = names(ctrans[floor(startpos/1000000000)])
- # Assuming all SNPs in this segment are on the same chromosome
- chrom = BAFvals[(switchpoints[i]+1):switchpoints[i+1],]$Chromosome[1]
- LogR = mean(LogRvals[LogRpos>=startpos&LogRpos<=endpos & !is.infinite(LogRvals[,3]),3],na.rm=T)
-
- # if we don't have a value for LogR, fill in 0
- if (is.na(LogR)) {
- LogR = 0
- }
- nMajor = (rho-1+l*psi*2^(LogR/gamma))/rho
- nMinor = (rho-1+(1-l)*psi*2^(LogR/gamma))/rho
-
- # Occasionally nMinor can be NA due to zero coverage, skip when this occurs
- if (is.na(nMinor)) {
- next
- }
-
- # Increase nMajor and nMinor together, to avoid impossible combinations (with negative subclonal fractions)
- if (nMinor<0) {
- if (l==1) {
- # Avoid calling infinite copy number
- nMajor = cn_upper_limit
- } else {
- nMajor = nMajor + l * (0.01 - nMinor) / (1-l)
- }
- nMinor = 0.01
- }
-
- # Note that these are sorted in the order of ascending BAF:
- nMaj = c(floor(nMajor), ceiling(nMajor), floor(nMajor), ceiling(nMajor))
- nMin = c(ceiling(nMinor), ceiling(nMinor), floor(nMinor), floor(nMinor))
- x = floor(nMinor)
- y = floor(nMajor)
-
- # Total copy number, to determine priority options
- ntot = nMajor + nMinor
-
- levels = (1-rho+rho*nMaj)/(2-2*rho+rho*(nMaj+nMin))
- # Problem if rho=1 and nMaj=0 and nMin=0
- levels[nMaj==0 & nMin==0] = 0.5
-
- #DCW - just test corners on the nearest edge to determine clonality
- # If the segment is called as subclonal, this is the edge that will be used to determine the subclonal proportions that are reported first
- all.edges = orderEdges(levels, l, ntot,x,y)
- nMaj.test = all.edges[1,c(1,3)]
- nMin.test = all.edges[1,c(2,4)]
- test.levels = (1-rho+rho*nMaj.test)/(2-2*rho+rho*(nMaj.test+nMin.test))
- whichclosestlevel.test = which.min(abs(test.levels-l))
-
- # Test whether a segment should be subclonal
- if (is.na(sd(BAFke)) || sd(BAFke)==0) {
- pval[i] = 0 # problem caused by segments with constant BAF (usually 1 or 2)
- } else {
- pval[i] = t.test(BAFke, alternative="two.sided", mu=test.levels[whichclosestlevel.test])$p.value
- }
- if (abs(l-test.levels[whichclosestlevel.test])0) {
- all.edges = rbind(all.edges[-na.indices,], all.edges[na.indices,])
- }
- nMaj1 = all.edges[,1]
- nMin1 = all.edges[,2]
- nMaj2 = all.edges[,3]
- nMin2 = all.edges[,4]
-
- tau = (1 - rho + rho * nMaj2 - 2 * l * (1 - rho) - l * rho * (nMin2 + nMaj2)) / (l * rho * (nMin1 + nMaj1) - l * rho * (nMin2 + nMaj2) - rho * nMaj1 + rho * nMaj2)
- sdl = sd(BAFke,na.rm=T)/sqrt(sum(!is.na(BAFke)))
- sdtau = abs((1 - rho + rho * nMaj2 - 2 * (l+sdl) * (1 - rho) - (l+sdl) * rho * (nMin2 + nMaj2)) / ((l+sdl) * rho * (nMin1 + nMaj1) - (l+sdl) * rho * (nMin2 + nMaj2) - rho * nMaj1 + rho * nMaj2) - tau) / 2 +
- abs((1 - rho + rho * nMaj2 - 2 * (l-sdl) * (1 - rho) - (l-sdl) * rho * (nMin2 + nMaj2)) / ((l-sdl) * rho * (nMin1 + nMaj1) - (l-sdl) * rho * (nMin2 + nMaj2) - rho * nMaj1 + rho * nMaj2) - tau) / 2
-
- # Bootstrapping to obtain 95% confidence intervals
- sdtaubootstrap = vector(length=length(tau), mode="numeric")
- tau25 = vector(length=length(tau), mode="numeric")
- tau975 = vector(length=length(tau), mode="numeric")
-
- for (option in 1:length(tau)) {
- nMaj1o = nMaj1[option]
- nMin1o = nMin1[option]
- nMaj2o = nMaj2[option]
- nMin2o = nMin2[option]
-
- permFraction = vector(length=noperms,mode="numeric")
- for (j in 1:noperms) {
- permBAFs=sample(BAFke,length(BAFke),replace=T)
- permMeanBAF=mean(permBAFs)
- permFraction[j] = (1 - rho + rho * nMaj2o - 2 * permMeanBAF * (1 - rho) - permMeanBAF * rho * (nMin2o + nMaj2o)) / (permMeanBAF * rho * (nMin1o + nMaj1o) - permMeanBAF * rho * (nMin2o + nMaj2o) - rho * nMaj1o + rho * nMaj2o)
- }
- orderedFractions = sort(permFraction)
- sdtaubootstrap[option] = sd(permFraction)
- tau25[option] = orderedFractions[25]
- tau975[option] = orderedFractions[975]
- }
-
- subcloneres = rbind(subcloneres, c(chrom,startpos-floor(startpos/1000000000)*1000000000,
- endpos-floor(endpos/1000000000)*1000000000,l,pval[i],LogR,ntot,
- nMaj1[1],nMin1[1],tau[1],nMaj2[1],nMin2[1],1-tau[1],sdtau[1],sdtaubootstrap[1],tau25[1],tau975[1],
- nMaj1[2],nMin1[2],tau[2],nMaj2[2],nMin2[2],1-tau[2],sdtau[2],sdtaubootstrap[2],tau25[2],tau975[2],
- nMaj1[3],nMin1[3],tau[3],nMaj2[3],nMin2[3],1-tau[3],sdtau[3],sdtaubootstrap[3],tau25[3],tau975[3],
- nMaj1[4],nMin1[4],tau[4],nMaj2[4],nMin2[4],1-tau[4],sdtau[4],sdtaubootstrap[4],tau25[4],tau975[4],
- nMaj1[5],nMin1[5],tau[5],nMaj2[5],nMin2[5],1-tau[5],sdtau[5],sdtaubootstrap[5],tau25[5],tau975[5],
- nMaj1[6],nMin1[6],tau[6],nMaj2[6],nMin2[6],1-tau[6],sdtau[6],sdtaubootstrap[6],tau25[6],tau975[6]))
- }else {
- #if called as clonal, use the best corner from the nearest edge
- subcloneres = rbind(subcloneres, c(chrom,startpos-floor(startpos/1000000000)*1000000000,
- endpos-floor(endpos/1000000000)*1000000000,l,pval[i],LogR,ntot,
- nMaj.test[whichclosestlevel.test],nMin.test[whichclosestlevel.test],1,rep(NA,57)))
-
- }
- }
- colnames(subcloneres) = c("chr","startpos","endpos","BAF","pval","LogR","ntot",
- "nMaj1_A","nMin1_A","frac1_A","nMaj2_A","nMin2_A","frac2_A","SDfrac_A","SDfrac_A_BS","frac1_A_0.025","frac1_A_0.975",
- "nMaj1_B","nMin1_B","frac1_B","nMaj2_B","nMin2_B","frac2_B","SDfrac_B","SDfrac_B_BS","frac1_B_0.025","frac1_B_0.975",
- "nMaj1_C","nMin1_C","frac1_C","nMaj2_C","nMin2_C","frac2_C","SDfrac_C","SDfrac_C_BS","frac1_C_0.025","frac1_C_0.975",
- "nMaj1_D","nMin1_D","frac1_D","nMaj2_D","nMin2_D","frac2_D","SDfrac_D","SDfrac_D_BS","frac1_D_0.025","frac1_D_0.975",
- "nMaj1_E","nMin1_E","frac1_E","nMaj2_E","nMin2_E","frac2_E","SDfrac_E","SDfrac_E_BS","frac1_E_0.025","frac1_E_0.975",
- "nMaj1_F","nMin1_F","frac1_F","nMaj2_F","nMin2_F","frac2_F","SDfrac_F","SDfrac_F_BS","frac1_F_0.025","frac1_F_0.975")
- subcloneres = as.data.frame(subcloneres)
- for (i in 2:ncol(subcloneres)) {
- subcloneres[,i] = as.numeric(as.character(subcloneres[,i]))
- }
- return(list(subcloneres=subcloneres, BAFpvals=BAFpvals))
-}
-
-
-#' Merge copy number segments
-#'
-#' Merges segments if there is not enough evidence for them to be separate. Two adjacent segments are merged
-#' when they are either fit with the same clonal copy number state or when their BAF is not significantly different
-#' and their logR puts them in the same square.
-#' @param subclones A completely fit copy number profile in Battenberg output format
-#' @param bafsegmented A BAFsegmented data.frame with the 5 columns that corresponds to the subclones file
-#' @param logR The raw logR data
-#' @param rho The rho estimate that the profile was fit with
-#' @param psi the psi estimate that the profile was fit with
-#' @param platform_gamma The gamma parameter for this platform
-#' @param calc_seg_baf_option Various options to recalculate the BAF of a segment. Options are: 1 - median, 2 - mean, 3 - ifelse median== 0|1, mean, median. (Default: 3)
-#' @param verbose A boolean to show merging operations (Default: FALSE)
-#' @return A list with two fields: bafsegmented and subclones. The subclones field contains a data.frame in
-#' Battenberg output format with the merged segments. The bafsegmented field contains the BAFsegmented data
-#' corresponding to the provided subclones data.frame.
-#' @author sd11, tl
-#' @noRd
-merge_segments=function(subclones, bafsegmented, logR, rho, psi, platform_gamma, calc_seg_baf_option=3, verbose=F) {
- calc_nmin = function(rho, psi, baf, logr, platform_gamma) {
- return((rho-1-(baf-1)*2^(logr/platform_gamma)*((1-rho)*2+rho*psi))/rho)
- }
- calc_nmaj = function(rho, psi, baf, logr, platform_gamma) {
- return((rho-1+baf*2^(logr/platform_gamma)*((1-rho)*2+rho*psi))/rho)
- }
- # Convert DF into GRanges objects
- df2gr=function(DF,chr,pos1,pos2) {
- return(GenomicRanges::makeGRangesFromDataFrame(df=DF,
- keep.extra.columns=T,
- ignore.strand=T,
- seqinfo=NULL,
- seqnames.field=chr,
- start.field=pos1,
- end.field=pos2,
- starts.in.df.are.0based=F))
- }
- # Function called when two segments have not been merged so there is no need to recheck those again
- updateNeighbour=function(subclones,INDEX,INDEX_N) {
- if (INDEX_N>INDEX) {
- subclones$Next_checked[INDEX]=T
- subclones$Prev_checked[INDEX_N]=T
- } else {
- subclones$Prev_checked[INDEX]=T
- subclones$Next_checked[INDEX_N]=T
- }
- return(subclones)
- }
- # Function called when two segments have been merged so we need to recheck its two neighbours
- updateAround=function(subclones,INDEX) {
- if (INDEX>1) {
- subclones$Prev_checked[INDEX]=F
- subclones$Next_checked[INDEX-1]=F
- } else {
- subclones$Prev_checked[INDEX]=T
- }
- if (INDEXINDEX) {
- # Largest segment (INDEX_N) is after smallest one (INDEX)
- stopifnot(subclones$Next_checked[INDEX]==subclones$Prev_checked[INDEX_N])
- if (subclones$Next_checked[INDEX] && subclones$Prev_checked[INDEX_N]) {
- return(T)
- } else {
- return(F)
- }
- } else {
- # Largest segment (INDEX_N) is before smallest one (INDEX)
- stopifnot(subclones$Prev_checked[INDEX]==subclones$Next_checked[INDEX_N])
- if (subclones$Prev_checked[INDEX] && subclones$Next_checked[INDEX_N]) {
- return(T)
- } else {
- return(F)
- }
- }
- }
- # Function to merge two segments
- merge_seg=function(subclones,bafsegmented,logR,INDEX,INDEX_N,calc_seg_baf_option) {
- # Update start/end information
- if (INDEX_N0) && all(sapply(bafsegmented,length)>0) && all(sapply(logR,length)>0))
- names(subclones)=chr_names
- names(bafsegmented)=chr_names
- names(logR)=chr_names
- # For each chromosome
- for (CHR in chr_names) {
- if (verbose) print(paste0('Merging segments within: ',CHR))
- # Define ID, Prev_checked and Next_checked to help processing data
- subclones[[CHR]]$ID=1:length(subclones[[CHR]])
- subclones[[CHR]]$Prev_checked=F
- subclones[[CHR]]$Next_checked=F
- subclones[[CHR]]$Prev_checked[1]=T
- subclones[[CHR]]$Next_checked[length(subclones[[CHR]])]=T
- # Pick all possible IDs
- IDs=subclones[[CHR]]$ID
- while (length(IDs)!=0) {
- # Amongst all IDs, select the ones that must be checked
- IDs=subclones[[CHR]]$ID[which(!subclones[[CHR]]$Prev_checked | !subclones[[CHR]]$Next_checked)]
- if (length(IDs)==0) break
- # Amongst all of those, select the smallest one
- INDEX=IDs[which.min(GenomicRanges::width(subclones[[CHR]][which(subclones[[CHR]]$ID %in% IDs)]))]
- # Select neighbours (two or one if segments is first or last)
- if (INDEX==1) {
- Neighbours=order(GenomicRanges::distance(subclones[[CHR]][INDEX],subclones[[CHR]][INDEX+1]))
- names(Neighbours)=INDEX+1
- } else if (INDEX==length(subclones[[CHR]])) {
- Neighbours=order(GenomicRanges::distance(subclones[[CHR]][INDEX],subclones[[CHR]][INDEX-1]))
- names(Neighbours)=INDEX-1
- } else {
- Neighbours=order(GenomicRanges::distance(subclones[[CHR]][INDEX],subclones[[CHR]][INDEX+c(-1,1)]))
- names(Neighbours)=INDEX+c(-1,1)
- }
- if (verbose) print(paste0('Working on segment: ',INDEX,' (',subclones[[CHR]][INDEX],')'))
- # For each neighbour
- for (i in Neighbours) {
- INDEX_N=as.numeric(names(Neighbours[i]))
- if (verbose) print(paste0('Checking neighbour: ',INDEX_N,' (',subclones[[CHR]][INDEX_N],'; distance=',GenomicRanges::distance(subclones[[CHR]][INDEX],subclones[[CHR]][INDEX_N]),')'))
- # Test whether seg and neighbour (INDEX and INDEX_N) have already been checked
- if (checkStatus(subclones[[CHR]],INDEX,INDEX_N)) {if (verbose) {print('Already checked')}; next}
- # Test whether seg and neighbour are far away from each other
- if (GenomicRanges::distance(subclones[[CHR]][INDEX],subclones[[CHR]][INDEX_N])>3e6) {
- if (verbose) print('Distance > 3Mb - do not merge')
- subclones[[CHR]]=updateNeighbour(subclones[[CHR]],INDEX,INDEX_N)
- } else {
- # Test whether seg and neighbour have the same clonal CN solution
- if (subclones[[CHR]]$nMaj1_A[INDEX]==subclones[[CHR]]$nMaj1_A[INDEX_N] && subclones[[CHR]]$nMin1_A[INDEX]==subclones[[CHR]]$nMin1_A[INDEX_N] && subclones[[CHR]]$frac1_A[INDEX]==1 && subclones[[CHR]]$frac1_A[INDEX_N]==1) {
- if (verbose) print('Same clonal CN solution - merge')
- res=merge_seg(subclones[[CHR]],bafsegmented[[CHR]],logR[[CHR]],INDEX,INDEX_N,calc_seg_baf_option)
- subclones[[CHR]]=res$subclones
- bafsegmented[[CHR]]=res$bafsegmented
- rm(res)
- break
- } else {
- # Test whether seg and neighbour have different BAF/logR distributions
- if (verbose) print('Different CN solutions: check BAF and logR')
- nmin_curr = round(calc_nmin(rho, psi, subclones[[CHR]]$BAF[INDEX], subclones[[CHR]]$LogR[INDEX], platform_gamma))
- nmaj_curr = round(calc_nmaj(rho, psi, subclones[[CHR]]$BAF[INDEX], subclones[[CHR]]$LogR[INDEX], platform_gamma))
- nmin_other = round(calc_nmin(rho, psi, subclones[[CHR]]$BAF[INDEX_N], subclones[[CHR]]$LogR[INDEX_N], platform_gamma))
- nmaj_other = round(calc_nmaj(rho, psi, subclones[[CHR]]$BAF[INDEX_N], subclones[[CHR]]$LogR[INDEX_N], platform_gamma))
- if (nmin_curr==nmin_other || nmaj_curr==nmaj_other) {
- # Test whether there are more than 10 values to check significance
- if (sum(!is.na(logR[[CHR]]$logR[GenomicRanges::findOverlaps(subclones[[CHR]][INDEX],logR[[CHR]])@to])) > 10 &&
- sum(!is.na(logR[[CHR]]$logR[GenomicRanges::findOverlaps(subclones[[CHR]][INDEX_N],logR[[CHR]])@to])) > 10 &&
- sum(!is.na(bafsegmented[[CHR]]$BAFphased[GenomicRanges::findOverlaps(subclones[[CHR]][INDEX],bafsegmented[[CHR]])@to])) > 10 &&
- sum(!is.na(bafsegmented[[CHR]]$BAFphased[GenomicRanges::findOverlaps(subclones[[CHR]][INDEX_N],bafsegmented[[CHR]])@to])) > 10) {
- logr_significant = t.test(logR[[CHR]]$logR[GenomicRanges::findOverlaps(subclones[[CHR]][INDEX],logR[[CHR]])@to],
- logR[[CHR]]$logR[GenomicRanges::findOverlaps(subclones[[CHR]][INDEX_N],logR[[CHR]])@to])$p.value < 0.05
- baf_significant = t.test(bafsegmented[[CHR]]$BAFphased[GenomicRanges::findOverlaps(subclones[[CHR]][INDEX],bafsegmented[[CHR]])@to],
- bafsegmented[[CHR]]$BAFphased[GenomicRanges::findOverlaps(subclones[[CHR]][INDEX_N],bafsegmented[[CHR]])@to])$p.value < 0.05
- if ((!logr_significant) && (!baf_significant)) {
- if (verbose) print('No significant difference - merge')
- res=merge_seg(subclones[[CHR]],bafsegmented[[CHR]],logR[[CHR]],INDEX,INDEX_N,calc_seg_baf_option)
- subclones[[CHR]]=res$subclones
- bafsegmented[[CHR]]=res$bafsegmented
- rm(res)
- break
- } else {
- if (verbose) print('Significant difference - do not merge')
- subclones[[CHR]]=updateNeighbour(subclones[[CHR]],INDEX,INDEX_N)
- }
- } else {
- if (verbose) print('Too few values - do not merge')
- subclones[[CHR]]=updateNeighbour(subclones[[CHR]],INDEX,INDEX_N)
- }
- } else {
- if (verbose) print('Different squares - do not merge')
- subclones[[CHR]]=updateNeighbour(subclones[[CHR]],INDEX,INDEX_N)
- }
- }
- }
- }; rm(i)
- }
- }; rm(CHR)
- if (verbose) print('Convert GRanges objects into DFs')
- bafsegmented=data.frame(Reduce(c,bafsegmented),stringsAsFactors=F)[,-c(3:5)]
- bafsegmented$seqnames=as.character(bafsegmented$seqnames)
- colnames(bafsegmented)[1:2]=c('Chromosome','Position')
- subclones=data.frame(Reduce(c,subclones),stringsAsFactors=F)[,-c(4:5)]
- subclones$seqnames=as.character(subclones$seqnames)
- colnames(subclones)[1:3]=c('chr','startpos','endpos')
- subclones$ID=NULL
- subclones$Prev_checked=NULL
- subclones$Next_checked=NULL
- return(list(bafsegmented=bafsegmented, subclones=subclones))
-}
-
-#' Mask segments that have a too high CN state
-#' @param subclones Subclones output data
-#' @param bafsegmented BAFsegmented data
-#' @param max_allowed_state The maximum state allowed before overruling takes place
-#' @return A list with the masked subclones, bafsegmented and the number of segments masked and their total genome size
-#' @author sd11
-mask_high_cn_segments = function(subclones, bafsegmented, max_allowed_state) {
- count = 0
- masked_size = 0
- for (i in 1:nrow(subclones)) {
- if (subclones$nMaj1_A[i] > max_allowed_state | subclones$nMin1_A[i] > max_allowed_state) {
- # Mask this segment
- subclones[i, "nMaj1_A"] = NA
- subclones[i, "nMin1_A"] = NA
- subclones[i, "nMaj2_A"] = NA
- subclones[i, "nMin2_A"] = NA
- # Mask the BAFsegmented
- bafsegmented[subclones$chr[i] == bafsegmented$Chromosome & subclones$startpos[i] < bafsegmented$Position & subclones$endpos[i] >= bafsegmented$Position,c("BAFseg")] = NA
- count = count+1
- masked_size = masked_size + (subclones$endpos[i]-subclones$startpos[i])
- }
- }
- return(list(subclones=subclones, bafsegmented=bafsegmented, masked_count=count, masked_size=masked_size))
-}
-
-
-#' Plot the copy number genome wide in two different ways. This creates the Battenberg average
-#' profile where subclonal copy number is represented as a mixture of two different states and
-#' the Battenberg subclones profile where subclonal copy number is plotted as two different
-#' separate states. The thickness of the line represents the fraction of tumour cells carying
-#' the particular state.
-#' @noRd
-plot.gw.subclonal.cn = function(subclones, BAFvals, rho, ploidy, goodness, output.gw.figures.prefix, chr.names, tumourname) {
- # Map start and end of each segment into the BAF values. The plot uses the index of this BAF table as x-axis
- pos_min = array(NA, nrow(subclones))
- pos_max = array(NA, nrow(subclones))
- for (i in 1:nrow(subclones)) {
- segm_chr = subclones$chr[i] == BAFvals$Chromosome & subclones$startpos[i] < BAFvals$Position & subclones$endpos[i] >= BAFvals$Position
- pos_min[i] = min(which(segm_chr))
- pos_max[i] = max(which(segm_chr))
- }
-
- # For those segments that are subclonal, Obtain the second state.
- is_subclonal = which(subclones$frac1_A < 1)
- subcl_min = array(NA, length(is_subclonal))
- subcl_max = array(NA, length(is_subclonal))
- for (i in 1:length(is_subclonal)) {
- segment_index = is_subclonal[i]
- segm_chr = subclones$chr[segment_index] == BAFvals$Chromosome & subclones$startpos[segment_index] < BAFvals$Position & subclones$endpos[segment_index] >= BAFvals$Position
- subcl_min[i] = min(which(segm_chr))
- subcl_max[i] = max(which(segm_chr))
- }
-
- # Determine whether it's the major or the minor allele that is represented by two states
- is_subclonal_maj = abs(subclones$nMaj1_A - subclones$nMaj2_A) > 0
- is_subclonal_min = abs(subclones$nMin1_A - subclones$nMin2_A) > 0
- is_subclonal_maj[is.na(is_subclonal_maj)] = F
- is_subclonal_min[is.na(is_subclonal_min)] = F
-
- # BB represents subclonal CN as a mixture of two CN states. Calculate this mixture for both minor allele and total CN.
- #segment_states_min = subclones$nMin1_A * ifelse(is_subclonal_min, subclones$frac1_A, 1) + ifelse(is_subclonal_min, subclones$nMin2_A, 0) * ifelse(is_subclonal_min, subclones$frac2_A, 0)
- #segment_states_tot = (subclones$nMaj1_A+subclones$nMin1_A) * ifelse(is_subclonal_maj, subclones$frac1_A, 1) + ifelse(is_subclonal_maj, subclones$nMaj2_A+subclones$nMin2_A, 0) * ifelse(is_subclonal_maj, subclones$frac2_A, 0)
-
- segment_states_min = subclones$nMin1_A * ifelse(is_subclonal_min, subclones$frac1_A, 1) + ifelse(is_subclonal_min, subclones$nMin2_A, 0) * ifelse(is_subclonal_min, subclones$frac2_A, 0)
- segment_states_maj = subclones$nMaj1_A * ifelse(is_subclonal_maj, subclones$frac1_A, 1) + ifelse(is_subclonal_maj, subclones$nMaj2_A, 0) * ifelse(is_subclonal_maj, subclones$frac2_A, 0)
- segment_states_tot = segment_states_maj + segment_states_min
-
- # Determine which SNPs are on which chromosome, to be used as a proxy for chromosome size in the plots
- chr.segs = lapply(1:length(chr.names), function(ch) { which(BAFvals$Chromosome==chr.names[ch]) })
-
- # Plot subclonal copy number as mixtures of two states
- png(filename = paste(output.gw.figures.prefix, "_average.png", sep=""), width = 2000, height = 500, res = 200, type = "cairo")
- create.bb.plot.average(bafsegmented=BAFvals,
- ploidy=ploidy,
- rho=rho,
- goodnessOfFit=goodness,
- pos_min=pos_min,
- pos_max=pos_max,
- segment_states_min=segment_states_min,
- segment_states_tot=segment_states_tot,
- chr.segs=chr.segs,
- chr.names=chr.names,
- tumourname=tumourname)
- dev.off()
-
- # Plot subclonal copy number as two separate states
- png(filename = paste(output.gw.figures.prefix, "_subclones.png", sep=""), width = 2000, height = 500, res = 200, type = "cairo")
- create.bb.plot.subclones(bafsegmented=BAFvals,
- subclones=subclones,
- ploidy=ploidy,
- rho=rho,
- goodnessOfFit=goodness,
- pos_min=pos_min,
- pos_max=pos_max,
- subcl_min=subcl_min,
- subcl_max=subcl_max,
- is_subclonal=is_subclonal,
- is_subclonal_maj=is_subclonal_maj,
- is_subclonal_min=is_subclonal_min,
- chr.segs=chr.segs,
- chr.names=chr.names,
- tumourname=tumourname)
- dev.off()
-}
-
-#' Load the rho and psi estimates from a file.
-#' @noRd
-load.rho.psi.file = function(rho.psi.file) {
- rho_psi_info = read.table(rho.psi.file, header=T, sep="\t", stringsAsFactors=F)
- # Always use best solution from grid search - reference segment sometimes gives strange results
- rho = rho_psi_info$rho[rownames(rho_psi_info)=="FRAC_GENOME"] # rho = tumour percentage (called tp in previous versions)
- psit = rho_psi_info$psi[rownames(rho_psi_info)=="FRAC_GENOME"] # psi of tumour cells
- goodness = rho_psi_info$distance[rownames(rho_psi_info)=="FRAC_GENOME"] # goodness of fit
- return(list(rho=rho, psit=psit, goodness=goodness))
-}
-
-#' Collapse a BAFsegmented file into segment start and end points
-#'
-#' This function looks through the BAFsegmented for stretches of equal
-#' BAFseg and records the start and end coordinates in a data.frame
-#' @param bafsegmented The BAFsegmented output from segmentation
-#' @return A data.frame with columns chromosome, start and end
-#' @author sd11
-#' @noRd
-collapse_bafsegmented_to_segments = function(bafsegmented) {
- segments_collapsed = data.frame()
- for (chrom in unique(bafsegmented$Chromosome)) {
- bafsegmented_chrom = bafsegmented[bafsegmented$Chromosome==chrom,]
- segments = rle(bafsegmented_chrom$BAFseg)
- startpoint = 1
- for (i in 1:length(segments$lengths)) {
- endpoint = startpoint+segments$lengths[i]-1
- segments_collapsed = rbind(segments_collapsed,
- data.frame(chromosome=chrom, start=bafsegmented_chrom$Position[startpoint], end=bafsegmented_chrom$Position[endpoint]))
- startpoint = endpoint+1
- }
- }
- return(segments_collapsed)
-}
-
-#' Function to make additional figures
-#'
-#' @param samplename Name of the sample for the plot title
-#' @param logr_file File containing all logR data
-#' @param bafsegmented_file File containing the BAFsegmented data
-#' @param logrsegmented_file File with the logRsegmented data
-#' @param allelecounts_file Optional file with raw allele counts (Default: NULL)
-#' @author sd11
-#' @export
-make_posthoc_plots = function(samplename, logr_file, bafsegmented_file, logrsegmented_file, allelecounts_file=NULL) {
- # Make some post-hoc plots
- logr = Battenberg::read_table_generic(logr_file)
- bafsegmented = as.data.frame(Battenberg::read_table_generic(bafsegmented_file))
- logrsegmented = as.data.frame(Battenberg::read_table_generic(logrsegmented_file, header=F))
- colnames(logrsegmented) = c("Chromosome", "Position", "logRseg")
- outputfile = paste0(samplename, "_alleleratio.png")
- allele_ratio_plot(samplename=samplename, logr=logr, bafsegmented=bafsegmented, logrsegmented=logrsegmented, outputfile=outputfile, max.plot.cn=8)
-
- if (!is.null(allelecounts_file)) {
- allelecounts = as.data.frame(Battenberg::read_table_generic(allelecounts_file))
- outputfile = paste0(samplename, "_coverage.png")
- coverage_plot(samplename, allelecounts, outputfile)
- }
-}
-
-
-#' Fit ChrX subclonal copy number (male only)
-#'
-#' Function to call ChrX copy number based on LogR (suitable for male samples). Copy number
-#' cannot be called for the non-PAR region of ChrX due to the hemizygosity of all 1000G SNPs.
-#' This function enables calling subclonal copy number for the non-PAR region by segmenting LogR.
-#' A number of correction steps are undertaken to account for the noisy nature of LogR. This function
-#' requires the following libraries: copynumber, data.table and ggplot2. It reads in three files generated
-#' by previous steps of Battenberg, namely samplename_mutantLogR_gcCorrected.tab, samplename_purity_ploidy.txt
-#' and samplename_copynumber_extended.txt.
-#' This function will also update the Battenberg genome-wide profile plots (average.png and subclones.png) to include the chrX profile by also
-#' reading in the samplename.BAFsegmented.txt and samplename_rho_psi.txt files
-#' @param tumourname The sample name used for Battenberg (i.e. the tumour BAM file name without the .bam extension)
-#' @param X_gamma The PCF gamma value for segmentation of 1000G SNP LogR values (Default 1000)
-#' @param X_kmin The min number of SNPs to support a segment in PCF of LogR values (Default 100)
-#' @param genomebuild The genome build used in running Battenberg (hg19 or hg38)
-#' @param AR Should the segment carrying the androgen receptor (AR) locus to be visually distinguished in average plot? (Default TRUE)
-#' @param prior_breakpoints_file A two column text file with prior genome-wide breakpoints, possibly from structural variants. This file must contain two columns with headers "chr" and "pos" representing chromosome and position.
-#' @param chrom_names A vector containing the names of chromosomes to be included in the final genome-wide Battenberg copy number plot with chrX
-#' @author naser.ansari-pour
-#' @export
-
-callChrXsubclones = function(tumourname,X_gamma=1000,X_kmin=100,genomebuild,AR=TRUE,prior_breakpoints_file=NULL,chrom_names,data_type="wgs"){
-
- print(tumourname)
-
- if (genomebuild=="hg19"){
- par_regions=c(2699520,155260560)
- x_centromere=c(58632012,61632012)
- ar=data.frame(startpos=66763874,endpos=66950461)
- } else if (genomebuild=="hg38") {
- par_regions=c(2781479,156030895)
- x_centromere=c(58605580,62412542)
- ar=data.frame(startpos=67544021,endpos=67730619)
- } else {
- stop("Genomebuild not supported for callChrXsubclones")
- }
-
- if (data_type=="wgs" | data_type=="WGS") {
- PCFinput=data.frame(read_table_generic(paste0(tumourname,"_mutantLogR_gcCorrected.tab")),stringsAsFactors=F)
- } else {
- PCFinput=data.frame(read_table_generic(paste0(tumourname,"_mutantLogR.tab")),stringsAsFactors=F)
- }
- ChrNotation=unique(PCFinput[which(!is.na(match(PCFinput$Chromosome,c("X","chrX")))),]$Chromosome) # find the chromosome notation
- PCFinput=PCFinput[which(PCFinput$Chromosome==ChrNotation & PCFinput$Position>par_regions[1] & PCFinput$Position0){
- # make sure all SV breakpoint positions are within the LogR data range and not outside of it
- svpos=sv[which((sv$pos > min(PCFinput$Position)) & (sv$pos < max(PCFinput$Position))),"pos"]
- breakpoints=c(min(PCFinput$Position),svpos,max(PCFinput$Position))
- PCF=data.frame()
- for (j in 1:(length(breakpoints)-1)) {
- PCFinput_sv=PCFinput[which(PCFinput$Position>=breakpoints[j] & PCFinput$Position0){
- BBcorr=-mean(cnloh$LogR)
- } else if (nrow(cnloh)==0){
- print("CRUDE estimation of BBcorr based on assumption of 2 copies vs ploidy")
- BBcorr=-log2(2/SAMPLEn)
- }
- }
- BBg1=BB[which(BB$nMaj1_A==2 & BB$nMin1_A==1 & BB$frac1_A==1),]
- BBg2=BB[which(BB$nMaj1_A==3 & BB$nMin1_A==1 & BB$frac1_A==1),]
- BBg3=BB[which(BB$nMaj1_A==4 & BB$nMin1_A==1 & BB$frac1_A==1),]
- BBg4=BB[which(BB$nMaj1_A==3 & BB$nMin1_A==2 & BB$frac1_A==1),] # likely observed in WGD samples
-
- # get max gain N:
- BBcomb=rbind(BBdip,BBg1,BBg2,BBg3,BBg4)
- maxNMaj=max(BBcomb$nMaj1_A)
-
- # SD for LogR values - diploid and gain regions
- BBsd=c(sd(BBdip$LogR),sd(BBg1$LogR),sd(BBg2$LogR),sd(BBg3$LogR))
- #BBsd_mean=mean(BBsd,na.rm=T)
- BBsd_max=max(BBsd, na.rm=T)
- BBsd_max=max(BBsd_max,0.05) # accept a minimum of 5% sd in LogR variation
-
- # BB LOH - estimating sd for LOH/loss events
- BBloh=BB[which(BB$nMaj1_A==1 & BB$nMin1_A==0 & BB$frac1_A==1),]
- if (nrow(BBloh)<=1){ #sd would be NA
- print("likely WGD sample or no clonal LOH event or just one single LOH event observed")
- BBloh=BB[which(BB$nMin1_A==0 & BB$frac1_A==1),] # all LOH events with varying nMaj1_A including 2:0 events
- }
-
- # expected ChrX logR values
- explogrgainX=function(x){log2((SAMPLEpurity*x+(1-SAMPLEpurity)*1)/1)}
- explogrGain=sapply(2:10000,explogrgainX) # up to 10000 copies!
-
- explogrLoss=max(log2(0+(1-SAMPLEpurity)*1),log2(0.01)) # if purity ~ 1, then purity of 0.99 is assumed for a realistic explogR estimate
-
- # assign CN
- SEG=data.frame()
- for (j in 1:nrow(SAMPLEsegs)){
- seg=SAMPLEsegs[j,]
- seg$type=ifelse(seg$mean<0,"loss","gain")
-
- # is segment different from zero?
- seg$mean=seg$mean+BBcorr
-
- if (seg$type=="gain"){
- seg$CNA=ifelse(seg$mean>(0+1.96*BBsd_max),"yes","no")
- } else {
- seg$CNA=ifelse(seg$mean<(0-1.96*BBsd_max),"yes","no")
- }
- # copy number
- if (seg$CNA=="yes"){
- if (seg$type=="gain"){
- rank=which(sort(c(explogrGain,seg$mean))==seg$mean) # rank of observed logR mean for segment among the expected logR values
- seg$CN=rank+1
- # clonality test
- if (rank==1){
- seg$clonal=ifelse(round(explogrGain[rank]-seg$mean,digits=2)<=round((BBsd_max/explogrGain[rank]),digits=2),"yes","no") # CV
-
- } else if (rank>=5){ # STOPS calling 'subclonal' events when copy number is >=5
- if (abs(seg$mean-explogrGain[rank-1])1){
- seg$clonal=ifelse(round(abs(explogrLoss-seg$mean),digits=2)x_centromere[1]-1e6 & seg$CNA=="yes" & seg$end.pos=0.95){
- seg$CCF=1
- seg$clonal="yes"
- }
- } else {
- seg$CCF=1
- }
- }
- } else {
- seg$CCF=1
- }
- CCF=rbind(CCF,seg)
- }
-
- # GENERATE FINAL OUTPUT
- SUBCLONES=data.frame()
- for (j in 1:nrow(CCF)){
- subclones=CCF[j,]
- if (subclones$CNA=="no"){
- subclones=data.frame(subclones,nMaj1=1,nMin1=0,frac1=1,nMaj2=0,nMin2=0,frac2=0)
- } else {
- if (subclones$type=="gain" & subclones$clonal=="yes"){
- subclones=data.frame(subclones,nMaj1=subclones$CN,nMin1=0,frac1=1,nMaj2=0,nMin2=0,frac2=0)
- }
- else if (subclones$type=="gain" & subclones$clonal=="no"){
- if(subclones$CCF>0.5){ # switch nMaj/nMin so that the first nMaj/nMin represent the MAJOR CLONE
- subclones=data.frame(subclones,nMaj1=subclones$CN,nMin1=0,frac1=subclones$CCF,nMaj2=subclones$CN-1,nMin2=0,frac2=1-subclones$CCF)
- } else {
- subclones=data.frame(subclones,nMaj1=subclones$CN-1,nMin1=0,frac1=1-subclones$CCF,nMaj2=subclones$CN,nMin2=0,frac2=subclones$CCF)
- }
- }
- else if (subclones$type=="loss" & subclones$clonal=="yes"){
- subclones=data.frame(subclones,nMaj1=subclones$CN,nMin1=0,frac1=1,nMaj2=0,nMin2=0,frac2=0) # very unlikely scenario; no sequencing reads should be present!
- }
- else if (subclones$type=="loss" & subclones$clonal=="no"){
- if(subclones$CCF>0.5){ # switch nMaj/nMin so that the first nMaj/nMin represent the MAJOR CLONE
- subclones=data.frame(subclones,nMaj1=subclones$CN,nMin1=0,frac1=subclones$CCF,nMaj2=1,nMin2=0,frac2=1-subclones$CCF)
- } else {
- subclones=data.frame(subclones,nMaj1=1,nMin1=0,frac1=1-subclones$CCF,nMaj2=subclones$CN,nMin2=0,frac2=subclones$CCF)
- }
- }
- }
- #print(j)
- SUBCLONES=rbind(SUBCLONES,subclones)
- }
-
- SUBCLONES$average=(SUBCLONES$nMaj1+SUBCLONES$nMin1)*SUBCLONES$frac1+(SUBCLONES$nMaj2+SUBCLONES$nMin2)*SUBCLONES$frac2
-
- SUBCLONESout=data.frame(SUBCLONES[,c("chrom","arm")],startpos=SUBCLONES$start.pos,endpos=SUBCLONES$end.pos,nSNPs=SUBCLONES$n.probes,
- LogR=SUBCLONES$mean,SUBCLONES[,c("type","CNA","CN","clonal","nMaj1","nMin1","frac1","nMaj2","nMin2","frac2")],
- subclonalCN=SUBCLONES$average,stringsAsFactors = F)
- SUBCLONESout$type[SUBCLONESout$type=="gain"]="+ve"
- SUBCLONESout$type[SUBCLONESout$type=="loss"]="-ve"
-
- # merge adjacent segments with same copy number
- SUBCLONESout$rank=1:nrow(SUBCLONESout)
- SUBCLONESout=SUBCLONESout[order(SUBCLONESout$subclonalCN),]
-
- SPLIT=split(SUBCLONESout$rank, cumsum(c(1, diff(SUBCLONESout$rank) != 1))) # find consecutive segments with same subclonalCN
- outputDF=data.frame()
- for (j in 1:length(SPLIT)){
- if (length(SPLIT[[j]])>1){
- #print(length(SPLIT[[j]]))
- SUBsplit=SUBCLONESout[which(!is.na(match(SUBCLONESout$rank,SPLIT[[j]]))),]
- if (length(unique(SUBsplit$arm))==1){
- if (sd(SUBsplit$subclonalCN)<=0.01){
- mergedseg=SUBsplit[1,]
- mergedseg$endpos=SUBsplit[length(SPLIT[[j]]),"endpos"]
- mergedseg$nSNPs=sum(SUBsplit$nSNPs)
- mergedseg$LogR=weighted.mean(SUBsplit$LogR,SUBsplit$nSNPs)
- outputDF=rbind(outputDF,mergedseg)
- } else {
- outputDF=rbind(outputDF,SUBsplit)
- print("adjacent not same subclonalCN in SPLIT")
- }
- } else if (length(SPLIT[[j]])==2){
- outputDF=rbind(outputDF,SUBsplit)
- } else{
- # if (length(SUBsplit$arm=="p"))
- pseg=SUBsplit[SUBsplit$arm=="p",]
- if (nrow(pseg)>1){
- if (sd(pseg$subclonalCN)<=0.01){
- mergedseg=pseg[1,]
- mergedseg$endpos=pseg[nrow(pseg),"endpos"]
- mergedseg$nSNPs=sum(pseg$nSNPs)
- mergedseg$LogR=weighted.mean(pseg$LogR,pseg$nSNPs)
- outputDF=rbind(outputDF,mergedseg)
- } else {
- outputDF=rbind(outputDF,pseg)
- print("adjacent not same subclonalCN in pseg")
- }
- } else {outputDF=rbind(outputDF,pseg)}
- qseg=SUBsplit[SUBsplit$arm=="q",]
- if (nrow(qseg)>1){
- if (sd(qseg$subclonalCN)<=0.01){
- mergedseg=qseg[1,]
- mergedseg$endpos=qseg[nrow(qseg),"endpos"]
- mergedseg$nSNPs=sum(qseg$nSNPs)
- mergedseg$LogR=weighted.mean(qseg$LogR,qseg$nSNPs)
- outputDF=rbind(outputDF,mergedseg)
- } else {
- outputDF=rbind(outputDF,qseg)
- print("adjacent not same subclonalCN in qseg")
- }
- } else {outputDF=rbind(outputDF,qseg)}
- }
- } else {
- SUBsplit=SUBCLONESout[which(SUBCLONESout$rank==SPLIT[[j]]),]
- outputDF=rbind(outputDF,SUBsplit)
- }
- }
- outputDF=outputDF[order(outputDF$startpos),]
-
- print(paste("Number of rows merged =",nrow(SUBCLONESout)-nrow(outputDF)))
-
- BBnew=BB[which(is.na(match(BB$chr,c("X","chrX")))),c(1:3,8:13)] # copynumber.txt columns to be populated with chrX calls
-
- outputDF_for_merge=data.frame(chr=outputDF$chrom,startpos=outputDF$startpos,endpos=outputDF$endpos,
- nMaj1_A=outputDF$nMaj1,nMin1_A=outputDF$nMin1,frac1_A=outputDF$frac1,
- nMaj2_A=outputDF$nMaj2,nMin2_A=outputDF$nMin2,frac2_A=outputDF$frac2,
- stringsAsFactors = F)
-
- BBnew=rbind(BBnew,outputDF_for_merge)
- write.table(BBnew,paste0(tumourname,"_copynumber.txt"),col.names = T,row.names = F,quote = F,sep="\t")
-
- BBnew_extended=BB[which(is.na(match(BB$chr,c("X","chrX")))),] # copynumber_extended.txt columns for chrX
-
- outputDF_for_merge_extended=data.frame(chr=outputDF$chrom,startpos=outputDF$startpos,endpos=outputDF$endpos,BAF=NA,pval=NA,LogR=outputDF$LogR,ntot=NA,
- nMaj1_A=outputDF$nMaj1,nMin1_A=outputDF$nMin1,frac1_A=outputDF$frac1,nMaj2_A=outputDF$nMaj2,nMin2_A=outputDF$nMin2,
- frac2_A=outputDF$frac2)
- BtoFsolutions=data.frame(matrix(nrow= nrow(outputDF),ncol = ncol(BB)-ncol(outputDF_for_merge_extended)))
- names(BtoFsolutions)=names(BB)[(ncol(outputDF_for_merge_extended)+1):ncol(BB)]
-
- BBnew_extended=rbind(BBnew_extended,cbind(outputDF_for_merge_extended,BtoFsolutions))
- write.table(BBnew_extended,paste0(tumourname,"_copynumber_extended.txt"),col.names = T,row.names = F,quote = F,sep="\t")
-
- # PLOT
- outputDF$diff=outputDF$endpos-outputDF$startpos
- # print(outputDF)
- if (nrow(outputDF[which(outputDF$CNA=="yes"),])>0){
- PGAclonal=sum(outputDF[which(outputDF$clonal=="yes"),]$diff)/sum(outputDF[which(!is.na(outputDF$clonal)),]$diff)
- print(paste("chrX-based PGA.is.clonal =",PGAclonal))
- } else {
- print("no chrX CNA identified")
- PGAclonal = "NA"
- }
-
- plot_BB=ggplot()+geom_hline(yintercept = 0:ceiling(max(outputDF$subclonalCN)),linetype="longdash",col="grey",linewidth=0.2)+
- geom_rect(data=outputDF,aes(xmin=startpos,xmax=endpos,ymin=subclonalCN-0.02,ymax=subclonalCN+0.02))+
- geom_vline(xintercept = x_centromere,linetype="longdash",col="green")+
- #geom_hline(yintercept = nonpar,linetype="dotted",col="blue")+
- ylim(-0.2,ceiling(max(outputDF$subclonalCN))+0.2)+labs(x="ChrX coordinate (bp)",y="Average Ploidy")+
- theme(plot.title = element_text(hjust = 0.5,size=12),panel.background = element_blank())+
- ggtitle(paste0(tumourname," , Ploidy: ",round(SAMPLEn,digits = 3)," , Purity: ",round(SAMPLEpurity*100,digits = 0),
- "%, chrX PGA.is.clonal: ",ifelse(PGAclonal=="NA","NA",paste0(round(PGAclonal*100,digits = 1),"%"))))
-
- # ANDROGEN RECEPTOR LOCUS
- if (AR){
- data.table::setDT(ar)
- data.table::setkey(ar,"startpos","endpos")
- data.table::setDT(outputDF)
- data.table::setkey(outputDF,"startpos","endpos")
- segAR=data.table::foverlaps(ar,outputDF,type="any",nomatch = 0)
- segAR$subclonalCN=(segAR$nMaj1+segAR$nMin1)*segAR$frac1+(segAR$nMaj2+segAR$nMin2)*segAR$frac2
- plot_BB=plot_BB+geom_rect(data=segAR,aes(xmin=startpos,xmax=endpos,ymin=subclonalCN-0.02,ymax=subclonalCN+0.02),fill="red")
- }
-
- pdf(paste0(tumourname,"_chrX_average_ploidy.pdf"))
- print(plot_BB)
- dev.off()
-
- # update outputDF (chrX-only copynumber output file)
- outputDF=outputDF[,c(1:6,11:17)]
- write.table(outputDF,paste0(tumourname,"_chrX_copynumber.txt"),col.names = T,row.names = F,quote = F,sep="\t")
-
- # Update the genomewide Battenberg plots
- # goodness from rho_psi file (i.e. column named 'distance')
- goodness=read.table(paste0(tumourname,"_rho_and_psi.txt"),header=T,stringsAsFactors = F,sep="\t")
- goodness=goodness[which(goodness$is.best=="TRUE"),"distance"]
- # rho and ploidy from purity_ploidy file
- rho_psi=read.table(paste0(tumourname,"_purity_ploidy.txt"),header=T,stringsAsFactors = F,sep="\t")
- # update for BB3 - replace cellularity with purity
- # rho=rho_psi$cellularity
- rho=rho_psi$purity
- ploidy=rho_psi$ploidy
- # Need BAFsegment file
- BAFvals=as.data.frame(Battenberg:::read_bafsegmented(paste0(tumourname,".BAFsegmented.txt")))
- print("BAFvals")
-
- # replacing constant value of 90000 with chrX_BAFvals_length as a sample-specific way of counting the typical no. of het SNPs expected based on chrX length (chr 7 and 8 average hetSNP count)
- # option 1 (may not always work if chr7 or chr8 have any kind of LOH in a pure or high-purity sample)
- #chrX_BAFvals_length=round((nrow(BAFvals[which(!is.na(match(BAFvals$Chromosome,c(7,"chr7")))),])+nrow(BAFvals[which(!is.na(match(BAFvals$Chromosome,c(8,"chr8")))),]))/2,0)
- # option 2 (based on the proportion of genome covered by chrX (i.e. 156e6/3e9 = 5%) and the number of hetSNPs in a sample-specific manner)
- chrX_BAFvals_length = round(nrow(BAFvals)*0.05,0)
- print(paste("chrX BAFvals length =",chrX_BAFvals_length))
-
-
- BAFvals=rbind(BAFvals[which(is.na(match(BAFvals$Chromosome,c("X","chrX")))),],
- data.frame(Chromosome="X",Position=sort(sample(1:155e6,chrX_BAFvals_length,replace=F)), # 155e6: approximate length of chrX
- BAF=sample(c(0,1),chrX_BAFvals_length,replace=T),BAFphased=1,BAFseg=1))
-
- Battenberg:::plot.gw.subclonal.cn(subclones=BBnew,
- BAFvals=BAFvals,
- rho=rho,
- ploidy=ploidy,
- goodness=goodness,
- output.gw.figures.prefix=paste(tumourname,"_BattenbergProfile", sep=""),
- chr.names=chrom_names,
- tumourname=tumourname)
-}
diff --git a/R/generate_plots.R b/R/generate_plots.R
new file mode 100644
index 00000000..f6c8df5c
--- /dev/null
+++ b/R/generate_plots.R
@@ -0,0 +1,77 @@
+#' Generate plots
+generate_plots_battenberg <- function(
+ analysis, distancepng, copynumberprofilespng, nonroundedprofilepng,
+ d, psi_opt1, rho_opt1, ploidy_opt1, goodness_of_fit_opt1, minimise,
+ b, r, s, gamma, ch, lrr, bafsegmented, chr_names, reliabilityFile
+) {
+ if (analysis == "paired") {
+ psi_opt1_plot <- psi_opt1
+ rho_opt1_plot <- rho_opt1
+
+ if (!is.na(distancepng)) {
+ grDevices::png(filename = distancepng, width = 1000, height = 1000, res = 1000 / 7, type = "cairo")
+ }
+ ASCAT::ascat.plotSunrise(-d, psi_opt1_plot, rho_opt1_plot, minimise)
+ if (!is.na(distancepng)) {
+ grDevices::dev.off()
+ }
+ }
+
+ nAfull <- (rho_opt1 - 1 - (b - 1) * 2^(r / gamma) * ((1 - rho_opt1) * 2 + rho_opt1 * psi_opt1)) / rho_opt1
+ nBfull <- (rho_opt1 - 1 + b * 2^(r / gamma) * ((1 - rho_opt1) * 2 + rho_opt1 * psi_opt1)) / rho_opt1
+ nA <- pmax(round(nAfull), 0)
+ nB <- pmax(round(nBfull), 0)
+
+ if (!is.na(reliabilityFile)) {
+ rBacktransform <- gamma * log((rho_opt1 * (nA + nB) + (1 - rho_opt1) * 2) / ((1 - rho_opt1) * 2 + rho_opt1 * psi_opt1), 2)
+ bBacktransform <- (1 - rho_opt1 + rho_opt1 * nB) / (2 - 2 * rho_opt1 + rho_opt1 * (nA + nB))
+ rConf <- ifelse(abs(rBacktransform) > 0.15, pmin(100, pmax(0, 100 * (1 - abs(rBacktransform - r) / abs(r)))), NA)
+ bConf <- ifelse(bBacktransform != 0.5, pmin(100, pmax(0, ifelse(b == 0.5, 100, 100 * (1 - abs(bBacktransform - b) / abs(b - 0.5))))), NA)
+
+ data.table::fwrite(
+ data.frame(
+ segmentedBAF = b, backTransformedBAF = bBacktransform, confidenceBAF = bConf,
+ segmentedR = r, backTransformedR = rBacktransform, confidenceR = rConf,
+ nA = nA, nB = nB, nAfull = nAfull, nBfull = nBfull
+ ),
+ reliabilityFile,
+ sep = ",", row.names = FALSE
+ )
+ }
+
+ if (!is.na(copynumberprofilespng)) {
+ grDevices::png(
+ filename = copynumberprofilespng,
+ width = 2000, height = 500,
+ res = 200, type = "cairo"
+ )
+ }
+ ASCAT::ascat.plotAscatProfile(
+ n1all = nA, n2all = nB, heteroprobes = TRUE,
+ ploidy = ploidy_opt1, rho = rho_opt1,
+ goodnessOfFit = goodness_of_fit_opt1 * 100,
+ nonaberrant = FALSE, ch = ch, lrr = lrr, bafsegmented = bafsegmented,
+ chrs = chr_names
+ )
+ if (!is.na(copynumberprofilespng)) {
+ grDevices::dev.off()
+ }
+
+ if (!is.na(nonroundedprofilepng)) {
+ grDevices::png(
+ filename = nonroundedprofilepng,
+ width = 2000, height = 500,
+ res = 200, type = "cairo"
+ )
+ }
+ ASCAT::ascat.plotNonRounded(
+ ploidy = ploidy_opt1, rho = rho_opt1,
+ goodnessOfFit = goodness_of_fit_opt1 * 100,
+ nonaberrant = FALSE, nAfull = nAfull,
+ nBfull = nBfull,
+ bafsegmented = bafsegmented, ch = ch, lrr = lrr, chrs = chr_names
+ )
+ if (!is.na(nonroundedprofilepng)) {
+ grDevices::dev.off()
+ }
+}
diff --git a/R/globals.R b/R/globals.R
new file mode 100644
index 00000000..d6a0a546
--- /dev/null
+++ b/R/globals.R
@@ -0,0 +1,14 @@
+if (getRversion() >= "2.15.1") {
+ utils::globalVariables(c(
+ "BAF", "CL_AC", "CL_AL", "CL_LogR", "CL_OHET", "GL_AC", "GL_AL",
+ "GL_LogR", "GL_OHET", "LogR", "baf", "cnMaj", "cnMin",
+ "copy_ratio_binned", "endpos", "flnMaj", "flnMin", "frac",
+ "i", "nMaj", "nMaj1_A", "nMin", "nMin1_A", "normal_binned",
+ "pcf", "plotChrom", "pos", "ratioBAFseg", "ratioBAFseg_alt",
+ "sol", "startpos", "subclonalCN", "total_cn_psi", "total_minor",
+ "tumour_binned", "xmax", "xmin", "y", "ymax", "ymin",
+ ".", ":=", "BAFphased", "BAFseg", "CHR", "Chromosome", "V2", "alt",
+ "alt_count", "dynamic_names", "fmean", "fmedian", "fnobs", "hap1", "hap2",
+ "parallel_grid_search", "ref", "ref_count"
+ ))
+}
diff --git a/R/grid_search.R b/R/grid_search.R
deleted file mode 100644
index d631b9a2..00000000
--- a/R/grid_search.R
+++ /dev/null
@@ -1,443 +0,0 @@
-#' Key optimizations:
-#' 1. Early termination after first good solution (like original)
-#' 2. Vectorized distance calculations
-#' 3. Optimized constraint checking
-#' 4. Smart search ordering (best regions first)
-#' 5. Reduced memory allocations
-runASCAT_enhanced = function(lrr, baf, lrrsegmented, bafsegmented, chromosomes, dist_choice,
- distancepng = NA, copynumberprofilespng = NA, nonroundedprofilepng = NA,
- cnaStatusFile = "copynumber_solution_status.txt", gamma = 0.55, allow100percent,
- reliabilityFile=NA, min.ploidy=1.6, max.ploidy=4.8, min.rho=0.1, max.rho=1.0,
- min.goodness=63, uninformative_BAF_threshold = 0.51, chr.names, analysis="paired",
- smart_ordering = TRUE, early_termination = TRUE, verbose = TRUE) {
-
- start_time <- Sys.time()
-
- # Setup data processing (IDENTICAL to original)
- ch = chromosomes
- b = bafsegmented
- r = lrrsegmented[names(bafsegmented)]
-
- dist_min_psi = max(min.ploidy-0.6, 0)
- dist_max_psi = max.ploidy+0.6
- dist_min_rho = max(min.rho-0.03, 0.05)
- dist_max_rho = max.rho+0.03
-
- s = ASCAT::make_segments(r,b)
- dist_matrix_info <- create_distance_matrix(s, dist_choice, gamma,
- uninformative_BAF_threshold=uninformative_BAF_threshold,
- min_psi=dist_min_psi, max_psi=dist_max_psi,
- min_rho=dist_min_rho, max_rho=dist_max_rho)
- d = dist_matrix_info$distance_matrix
- minimise = dist_matrix_info$minimise
-
- TheoretMaxdist = sum(rep(0.25,dim(s)[1]) * s[,"length"],na.rm=T)
-
- if( !(minimise) ) {
- d = -d
- }
-
- if (verbose) {
- cat("Optimized Battenberg with smart ordering and early termination...\n")
- }
-
- # Pre-compute values for speed
- rho_values <- as.numeric(colnames(d))
- psi_values <- as.numeric(rownames(d))
- s_length <- s[,"length"]
- s_b <- s[,"b"]
- s_r <- s[,"r"]
- total_length <- sum(s_length)
-
- # Create search order - most promising regions first
- search_order <- create_smart_search_order(d, smart_ordering, verbose)
-
- # OPTIMIZED SEARCH with early termination
- nropt = 0
- localmin = NULL
- optima = list()
- points_checked = 0
-
- for (idx in 1:length(search_order)) {
- point <- search_order[[idx]]
- i <- point$i
- j <- point$j
- points_checked <- points_checked + 1
-
- m = d[i,j]
- if (!is.finite(m)) next
-
- # Fast local minimum check (7x7 like original for speed)
- if (is_local_minimum_fast(d, i, j, m)) {
- psi = psi_values[i]
- rho = rho_values[j]
-
- # Fast solution calculation
- solution <- calculate_solution_fast(psi, rho, s_b, s_r, s_length, total_length, gamma,
- min.ploidy, max.ploidy, min.rho, max.rho,
- min.goodness, m, TheoretMaxdist, minimise, allow100percent)
-
- if (!is.null(solution)) {
- nropt = nropt + 1
- optima[[nropt]] = c(m, i, j, solution$ploidy, solution$goodness)
- localmin[nropt] = m
-
- if (verbose) {
- cat("Found solution", nropt, "at point", points_checked, "/", length(search_order),
- ": rho=", round(solution$rho, 3), ", psi=", round(solution$psi, 3),
- ", goodness=", round(solution$goodness, 2), "\n")
- }
-
- # Early termination if we found a good solution
- if (early_termination && solution$goodness >= (min.goodness + 5)) {
- if (verbose) cat("Early termination - found high quality solution\n")
- break
- }
- }
- }
-
- # Progress update
- if (verbose && points_checked %% 2000 == 0) {
- cat("Progress:", points_checked, "/", length(search_order), "points checked\n")
- }
- }
-
- # Handle 100% aberrant case (only if no solutions found)
- if (allow100percent & nropt == 0) {
- if (verbose) cat("Trying 100% aberrant solutions...\n")
-
- cold = which(rho_values > 1)
- d_modified <- d
- d_modified[,cold] = 1E20
-
- # Use same optimized search for 100% case
- search_order_100 <- create_smart_search_order(d_modified, smart_ordering, FALSE)
-
- for (idx in 1:length(search_order_100)) {
- point <- search_order_100[[idx]]
- i <- point$i
- j <- point$j
-
- m = d_modified[i,j]
- if (!is.finite(m)) next
-
- if (is_local_minimum_fast(d_modified, i, j, m)) {
- psi = psi_values[i]
- rho = rho_values[j]
-
- solution <- calculate_solution_fast(psi, rho, s_b, s_r, s_length, total_length, gamma,
- min.ploidy, max.ploidy, min.rho, max.rho,
- min.goodness, m, TheoretMaxdist, minimise, allow100percent,
- skip_zero_check = TRUE)
-
- if (!is.null(solution)) {
- nropt = nropt + 1
- optima[[nropt]] = c(m, i, j, solution$ploidy, solution$goodness)
- localmin[nropt] = m
- break # Early termination for 100% case too
- }
- }
- }
- }
-
- optimization_time <- as.numeric(difftime(Sys.time(), start_time, units = "secs"))
-
- # Process results (IDENTICAL to original logic)
- psi_opt1_plot = vector(mode="numeric")
- rho_opt1_plot = vector(mode="numeric")
-
- if (nropt>0) {
- write.table(paste(nropt, " copy number solutions found", sep=""), file=cnaStatusFile, quote=F, col.names=F, row.names=F)
- optlim = sort(localmin)[1]
-
- for (i in 1:length(optima)) {
- if(optima[[i]][1] == optlim) {
- psi_opt1 = psi_values[optima[[i]][2]]
- rho_opt1 = rho_values[optima[[i]][3]]
- if(rho_opt1 > 1) {
- rho_opt1 = 1
- }
- ploidy_opt1 = optima[[i]][4]
- goodnessOfFit_opt1 = optima[[i]][5]
- psi_opt1_plot = c(psi_opt1_plot, psi_opt1)
- rho_opt1_plot = c(rho_opt1_plot, rho_opt1)
- }
- }
- } else {
- write.table(paste("no copy number solutions found", sep=""), file=cnaStatusFile, quote=F, col.names=F, row.names=F)
- if (verbose) cat("No suitable copy number solution found\n")
- psi = NA
- ploidy = NA
- rho = NA
- psi_opt1_plot = -1
- rho_opt1_plot = -1
-
- return(list(
- psi = psi,
- rho = rho,
- ploidy = ploidy,
- convergence_info = list(
- converged = FALSE,
- optimization_time = optimization_time,
- points_checked = points_checked
- )
- ))
- }
-
- if (verbose) {
- cat("Found", nropt, "solutions in", round(optimization_time, 2), "seconds\n")
- cat("Checked", points_checked, "/", length(search_order), "points (",
- round(100 * points_checked / length(search_order), 1), "% of search space)\n")
- cat("Best solution: rho =", round(rho_opt1, 3), ", psi =", round(psi_opt1, 3),
- ", ploidy =", round(ploidy_opt1, 3), ", goodness =", round(goodnessOfFit_opt1, 2), "\n")
- }
-
- # Generate plots (IDENTICAL to original)
- if (analysis=="paired"){
- if (!is.na(distancepng)) {
- png(filename = distancepng, width = 1000, height = 1000, res = 1000/7, type = "cairo")
- }
- ASCAT::ascat.plotSunrise(-d, psi_opt1_plot, rho_opt1_plot, minimise)
- if (!is.na(distancepng)) { dev.off() }
- }
-
- rho = rho_opt1
- psi = psi_opt1
- ploidy = ploidy_opt1
-
- nAfull = (rho-1-(b-1)*2^(r/gamma)*((1-rho)*2+rho*psi))/rho
- nBfull = (rho-1+b*2^(r/gamma)*((1-rho)*2+rho*psi))/rho
- nA = pmax(round(nAfull),0)
- nB = pmax(round(nBfull),0)
-
- rBacktransform = gamma*log((rho*(nA+nB)+(1-rho)*2)/((1-rho)*2+rho*psi),2)
- bBacktransform = (1-rho+rho*nB)/(2-2*rho+rho*(nA+nB))
- rConf = ifelse(abs(rBacktransform)>0.15,pmin(100,pmax(0,100*(1-abs(rBacktransform-r)/abs(r)))),NA)
- bConf = ifelse(bBacktransform!=0.5,pmin(100,pmax(0,ifelse(b==0.5,100,100*(1-abs(bBacktransform-b)/abs(b-0.5))))),NA)
-
- if(!is.na(reliabilityFile)){
- write.table(data.frame(segmentedBAF=b,backTransformedBAF=bBacktransform,confidenceBAF=bConf,segmentedR=r,backTransformedR=rBacktransform,confidenceR=rConf,nA=nA,nB=nB,nAfull=nAfull,nBfull=nBfull), reliabilityFile,sep=",",row.names=F)
- }
- confidence = ifelse(is.na(rConf),bConf,ifelse(is.na(bConf),rConf,(rConf+bConf)/2))
-
- # Create plots
- if (!is.na(copynumberprofilespng)) {
- png(filename = copynumberprofilespng, width = 2000, height = 500, res = 200, type = "cairo")
- }
- ASCAT::ascat.plotAscatProfile(n1all = nA, n2all = nB, heteroprobes = TRUE, ploidy = ploidy_opt1, rho = rho_opt1, goodnessOfFit = goodnessOfFit_opt1, nonaberrant = FALSE, ch = ch, lrr = lrr, bafsegmented = bafsegmented, chrs=chr.names)
- if (!is.na(copynumberprofilespng)) { dev.off() }
-
- if (!is.na(nonroundedprofilepng)) {
- png(filename = nonroundedprofilepng, width = 2000, height = 500, res = 200, type = "cairo")
- }
- ASCAT::ascat.plotNonRounded(ploidy = ploidy_opt1, rho = rho_opt1, goodnessOfFit = goodnessOfFit_opt1, nonaberrant = FALSE, nAfull = nAfull, nBfull = nBfull, bafsegmented = bafsegmented, ch = ch, lrr = lrr, chrs=chr.names)
- if (!is.na(nonroundedprofilepng)) { dev.off() }
-
- return(list(
- psi = psi,
- rho = rho,
- ploidy = ploidy,
- convergence_info = list(
- converged = TRUE,
- n_solutions_found = nropt,
- optimization_time = optimization_time,
- points_checked = points_checked,
- search_efficiency = points_checked / length(search_order)
- )
- ))
-}
-
-#' Create smart search order - best regions first
-create_smart_search_order <- function(d, smart_ordering, verbose) {
-
- # Get all valid search points (excluding borders)
- nr <- nrow(d)
- nc <- ncol(d)
- search_points <- list()
-
- for (i in 4:(nr-3)) {
- for (j in 4:(nc-3)) {
- if (is.finite(d[i,j])) {
- search_points[[length(search_points) + 1]] <- list(i = i, j = j, distance = d[i,j])
- }
- }
- }
-
- if (!smart_ordering) {
- # Return in original order
- return(lapply(search_points, function(p) list(i = p$i, j = p$j)))
- }
-
- # Smart ordering: best distances first
- distances <- sapply(search_points, function(p) p$distance)
- order_idx <- order(distances)
- ordered_points <- search_points[order_idx]
-
- if (verbose) {
- cat("Smart ordering: searching best", length(ordered_points), "regions first\n")
- cat("Distance range:", round(min(distances), 4), "to", round(max(distances), 4), "\n")
- }
-
- return(lapply(ordered_points, function(p) list(i = p$i, j = p$j)))
-}
-
-#' Fast local minimum check (optimized version of original 7x7)
-is_local_minimum_fast <- function(d, i, j, center_value) {
-
- # Check 7x7 neighborhood (same as original)
- i_min <- i - 3
- i_max <- i + 3
- j_min <- j - 3
- j_max <- j + 3
-
- # Bounds checking
- if (i_min < 1 || i_max > nrow(d) || j_min < 1 || j_max > ncol(d)) {
- return(FALSE)
- }
-
- # Extract neighborhood
- neighborhood <- d[i_min:i_max, j_min:j_max]
-
- # Set center to maximum to exclude it from minimum check
- neighborhood[4, 4] <- max(neighborhood, na.rm = TRUE)
-
- # Check if center is local minimum
- return(min(neighborhood, na.rm = TRUE) > center_value)
-}
-
-#' Fast solution calculation (vectorized and optimized)
-calculate_solution_fast <- function(psi, rho, s_b, s_r, s_length, total_length, gamma,
- min.ploidy, max.ploidy, min.rho, max.rho,
- min.goodness, distance_value, TheoretMaxdist, minimise,
- allow100percent, skip_zero_check = FALSE) {
-
- # Quick input validation
- if (is.na(psi) || is.na(rho) || psi <= 0 || rho <= 0 || rho > 1.1) {
- return(NULL)
- }
-
- # Quick constraint pre-check
- if (psi < min.ploidy || psi > max.ploidy || rho < min.rho || rho > max.rho) {
- return(NULL)
- }
-
- # Vectorized copy number calculation
- multiplier <- 2^(s_r/gamma) * ((1-rho)*2 + rho*psi)
- nA <- (rho - 1 - (s_b - 1) * multiplier) / rho
- nB <- (rho - 1 + s_b * multiplier) / rho
-
- # Quick validation
- if (any(is.na(nA)) || any(is.na(nB)) || any(!is.finite(nA)) || any(!is.finite(nB))) {
- return(NULL)
- }
-
- # Vectorized ploidy calculation
- ploidy <- sum((nA + nB) * s_length) / total_length
-
- if (is.na(ploidy) || !is.finite(ploidy) || ploidy <= 0) {
- return(NULL)
- }
-
- # Final ploidy constraint check
- if (ploidy < min.ploidy || ploidy > max.ploidy) {
- return(NULL)
- }
-
- # Fast goodness calculation
- if(minimise) {
- goodnessOfFit <- (1 - distance_value/TheoretMaxdist) * 100
- } else {
- goodnessOfFit <- -distance_value/TheoretMaxdist * 100
- }
-
- if (is.na(goodnessOfFit) || !is.finite(goodnessOfFit) || goodnessOfFit < min.goodness) {
- return(NULL)
- }
-
- # Zero check (only if needed)
- if (!skip_zero_check && !allow100percent) {
- nA_rounded <- round(nA)
- nB_rounded <- round(nB)
-
- percentzero <- (sum((nA_rounded == 0) * s_length) + sum((nB_rounded == 0) * s_length)) / total_length
-
- # Fast perczeroAbb calculation
- baf_mask <- s_b != 0.5
- if (any(baf_mask)) {
- denom <- sum(s_length[baf_mask])
- if (denom > 0) {
- perczeroAbb <- (sum((nA_rounded == 0) * s_length * baf_mask) +
- sum((nB_rounded == 0) * s_length * baf_mask)) / denom
- } else {
- perczeroAbb <- 0
- }
- } else {
- perczeroAbb <- 0
- }
-
- if (is.na(perczeroAbb)) perczeroAbb <- 0
-
- if (!(percentzero > 0.01 || perczeroAbb > 0.1)) {
- return(NULL)
- }
- }
-
- return(list(
- psi = psi,
- rho = min(rho, 1.0),
- ploidy = ploidy,
- goodness = goodnessOfFit,
- distance = distance_value
- ))
-}
-
-#' Generate plots
-generate_plots_battenberg <- function(analysis, distancepng, copynumberprofilespng, nonroundedprofilepng,
- d, psi_opt1, rho_opt1, ploidy_opt1, goodnessOfFit_opt1, minimise,
- b, r, s, gamma, ch, lrr, bafsegmented, chr.names, reliabilityFile) {
-
- if (analysis == "paired") {
- psi_opt1_plot <- psi_opt1
- rho_opt1_plot <- rho_opt1
-
- if (!is.na(distancepng)) {
- png(filename = distancepng, width = 1000, height = 1000, res = 1000/7, type = "cairo")
- }
- ASCAT::ascat.plotSunrise(-d, psi_opt1_plot, rho_opt1_plot, minimise)
- if (!is.na(distancepng)) { dev.off() }
- }
-
- nAfull <- (rho_opt1-1-(b-1)*2^(r/gamma)*((1-rho_opt1)*2+rho_opt1*psi_opt1))/rho_opt1
- nBfull <- (rho_opt1-1+b*2^(r/gamma)*((1-rho_opt1)*2+rho_opt1*psi_opt1))/rho_opt1
- nA <- pmax(round(nAfull),0)
- nB <- pmax(round(nBfull),0)
-
- if(!is.na(reliabilityFile)){
- rBacktransform <- gamma*log((rho_opt1*(nA+nB)+(1-rho_opt1)*2)/((1-rho_opt1)*2+rho_opt1*psi_opt1),2)
- bBacktransform <- (1-rho_opt1+rho_opt1*nB)/(2-2*rho_opt1+rho_opt1*(nA+nB))
- rConf <- ifelse(abs(rBacktransform)>0.15,pmin(100,pmax(0,100*(1-abs(rBacktransform-r)/abs(r)))),NA)
- bConf <- ifelse(bBacktransform!=0.5,pmin(100,pmax(0,ifelse(b==0.5,100,100*(1-abs(bBacktransform-b)/abs(b-0.5))))),NA)
-
- write.table(data.frame(segmentedBAF=b,backTransformedBAF=bBacktransform,confidenceBAF=bConf,
- segmentedR=r,backTransformedR=rBacktransform,confidenceR=rConf,
- nA=nA,nB=nB,nAfull=nAfull,nBfull=nBfull),
- reliabilityFile, sep=",", row.names=F)
- }
-
- if (!is.na(copynumberprofilespng)) {
- png(filename = copynumberprofilespng, width = 2000, height = 500, res = 200, type = "cairo")
- }
- ASCAT::ascat.plotAscatProfile(n1all = nA, n2all = nB, heteroprobes = TRUE,
- ploidy = ploidy_opt1, rho = rho_opt1, goodnessOfFit = goodnessOfFit_opt1,
- nonaberrant = FALSE, ch = ch, lrr = lrr, bafsegmented = bafsegmented,
- chrs = chr.names)
- if (!is.na(copynumberprofilespng)) { dev.off() }
-
- if (!is.na(nonroundedprofilepng)) {
- png(filename = nonroundedprofilepng, width = 2000, height = 500, res = 200, type = "cairo")
- }
- ASCAT::ascat.plotNonRounded(ploidy = ploidy_opt1, rho = rho_opt1, goodnessOfFit = goodnessOfFit_opt1,
- nonaberrant = FALSE, nAfull = nAfull, nBfull = nBfull,
- bafsegmented = bafsegmented, ch = ch, lrr = lrr, chrs = chr.names)
- if (!is.na(nonroundedprofilepng)) { dev.off() }
-}
diff --git a/R/haplotype.R b/R/haplotype.R
index d96adf92..c8ffedfe 100644
--- a/R/haplotype.R
+++ b/R/haplotype.R
@@ -1,5 +1,5 @@
#' Morphs phased SNPs from SNP6 input into haplotype blocks
-#'
+#'
#' This function matches allele frequencies and halplotype info, reverses frequencies by haplotype, combines the output and saves it to disk.
#' @param chrom The chromosome number for which this function should run.
#' @param alleleFreqFile File containing allele frequency information.
@@ -9,34 +9,32 @@
#' @param chr_names Vector of chromosome names
#' @author dw9
#' @export
-GetChromosomeBAFs_SNP6 = function(chrom, alleleFreqFile, haplotypeFile, samplename, outputfile, chr_names) {
+GetChromosomeBAFs_SNP6 <- function(chrom, alleleFreqFile, haplotypeFile, samplename, outputfile, chr_names) {
# Read in the allele frequencies and variant info
- alleleFreqData = read.csv(alleleFreqFile, header=T)
- variant_data = read.table(haplotypeFile, header=F)
-
- # TODO: Check columns input
-
+ alleleFreqData <- data.table::fread(alleleFreqFile, header = TRUE, data.table = FALSE)
+ variant_data <- data.table::fread(haplotypeFile, header = FALSE, data.table = FALSE)
+
# Match the two
- alleleFreqData = alleleFreqData[alleleFreqData[,1] %in% variant_data[,3],]
- select = match(alleleFreqData[,1], variant_data[,3])
- variant_data = variant_data[select,]
-
- chr_name = chrom
- print(chr_name)
+ alleleFreqData <- alleleFreqData[alleleFreqData[, 1] %in% variant_data[, 3], ]
+ select <- match(alleleFreqData[, 1], variant_data[, 3])
+ variant_data <- variant_data[select, ]
+
+ chr_name <- chrom
+ log_info("Processing: {chr_name}")
# Switch the haplotypes where required
- alleleFreqs = alleleFreqData$allele.frequency
- reversedHaplotypes = variant_data[,6]==1
- alleleFreqs[reversedHaplotypes] = 1.0-alleleFreqs[reversedHaplotypes]
-
- print(paste(nrow(variant_data),length(alleleFreqs),sep=","))
+ alleleFreqs <- alleleFreqData$allele.frequency
+ reversedHaplotypes <- variant_data[, 6] == 1
+ alleleFreqs[reversedHaplotypes] <- 1.0 - alleleFreqs[reversedHaplotypes]
+
+ log_info("{nrow(variant_data)},{length(alleleFreqs)}")
# Combine the allele frequencies and variant info and save output
- knownMutBAFs = cbind(chr_name,variant_data[,3],alleleFreqs)
- write.table(knownMutBAFs, outputfile, sep="\t", row.names=F, col.names=c("Chromosome", "Position", samplename), quote=F)
+ knownMutBAFs <- cbind(chr_name, variant_data[, 3], alleleFreqs)
+ data.table::fwrite(knownMutBAFs, outputfile, sep = "\t", col.names = c("Chromosome", "Position", samplename), quote = FALSE)
}
#' Morphs phased SNPs from WGS input into haplotype blocks
-#'
+#'
#' @param chrom The chromosome number for which this function is called.
#' @param SNP_file File containing allele counts for each SNP location.
#' @param haplotypeFile File containing impute phasing output.
@@ -45,107 +43,339 @@ GetChromosomeBAFs_SNP6 = function(chrom, alleleFreqFile, haplotypeFile, samplena
#' @param chr_names Names of all allowed chromosomes as a Vector.
#' @param minCounts An integer describing the minimum number of reads covering this position to be included in the output.
#' @author dw9
+#' @importFrom data.table :=
#' @export
-GetChromosomeBAFs = function(chrom, SNP_file, haplotypeFile, samplename, outfile, chr_names, minCounts=1) {
- # Read in the SNP and haplotype info
- snp_data = read.table(SNP_file, comment.char="", sep="\t", header=T, stringsAsFactors=F)
- variant_data = read.table(haplotypeFile, header=F)
-
- # TODO: Check columns input
-
- print(snp_data[1:3,])
- print(chr_names)
- print(chrom)
-
- # Just select heterozygous SNPs
- het_variant_data = variant_data[variant_data[,6] != variant_data[,7],]
-
- chr_name = chrom
- print(chr_name)
-
- # Match allele counts and phasing info
- indices = match(het_variant_data[,3],snp_data[,2])
- het_variant_data = het_variant_data[!is.na(indices),]
- snp_indices = indices[!is.na(indices)]
- filtered_snp_data = snp_data[snp_indices,]
-
- # No matches found, save empty file and quit
- if(nrow(het_variant_data)==0 | is.null(het_variant_data)) {
- write.table(array(NA,c(0,3)),outfile,sep="\t",col.names=c("Chromosome","Position",samplename),quote=F,row.names=F)
- return()
- }
- print(filtered_snp_data[1:3,])
-
- # Decode 1,2,3,4 to A,C,G,T (encoding used in the variant_data input files)
- # TODO: place this in utils script? Isn't this also performed in GenerateImputeInputFromAlleleFrequencies.R?
- nucleotides=c("A","C","G","T")
- ref_indices = match(het_variant_data[cbind(1:nrow(het_variant_data),4+het_variant_data[,6])],nucleotides)
- alt_indices = match(het_variant_data[cbind(1:nrow(het_variant_data),4+het_variant_data[,7])],nucleotides)
-
- # Obtain counts for both alleles and the total
- ref.count = as.numeric(filtered_snp_data[cbind(1:nrow(filtered_snp_data),alt_indices+2)])
- alt.count = as.numeric(filtered_snp_data[cbind(1:nrow(filtered_snp_data),ref_indices+2)])
- denom = ref.count+alt.count
-
- # Filter out those SNPs that have less than minCounts reads
- min_indices = denom>=minCounts
- filtered_snp_data = filtered_snp_data[min_indices,]
- denom = denom[min_indices]
- alt.count = alt.count[min_indices]
-
- # No matches found, save empty file and quit
- if(nrow(filtered_snp_data)==0 | is.null(filtered_snp_data)) {
- write.table(array(NA,c(0,3)),outfile,sep="\t",col.names=c("Chromosome","Position",samplename),quote=F,row.names=F)
- return()
- }
-
- # Save all to disk
- hetMutBAFs = cbind(chr_name,filtered_snp_data[,2],alt.count/denom)
- write.table(hetMutBAFs,outfile,sep="\t",row.names=F,col.names=c("Chromosome","Position",samplename),quote=F)
+GetChromosomeBAFs <- function(
+ chrom,
+ SNP_file,
+ haplotypeFile,
+ samplename,
+ outfile,
+ chr_names,
+ minCounts = 1L
+) {
+ # Input validation
+ if (!chrom %in% chr_names) {
+ log_failure("chrom must be one of the allowed chromosomes specified in chr_names")
+ }
+ if (!file.exists(SNP_file)) log_failure("SNP_file not found: {SNP_file}")
+ if (!file.exists(haplotypeFile)) log_failure("haplotypeFile not found: {haplotypeFile}")
+ minCounts <- as.integer(minCounts)
+
+ log_info("Reading SNP file: {SNP_file}")
+ log_info("Reading haplotype file: {haplotypeFile}")
+
+ snp_dt <- data.table::fread(
+ SNP_file,
+ sep = "\t",
+ header = FALSE,
+ colClasses = list(character = 1)
+ )
+
+ phase_dt <- data.table::fread(
+ haplotypeFile,
+ header = FALSE
+ )
+ snp_pos_col_idx <- 2
+
+ # We assume the file HAS NO HEADER as per user feedback.
+ v2_is_num <- suppressWarnings(!is.na(as.numeric(snp_dt$V2[1])))
+ v3_is_num <- suppressWarnings(!is.na(as.numeric(snp_dt$V3[1])))
+
+ if (!v2_is_num && v3_is_num) {
+ log_info("Detected ID/RSID/POS format. Using Column 3 as Position.")
+ snp_pos_col_idx <- 3
+ }
+
+ # Convert the identified Position column to V2 (internal standard)
+ if (snp_pos_col_idx == 3) {
+ data.table::set(snp_dt, j = "V2", value = as.integer(as.numeric(snp_dt[[3]])))
+ } else {
+ # Standard V2 is Pos
+ suppressWarnings(
+ data.table::set(snp_dt, j = "V2", value = as.integer(as.numeric(snp_dt[[2]])))
+ )
+ }
+
+ # Ensure count columns (V3-V7) are integer if they look numeric
+ # This prevents "string" columns from breaking downstream math
+ for (col in paste0("V", 3:7)) {
+ if (col %in% names(snp_dt)) {
+ # Don't force if it's the Position column we just set (it's already int)
+ if (col == "V3" && snp_pos_col_idx == 3) next
+
+ val <- snp_dt[[col]]
+ if (is.numeric(val) || (is.character(val) && all(grepl("^[0-9]+$", na.omit(val))))) {
+ suppressWarnings(
+ data.table::set(snp_dt, j = col, value = as.integer(as.numeric(val)))
+ )
+ }
+ }
+ }
+
+ # Phase: V3 (Pos) -> int
+ if ("V3" %in% names(phase_dt)) {
+ suppressWarnings(
+ data.table::set(phase_dt, j = "V3", value = as.integer(as.numeric(phase_dt[["V3"]])))
+ )
+ }
+
+ # Check for empty data after filtering/type conversion
+ if (nrow(snp_dt) == 0) {
+ log_warning("SNP file is empty after filtering/type conversion: {SNP_file}")
+ write_empty_output(chrom, samplename, outfile)
+ return(invisible(NULL))
+ }
+ if (nrow(phase_dt) == 0) {
+ log_info("Haplotype file is empty (likely 0 phased SNPs): {haplotypeFile}")
+ write_empty_output(chrom, samplename, outfile)
+ return(invisible(NULL))
+ }
+
+ # Ensure column names exist before extraction
+ required_cols <- c("V3", "V6", "V7", "V4", "V5")
+ missing <- setdiff(required_cols, names(phase_dt))
+ if (length(missing) > 0) {
+ log_warning("Haplotype file {haplotypeFile} is missing required columns: {paste(missing, collapse=', ')}")
+ write_empty_output(chrom, samplename, outfile)
+ return(invisible(NULL))
+ }
+
+ # Use [[ indexing to explicitly reference columns by name (strings)
+ het_phase <- phase_dt[phase_dt[["V6"]] != phase_dt[["V7"]]]
+
+ if (nrow(het_phase) == 0) {
+ log_info("No heterozygous phased SNPs found on chromosome {chrom}")
+ write_empty_output(chrom, samplename, outfile)
+ return(invisible(NULL))
+ }
+
+ # Match positions using setkeyv (the string-based version of setkey)
+ data.table::setkeyv(snp_dt, "V2")
+
+ # Use list() instead of .() to avoid global function warnings
+ matched <- snp_dt[list(het_phase[["V3"]]), nomatch = NULL]
+
+ if (nrow(matched) == 0) {
+ write_empty_output(chrom, samplename, outfile)
+ return(invisible(NULL))
+ }
+
+ # Ensure count columns are numeric before matrix conversion
+ for (col in names(matched)[3:6]) {
+ matched[[col]] <- as.numeric(as.character(matched[[col]]))
+ }
+
+ if (nrow(matched) == 0) {
+ write_empty_output(chrom, samplename, outfile)
+ return(invisible(NULL))
+ }
+
+ # Filter het_phase based on matched positions
+ het_phase <- het_phase[het_phase[["V3"]] %in% matched[["V2"]]]
+
+ # Map nucleotide characters to column offsets (A=3, C=4, G=5, T=6)
+ nuc_to_col <- c(A = 3L, C = 4L, G = 5L, "T" = 6L)
+
+ # Extract phased alleles as characters
+ ref_allele <- toupper(ifelse(het_phase[["V6"]] == 0, het_phase[["V4"]], het_phase[["V5"]]))
+ alt_allele <- toupper(ifelse(het_phase[["V6"]] == 1, het_phase[["V4"]], het_phase[["V5"]]))
+
+ # Use matrix indexing to get counts safely without dynamic column warnings
+ # We select only the count columns (3 through 6)
+ count_matrix <- as.matrix(matched[, 3:6, with = FALSE])
+
+ # ref_allele and alt_allele map to 1:4 relative to the count_matrix
+ ref_idx <- nuc_to_col[ref_allele] - 2L
+ alt_idx <- nuc_to_col[alt_allele] - 2L
+
+ row_indices <- seq_len(nrow(count_matrix))
+ ref_count <- count_matrix[cbind(row_indices, ref_idx)]
+ alt_count <- count_matrix[cbind(row_indices, alt_idx)]
+
+ total_depth <- ref_count + alt_count
+ valid <- total_depth >= minCounts
+
+ if (!any(valid)) {
+ write_empty_output(chrom, samplename, outfile)
+ return(invisible(NULL))
+ }
+
+ # Construct output data.table
+ output_dt <- data.table::data.table(
+ Chromosome = chrom,
+ Position = matched[["V2"]][valid],
+ BAF = alt_count[valid] / total_depth[valid]
+ )
+ data.table::setnames(output_dt, "BAF", samplename)
+ data.table::fwrite(output_dt, file = outfile, sep = "\t", quote = FALSE)
}
-#' Plot haplotyped SNPs
-#'
-#' This function takes haplotyped SNPs and plots them to a png file.
-#' @param haplotyped.baf.file File containing the haplotyped SNP info.
-#' @param imageFileName Filename as which the png will be saved.
-#' @param samplename Name of the sample to be used in image title.
-#' @param chrom The chromosome that is plotted.
-#' @param chr_names A list of allowed chromosome names.
-#' @author dw9
+# Helper function to avoid code duplication
+write_empty_output <- function(chrom, samplename, outfile) {
+ empty_dt <- data.table::data.table(
+ Chromosome = character(),
+ Position = integer(),
+ dummy = numeric()
+ )
+ data.table::setnames(empty_dt, "dummy", samplename)
+ data.table::fwrite(empty_dt, file = outfile, sep = "\t", quote = FALSE)
+}
+
+#' Plot haplotyped BAF values for a single chromosome
+#'
+#' Reads a tab-separated file produced by GetChromosomeBAFs() (columns: Chromosome, Position, )
+#' and creates a high-resolution PNG showing the B Allele Frequency (BAF) mirrored around 0.5
+#' (standard haplotype/ASCAT-style plot).
+#'
+#' @param haplotyped_baf_file Path to the input TSV file with haplotyped BAF data.
+#' @param image_file_name Path to the output PNG file.
+#' @param samplename Name of the sample (used in plot title).
+#' @param chrom Chromosome identifier (used only for validation and title if data is empty).
+#'
+#' @return Invisibly returns NULL; side effect is writing the PNG file.
+#' @author Original: dw9; Modernized version
#' @export
-plot.haplotype.data = function(haplotyped.baf.file, imageFileName, samplename, chrom, chr_names) {
- chr_name = chrom
- mut_data = read.table(haplotyped.baf.file,sep="\t",header=T)
-
- if (nrow(mut_data) > 0) {
- x_min = min(mut_data$Position,na.rm=T)
- x_max = max(mut_data$Position,na.rm=T)
+plot_haplotype_data <- function(haplotyped_baf_file,
+ image_file_name,
+ samplename,
+ chrom) {
+ # Input validation
+ if (!file.exists(haplotyped_baf_file)) {
+ log_failure("Input file not found: ", haplotyped_baf_file)
+ }
+
+ # Read data (expecting columns: Chromosome, Position, )
+ baf_dt <- data.table::fread(haplotyped_baf_file, header = TRUE)
+
+ # Determine x-axis limits
+ if (nrow(baf_dt) == 0) {
+ log_info("No data in '{haplotyped_baf_file}' - creating empty plot")
+ x_min <- 1
+ x_max <- 2
+ positions <- numeric()
+ baf_values <- numeric()
+ plot_chrom <- chrom
} else {
- x_min = 1
- x_max = 2
- }
-
- png(filename = imageFileName, width = 10000, height = 2500, res = 500, type = "cairo")
- create.haplotype.plot(chrom.position=mut_data$Position,
- points.blue=mut_data[,3],
- points.red=1-mut_data[,3],
- x.min=x_min,
- x.max=x_max,
- title=paste(samplename,", chromosome",mut_data[1,1], sep=" "),
- xlab="pos",
- ylab="BAF")
- dev.off()
-}
+ x_min <- min(baf_dt$Position, na.rm = TRUE)
+ x_max <- max(baf_dt$Position, na.rm = TRUE)
+ positions <- baf_dt$Position
+ # third column is the sample BAF
+ baf_values <- baf_dt[[3]]
+ plot_chrom <- baf_dt$Chromosome[1]
+ }
-#' Combines all separate BAF files per chromosome into a single file
+ # Open PNG device with reasonable size and resolution
+ grDevices::png(
+ filename = image_file_name,
+ width = 1200, height = 600, res = 150, type = "cairo"
+ )
+
+ # Assuming create_haplotype_plot is a custom function available in your package/environment
+ create_haplotype_plot(
+ chrom_position = positions,
+ points.blue = baf_values,
+ points.red = 1 - baf_values,
+ x_min = x_min,
+ x_max = x_max,
+ title = paste(samplename, ", chromosome", plot_chrom),
+ xlab = "Position",
+ ylab = "BAF"
+ )
+
+ grDevices::dev.off()
+ invisible(NULL)
+}
+#' Combine per-chromosome BAF files into a single table
#'
-#' @param inputfile.prefix Prefix of the input files until the chromosome number. The chromosome number will be added internally
-#' @param inputfile.postfix Postfix of the input files from the chromosome number
-#' @param outputfile Full path to where the output will be written
-#' @param chr_names A list of allowed chromosome names.
-#' @author dw9
+#' @param prefix File path prefix before chromosome name
+#' @param suffix File path suffix after chromosome name
+#' @param chroms Character vector of chromosome names
+#' @param output Path to output TSV file
+#'
+#' @return Invisibly returns the combined data.frame
#' @export
-combine.baf.files = function(inputfile.prefix, inputfile.postfix, outputfile, chr_names) {
- concatenateBAFfiles(inputfile.prefix, inputfile.postfix, outputfile, chr_names)
+concatenate_baf_files <- function(
+ input_start,
+ input_end,
+ output_file,
+ chr_names
+) {
+ files <- paste0(input_start, chr_names, input_end)
+
+ log_info("Starting concatenation for {length(chr_names)} expected BAF files {files}")
+
+
+ # Filter for existing and non-empty files
+ valid_files <- files[
+ fs::file_exists(files) &
+ fs::file_size(files) > 0
+ ]
+
+ exists_mask <- fs::file_exists(files)
+ size_mask <- fs::file_size(files) > 0
+ missing_chrs <- chr_names[!exists_mask]
+ if (base::length(missing_chrs) > 0) {
+ log_info("Chromosomes missing files: {base::paste(missing_chrs, collapse = ', ')}")
+ }
+
+ empty_chrs <- chr_names[exists_mask & !size_mask]
+ if (base::length(empty_chrs) > 0) {
+ log_info("DATA ISSUE: Chromosomes with 0-byte files: {base::paste(empty_chrs, collapse = ', ')}")
+ }
+
+ valid_files <- files[exists_mask & size_mask]
+
+ if (base::length(valid_files) == 0) {
+ log_info("CRITICAL: Zero valid BAF files found across all chromosomes.")
+ }
+
+ log_info("Proceeding to combine {base::length(valid_files)} valid files")
+
+ # Force first column to character
+ # Use column index 1 to avoid needing names(vroom(...)) twice
+ first_file_cols <- names(vroom::vroom(
+ valid_files[1],
+ n_max = 0,
+ progress = FALSE,
+ show_col_types = FALSE
+ ))
+ col_spec <- vroom::cols(
+ .default = vroom::col_guess(),
+ !!!stats::setNames(list(vroom::col_character()), first_file_cols[1])
+ )
+
+ log_info("Reading data using column spec based on {fs::path_file(valid_files[1])}")
+
+ combined <- vroom::vroom(
+ valid_files,
+ id = "file_path",
+ delim = "\t",
+ col_types = col_spec,
+ progress = FALSE,
+ show_col_types = FALSE,
+ .name_repair = "minimal"
+ ) |>
+ dplyr::select(-dplyr::any_of("file_path"))
+
+ total_rows <- base::nrow(combined)
+ if (total_rows == 0) {
+ log_failure("DATA ISSUE: Files were read but the resulting table is empty.")
+ }
+
+ log_info("Total combined rows: {base::format(total_rows, big.mark = ',')}")
+
+ # Ensure output directory exists
+ fs::dir_create(fs::path_dir(output_file), recurse = TRUE)
+
+ # Write output
+ vroom::vroom_write(
+ combined,
+ file = output_file,
+ delim = "\t",
+ na = "NA",
+ quote = "none"
+ )
+
+ log_info("BAF concatenation complete. Final file size: {fs::file_size(output_file)}")
}
diff --git a/R/haplotype_external.R b/R/haplotype_external.R
index 246ad2b5..36d75c10 100644
--- a/R/haplotype_external.R
+++ b/R/haplotype_external.R
@@ -1,30 +1,31 @@
-
#' Split a single vcf into separate vcfs for each chromosome
#' @param chrom_names Names of the chromosomes
#' @param externalHaplotypeFile Full path of the external vcf containing phased haplotypes (Default: NA)
#' @param outprefix Full path and prefix of the output files
#' @author jdemeul
#' @export
-split_input_haplotypes <- function(chrom_names, externalhaplotypefile=NA, outprefix) {
+split_input_haplotypes <- function(chrom_names, externalhaplotypefile = NA, outprefix) {
+ if (is.na(externalhaplotypefile)) {
+ return(NULL)
+ }
+
+ hetsnps <- VariantAnnotation::readVcf(
+ file = externalhaplotypefile,
+ param = VariantAnnotation::ScanVcfParam(fixed = "ALT", info = NA, geno = c("GT", "PS"), trimEmpty = TRUE)
+ )
- if (is.na(externalhaplotypefile)) return(NULL)
-
- hetsnps <- VariantAnnotation::readVcf(file = externalhaplotypefile,
- param = VariantAnnotation::ScanVcfParam(fixed = "ALT", info = NA, geno = c("GT", "PS"), trimEmpty = T))
-
hetsnps <- split(x = hetsnps, f = GenomicRanges::seqnames(hetsnps))
hetsnps <- hetsnps[chrom_names]
-
+
lapply(X = chrom_names, FUN = function(chrom, chrom_names, snps, outbase) {
VariantAnnotation::writeVcf(obj = snps[[chrom]], filename = paste0(outbase, chrom, ".vcf"))
}, snps = hetsnps, outbase = outprefix, chrom_names = chrom_names)
-
+
return(NULL)
}
-
-#' Combine imputation results with external haplotype blocks
+#' Combine imputation results with external haplotype blocks
#' @param chrom_names Names of the chromosomes
#' @param chrom chromosome for which to reconstruct haplotypes
#' @param imputedHaplotypeFile Full path to the imputed haplotyope file for the indexed chromosome
@@ -32,154 +33,240 @@ split_input_haplotypes <- function(chrom_names, externalhaplotypefile=NA, outpre
#' @param oldfilesuffix Suffix to be added to the original imputedHaplotypeFile (Default: _noExt.txt)
#' @author jdemeul
#' @export
-input_known_haplotypes = function(chrom_names, chrom, imputedHaplotypeFile, externalHaplotypeFile=NA, oldfilesuffix = "_noExt.txt") {
+input_known_haplotypes <- function(chrom_names, chrom, imputedHaplotypeFile, externalHaplotypeFile = NA, oldfilesuffix = "_noExt.txt") {
+ if (is.na(externalHaplotypeFile)) {
+ return(NULL)
+ }
- if (is.na(externalHaplotypeFile)) return(NULL)
-
# read BB phasing input
- bbphasin <- read_imputed_output(file = imputedHaplotypeFile)
-
+ bbphasin <- read_imputed_output(filename = imputedHaplotypeFile)
+
# turn into GRanges and subset for het SNPs
bbphasingr <- GenomicRanges::GRanges(seqnames = rep(chrom, nrow(bbphasin)), ranges = IRanges::IRanges(start = bbphasin$pos, width = 1))
S4Vectors::mcols(bbphasingr) <- bbphasin[, c("alt", "hap1", "hap2")]
bbphasingr <- bbphasingr[which(xor(bbphasingr$hap1 == 1, bbphasingr$hap2 == 1))]
-
+
# load vcf containing external haplotyped variants
- hetsnps <- suppressWarnings(VariantAnnotation::readVcf(file = externalHaplotypeFile,
- param = VariantAnnotation::ScanVcfParam(fixed = "ALT", info = NA, geno = c("GT", "PS"), trimEmpty = T)))
-
+ hetsnps <- suppressWarnings(VariantAnnotation::readVcf(
+ file = externalHaplotypeFile,
+ param = VariantAnnotation::ScanVcfParam(fixed = "ALT", info = NA, geno = c("GT", "PS"), trimEmpty = TRUE)
+ ))
+
# subset to phased het SNPs on chrom & drop any multiallelic var & indels if present
hetsnps <- hetsnps[which(VariantAnnotation::geno(hetsnps)$GT %in% c("0|1", "1|0"))]
hetsnps <- hetsnps[which(lengths(VariantAnnotation::alt(hetsnps)) == 1)]
hetsnps <- hetsnps[which(S4Vectors::nchar(VariantAnnotation::ref(hetsnps)) == 1 & unlist(S4Vectors::nchar(VariantAnnotation::alt(hetsnps))) == 1)]
-
+
# e.g. if no phasing on X, no need to continue
- if (length(hetsnps) == 0) return(NULL)
-
+ if (length(hetsnps) == 0) {
+ return(NULL)
+ }
+
# match Battenberg het SNPs with those in external file, take only ranges to avoid chrom names mismatch
snvoverlaps <- IRanges::findOverlaps(query = IRanges::ranges(bbphasingr), subject = IRanges::ranges(hetsnps), type = "equal")
- # and make sure we're phasing the same REF/ALT alleles (ref will always be the same)
- snvoverlaps_sub <- snvoverlaps[which(bbphasingr[S4Vectors::queryHits(snvoverlaps)]$alt ==
- as.character(unlist(VariantAnnotation::alt(hetsnps[S4Vectors::subjectHits(snvoverlaps)]))))]
-
+
# add the corresponding phaseblocks (PS) and genotypes (GT)
bbphasingr$PS <- vector(mode = "integer", length = length(bbphasingr))
bbphasingr$GT <- vector(mode = "character", length = length(bbphasingr))
bbphasingr[S4Vectors::queryHits(snvoverlaps)]$PS <- VariantAnnotation::geno(hetsnps[S4Vectors::subjectHits(snvoverlaps)])$PS
bbphasingr[S4Vectors::queryHits(snvoverlaps)]$GT <- VariantAnnotation::geno(hetsnps[S4Vectors::subjectHits(snvoverlaps)])$GT
-
+
# extract external haplotype 1 and match to imputed haplotypes
bbphasingr$hap1_10X <- substr(bbphasingr$GT, start = 1, stop = 1)
bbphasingr$isH1 <- ifelse(bbphasingr$hap1_10X == "", NA, bbphasingr$hap1_10X == bbphasingr$hap1)
-
+
# complete and extend the known haplotype blocks
# by transfering imputed haplotypes to nearest non-phased het SNPs
- # bbphasingr <- GenomicRanges::GRangesList(split(x = bbphasingr, f = bbphasingr$hap1_10X != ""), compress = F)
- bbphasingr <- as(object = split(x = bbphasingr, f = bbphasingr$hap1_10X != ""), Class = "GRangesList")
- if (length(bbphasingr$'FALSE') > 0) {
- nearestidxs <- GenomicRanges::nearest(x = bbphasingr$'FALSE', subject = bbphasingr$'TRUE', select = "arbitrary")
- bbphasingr$'FALSE'$isH1 <- bbphasingr$'TRUE'$isH1[nearestidxs]
- bbphasingr$'FALSE'$PS <- bbphasingr$'TRUE'$PS[nearestidxs]
+ # bbphasingr <- GenomicRanges::GRangesList(split(x = bbphasingr, f = bbphasingr$hap1_10X != ""), compress = FALSE)
+ bbphasingr <- methods::as(object = split(x = bbphasingr, f = bbphasingr$hap1_10X != ""), Class = "GRangesList")
+ if (length(bbphasingr$"FALSE") > 0) {
+ nearestidxs <- GenomicRanges::nearest(x = bbphasingr$"FALSE", subject = bbphasingr$"TRUE", select = "arbitrary")
+ bbphasingr$"FALSE"$isH1 <- bbphasingr$"TRUE"$isH1[nearestidxs]
+ bbphasingr$"FALSE"$PS <- bbphasingr$"TRUE"$PS[nearestidxs]
}
- bbphasingr <- GenomicRanges::sort(unlist(bbphasingr, use.names = F))
-
+ bbphasingr <- GenomicRanges::sort(unlist(bbphasingr, use.names = FALSE))
+
# build final haplotypes by flipping blocks according to imputation
# last haplotype assignment of first block must match first haplotype assignment of second block
psrle <- S4Vectors::Rle(bbphasingr$PS)
- flip <- cumsum(c(F, bbphasingr$isH1[S4Vectors::start(psrle)[-1]] == bbphasingr$isH1[S4Vectors::end(psrle)[-S4Vectors::nrun(psrle)]])) %% 2
+ flip <- cumsum(c(FALSE, bbphasingr$isH1[S4Vectors::start(psrle)[-1]] == bbphasingr$isH1[S4Vectors::end(psrle)[-S4Vectors::nrun(psrle)]])) %% 2
S4Vectors::runValue(psrle) <- flip
bbphasingr$isH1 <- ifelse(as.vector(psrle, mode = "logical"), !bbphasingr$isH1, bbphasingr$isH1)
bbphasingr$hapfinal <- ifelse(bbphasingr$isH1, bbphasingr$hap1, bbphasingr$hap2)
-
+
# reinsert the phased het SNP haplotypes into the total chromosomal haplotypes
matchidxs <- match(x = GenomicRanges::start(bbphasingr), table = bbphasin$pos)
bbphasin[matchidxs, "hap1"] <- bbphasingr$hapfinal
bbphasin[matchidxs, "hap2"] <- abs(bbphasin[matchidxs, "hap1"] - 1)
-
+
# backup original imputedHaplotypeFile
if (file.exists(imputedHaplotypeFile)) {
- file.copy(from = imputedHaplotypeFile, to = gsub(pattern = "\\.txt$", replacement = oldfilesuffix, x = imputedHaplotypeFile), overwrite = T)
+ file.copy(from = imputedHaplotypeFile, to = gsub(pattern = "\\.txt$", replacement = oldfilesuffix, x = imputedHaplotypeFile), overwrite = TRUE)
}
-
+
# and write new version
- write.table(x = bbphasin, file=imputedHaplotypeFile, row.names=F, col.names=F, quote=F, sep="\t")
+ data.table::fwrite(x = bbphasin, file = imputedHaplotypeFile, row.names = FALSE, col.names = FALSE, quote = FALSE, sep = "\t")
return(NULL)
}
-
-
-#' Writes the imputation and copy number phased haplotypes to a vcf
+#' Writes the imputation and copy number phased haplotypes to a VCF
#' @param tumourname Sample name
-#' @param SNPfiles Character vector of the paths to the alleleFrequencies files, ordered by chromosome index
-#' @param imputedHaplotypeFiles Character vector of the paths to the impute_output files, ordered by chromosome index
-#' @param bafsegmented_file Path to the BAFSegmented file
-#' @param outprefix Prefix to write the output vcf files to
-#' @param chrom_names Names of the chromosomes
-#' @param include_homozygous Include homozygous SNPs in the output vcf file (Default = FALSE)
-#' @author jdemeul
+#' @param SNPfiles Character vector of alleleFrequency files (per chromosome)
+#' @param imputedHaplotypeFiles Character vector of impute2 haplotype files
+#' @param bafsegmented_file Path to BAFSegmented file
+#' @param outprefix Output VCF prefix
+#' @param chrom_names Chromosome names
+#' @param include_homozygous Include homozygous SNPs (default FALSE)
+#' @importFrom data.table :=
#' @export
-write_battenberg_phasing <- function(tumourname, SNPfiles, imputedHaplotypeFiles, bafsegmented_file, outprefix, chrom_names, include_homozygous = F) {
-
- bafsegmented <- read_bafsegmented(bafsegmented_file)[, c("Chromosome", "Position", "BAFphased", "BAFseg")]
- bafsegmented <- split(x = bafsegmented[, c("Position", "BAFphased", "BAFseg")], f = bafsegmented$Chromosome)
- for (i in 1:length(chrom_names)) {
- chrom = chrom_names[i]
- # read allele counts and imputed haplotypes (for the actually used alleles & loci)
- snp_data <- read_alleleFrequencies(SNPfiles[i])
- allele_data <- read_imputed_output(imputedHaplotypeFiles[i])[, c("pos", "ref", "alt", "hap1", "hap2")]
- merge_data <- merge(x = allele_data, y = snp_data, by.x = "pos", by.y = "POS", sort = F)
-
- # map counts to ref/alt
- merge_data$ref_count <- ifelse(merge_data$ref == "A", merge_data$Count_A,
- ifelse(merge_data$ref == "C", merge_data$Count_C,
- ifelse(merge_data$ref == "G", merge_data$Count_G, merge_data$Count_T)))
- merge_data$alt_count <- ifelse(merge_data$alt == "A", merge_data$Count_A,
- ifelse(merge_data$alt == "C", merge_data$Count_C,
- ifelse(merge_data$alt == "G", merge_data$Count_G, merge_data$Count_T)))
- merge_data <- cbind(merge_data[, c("CHR", "pos", "ref", "alt", "ref_count", "alt_count", "hap1", "hap2")], BAF = merge_data$alt_count/(merge_data$ref_count+merge_data$alt_count))
-
- # add in the segmented BAF values and start creating output vcf
- merge_data <- merge(x = merge_data, y = bafsegmented[[chrom]], by.x = "pos", by.y = "Position",
- all.x = include_homozygous, sort = T)
-
- bbphasing_vr <- VariantAnnotation::VRanges(seqnames = merge_data$CHR, ranges = IRanges::IRanges(start = merge_data$pos, width = 1),
- ref = merge_data$ref, alt = merge_data$alt,
- totalDepth = merge_data$ref_count+merge_data$alt_count,
- refDepth = merge_data$ref_count, altDepth = merge_data$alt_count)
-
- # assign the genotypes based on flipping of individual BAF values in regions of allelic imbalance according to BAFseg
- S4Vectors::mcols(bbphasing_vr)$GT <- ifelse(is.na(merge_data$BAFphased), paste0(merge_data$hap1, "|", merge_data$hap2),
- ifelse(merge_data$BAFseg > 0.525 | is.na(merge_data$BAFseg),
- ifelse(abs(merge_data$BAFphased-merge_data$BAF) < 1e-5, "1|0", "0|1"),
- ifelse(abs(merge_data$BAFphased-merge_data$BAF) < 1e-5, "1/0", "0/1")))
-
- # add phase set annotation based on segmented BAF: every segment = phase set
- S4Vectors::mcols(bbphasing_vr)$PS <- as.integer(NA)
- phasedidx <- which(merge_data$BAFseg > 0.525)
- if (length(phasedidx) > 0) {
- hetsegrle <- S4Vectors::Rle(merge_data$BAFseg[phasedidx])
- S4Vectors::mcols(bbphasing_vr)$PS[phasedidx] <- rep(GenomicRanges::start(bbphasing_vr)[phasedidx][S4Vectors::start(hetsegrle)], S4Vectors::runLength(hetsegrle))
-
- if (length(phasedidx) < nrow(merge_data)) {
- S4Vectors::mcols(bbphasing_vr)$PS[-phasedidx] <- S4Vectors::mcols(bbphasing_vr)$PS[phasedidx][GenomicRanges::nearest(x = bbphasing_vr[-phasedidx], subject = bbphasing_vr[phasedidx], select = "arbitrary")]
+write_battenberg_phasing <- function(
+ tumourname,
+ SNPfiles,
+ imputedHaplotypeFiles,
+ bafsegmented_file,
+ outprefix,
+ chrom_names,
+ include_homozygous = FALSE
+) {
+ ## ---- Load & standardize BAF segments ----
+ baf_dt <- read_bafsegmented(bafsegmented_file)
+ data.table::setDT(baf_dt)
+ data.table::setnames(
+ baf_dt,
+ old = c("Chromosome", "chrom", "chr"),
+ new = c("CHR", "CHR", "CHR"),
+ skip_absent = TRUE
+ )
+ baf_dt[, Position := as.integer(Position)]
+ data.table::setkey(baf_dt, CHR, Position)
+
+ ## ---- Impute2 schema ----
+ impute_cols <- c("index", "rsid", "Position", "ref", "alt", "hap1", "hap2")
+
+ for (idx in seq_along(chrom_names)) {
+ chrom <- chrom_names[[idx]]
+
+ ## ---- SNP / allele frequency ----
+ snp_dt <- data.table::fread(SNPfiles[[idx]])
+ data.table::setDT(snp_dt)
+ data.table::setnames(
+ snp_dt,
+ old = c("Chromosome", "Chr", "POS"),
+ new = c("CHR", "CHR", "Position"),
+ skip_absent = TRUE
+ )
+ snp_dt[, Position := as.integer(Position)]
+ snp_dt[, CHR := chrom]
+ data.table::setkey(snp_dt, Position)
+
+ ## ---- Imputed haplotypes ----
+ hap_dt <- data.table::fread(
+ imputedHaplotypeFiles[[idx]],
+ header = FALSE,
+ col.names = impute_cols
+ )
+ hap_dt <- hap_dt[, .(Position, ref, alt, hap1, hap2)]
+ hap_dt[, Position := as.integer(Position)]
+ data.table::setkey(hap_dt, Position)
+
+ ## ---- Merge SNP + haplotypes ----
+ dt <- snp_dt[hap_dt, nomatch = NULL]
+ if (nrow(dt) == 0L) next
+
+ ## ---- Allele counts ----
+ dt[, ref_count := data.table::fcase(
+ dt[["ref"]] == "A", dt[["Count_A"]],
+ dt[["ref"]] == "C", dt[["Count_C"]],
+ dt[["ref"]] == "G", dt[["Count_G"]],
+ dt[["ref"]] == "T", dt[["Count_T"]],
+ default = NA_real_
+ )]
+
+ dt[, alt_count := data.table::fcase(
+ dt[["alt"]] == "A", dt[["Count_A"]],
+ dt[["alt"]] == "C", dt[["Count_C"]],
+ dt[["alt"]] == "G", dt[["Count_G"]],
+ dt[["alt"]] == "T", dt[["Count_T"]],
+ default = NA_real_
+ )]
+
+ dt[, BAF := dt[["alt_count"]] / (dt[["ref_count"]] + dt[["alt_count"]])]
+
+ ## ---- Merge BAF segments ----
+ baf_chr <- baf_dt[CHR == chrom, .(Position, BAFphased, BAFseg)]
+ data.table::setkey(baf_chr, Position)
+
+ if (include_homozygous) {
+ dt <- baf_chr[dt]
+ } else {
+ dt <- dt[baf_chr, nomatch = NULL]
+ }
+ if (nrow(dt) == 0L) next
+
+ ## ---- Build VRanges ----
+ vr <- VariantAnnotation::VRanges(
+ seqnames = dt[["CHR"]],
+ ranges = IRanges::IRanges(start = dt[["Position"]], width = 1),
+ ref = dt[["ref"]],
+ alt = dt[["alt"]],
+ totalDepth = dt[["ref_count"]] + dt[["alt_count"]],
+ refDepth = dt[["ref_count"]],
+ altDepth = dt[["alt_count"]]
+ )
+
+ ## ---- Genotype logic ----
+ gt_vec <- data.table::fcase(
+ is.na(dt[["BAFphased"]]),
+ paste0(dt[["hap1"]], "|", dt[["hap2"]]),
+ dt[["BAFseg"]] > 0.525 | is.na(dt[["BAFseg"]]),
+ ifelse(abs(dt[["BAFphased"]] - dt[["BAF"]]) < 1e-5, "1|0", "0|1"),
+ default =
+ ifelse(abs(dt[["BAFphased"]] - dt[["BAF"]]) < 1e-5, "1/0", "0/1")
+ )
+
+ ## ---- Phase set (PS) ----
+ n <- nrow(dt)
+ ps <- rep(NA_integer_, n)
+ phased_idx <- which(dt[["BAFseg"]] > 0.525)
+
+ if (length(phased_idx) > 0) {
+ rle_seg <- S4Vectors::Rle(dt[["BAFseg"]][phased_idx])
+ ps[phased_idx] <- rep(
+ dt[["Position"]][phased_idx][S4Vectors::start(rle_seg)],
+ S4Vectors::runLength(rle_seg)
+ )
+
+ unphased <- setdiff(seq_len(n), phased_idx)
+ if (length(unphased) > 0) {
+ nearest <- GenomicRanges::nearest(
+ vr[unphased],
+ vr[phased_idx],
+ select = "arbitrary"
+ )
+ ps[unphased] <- ps[phased_idx][nearest]
}
} else {
- S4Vectors::mcols(bbphasing_vr)$PS <- rep(GenomicRanges::start(bbphasing_vr)[1], nrow(merge_data))
+ ps[] <- dt[["Position"]][1]
}
-
- # write out vcf
- VariantAnnotation::sampleNames(bbphasing_vr) <- tumourname
-
- VariantAnnotation::writeVcf(obj = bbphasing_vr, filename = paste0(outprefix, chrom, ".vcf"), index = F)
-
- }
- return(NULL)
-}
+ ## ---- Attach metadata ----
+ S4Vectors::mcols(vr)$GT <- gt_vec
+ S4Vectors::mcols(vr)$PS <- ps
+ VariantAnnotation::sampleNames(vr) <- tumourname
+ ## ---- Write VCF ----
+ VariantAnnotation::writeVcf(
+ vr,
+ filename = paste0(outprefix, chrom, ".vcf"),
+ index = FALSE
+ )
+ }
+ invisible(NULL)
+}
-#' Generates phased haplotypes from multisample Battenberg runs
+#' Compute multisample phasing for common hetSNPs
+#'
#' @param chrom chromosome for which to obtain haplotypes
#' @param bbphasingprefixes Vector containing prefixes of the Battenberg_phased_chr files for the multiple samples
#' @param maxlag Maximal number of upstream SNPs used to inform the haplotype at another SNPs
@@ -189,105 +276,108 @@ write_battenberg_phasing <- function(tumourname, SNPfiles, imputedHaplotypeFiles
#' @export
get_multisample_phasing <- function(chrom, bbphasingprefixes, maxlag = 90, relative_weight_balanced = .25, outprefix) {
vcfs <- lapply(X = paste0(bbphasingprefixes, chrom, ".vcf"), FUN = VariantAnnotation::readVcf)
- samplenames <- sapply(X = vcfs, FUN = function(x) VariantAnnotation::samples(VariantAnnotation::header(x)))
-
+
# get common hetSNP loci
temp <- do.call(c, lapply(X = vcfs, FUN = SummarizedExperiment::rowRanges))
- commonloci <- unique(names(which(GenomicRanges::countOverlaps(query = temp, type = "equal", drop.self = F, drop.redundant = F) == length(vcfs))))
+ commonloci <- unique(names(which(GenomicRanges::countOverlaps(query = temp, type = "equal", drop.self = FALSE, drop.redundant = FALSE) == length(vcfs))))
vcfs_common <- lapply(X = vcfs, FUN = function(x, commonloci) GenomicRanges::sort(x[commonloci]), commonloci = commonloci)
-
+
# clean up
rm(vcfs, temp, commonloci)
-
+
# go through each vcf and add relevant columns as appropriate
loci <- SummarizedExperiment::rowRanges(vcfs_common[[1]])
- for (vcfidx in 1:length(vcfs_common)) {
+ for (vcfidx in seq_along(vcfs_common)) {
# add the genotype, BAF and phaseblock info for each sample to all common loci
singlevcf <- vcfs_common[[vcfidx]]
sid <- VariantAnnotation::samples(VariantAnnotation::header(singlevcf))
- adddf <- S4Vectors::DataFrame(Major = VariantAnnotation::geno(singlevcf)$GT[,1], #Major = as.integer(ifelse(test = grepl(pattern = "|", x = geno(singlevcf)$GT, fixed = T), substr(x = geno(singlevcf)$GT, 1, 1), NA)),
- #BAF = VariantAnnotation::geno(singlevcf)$AD[,1,2]/BiocGenerics::rowSums(VariantAnnotation::geno(singlevcf)$AD[,1,]),
- BAF = VariantAnnotation::geno(singlevcf)$AD[,1,2]/rowSums(VariantAnnotation::geno(singlevcf)$AD[,1,]),
- PS = VariantAnnotation::geno(singlevcf)$PS[,1])
+ adddf <- S4Vectors::DataFrame(
+ Major = VariantAnnotation::geno(singlevcf)$GT[, 1], # Major = as.integer(ifelse(test = grepl(pattern = "|", x = geno(singlevcf)$GT, fixed = TRUE), substr(x = geno(singlevcf)$GT, 1, 1), NA)),
+ # BAF = VariantAnnotation::geno(singlevcf)$AD[,1,2]/BiocGenerics::rowSums(VariantAnnotation::geno(singlevcf)$AD[,1,]),
+ BAF = VariantAnnotation::geno(singlevcf)$AD[, 1, 2] / rowSums(VariantAnnotation::geno(singlevcf)$AD[, 1, ]),
+ PS = VariantAnnotation::geno(singlevcf)$PS[, 1]
+ )
colnames(adddf) <- paste0(sid, "_", colnames(adddf))
S4Vectors::mcols(loci) <- cbind(S4Vectors::mcols(loci), adddf)
}
-
-
+
+
# get call for alt-ref switches at different lag intervals 1:maxlag
# also keep track of which are evidenced by allelic imbalance in >= 1 sample and downweight the inference contribution from the other samples to relative_weight_balanced
gtswitcheslist <- list()
evidencelist <- list()
-
+
for (lag in 1:maxlag) {
# lag <- 1
- gtswitcheslist[[lag]] <- rbind(matrix(NA, nrow = lag - 1, ncol = length(vcfs_common)), apply(MARGIN = 2, X = S4Vectors::mcols(loci)[,grep(pattern = "Major", x = colnames(S4Vectors::mcols(loci)))],
- FUN = function(x, lag) abs(diff(as.integer(substr(x,1,1)), lag = lag)), lag = lag))
-
+ gtswitcheslist[[lag]] <- rbind(matrix(NA, nrow = lag - 1, ncol = length(vcfs_common)), apply(
+ MARGIN = 2, X = S4Vectors::mcols(loci)[, grep(pattern = "Major", x = colnames(S4Vectors::mcols(loci)))],
+ FUN = function(x, lag) abs(diff(as.integer(substr(x, 1, 1)), lag = lag)), lag = lag
+ ))
+
# check whether all are phased, note that the filter takes into account past values only here! So needs to be shifted in next step
- #evidencelist[[lag]] <- apply(MARGIN = 2, X = S4Vectors::mcols(loci)[,grep(pattern = "Major", x = colnames(S4Vectors::mcols(loci)))],
- # FUN = function(x, lag) dplyr::filter(x = grepl(pattern = "|", x = x, fixed = T), filter = rep(1, lag + 1), sides = 1) == lag+1, lag = lag)
- evidencelist[[lag]] <- apply(
- MARGIN = 2,
- X = S4Vectors::mcols(loci)[, grep(pattern = "Major", x = colnames(S4Vectors::mcols(loci)))],
- FUN = function(x, lag) {
- # First, find positions where the pattern "|" exists
- logical_vector <- grepl(pattern = "|", x = x, fixed = TRUE)
- numeric_vector <- as.numeric(logical_vector)
- result <- rep(FALSE, length(numeric_vector))
-
- if (length(numeric_vector) > lag) {
- # Then apply time series smoothing using stats::filter
- smoothed <- stats::filter(x = numeric_vector, filter = rep(1, lag + 1), sides = 1)
- smoothed[is.na(smoothed)] <- 0
-
- # Check where the smoothed values equal lag+1
- result[1:length(smoothed)] <- (smoothed == lag + 1)
- }
- return(result)
- },
- lag = lag
- )
+ evidencelist[[lag]] <- apply(
+ MARGIN = 2,
+ X = S4Vectors::mcols(loci)[, grep(pattern = "Major", x = colnames(S4Vectors::mcols(loci)))],
+ FUN = function(x, lag) {
+ # First, find positions where the pattern "|" exists
+ logical_vector <- grepl(pattern = "|", x = x, fixed = TRUE)
+ numeric_vector <- as.numeric(logical_vector)
+ result <- rep(FALSE, length(numeric_vector))
+
+ if (length(numeric_vector) > lag) {
+ # Then apply time series smoothing using stats::filter
+ smoothed <- stats::filter(x = numeric_vector, filter = rep(1, lag + 1), sides = 1)
+ smoothed[is.na(smoothed)] <- 0
+
+ # Check where the smoothed values equal lag+1
+ result[seq_along(smoothed)] <- (smoothed == lag + 1)
+ }
+ return(result)
+ },
+ lag = lag
+ )
# and they have the same PS
# evidencelist[[lag]] <- (evidencelist[[lag]][-1,] * rbind(matrix(NA, nrow = lag-1, ncol = length(vcfs_common)), apply(MARGIN = 2, X = mcols(loci)[,grep(pattern = "PS", x = colnames(mcols(loci)))],
# FUN = function(x, lag) diff(x = x, lag = lag) == 0, lag = lag))) == 1
- evidencelist[[lag]] <- evidencelist[[lag]][-1,] * rbind(matrix(NA, nrow = lag-1, ncol = length(vcfs_common)), apply(MARGIN = 2, X = S4Vectors::mcols(loci)[,grep(pattern = "PS", x = colnames(S4Vectors::mcols(loci)))],
- FUN = function(x, lag) diff(x = x, lag = lag) == 0, lag = lag))
+ evidencelist[[lag]] <- evidencelist[[lag]][-1, ] * rbind(matrix(NA, nrow = lag - 1, ncol = length(vcfs_common)), apply(
+ MARGIN = 2, X = S4Vectors::mcols(loci)[, grep(pattern = "PS", x = colnames(S4Vectors::mcols(loci)))],
+ FUN = function(x, lag) diff(x = x, lag = lag) == 0, lag = lag
+ ))
evidencelist[[lag]][evidencelist[[lag]] == 0] <- relative_weight_balanced
- evidencelist[[lag]] <- evidencelist[[lag]]/rowSums(evidencelist[[lag]])
+ evidencelist[[lag]] <- evidencelist[[lag]] / rowSums(evidencelist[[lag]])
}
-
+
# initiate the vector which will cntain the combined phased haplotype
haplovect <- as.integer(rep(NA, length(loci)))
-
+
# start with a simple majorty call for the first hetSNP
- haplovect[1] <- as.integer(names(sort(table(substr(unlist(S4Vectors::mcols(loci)[1,grep(pattern = "Major", x = colnames(S4Vectors::mcols(loci))), drop = T]),1,1)), decreasing = T)[1]))
-
+ haplovect[1] <- as.integer(names(sort(table(substr(unlist(S4Vectors::mcols(loci)[1, grep(pattern = "Major", x = colnames(S4Vectors::mcols(loci))), drop = T]), 1, 1)), decreasing = TRUE)[1]))
+
# votes for next positions integrate more laged inferences
for (pos in 2:length(loci)) {
nvotesalt <- 0
if (pos - 1 > maxlag) maxlag_used <- maxlag else maxlag_used <- pos - 1
lagwsum <- sum(1:maxlag_used) # used to downweight larger distances
for (lag in 1:maxlag_used) {
- nvotesalt <- nvotesalt + sum(abs(haplovect[pos-lag] - gtswitcheslist[[lag]][pos-1, ])*evidencelist[[lag]][pos-1,]) * (maxlag_used + 1 - lag) / lagwsum
+ nvotesalt <- nvotesalt + sum(abs(haplovect[pos - lag] - gtswitcheslist[[lag]][pos - 1, ]) * evidencelist[[lag]][pos - 1, ]) * (maxlag_used + 1 - lag) / lagwsum
}
haplovect[pos] <- round(nvotesalt)
# haplovect[pos] <- round(nvotesalt/maxlag_used)
}
-
+
# write out the joint phasing
jointphasing_vr <- VariantAnnotation::VRanges(seqnames = GenomicRanges::seqnames(loci), ranges = GenomicRanges::ranges(loci), ref = loci$REF, alt = unlist(loci$ALT))
-
+
# assign the genotypes based on flipping of individual BAF values in regions of allelic imbalance according to BAFseg
S4Vectors::mcols(jointphasing_vr)$GT <- paste0(haplovect, "|", ifelse(haplovect == 0, 1, 0))
-
+
# add phase set annotation based on segmented BAF: every segment = phase set
S4Vectors::mcols(jointphasing_vr)$PS <- GenomicRanges::start(loci)[1]
-
+
# write out vcf
VariantAnnotation::sampleNames(jointphasing_vr) <- "multisample"
- VariantAnnotation::writeVcf(obj = jointphasing_vr, filename = paste0(outprefix, chrom, ".vcf"), index = F)
-
+ VariantAnnotation::writeVcf(obj = jointphasing_vr, filename = paste0(outprefix, chrom, ".vcf"), index = FALSE)
+
# write out loci + haplovect to do MSAI detection and plotting after final multisample CN calling
S4Vectors::mcols(loci)$multisample_haplo <- haplovect
saveRDS(object = loci, file = paste0(outprefix, chrom, "_loci.RDS"))
@@ -296,7 +386,7 @@ get_multisample_phasing <- function(chrom, bbphasingprefixes, maxlag = 90, relat
}
-#' Generates haplotype blocks, MSAI results, and plots from phasing information contained in multisample Battenberg runs
+#' Generates haplotype blocks, MSAI results, and plots from phasing information contained in multisample Battenberg runs
#' @param rdsprefix Prefix of the RDS files containing the multisample haplotypes and BAF
#' @param subclonesfiles Vectors containing the paths to the different subclones.txt files
#' @param chrom_names Names of the chromosomes
@@ -304,57 +394,75 @@ get_multisample_phasing <- function(chrom, bbphasingprefixes, maxlag = 90, relat
#' @param plotting Should the multisample phasing plots be made? (Default: TRUE)
#' @author jdemeul
#' @export
-call_multisample_MSAI <- function(rdsprefix, subclonesfiles, chrom_names, tumournames, plotting = T) {
-
+call_multisample_MSAI <- function(
+ rdsprefix,
+ subclonesfiles,
+ chrom_names,
+ tumournames,
+ plotting = TRUE
+) {
# compile all CN results
- subclonescat <- lapply(X = subclonesfiles, FUN = function(x) read.delim(file = x, as.is = T))
+ subclonescat <- lapply(
+ X = subclonesfiles, FUN = function(x) utils::read.delim(file = x, as.is = TRUE)
+ )
imbalancedregions <- do.call(rbind, subclonescat)
# add sample identifiers
- imbalancedregions$sampleid <- rep(x = tumournames, sapply(X = subclonescat, FUN = nrow))
+ imbalancedregions$sampleid <- rep(
+ x = tumournames, sapply(X = subclonescat, FUN = nrow)
+ )
# subset to regions which are imbalanced in at least 2 samples
imbalancedregions <- imbalancedregions[which(imbalancedregions$nMaj1_A != imbalancedregions$nMin1_A | imbalancedregions$nMaj2_A != imbalancedregions$nMin2_A), ]
- imbalancedregions <- GenomicRanges::GRanges(seqnames = imbalancedregions$chr, ranges = IRanges::IRanges(start = imbalancedregions$startpos, end = imbalancedregions$endpos), sampleid = imbalancedregions$sampleid)
+ imbalancedregions <- GenomicRanges::GRanges(
+ seqnames = imbalancedregions$chr,
+ ranges = IRanges::IRanges(
+ start = imbalancedregions$startpos,
+ end = imbalancedregions$endpos
+ ),
+ sampleid = imbalancedregions$sampleid
+ )
imbalancedregions_disj <- GenomicRanges::disjoin(imbalancedregions)
imbalancedregions_disj <- imbalancedregions_disj[GenomicRanges::countOverlaps(query = imbalancedregions_disj, subject = imbalancedregions) > 1]
-
+
# if nothing remains, stop here
if (length(imbalancedregions_disj) == 0) {
- print("No recurrently copy number imbalanced regions")
+ log_info("No recurrently copy number imbalanced regions")
return(NULL)
}
-
+
# add the identifiers of aberrated samples to each region
samplehits <- GenomicRanges::findOverlaps(query = imbalancedregions_disj, subject = imbalancedregions)
S4Vectors::mcols(imbalancedregions_disj)$sampleids <- split(x = imbalancedregions$sampleid[S4Vectors::subjectHits(samplehits)], f = S4Vectors::queryHits(samplehits))
-
+
# split per chromosome, keeping only the imbalanced ones
- imbalancedregions_disj <- as(object = split(x = imbalancedregions_disj, f = GenomicRanges::seqnames(imbalancedregions_disj)), Class = "GRangesList")
-
+ imbalancedregions_disj <- methods::as(object = split(x = imbalancedregions_disj, f = GenomicRanges::seqnames(imbalancedregions_disj)), Class = "GRangesList")
+
# for every chromosome with imbalance
- for (i in 1:length(chrom_names)) {
- chrom = as.character(chrom_names[i])
+ for (i in seq_along(chrom_names)) {
+ chrom <- as.character(chrom_names[i])
# load loci.RDS file and simplify genotype formatting
loci <- readRDS(file = paste0(rdsprefix, chrom, "_loci.RDS"))
- S4Vectors::mcols(loci)[,paste0(tumournames, "_Major")] <- S4Vectors::DataFrame(apply(X = S4Vectors::mcols(loci)[,paste0(tumournames, "_Major")],
- MARGIN = 2, FUN = function(x) as.numeric(substr(x = x, start = 1, stop = 1))))
-
- #if (length(imbalancedregions_disj[[chrom]]) > 0) {
+ S4Vectors::mcols(loci)[, paste0(tumournames, "_Major")] <- S4Vectors::DataFrame(apply(
+ X = S4Vectors::mcols(loci)[, paste0(tumournames, "_Major")],
+ MARGIN = 2, FUN = function(x) as.numeric(substr(x = x, start = 1, stop = 1))
+ ))
+
+ # if (length(imbalancedregions_disj[[chrom]]) > 0) {
if (chrom %in% names(imbalancedregions_disj)) {
# split loci by abberrated region, compare only ranges to avoid chr naming scheme mismatch
locioverlaps <- IRanges::findOverlaps(query = IRanges::ranges(imbalancedregions_disj[[chrom]]), subject = IRanges::ranges(loci))
- imballoci <- split(x = loci[S4Vectors::subjectHits(locioverlaps)], f = S4Vectors::queryHits(locioverlaps), drop = F)
-
+ imballoci <- split(x = loci[S4Vectors::subjectHits(locioverlaps)], f = S4Vectors::queryHits(locioverlaps), drop = FALSE)
+
# now check for each region the GT of major allele (in imbalanced samples)
imbalancedregions_disj[[chrom]] <- imbalancedregions_disj[[chrom]][unique(S4Vectors::queryHits(locioverlaps))]
frac_consensus <- mapply(haps = imballoci, samples = imbalancedregions_disj[[chrom]]$sampleids, FUN = function(haps, samples) {
- colSums(x = S4Vectors::as.matrix(S4Vectors::mcols(haps)[,paste0(samples, "_Major")]) == S4Vectors::mcols(haps)[, "multisample_haplo"], na.rm = T) / length(haps)
- }, SIMPLIFY = F)
-
- #simplify notation and call MSAI
+ colSums(x = S4Vectors::as.matrix(S4Vectors::mcols(haps)[, paste0(samples, "_Major")]) == S4Vectors::mcols(haps)[, "multisample_haplo"], na.rm = TRUE) / length(haps)
+ }, SIMPLIFY = FALSE)
+
+ # simplify notation and call MSAI
imbalancedregions_disj[[chrom]]$frac_consensus <- sapply(X = frac_consensus, FUN = function(x) paste0(names(x), "=", round(x, digits = 2), collapse = ";"))
- imbalancedregions_disj[[chrom]]$msai <- sapply(X = frac_consensus, FUN = function(x) max(x, na.rm = T) - min(x, na.rm = T) > .9)
-
+ imbalancedregions_disj[[chrom]]$msai <- sapply(X = frac_consensus, FUN = function(x) max(x, na.rm = TRUE) - min(x, na.rm = TRUE) > .9)
+
if (length(GenomicRanges::mcols(imbalancedregions_disj[[chrom]])$msai) > 0) {
msaidf <- GenomicRanges::as.data.frame(imbalancedregions_disj[[chrom]][GenomicRanges::mcols(imbalancedregions_disj[[chrom]])$msai])
} else {
@@ -363,37 +471,60 @@ call_multisample_MSAI <- function(rdsprefix, subclonesfiles, chrom_names, tumour
} else {
msaidf <- data.frame()
}
-
+
if (plotting) {
# Plot the resulting data
- df1 <- data.frame(pos = GenomicRanges::start(loci), haplo = S4Vectors::mcols(loci)$multisample_haplo, BAF = as.numeric(rep(NA, length(loci))))
-
+ df1 <- data.frame(
+ pos = GenomicRanges::start(loci),
+ haplo = S4Vectors::mcols(loci)$multisample_haplo,
+ BAF = as.numeric(rep(NA, length(loci)))
+ )
+
# visualise the haplotypes for the different samples
for (tumour in tumournames) {
- # df1 <- data.frame(pos = GenomicRanges::start(loci), BAF = S4Vectors::mcols(loci)[,paste0(tumour, "_BAF")])
- df1$BAF <- ifelse(df1$haplo == 1, S4Vectors::mcols(loci)[,paste0(tumour, "_BAF")], 1-S4Vectors::mcols(loci)[,paste0(tumour, "_BAF")])
-
+ df1$BAF <- ifelse(df1$haplo == 1, S4Vectors::mcols(
+ loci
+ )[, paste0(tumour, "_BAF")],
+ 1 - S4Vectors::mcols(loci)[, paste0(tumour, "_BAF")]
+ )
+
p1 <- ggplot2::ggplot()
if (nrow(msaidf) > 0) {
- p1 <- p1 + ggplot2::geom_rect(data = msaidf, mapping = ggplot2::aes(xmin = start, xmax = end, ymin = 0, ymax = 1), alpha = .05, color = "gray", size = 0)
+ p1 <- p1 + ggplot2::geom_rect(
+ data = msaidf, mapping = ggplot2::aes(
+ xmin = start,
+ xmax = end,
+ ymin = 0, ymax = 1
+ ),
+ alpha = .05, color = "gray", size = 0
+ )
}
- p1 <- p1 + ggplot2::geom_point(data = df1, mapping = ggplot2::aes(x = pos, y = 1-BAF), alpha = .6, colour = "#67a9cf", shape = 46, show.legend = F)
- p1 <- p1 + ggplot2::geom_point(data = df1, mapping = ggplot2::aes(x = pos, y = BAF), alpha = .6, colour = "#ef8a62", shape = 46, show.legend = F) + ggplot2::theme_minimal()
- p1 <- p1 + ggplot2::labs(x = "Position", y = "BAF", title = paste0(tumour, ": multisample phasing chr", chrom))
-
- ggplot2::ggsave(filename = paste0(tumour, "_multisample_phasing_chr", chrom, ".png"), plot = p1, width = 20, height = 5)
+ p1 <- p1 + ggplot2::geom_point(data = df1, mapping = ggplot2::aes(
+ x = pos, y = 1 - BAF
+ ), alpha = .6, colour = "#67a9cf", shape = 46, show.legend = FALSE)
+ p1 <- p1 + ggplot2::geom_point(
+ data = df1, mapping = ggplot2::aes(x = pos, y = BAF),
+ alpha = .6, colour = "#ef8a62", shape = 46, show.legend = FALSE
+ ) + ggplot2::theme_minimal()
+ p1 <- p1 + ggplot2::labs(
+ x = "Position", y = "BAF",
+ title = paste0(tumour, ": multisample phasing chr", chrom)
+ )
+
+ ggplot2::ggsave(
+ filename = paste0(tumour, "_multisample_phasing_chr", chrom, ".png"),
+ plot = p1, width = 20, height = 5
+ )
}
}
}
# write out final MSAI dataframe
- msaiout <- GenomicRanges::as.data.frame(unlist(imbalancedregions_disj, use.names = F))
+ msaiout <- GenomicRanges::as.data.frame(unlist(imbalancedregions_disj, use.names = FALSE))
list_cols <- sapply(msaiout, is.list)
for (col in names(msaiout)[list_cols]) {
- msaiout[[col]] <- sapply(msaiout[[col]], function(x) paste(x, collapse=","))
+ msaiout[[col]] <- sapply(msaiout[[col]], function(x) paste(x, collapse = ","))
}
- write.table(x = msaiout[, -c(4:6)], file = paste0("multisample_MSAI.txt"), row.names = F, sep = "\t", quote = F)
+ data.table::fwrite(x = msaiout[, -c(4:6)], file = paste0("multisample_MSAI.txt"), row.names = FALSE, sep = "\t", quote = FALSE)
return(NULL)
}
-
-
diff --git a/R/impute.R b/R/impute.R
index 783a2d1c..fc2aefcd 100644
--- a/R/impute.R
+++ b/R/impute.R
@@ -1,644 +1,264 @@
-#' Run impute on the specified inputfile
-#'
-#' This function runs impute across the input using the specified region.size.
-#' @param inputfile Full path to a csv file with columns: Physical.Position, Allele.A, Allele.B, allele.frequency, id ,position, a0, a1
-#' @param outputfile.prefix Prefix to the output file. Region boundaries are added as suffix.
-#' @param is.male Boolean describing whether the sample is male (TRUE) or female (FALSE)
-#' @param imputeinfofile Path to the imputeinfofile on disk.
-#' @param impute.exe Pointer to where the impute2 executable can be found (optional).
-#' @param region.size An integer describing the region size to be used by impute (optional).
-#' @param chrom The name of a chromosome on which this function should run (names are used, supply X as 'X') (optional).
-#' @param seed The seed to be set
-#' @author dw9
-#' @export
-run.impute = function(inputfile, outputfile.prefix, is.male, imputeinfofile, impute.exe="impute2", region.size=5000000, chrom=NA, seed=as.integer(Sys.time())) {
-
- # Read in the impute file information
- impute.info = parse.imputeinfofile(imputeinfofile, is.male, chrom=chrom)
-
- # Run impute for each region of the size specified above
- for(r in 1:nrow(impute.info)){
- boundaries = seq(as.numeric(impute.info[r,]$start),as.numeric(impute.info[r,]$end),region.size)
- if(boundaries[length(boundaries)] != impute.info[r,]$end){
- boundaries = c(boundaries,impute.info[r,]$end)
- }
-
- # Take the start of the region+1 here to make sure there are no overlapping regions, wich causes a
- # problem with SNPs on exactly the boundary. It does mean the first base on the first chromosome
- # cannot be phased
- for(b in 1:(length(boundaries)-1)){
- cmd = paste(impute.exe,
- " -m ", impute.info[r,]$genetic_map,
- " -h ", impute.info[r,]$impute_hap,
- " -l ", impute.info[r,]$impute_legend,
- " -g ", inputfile,
- " -int ", boundaries[b]+1, " ", boundaries[b+1],
- " -Ne 20000", # Authors of impute2 mention that this parameter works best on all population types, thus hardcoded.
- " -o ", outputfile.prefix, "_", boundaries[b]/1000, "K_", boundaries[b+1]/1000, "K.txt",
- " -phase",
- " -seed ",
- " -os 2", sep="") # lowers computational cost by not imputing reference only SNPs
- EXIT_CODE=system(cmd, wait=T)
- stopifnot(EXIT_CODE==0)
- }
- }
-}
-
-#' Read in the imputeinfofile.
-#'
-#' Reads in a file with the following columns:
-#' chromosome : 1-X
-#' impute_legend : Legend file in IMPUTE -l format
-#' genetic_map : Genetic map file in IMPUTE -m format
-#' impute_hap : Phased haplotype file in IMPUTE -h format
-#' start : Start of the chromosome
-#' end : End of the chromosome
-#' is_par : 1 when pseudo autosomal region, 0 when not
-#'
-#' @param imputeinfofile Path to the imputeinfofile on disk.
-#' @param is.male A boolean describing whether the sample under study is male.
-#' @param chrom The name of a chromosome to subset the contents of the imputeinfofile with (optional)
-#' @return A data.frame with 7 columns: Chromosome, impute_legend, genetic_map, impute_hap, start, end, is_par
-#' @author sd11
-#' @export
-parse.imputeinfofile = function(imputeinfofile, is.male, chrom=NA) {
- impute.info = read.table(imputeinfofile, stringsAsFactors=F)
- colnames(impute.info) = c("chrom", "impute_legend", "genetic_map", "impute_hap", "start", "end", "is_par")
- # Remove the non-pseudo autosomal region (i.e. where not both men and woman are diploid)
- if(is.male){ impute.info = impute.info[impute.info$is_par==1,] }
- chr_names=unique(impute.info$chrom)
- # Subset for a particular chromosome
- if (!is.na(chrom)) {
- impute.info = impute.info[impute.info$chrom==chrom,]
- }
- return(impute.info)
-}
-
-#' Check impute info file consistency
-#' @param imputeinfofile Path to the imputeinfofile on disk.
-#' @author sd11
-check.imputeinfofile = function(imputeinfofile, is.male, usebeagle) {
- impute.info = parse.imputeinfofile(imputeinfofile, is.male)
- if (usebeagle){
- if (any(!file.exists(impute.info$impute_legend))) {
- print("Could not find reference files, make sure paths in impute_info.txt point to the correct location")
- stop("Could not find reference files, make sure paths in impute_info.txt point to the correct location")
- }
- } else {
- if (any(!file.exists(impute.info$impute_legend) | !file.exists(impute.info$genetic_map) | !file.exists(impute.info$impute_hap))) {
- print("Could not find reference files, make sure paths in impute_info.txt point to the correct location")
- stop("Could not find reference files, make sure paths in impute_info.txt point to the correct location")
- }
- }
-}
-
-#' Returns the chromosome names that are supported
-#' @param imputeinfofile Path to the imputeinfofile on disk.
-#' @param is.male A boolean describing whether the sample under study is male.
-#' @param chrom The name of a chromosome to subset the contents of the imputeinfofile with (optional)
-#' @param analaysis Depending on the type of analysis different sets of chromosomes are returned (Default: paired)
-#' @return A vector containing the supported chromosome names
-#' @author sd11
-#' @export
-get.chrom.names = function(imputeinfofile, is.male, chrom=NA, analysis="paired") {
- chrom_names = unique(parse.imputeinfofile(imputeinfofile, is.male, chrom=chrom)$chrom)
- if (analysis=="cell_line" | analysis=="germline") {
- # Both cell line and germline analysis do not yield usable data on X and Y, so remove
- chrom_names = chrom_names[!chrom_names %in% c("X", "Y")]
- }
- return(chrom_names)
-}
-
-#' Concatenate the impute output generated for each of the regions.
-#'
-#' This function assembles the impute output generated.
-#' @param inputfile.prefix Prefix of the input files (this is typically the outputfile.prefix option supplied when calling run.impute).
-#' @param outputfile Where to store the output.
-#' @param is.male Boolean describing whether the sample is male (TRUE) or female (FALSE).
-#' @param imputeinfofile Path to the imputeinfofile on disk.
-#' @param region.size An integer describing the region size to be used by impute (optional).
-#' @param chrom The name of a chromosome on which this function should run (names are used, supply X as 'X').
-#' @author dw9
-#' @export
-combine.impute.output = function(inputfile.prefix, outputfile, is.male, imputeinfofile, region.size=5000000, chrom=NA) {
- # Read in the impute file information
- impute.info = parse.imputeinfofile(imputeinfofile, is.male, chrom=chrom)
-
- # Assemble the start and end points of all regions
- all.boundaries = array(0,c(0,2))
- for(r in 1:nrow(impute.info)){
- boundaries = seq(as.numeric(impute.info[r,]$start),as.numeric(impute.info[r,]$end),region.size)
- if(boundaries[length(boundaries)] != impute.info[r,]$end){
- boundaries = c(boundaries,impute.info[r,]$end)
- }
- all.boundaries = rbind(all.boundaries,cbind(boundaries[-(length(boundaries))],boundaries[-1]))
- }
- # Concatenate all the regions
- impute.output = concatenateImputeFiles(inputfile.prefix, all.boundaries)
- write.table(impute.output, file=outputfile, row.names=F, col.names=F, quote=F, sep=" ")
-}
-
-
+# Phasing Dispatcher for Battenberg
+# This file handles the high-level orchestration of haplotyping/phasing.
-#' Converts impute input to a beagle input
-#'
-#' This function takes the impute input file and converts it to a beagle input
-#'
-#' @param imputeinput path to the impute input file
-#' @param chrom chromosome
-#' @author maxime.tarabichi
+#' @param phasing_results_dir Directory containing the phasing output files
+#' @author sd11, maxime.tarabichi, jdemeul
#' @export
-convert.impute.input.to.beagle.input = function(imputeinput,
- chrom)
-{
- chrom <- if(chrom=="23") "X" else chrom
- inp <- read_impute_input(imputeinput)
- coln <- c("#CHROM",
- "POS",
- "ID",
- "REF",
- "ALT",
- "QUAL",
- "FILTER",
- "INFO",
- "FORMAT",
- "SAMP001")
- vcf <- cbind(rep(chrom,nrow(inp)),
- inp[,3],
- rep(".",nrow(inp)),
- inp[,4],
- inp[,5],
- rep(".",nrow(inp)),
- rep("PASS",nrow(inp)),
- rep(".",nrow(inp)),
- rep("GT",nrow(inp)),
- paste(inp$X6,inp$X7,inp$X8,sep="-"), stringsAsFactors = F)
- vcf[vcf[,10]=="1-0-0",10] <- "0/0"
- vcf[vcf[,10]=="0-1-0",10] <- "0/1"
- vcf[vcf[,10]=="0-0-1",10] <- "1/1"
- vcf <- vcf[vcf[,10]!="0-0-0",]
- colnames(vcf) <- coln
- vcf
-}
+run_haplotyping <- function(
+ chrom, tumourname, normalname,
+ ismale, problemloci,
+ phasing_results_dir, min_normal_depth, chrom_names,
+ reference_info_file = NA,
+ externalhaplotypeprefix = NA,
+ beagle_input_dir = NA,
+ allele_frequencies_dir = NA,
+ chrom_coord_file = NA,
+ beaglejar = NA,
+ beagleref_dir = NA,
+ phasing_engine = "impute2",
+ threads_per_chromosome = 1
+) {
+ # determine if we are using beagle based on engine flag or provided jar
+ usebeagle <- (phasing_engine == "beagle") || (!is.na(beaglejar) && file.exists(beaglejar))
-#' Writes input file for beagle5
-#'
-#' This function writes a table formatted as a vcf to the drive for beagle5 to run on
-#'
-#' @param vcf data frame vcf-like for beagle
-#' @param filepath character string for path to the file to write on disk
-#' @param vcfversion character string for version for the vcf (default 4.2)
-#' @param genomereference character string for genome build (default GRCh37)
-#' @author maxime.tarabichi
-#' @export
-writevcf.beagle = function(vcf,
- filepath,
- vcfversion="4.2",
- genomereference="GRCh37")
-{
- cat(paste0('##fileformat=VCFv',vcfversion,
- '\n##FORMAT=\n##reference=',
- genomereference,
- '\n'),
- file=filepath)
- suppressWarnings(write.table(vcf,
- file=filepath,
- sep="\t",col.names=T,row.names=F,quote=F,append=T))
-}
+ # 1. DISCOVER OR GENERATE HAPLOTYPES
+ local_haplo <- paste0(tumourname, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt")
+ if (file.exists(local_haplo)) {
+ haplotype_file <- local_haplo
+ log_info("Using existing local haplotype file: {haplotype_file}")
+ } else if (usebeagle) {
+ # BEAGLE FLOW
+ if (!is.na(beaglejar) && file.exists(beaglejar)) {
+ # Running Beagle Internal
+ if (is.na(beagleref_dir) || !dir.exists(beagleref_dir)) {
+ log_failure("Running Beagle internally requires a reference directory: beagleref_dir")
+ }
+ log_info("Running Beagle Phasing for Chromosome {chrom}")
+ beagle_in <- paste0("beagle_in_chr", chrom, ".vcf")
-#' Writes output of beagle as output from impute (interface bealge/impute for Battenberg)
-#'
-#' This function writes a table formatted as a vcf to the drive for beagle5 to run on
-#'
-#' @param vcf character string path for output from beagle
-#' @param outfile character string path for impute-like outputfile
-#' @author maxime.tarabichi
-#' @export
-writebeagle.as.impute = function(vcf,
- outfile)
-{
- beagleout <- read_beagle_output(vcf)
- haplotypes <- strsplit(beagleout$SAMP001,split="\\|")
- dt <- cbind(paste0("snp_index",1:nrow(beagleout)),
- paste0("rs_index",1:nrow(beagleout)),
- beagleout[,2],
- beagleout[,4],
- beagleout[,5],
- sapply(haplotypes,"[",1),
- sapply(haplotypes,"[",2))
- write.table(dt,
- file=outfile,
- quote=F,
- col.names=F,
- row.names=F,
- sep="\t")
-}
+ # Generate input for Beagle
+ find_ac_file <- function(dir, sample, chrom) {
+ options <- c(
+ file.path(dir, paste0(sample, "_alleleFrequencies_chr", chrom, ".txt")),
+ file.path(dir, paste0(sample, "_alleleFrequencies_chr", gsub("chr", "", as.character(chrom), ignore.case = TRUE), ".txt")),
+ file.path(dir, paste0(sample, "_alleleFrequencies_", gsub("chr", "", as.character(chrom), ignore.case = TRUE), ".txt"))
+ )
+ for (f in options) {
+ if (file.exists(f)) {
+ return(f)
+ }
+ }
+ return(NULL)
+ }
+ t_file <- find_ac_file(allele_frequencies_dir, tumourname, chrom)
+ n_file <- find_ac_file(allele_frequencies_dir, normalname, chrom)
+ if (is.null(t_file) || is.null(n_file)) log_failure("Could not find allele counts for phasing.")
+ generate_beagle_input_from_counts(
+ chrom = chrom, tumour_allele_counts_file = t_file, normal_allele_counts_file = n_file,
+ output_file = beagle_in, reference_info_file = reference_info_file,
+ is_male = ismale, problem_loci_file = problemloci, beagleref_dir = beagleref_dir
+ )
-#' Command to run beagle5
-#'
-#' This runs beagle through a system call to the beagle java jar file.
-#' It requires pre-formatted reference and plink files for the correct genome build.
-#'
-#' @param beaglejar character string path to Beagle5 java jar file
-#' @param vcfpath character string path to the vcf input file to be phased
-#' @param reffile character string path to the Beagle5 reference file
-#' @param outpath character string path to Beagle's output vcf.gz file
-#' @param plinkfile character string path to the plink file
-#' @param nthreads integer number of threads
-#' @param window integer max size of genomic window to be phased (cM; default 40; decrease for less memory usage; should be >1.1*overlap)
-#' @param overlap integer overlap of windows (cM; default 4)
-#' @param javajre Path to the Java JRE executable (default java, i.e. in $PATH)
-#' @param maxheap.gb integer maximum heap size for the java process in gigabytes (default 10)
-#' @author maxime.tarabichi
-#' @export
-run.beagle5 = function(beaglejar,
- vcfpath,
- reffile,
- outpath,
- plinkfile,
- nthreads=1,
- window=40,
- overlap=4,
- maxheap.gb=10,
- javajre="java")
-{
- cmd <- paste0(javajre,
- " -Xmx",maxheap.gb,"g",
- " -Xms", maxheap.gb, "g",
- " -XX:+UseParallelOldGC",
- " -jar ",beaglejar,
- " gt=",vcfpath,
- " ref=",reffile ,
- " out=",outpath,
- " map=",plinkfile,
- " nthreads=",nthreads,
- " window=",window,
- " overlap=",overlap,
- " impute=false")
- EXIT_CODE=system(cmd, wait=T)
- stopifnot(EXIT_CODE==0)
-}
+ out_prefix <- paste0(tumourname, "_beagle_output_chr", chrom)
+ vcf_out <- run_beagle_internal(
+ chrom, tumourname, beagle_in, out_prefix, beaglejar, beagleref_dir,
+ threads_per_chromosome = threads_per_chromosome
+ )
+ if (file.exists(beagle_in)) file.remove(beagle_in)
+ convert_beagle_to_impute(vcf_out, local_haplo)
+ haplotype_file <- local_haplo
+ } else {
+ # Beagle Discovery Flow (Using pre-calculated Beagle)
+ beagle_search_dir <- if (!is.na(beagle_input_dir)) beagle_input_dir else phasing_results_dir
+ beagle_vcf_p <- file.path(beagle_search_dir, paste0(tumourname, "_beagle5_output_chr", chrom, "_P.vcf.gz"))
+ beagle_vcf_q <- file.path(beagle_search_dir, paste0(tumourname, "_beagle5_output_chr", chrom, "_Q.vcf.gz"))
-#' Construct haplotypes for a chromosome
-#'
-#' This function takes preprocessed data and performs haplotype reconstruction.
-#'
-#' @param chrom The chromosome for which to reconstruct haplotypes
-#' @param tumourname Identifier of the tumour, used to match data files on disk
-#' @param normalname Identifier of the normal, used to match data files on disk
-#' @param ismale Boolean, set to TRUE if the sample is male
-#' @param imputeinfofile Full path to the imputeinfo reference file
-#' @param problemloci Full path to the problematic loci reference file
-#' @param impute_exe Path to the impute executable (can be found if its in $PATH)
-#' @param min_normal_depth Minimal depth in the matched normal required for a SNP to be used
-#' @param chrom_names A vector containing the names of chromosomes to be included
-#' @param snp6_reference_info_file SNP6 only parameter Default: NA
-#' @param heterozygousFilter SNP6 only parameter Default: NA
-#' @param usebeagle Should use beagle5 instead of impute2 Default: FALSE
-#' @param beaglejar Full path to Beagle java jar file Default: NA
-#' @param beagleref Full path to Beagle reference file Default: NA
-#' @param beagleplink Full path to Beagle plink file Default: NA
-#' @param beaglemaxmem Integer Beagle max heap size in Gb Default: 10
-#' @param beaglenthreads Integer number of threads used by beagle5 Default:1
-#' @param beaglewindow Integer size of the genomic window for beagle5 (cM) Default:40
-#' @param beagleoverlap Integer size of the overlap between windows beagle5 Default:4
-#' @param javajre Path to the Java JRE executable (default java, i.e. in $PATH)
-#' @author sd11, maxime.tarabichi, jdemeul
-#' @export
-run_haplotyping = function(chrom, tumourname, normalname, ismale, imputeinfofile, problemloci, impute_exe, min_normal_depth, chrom_names,
- externalhaplotypeprefix = NA,
- use_previous_imputation=F,
- snp6_reference_info_file=NA, heterozygousFilter=NA,
- usebeagle=FALSE,
- beaglejar=NA,
- beagleref=NA,
- beagleplink=NA,
- beaglemaxmem=10,
- beaglenthreads=1,
- beaglewindow=40,
- beagleoverlap=4,
- javajre="java")
-{
-
- previoushaplotypefile <- list.files(pattern = paste0("_impute_output_chr", chrom, "_allHaplotypeInfo.txt"))[1]
- if (use_previous_imputation & !is.na(previoushaplotypefile)) {
-
- print(paste0("Previous imputation results found, copying info from", previoushaplotypefile, " to flip alleles"))
- currenthaplotypefile <- paste(tumourname, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt", sep="")
- if (previoushaplotypefile != currenthaplotypefile) {
- file.copy(from = previoushaplotypefile, to = paste(tumourname, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt", sep=""))
+ if (file.exists(beagle_vcf_p) || file.exists(beagle_vcf_q)) {
+ writebeagle_as_impute_arms(vcfP = beagle_vcf_p, vcfQ = beagle_vcf_q, outfile = local_haplo)
+ } else {
+ # Single pattern discovery
+ patterns <- c(
+ paste0(tumourname, "_beagle5_output_chr", chrom, ".txt.vcf.gz"),
+ paste0(tumourname, "_beagle_output_chr", chrom, ".vcf.gz")
+ )
+ found_vcf <- NA
+ for (p in patterns) {
+ tmp <- file.path(beagle_search_dir, p)
+ if (file.exists(tmp)) {
+ found_vcf <- tmp
+ break
+ }
+ }
+ if (is.na(found_vcf)) log_failure("Could not find pre-calculated Beagle VCF for {tumourname} chr {chrom} in {beagle_search_dir}")
+ convert_beagle_to_impute(found_vcf, local_haplo)
+ }
+ haplotype_file <- local_haplo
}
-
} else {
-
- if (file.exists(paste(tumourname, "_alleleFrequencies_chr", chrom, ".txt", sep=""))) {
- generate.impute.input.wgs(chrom=chrom,
- tumour.allele.counts.file=paste(tumourname,"_alleleFrequencies_chr", chrom, ".txt", sep=""),
- normal.allele.counts.file=paste(normalname,"_alleleFrequencies_chr", chrom, ".txt", sep=""),
- output.file=paste(tumourname, "_impute_input_chr", chrom, ".txt", sep=""),
- imputeinfofile=imputeinfofile,
- is.male=ismale,
- problemLociFile=problemloci,
- useLociFile=NA)
+ # IMPUTE2 / DIRECT DISCOVERY FLOW
+ if (!is.na(phasing_results_dir)) {
+ haplotype_file <- file.path(phasing_results_dir, local_haplo)
} else {
- generate.impute.input.snp6(infile.germlineBAF=paste(tumourname, "_germlineBAF.tab", sep=""),
- infile.tumourBAF=paste(tumourname, "_mutantBAF.tab", sep=""),
- outFileStart=paste(tumourname, "_impute_input_chr", sep=""),
- chrom=chrom,
- chr_names=chrom_names,
- problemLociFile=problemloci,
- snp6_reference_info_file=snp6_reference_info_file,
- imputeinfofile=imputeinfofile,
- is.male=ismale,
- heterozygousFilter=heterozygousFilter)
- }
-
- if(usebeagle){
- ## Convert input files for beagle5
- imputeinputfile <- paste(tumourname,
- "_impute_input_chr",
- chrom, ".txt", sep="")
- vcfbeagle <- convert.impute.input.to.beagle.input(imputeinput=imputeinputfile,
- chrom=chrom)
- vcfbeagle_path <- paste(tumourname,"_beagle5_input_chr",chrom,".txt",sep="")
- outbeagle_path <- paste(tumourname,"_beagle5_output_chr",chrom,".txt",sep="")
- writevcf.beagle(vcfbeagle, filepath=vcfbeagle_path)
- ## Run beagle5 on the files
- run.beagle5(beaglejar=beaglejar,
- vcfpath=vcfbeagle_path,
- reffile=beagleref,
- outpath=outbeagle_path,
- plinkfile=beagleplink,
- maxheap.gb=beaglemaxmem,
- nthreads=beaglenthreads,
- window=beaglewindow,
- overlap=beagleoverlap,
- javajre=javajre)
- outfile <- paste(tumourname,
- "_impute_output_chr",
- chrom, "_allHaplotypeInfo.txt", sep="")
- vcfout <- paste(outbeagle_path,".vcf.gz",sep="")
- ## Convert beagle output file to impute2-like file
- writebeagle.as.impute(vcf=vcfout,
- outfile=outfile)
+ haplotype_file <- local_haplo
}
- else {
- # Run impute on the files
- run.impute(inputfile=paste(tumourname, "_impute_input_chr", chrom, ".txt", sep=""),
- outputfile.prefix=paste(tumourname, "_impute_output_chr", chrom, ".txt", sep=""),
- is.male=ismale,
- imputeinfofile=imputeinfofile,
- impute.exe=impute_exe,
- region.size=5000000,
- chrom=chrom)
-
- # As impute runs in windows across a chromosome we need to assemble the output
- combine.impute.output(inputfile.prefix=paste(tumourname, "_impute_output_chr", chrom, ".txt", sep=""),
- outputfile=paste(tumourname, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt", sep=""),
- is.male=ismale,
- imputeinfofile=imputeinfofile,
- region.size=5000000,
- chrom=chrom)
- # Cleanup temp Impute output
- unlink(paste(tumourname, "_impute_output_chr", chrom, ".txt*K.txt*", sep=""))
+ if (!file.exists(haplotype_file)) log_failure("No haplotype file found for {tumourname} chr {chrom} and no pre-phased results provided.")
+ }
+
+ # 2. TRANSFORM HAPLOTYPES INTO BAFs
+ # Discovery of Allele Frequency Data
+ find_ac_file <- function(dir, sample, chrom) {
+ options <- c(
+ file.path(dir, paste0(sample, "_alleleFrequencies_chr", chrom, ".txt")),
+ file.path(dir, paste0(sample, "_alleleFrequencies_chr", gsub("chr", "", as.character(chrom), ignore.case = TRUE), ".txt")),
+ file.path(dir, paste0(sample, "_alleleFrequencies_", gsub("chr", "", as.character(chrom), ignore.case = TRUE), ".txt"))
+ )
+ for (f in options) {
+ if (file.exists(f)) {
+ return(f)
+ }
}
-
+ return(NULL)
}
-
+ allelefrequenciesfile <- find_ac_file(allele_frequencies_dir, tumourname, chrom)
- # If an allele counts file exists we assume this is a WGS sample and run the corresponding step, otherwise it must be SNP6
- allelefrequenciesfile <- paste0(tumourname, "_alleleFrequencies_chr", chrom, ".txt")
- print(allelefrequenciesfile)
- print(file.exists(allelefrequenciesfile))
-
- if (file.exists(allelefrequenciesfile)) {
- # WGS - Transform the impute output into haplotyped BAFs
-
- # if present, input external haplotype blocks
+ if (!is.null(allelefrequenciesfile) && file.exists(allelefrequenciesfile)) {
+ # WGS FLOW
if (!is.na(externalhaplotypeprefix) && file.exists(paste0(externalhaplotypeprefix, chrom, ".vcf"))) {
- print("Adding in the external haplotype blocks")
-
- # output BAFs to plot pre-external haplotyping
- GetChromosomeBAFs(chrom=chrom,
- SNP_file=allelefrequenciesfile,
- haplotypeFile=paste(tumourname, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt", sep=""),
- samplename=tumourname,
- outfile=paste(tumourname, "_chr", chrom, "_heterozygousMutBAFs_haplotyped_noExt.txt", sep=""),
- chr_names=chrom_names,
- minCounts=min_normal_depth)
-
- # Plot what we have before external haplotyping is incorporated
- plot.haplotype.data(haplotyped.baf.file=paste(tumourname, "_chr", chrom, "_heterozygousMutBAFs_haplotyped_noExt.txt", sep=""),
- imageFileName=paste(tumourname,"_chr",chrom,"_heterozygousData_noExt.png",sep=""),
- samplename=tumourname,
- chrom=chrom,
- chr_names=chrom_names)
-
- input_known_haplotypes(chrom = chrom,
- chrom_names = chrom_names,
- imputedHaplotypeFile = paste0(tumourname, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt"),
- externalHaplotypeFile = paste0(externalhaplotypeprefix, chrom, ".vcf"))
-
+ # Incorporate external hapblocks
+ ext_baf <- paste0(tumourname, "_chr", chrom, "_heterozygousMutBAFs_haplotyped_noExt.txt")
+ GetChromosomeBAFs(chrom, allelefrequenciesfile, haplotype_file, tumourname, ext_baf, chrom_names, min_normal_depth)
+ plot_haplotype_data(ext_baf, paste0(tumourname, "_chr", chrom, "_heterozygousData_noExt.png"), tumourname, chrom)
+ input_known_haplotypes(chrom, chrom_names, haplotype_file, paste0(externalhaplotypeprefix, chrom, ".vcf"))
}
-
- GetChromosomeBAFs(chrom=chrom,
- SNP_file=paste(tumourname, "_alleleFrequencies_chr", chrom, ".txt", sep=""),
- haplotypeFile=paste(tumourname, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt", sep=""),
- samplename=tumourname,
- outfile=paste(tumourname, "_chr", chrom, "_heterozygousMutBAFs_haplotyped.txt", sep=""),
- chr_names=chrom_names,
- minCounts=min_normal_depth)
+ GetChromosomeBAFs(
+ chrom, allelefrequenciesfile, haplotype_file, tumourname,
+ paste0(tumourname, "_chr", chrom, "_heterozygousMutBAFs_haplotyped.txt"),
+ chrom_names, min_normal_depth
+ )
} else {
- print("SNP6 get BAFs")
- # SNP6 - Transform the impute output into haplotyped BAFs
- GetChromosomeBAFs_SNP6(chrom=chrom,
- alleleFreqFile=paste(tumourname, "_impute_input_chr", chrom, "_withAlleleFreq.csv", sep=""),
- haplotypeFile=paste(tumourname, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt", sep=""),
- samplename=tumourname,
- outputfile=paste(tumourname, "_chr", chrom, "_heterozygousMutBAFs_haplotyped.txt", sep=""),
- chr_names=chrom_names)
+ # SNP6 FLOW
+ GetChromosomeBAFs_SNP6(
+ chrom, paste0(tumourname, "_impute_input_chr", chrom, "_withAlleleFreq.csv"),
+ haplotype_file, tumourname,
+ paste0(tumourname, "_chr", chrom, "_heterozygousMutBAFs_haplotyped.txt"), chrom_names
+ )
}
-
- # Plot what we have until this point
- plot.haplotype.data(haplotyped.baf.file=paste(tumourname, "_chr", chrom, "_heterozygousMutBAFs_haplotyped.txt", sep=""),
- imageFileName=paste(tumourname,"_chr",chrom,"_heterozygousData.png",sep=""),
- samplename=tumourname,
- chrom=chrom,
- chr_names=chrom_names)
+
+ # Final Plot
+ plot_haplotype_data(
+ paste0(tumourname, "_chr", chrom, "_heterozygousMutBAFs_haplotyped.txt"),
+ paste0(tumourname, "_chr", chrom, "_heterozygousData.png"), tumourname, chrom
+ )
}
-#' Construct haplotypes for a chromosome - germline WGS version
-#'
-#' This function takes preprocessed data and performs haplotype reconstruction.
-#'
-#' @param chrom The chromosome for which to reconstruct haplotypes
-#' @param germlinename Identifier of the germline sample, used to match data files on disk
-#' @param normalname Identifier of the reconstructed normal, used to match data files on disk
-#' @param ismale Boolean, set to TRUE if the sample is male
-#' @param imputeinfofile Full path to the imputeinfo reference file
-#' @param problemloci Full path to the problematic loci reference file
-#' @param impute_exe Path to the impute executable (can be found if its in $PATH)
-#' @param min_normal_depth Minimal depth in the matched normal required for a SNP to be used
-#' @param chrom_names A vector containing the names of chromosomes to be included
-#' @param snp6_reference_info_file SNP6 only parameter Default: NA
-#' @param heterozygousFilter SNP6 only parameter Default: NA
-#' @param usebeagle Should use beagle5 instead of impute2 Default: FALSE
-#' @param beaglejar Full path to Beagle java jar file Default: NA
-#' @param beagleref Full path to Beagle reference file Default: NA
-#' @param beagleplink Full path to Beagle plink file Default: NA
-#' @param beaglemaxmem Integer Beagle max heap size in Gb Default: 10
-#' @param beaglenthreads Integer number of threads used by beagle5 Default:1
-#' @param beaglewindow Integer size of the genomic window for beagle5 (cM) Default:40
-#' @param beagleoverlap Integer size of the overlap between windows beagle5 Default:4
-#' @param javajre Path to the Java JRE executable (default java, i.e. in $PATH)
-#' @author sd11, maxime.tarabichi, jdemeul, Naser Ansari-Pour (BDI, Oxford)
#' @export
+run_haplotyping_germline <- function(
+ chrom, germlinename, normalname, ismale, problemloci,
+ phasing_results_dir, min_normal_depth, chrom_names,
+ reference_info_file = NA,
+ externalhaplotypeprefix = NA,
+ beagle_input_dir = NA,
+ allele_frequencies_dir = NA,
+ chrom_coord_file = NA,
+ beaglejar = NA,
+ beagleref_dir = NA,
+ phasing_engine = "impute2",
+ threads_per_chromosome = 8
+) {
+ usebeagle <- (phasing_engine == "beagle") || (!is.na(beaglejar) && file.exists(beaglejar))
+ local_haplo <- paste0(germlinename, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt")
-run_haplotyping_germline = function(chrom, germlinename, normalname, ismale, imputeinfofile, problemloci, impute_exe, min_normal_depth, chrom_names,
- externalhaplotypeprefix = NA,
- use_previous_imputation=F,
- snp6_reference_info_file=NA, heterozygousFilter=NA,
- usebeagle=FALSE,
- beaglejar=NA,
- beagleref=NA,
- beagleplink=NA,
- beaglemaxmem=10,
- beaglenthreads=1,
- beaglewindow=40,
- beagleoverlap=4,
- javajre="java")
-{
-
- previoushaplotypefile <- list.files(pattern = paste0("_impute_output_chr", chrom, "_allHaplotypeInfo.txt"))[1]
- if (use_previous_imputation & !is.na(previoushaplotypefile)) {
-
- print(paste0("Previous imputation results found, copying info from", previoushaplotypefile, " to flip alleles"))
- currenthaplotypefile <- paste(germlinename, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt", sep="")
- if (previoushaplotypefile != currenthaplotypefile) {
- file.copy(from = previoushaplotypefile, to = paste(germlinename, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt", sep=""))
- }
-
- } else {
-
- if (file.exists(paste(germlinename, "_alleleFrequencies_chr", chrom, ".txt", sep=""))) {
- generate.impute.input.wgs.germline(chrom=chrom,
- germline.allele.counts.file=paste(germlinename,"_alleleFrequencies_chr", chrom, ".txt", sep=""),
- normal.allele.counts.file=paste(normalname,"_alleleFrequencies_chr", chrom, ".txt", sep=""),
- output.file=paste(germlinename, "_impute_input_chr", chrom, ".txt", sep=""),
- imputeinfofile=imputeinfofile,
- is.male=ismale,
- problemLociFile=problemloci,
- useLociFile=NA)
+ if (file.exists(local_haplo)) {
+ haplotype_file <- local_haplo
+ } else if (usebeagle) {
+ if (!is.na(beaglejar) && file.exists(beaglejar)) {
+ if (is.na(beagleref_dir) || !dir.exists(beagleref_dir)) log_failure("Internal Beagle requires beagleref_dir")
+ log_info("Running internal Beagle for Germline chr {chrom}")
+ beagle_in <- paste0("beagle_in_chr", chrom, ".vcf")
+
+ find_ac_file <- function(dir, sample, chrom) {
+ opts <- c(
+ file.path(dir, paste0(sample, "_alleleFrequencies_chr", chrom, ".txt")),
+ file.path(dir, paste0(sample, "_alleleFrequencies_chr", gsub("chr", "", as.character(chrom), ignore.case = TRUE), ".txt"))
+ )
+ for (f in opts) {
+ if (file.exists(f)) {
+ return(f)
+ }
+ }
+ return(NULL)
+ }
+ ac_file <- find_ac_file(allele_frequencies_dir, germlinename, chrom)
+ if (is.null(ac_file)) log_failure("No allele frequencies for Germline chr {chrom}")
+
+ generate_beagle_input_from_counts(chrom, ac_file, ac_file, beagle_in, reference_info_file, ismale, problemloci, beagleref_dir = beagleref_dir)
+ vcf_out <- run_beagle_internal(
+ chrom, germlinename, beagle_in, paste0(germlinename, "_beagle_output_chr", chrom),
+ beaglejar, beagleref_dir,
+ threads_per_chromosome = threads_per_chromosome
+ )
+ if (file.exists(beagle_in)) file.remove(beagle_in)
+ convert_beagle_to_impute(vcf_out, local_haplo)
+ haplotype_file <- local_haplo
} else {
- stop("Germline calling is currently on WGS data only - SNP array data is not sufficiently dense to detect all germline CNVs")
+ beagle_search_dir <- if (!is.na(beagle_input_dir)) beagle_input_dir else phasing_results_dir
+ patterns <- c(
+ paste0(germlinename, "_beagle_output_chr", chrom, ".vcf.gz"),
+ paste0(germlinename, "_beagle5_output_chr", chrom, ".txt.vcf.gz")
+ )
+ found_vcf <- NA
+ for (p in patterns) {
+ tmp <- file.path(beagle_search_dir, p)
+ if (file.exists(tmp)) {
+ found_vcf <- tmp
+ break
+ }
+ }
+ if (is.na(found_vcf)) log_failure("Could not find pre-phased Beagle VCF for Germline")
+ convert_beagle_to_impute(found_vcf, local_haplo)
+ haplotype_file <- local_haplo
}
-
- if(usebeagle){
- ## Convert input files for beagle5
- imputeinputfile <- paste(germlinename,
- "_impute_input_chr",
- chrom, ".txt", sep="")
- vcfbeagle <- convert.impute.input.to.beagle.input(imputeinput=imputeinputfile,
- chrom=chrom)
- vcfbeagle_path <- paste(germlinename,"_beagle5_input_chr",chrom,".txt",sep="")
- outbeagle_path <- paste(germlinename,"_beagle5_output_chr",chrom,".txt",sep="")
- writevcf.beagle(vcfbeagle, filepath=vcfbeagle_path)
- ## Run beagle5 on the files
- run.beagle5(beaglejar=beaglejar,
- vcfpath=vcfbeagle_path,
- reffile=beagleref,
- outpath=outbeagle_path,
- plinkfile=beagleplink,
- maxheap.gb=beaglemaxmem,
- nthreads=beaglenthreads,
- window=beaglewindow,
- overlap=beagleoverlap,
- javajre=javajre)
- outfile <- paste(germlinename,
- "_impute_output_chr",
- chrom, "_allHaplotypeInfo.txt", sep="")
- vcfout <- paste(outbeagle_path,".vcf.gz",sep="")
- ## Convert beagle output file to impute2-like file
- writebeagle.as.impute(vcf=vcfout,
- outfile=outfile)
- }
- else {
- # Run impute on the files
- run.impute(inputfile=paste(germlinename, "_impute_input_chr", chrom, ".txt", sep=""),
- outputfile.prefix=paste(germlinename, "_impute_output_chr", chrom, ".txt", sep=""),
- is.male=ismale,
- imputeinfofile=imputeinfofile,
- impute.exe=impute_exe,
- region.size=5000000,
- chrom=chrom)
-
- # As impute runs in windows across a chromosome we need to assemble the output
- combine.impute.output(inputfile.prefix=paste(germlinename, "_impute_output_chr", chrom, ".txt", sep=""),
- outputfile=paste(germlinename, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt", sep=""),
- is.male=ismale,
- imputeinfofile=imputeinfofile,
- region.size=5000000,
- chrom=chrom)
- # Cleanup temp Impute output
- unlink(paste(germlinename, "_impute_output_chr", chrom, ".txt*K.txt*", sep=""))
+ } else {
+ haplotype_file <- if (!is.na(phasing_results_dir)) file.path(phasing_results_dir, local_haplo) else local_haplo
+ if (!file.exists(haplotype_file)) log_failure("Expected haplotype file for germline missing: {haplotype_file}")
+ }
+
+ # 2. TRANSFORM HAPLOTYPES INTO BAFs (Restore missing logic for germline)
+ find_ac_file <- function(dir, sample, chrom) {
+ opts <- c(
+ file.path(dir, paste0(sample, "_alleleFrequencies_chr", chrom, ".txt")),
+ file.path(dir, paste0(sample, "_alleleFrequencies_chr", gsub("chr", "", as.character(chrom), ignore.case = TRUE), ".txt"))
+ )
+ for (f in opts) {
+ if (file.exists(f)) {
+ return(f)
+ }
}
-
+ return(NULL)
}
-
-
- # If an allele counts file exists we assume this is a WGS sample and run the corresponding step, otherwise it must be SNP6
- allelefrequenciesfile <- paste0(germlinename, "_alleleFrequencies_chr", chrom, ".txt")
- print(allelefrequenciesfile)
- print(file.exists(allelefrequenciesfile))
-
- if (file.exists(allelefrequenciesfile)) {
- # WGS - Transform the impute output into haplotyped BAFs
-
- # if present, input external haplotype blocks
+ allelefrequenciesfile <- find_ac_file(allele_frequencies_dir, germlinename, chrom)
+
+ if (!is.null(allelefrequenciesfile) && file.exists(allelefrequenciesfile)) {
if (!is.na(externalhaplotypeprefix) && file.exists(paste0(externalhaplotypeprefix, chrom, ".vcf"))) {
- print("Adding in the external haplotype blocks")
-
- # output BAFs to plot pre-external haplotyping
- GetChromosomeBAFs(chrom=chrom,
- SNP_file=allelefrequenciesfile,
- haplotypeFile=paste(germlinename, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt", sep=""),
- samplename=germlinename,
- outfile=paste(germlinename, "_chr", chrom, "_heterozygousMutBAFs_haplotyped_noExt.txt", sep=""),
- chr_names=chrom_names,
- minCounts=min_normal_depth)
-
- # Plot what we have before external haplotyping is incorporated
- plot.haplotype.data(haplotyped.baf.file=paste(germlinename, "_chr", chrom, "_heterozygousMutBAFs_haplotyped_noExt.txt", sep=""),
- imageFileName=paste(germlinename,"_chr",chrom,"_heterozygousData_noExt.png",sep=""),
- samplename=germlinename,
- chrom=chrom,
- chr_names=chrom_names)
-
- input_known_haplotypes(chrom = chrom,
- chrom_names = chrom_names,
- imputedHaplotypeFile = paste0(germlinename, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt"),
- externalHaplotypeFile = paste0(externalhaplotypeprefix, chrom, ".vcf"))
-
+ ext_baf <- paste0(germlinename, "_chr", chrom, "_heterozygousMutBAFs_haplotyped_noExt.txt")
+ GetChromosomeBAFs(chrom, allelefrequenciesfile, haplotype_file, germlinename, ext_baf, chrom_names, min_normal_depth)
+ plot_haplotype_data(ext_baf, paste0(germlinename, "_chr", chrom, "_heterozygousData_noExt.png"), germlinename, chrom)
+ input_known_haplotypes(chrom, chrom_names, haplotype_file, paste0(externalhaplotypeprefix, chrom, ".vcf"))
}
-
- GetChromosomeBAFs(chrom=chrom,
- SNP_file=paste(germlinename, "_alleleFrequencies_chr", chrom, ".txt", sep=""),
- haplotypeFile=paste(germlinename, "_impute_output_chr", chrom, "_allHaplotypeInfo.txt", sep=""),
- samplename=germlinename,
- outfile=paste(germlinename, "_chr", chrom, "_heterozygousMutBAFs_haplotyped.txt", sep=""),
- chr_names=chrom_names,
- minCounts=min_normal_depth)
+ GetChromosomeBAFs(
+ chrom, allelefrequenciesfile, haplotype_file, germlinename,
+ paste0(germlinename, "_chr", chrom, "_heterozygousMutBAFs_haplotyped.txt"),
+ chrom_names, min_normal_depth
+ )
} else {
- stop("Germline calling is only on WGS data - SNParray data not sufficiently dense")
+ log_failure("Germline calling requires WGS allele counts.")
}
-
- # Plot what we have until this point
- plot.haplotype.data(haplotyped.baf.file=paste(germlinename, "_chr", chrom, "_heterozygousMutBAFs_haplotyped.txt", sep=""),
- imageFileName=paste(germlinename,"_chr",chrom,"_heterozygousData.png",sep=""),
- samplename=germlinename,
- chrom=chrom,
- chr_names=chrom_names)
+
+ plot_haplotype_data(
+ paste0(germlinename, "_chr", chrom, "_heterozygousMutBAFs_haplotyped.txt"),
+ paste0(germlinename, "_chr", chrom, "_heterozygousData.png"), germlinename, chrom
+ )
}
diff --git a/R/impute_beagle.R b/R/impute_beagle.R
new file mode 100644
index 00000000..378ce210
--- /dev/null
+++ b/R/impute_beagle.R
@@ -0,0 +1,247 @@
+#' Helper for writing Beagle VCFs
+#' @export
+writevcf_beagle <- function(vcf, filepath, vcfversion = "4.2", genomereference = "GRCh38") {
+ header <- paste0(
+ "##fileformat=VCFv", vcfversion, "\n",
+ "##FORMAT=\n",
+ "##reference=", genomereference, "\n"
+ )
+ cat(header, file = filepath)
+ data.table::fwrite(vcf, file = filepath, sep = "\t", append = TRUE, col.names = TRUE, quote = FALSE)
+}
+
+#' Convert intermediate Battenberg format to Beagle VCF
+#' @export
+convert_impute_input_to_beagle_vcf <- function(impute_input_data, chrom) {
+ chr_vcf <- if (chrom == "23") "X" else as.character(chrom)
+ coln <- c("#CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO", "FORMAT", "SAMP001")
+
+ vcf <- data.frame(
+ CHROM = rep(chr_vcf, nrow(impute_input_data)),
+ POS = impute_input_data$X3,
+ ID = rep(".", nrow(impute_input_data)),
+ REF = impute_input_data$X4,
+ ALT = impute_input_data$X5,
+ QUAL = rep(".", nrow(impute_input_data)),
+ FILTER = rep("PASS", nrow(impute_input_data)),
+ INFO = rep(".", nrow(impute_input_data)),
+ FORMAT = rep("GT", nrow(impute_input_data)),
+ GT = paste(impute_input_data$X6, impute_input_data$X7, impute_input_data$X8, sep = "-"),
+ stringsAsFactors = FALSE
+ )
+ vcf$GT[vcf$GT == "1-0-0"] <- "0/0"
+ vcf$GT[vcf$GT == "0-1-0"] <- "0/1"
+ vcf$GT[vcf$GT == "0-0-1"] <- "1/1"
+ vcf <- vcf[vcf$GT != "0-0-0", ]
+ colnames(vcf) <- coln
+ return(vcf)
+}
+
+#' Generate Beagle input directly from allele counts
+#' @export
+generate_beagle_input_from_counts <- function(chrom, tumour_allele_counts_file, normal_allele_counts_file,
+ output_file, reference_info_file = NA, is_male = NA,
+ problem_loci_file = NA, heterozygous_filter = 0.1,
+ beagleref_dir = NA) {
+ # Try to find a reference legend
+ known_SNPs <- NULL
+ if (!is.na(reference_info_file) && file.exists(reference_info_file)) {
+ impute_info <- parse_imputeinfofile(reference_info_file, is_male, chrom = chrom)
+ if (nrow(impute_info) > 0) {
+ log_info("Reading legend from {impute_info$impute_legend}")
+ known_SNPs <- vroom::vroom(unlist(impute_info$impute_legend), delim = " ", col_types = "ciccc", show_col_types = FALSE)
+ data.table::setDT(known_SNPs)
+ }
+ }
+
+ # If no reference info provided, attempt discovery in beagleref_dir (expecting LEGEND-style files or subsetting reference VCF)
+ # Actually, if we don't have a legend, we'll try to use the matched normal loci themselves as the "legend" if no reference is specified.
+ # But for a high-quality Beagle run, we really want that legend.
+ if (is.null(known_SNPs)) {
+ log_warning("No reference legend found for chr {chrom}. Using all loci from normal allele counts.")
+ # This might be slow if the allele counts file is huge, but it's a fallback.
+ }
+
+ log_info("Reading normal allele counts from {normal_allele_counts_file}")
+ snp_normal <- read_alleleFrequencies(normal_allele_counts_file)
+
+ # Filter problem SNPs
+ if (!is.na(problem_loci_file) && problem_loci_file != "NA" && file.exists(problem_loci_file)) {
+ problem_snps_raw <- data.table::fread(problem_loci_file, header = TRUE, sep = "\t", data.table = FALSE)
+ problem_positions <- problem_snps_raw$Pos[problem_snps_raw$Chr == chrom]
+ snp_normal <- snp_normal[!(snp_normal$POS %in% problem_positions), ]
+ }
+
+ if (!is.null(known_SNPs)) {
+ common_pos <- intersect(known_SNPs$position, snp_normal$POS)
+ valid_known_snps <- known_SNPs[match(common_pos, known_SNPs$position), ]
+ found_normal_data <- snp_normal[match(common_pos, snp_normal$POS), ]
+
+ # Define base columns (A=3, C=4, G=5, T=6 in our table)
+ bases <- c("A", "C", "G", "T")
+ ref_base_idx <- match(valid_known_snps$a0, bases)
+ alt_base_idx <- match(valid_known_snps$a1, bases)
+
+ # Extract counts
+ normal_counts_matrix <- as.matrix(found_normal_data[, 3:6, with = FALSE])
+ ref_counts <- as.numeric(vapply(seq_along(ref_base_idx), function(i) {
+ if (is.na(ref_base_idx[i])) {
+ return(0)
+ }
+ normal_counts_matrix[i, ref_base_idx[i]]
+ }, numeric(1)))
+ alt_counts <- as.numeric(vapply(seq_along(alt_base_idx), function(i) {
+ if (is.na(alt_base_idx[i])) {
+ return(0)
+ }
+ normal_counts_matrix[i, alt_base_idx[i]]
+ }, numeric(1)))
+
+ total_counts <- ref_counts + alt_counts
+ keep_mask <- total_counts > 0
+
+ vcf <- data.table::data.table(
+ "#CHROM" = if (chrom == "23") "X" else as.character(chrom),
+ POS = valid_known_snps$position[keep_mask],
+ ID = valid_known_snps$id[keep_mask],
+ REF = valid_known_snps$a0[keep_mask],
+ ALT = valid_known_snps$a1[keep_mask],
+ QUAL = ".",
+ FILTER = "PASS",
+ INFO = ".",
+ FORMAT = "GT"
+ )
+
+ bafs <- alt_counts[keep_mask] / total_counts[keep_mask]
+ gt <- rep("0/1", length(bafs))
+ gt[bafs <= heterozygous_filter] <- "0/0"
+ gt[bafs >= (1.0 - heterozygous_filter)] <- "1/1"
+ vcf$SAMP001 <- gt
+ } else {
+ # No legend: Infer REF/ALT from counts (largest count is Ref, second largest is Alt)
+ # This is sub-optimal but works for pre-phasing.
+ log_info("Inferring alleles from counts for chr {chrom}")
+ # (Simplified logic for now: only use the top 2 bases)
+ # Actually, legacy Battenberg ALWAYS requires a legend or it fails elsewhere.
+ log_failure("A reference legend is currently required to generate Beagle input. Please provide a reference_info_file.")
+ }
+
+ log_info("Writing {nrow(vcf)} SNPs to {output_file}")
+ writevcf_beagle(vcf, output_file)
+}
+
+#' Convert Beagle VCF to IMPUTE format
+#' @export
+convert_beagle_to_impute <- function(beagle_file, output_file) {
+ if (!file.exists(beagle_file)) log_failure("Beagle VCF file not found: {beagle_file}")
+
+ vcf <- tryCatch(
+ {
+ data.table::fread(beagle_file, skip = "#CHROM", header = TRUE)
+ },
+ error = function(e) {
+ if (file.info(beagle_file)$size < 500) {
+ return(data.table::data.table())
+ }
+ stop(e)
+ }
+ )
+
+ if (nrow(vcf) == 0) {
+ log_info("Beagle VCF is empty. Writing empty output.")
+ data.table::fwrite(data.table::data.table(), file = output_file, sep = " ", col.names = FALSE)
+ return(NULL)
+ }
+
+ gt_data <- vcf[[10]]
+ gt_only <- data.table::tstrsplit(gt_data, ":")[[1]]
+ haplo <- data.table::tstrsplit(gt_only, "[|/]")
+
+ impute_dt <- data.table::data.table(
+ V1 = "---",
+ V2 = vcf[["ID"]],
+ V3 = as.integer(as.numeric(vcf[["POS"]])),
+ V4 = vcf[["REF"]],
+ V5 = vcf[["ALT"]],
+ V6 = haplo[[1]],
+ V7 = haplo[[2]]
+ )
+ data.table::fwrite(impute_dt, file = output_file, sep = " ", col.names = FALSE, quote = FALSE)
+}
+
+#' Split a VCF into p and q arms
+#' @export
+split_and_writevcf_by_arm <- function(vcf, chrom, pathP, pathQ, coord_file) {
+ centromere_split <- load_centromere_splits(coord_file)
+ lookup_chrom <- if (chrom == "X") "23" else as.character(chrom)
+ if (!(lookup_chrom %in% names(centromere_split))) {
+ log_warning("Chromosome '{chrom}' not found in centromere table. Phasing as single unit.")
+ writevcf_beagle(vcf, pathP)
+ return(invisible(NULL))
+ }
+ split_point <- centromere_split[[lookup_chrom]]
+ vcf_p <- vcf[as.numeric(vcf$POS) <= split_point]
+ vcf_q <- vcf[as.numeric(vcf$POS) > split_point]
+ if (nrow(vcf_p) > 0) writevcf_beagle(vcf_p, pathP)
+ if (nrow(vcf_q) > 0) writevcf_beagle(vcf_q, pathQ)
+}
+
+#' Merge Beagle output from p and q arms back into IMPUTE format
+#' @export
+writebeagle_as_impute_arms <- function(vcfP = NULL, vcfQ = NULL, outfile) {
+ read_vcf <- function(path) {
+ if (!is.null(path) && file.exists(path)) {
+ return(data.table::fread(path, skip = "#CHROM", header = TRUE))
+ }
+ return(NULL)
+ }
+ outP <- read_vcf(vcfP)
+ outQ <- read_vcf(vcfQ)
+ if (is.null(outP) && is.null(outQ)) log_failure("Neither p-arm nor q-arm Beagle output found.")
+ combined <- data.table::rbindlist(list(outP, outQ), use.names = TRUE)
+ gt_data <- combined[[10]]
+ haplo <- data.table::tstrsplit(gt_data, "[|/]")
+ impute_dt <- data.table::data.table(
+ V1 = "---", V2 = combined$ID, V3 = combined$POS, V4 = combined$REF, V5 = combined$ALT,
+ V6 = haplo[[1]], V7 = haplo[[2]]
+ )
+ data.table::fwrite(impute_dt, file = outfile, sep = " ", col.names = FALSE, quote = FALSE)
+}
+
+#' Run Beagle 5 internal phasing
+#' @export
+run_beagle_internal <- function(chrom, samplename, beagle_in, out_prefix,
+ beaglejar, beagleref_dir,
+ threads_per_chromosome = 1) {
+ norm_c <- gsub("chr", "", as.character(chrom), ignore.case = TRUE)
+
+ # Discover reference VCF
+ ref_vcf <- NA
+ if (!is.na(beagleref_dir) && dir.exists(beagleref_dir)) {
+ ref_pats <- c(paste0("chr", chrom, ".*vcf.gz"), paste0("chr", norm_c, ".*vcf.gz"))
+ for (p in ref_pats) {
+ matches <- list.files(beagleref_dir, pattern = p, full.names = TRUE)
+ if (length(matches) > 0) {
+ ref_vcf <- matches[1]
+ break
+ }
+ }
+ }
+ if (is.na(ref_vcf)) {
+ log_failure("Running Beagle internal requires a reference VCF. Could not find one for chr {chrom} in {beagleref_dir}")
+ }
+
+ beagle_cmd <- sprintf(
+ "java -jar %s gt=%s out=%s nthreads=%d impute=false",
+ beaglejar, beagle_in, out_prefix, threads_per_chromosome
+ )
+ if (!is.na(ref_vcf)) beagle_cmd <- paste0(beagle_cmd, " ref=", ref_vcf)
+
+ log_info("Executing Beagle: {beagle_cmd}")
+ system(beagle_cmd)
+
+ # Return the expected output file path
+ vcf_out <- paste0(out_prefix, ".vcf.gz")
+ if (!file.exists(vcf_out)) vcf_out <- paste0(out_prefix, ".vcf")
+ return(vcf_out)
+}
diff --git a/R/impute_utils.R b/R/impute_utils.R
new file mode 100644
index 00000000..42bbfe4c
--- /dev/null
+++ b/R/impute_utils.R
@@ -0,0 +1,170 @@
+#' Read in the reference_info_file.
+#'
+#' Reads in a file with the following columns:
+#' chromosome : 1-X
+#' impute_legend : Legend file in IMPUTE -l format
+#' genetic_map : Genetic map file in IMPUTE -m format
+#' impute_hap : Phased haplotype file in IMPUTE -h format
+#' start : Start of the chromosome
+#' end : End of the chromosome
+#' is_par : 1 when pseudo autosomal region, 0 when not
+#'
+#' @param reference_info_file Path to the reference_info_file on disk.
+#' @param is_male A boolean describing whether the sample under study is male.
+#' @param chrom The name of a chromosome to subset the contents of the reference_info_file with (optional)
+#' @return A data.frame with 7 columns: Chromosome, impute_legend, genetic_map, impute_hap, start, end, is_par
+#' @author sd11
+#' @export
+parse_imputeinfofile <- function(reference_info_file, is_male, chrom = NA) {
+ if (is.na(reference_info_file) || !file.exists(reference_info_file)) {
+ return(data.table::data.table())
+ }
+
+ # Use fread for high-speed reading.
+ impute_info <- data.table::fread(
+ reference_info_file,
+ col.names = c(
+ "chrom", "impute_legend", "genetic_map",
+ "impute_hap", "start", "end", "is_par"
+ ),
+ stringsAsFactors = FALSE
+ )
+
+ expected_cols <- c("chrom", "impute_legend", "genetic_map", "impute_hap", "start", "end", "is_par")
+ if (!all(expected_cols %in% names(impute_info))) {
+ # If columns are missing, try to assign them if possible, or fail
+ if (ncol(impute_info) == length(expected_cols)) {
+ names(impute_info) <- expected_cols
+ } else {
+ log_failure("Reference info file does not have the expected number of columns (7). Found: {ncol(impute_info)}")
+ }
+ }
+
+ # Filter based on gender
+ if (!is.na(is_male) && !is_male) {
+ impute_info <- impute_info[impute_info[["chrom"]] != "Y", ]
+ }
+ # Subset for a particular chromosome
+ if (!is.na(chrom)) {
+ impute_info <- impute_info[impute_info[["chrom"]] == chrom, ]
+ }
+ return(impute_info)
+}
+
+#' Check reference info file consistency
+#' @param reference_info_file Path to the reference_info_file on disk.
+#' @author sd11
+check_imputeinfofile <- function(reference_info_file, is_male, usebeagle) {
+ if (is.na(reference_info_file)) {
+ return(invisible(NULL))
+ }
+
+ impute_info <- parse_imputeinfofile(reference_info_file, is_male)
+ if (nrow(impute_info) == 0) {
+ return(invisible(NULL))
+ }
+
+ if (usebeagle) {
+ # For Beagle input generation, we only strictly need the legend file
+ if (any(!file.exists(as.character(impute_info$impute_legend)))) {
+ log_failure("Could not find reference legend files, make sure paths in reference_info_file point to the correct location")
+ }
+ } else {
+ if (any(!file.exists(as.character(impute_info$impute_legend)) |
+ !file.exists(as.character(impute_info$genetic_map)) |
+ !file.exists(as.character(impute_info$impute_hap)))) {
+ log_failure("Could not find reference files, make sure paths in reference_info_file point to the correct location")
+ }
+ }
+}
+
+#' Returns the chromosome names that are supported
+#' @param chrom_names A vector of chromosome names to use directly (optional)
+#' @return A vector containing the supported chromosome names
+#' @author sd11
+#' @export
+get_chrom_names <- function(reference_info_file = NA, is_male = NA, chrom = NA, analysis = "paired", chrom_names = NULL,
+ usebeagle = FALSE, beagleref_dir = NA) {
+ if (!is.null(chrom_names)) {
+ return(chrom_names)
+ }
+
+ if (is.na(reference_info_file)) {
+ # If we are using Beagle, we might be able to infer chroms from beagleref_dir
+ if (usebeagle && !is.na(beagleref_dir) && dir.exists(beagleref_dir)) {
+ vcfs <- list.files(beagleref_dir, pattern = "\\.vcf(\\.gz)?$")
+ found_chroms <- gsub(".*chr([0-9XY]+).*", "\\1", vcfs)
+ found_chroms <- unique(found_chroms[found_chroms %in% c(as.character(1:22), "X", "Y")])
+ if (length(found_chroms) > 0) {
+ log_info("Inferred chromosomes from Beagle reference directory: {paste(found_chroms, collapse=', ')}")
+ return(sort(found_chroms))
+ }
+ }
+ # Fallback to standard human autosomes if nothing else provided
+ log_warning("No reference_info_file or chrom_names provided. Defaulting to 1-22.")
+ return(as.character(1:22))
+ }
+
+ chrom_names <- unique(parse_imputeinfofile(reference_info_file, is_male, chrom = chrom)$chrom)
+ if (analysis == "cell_line" || analysis == "germline") {
+ # Both cell line and germline analysis do not yield usable data on X and Y, so remove
+ chrom_names <- chrom_names[!chrom_names %in% c("X", "Y")]
+ }
+ return(chrom_names)
+}
+
+#' Concatenate the impute output generated for each of the regions.
+#'
+#' This function assembles the impute output generated.
+#' @param inputfile.prefix Prefix of the input files.
+#' @param outputfile Where to store the output.
+#' @param is_male Boolean describing whether the sample is male (TRUE) or female (FALSE).
+#' @param reference_info_file Path to the reference_info_file on disk.
+#' @param region.size An integer describing the region size to be used by impute (optional).
+#' @param chrom The name of a chromosome on which this function should run.
+#' @author dw9
+#' @export
+combine_impute_output <- function(inputfile.prefix, outputfile, is_male, reference_info_file, region.size = 5000000, chrom = NA) {
+ # Read in the impute file information
+ impute_info <- parse_imputeinfofile(reference_info_file, is_male, chrom = chrom)
+
+ # Assemble the start and end points of all regions
+ all.boundaries <- array(0, c(0, 2))
+ for (r in seq_len(nrow(impute_info))) {
+ boundaries <- seq(as.numeric(impute_info[r, ]$start), as.numeric(impute_info[r, ]$end), region.size)
+ if (boundaries[length(boundaries)] != impute_info[r, ]$end) {
+ boundaries <- c(boundaries, impute_info[r, ]$end)
+ }
+ all.boundaries <- rbind(all.boundaries, cbind(boundaries[-(length(boundaries))], boundaries[-1]))
+ }
+ # Concatenate all the regions
+ impute.output <- concatenateImputeFiles(inputfile.prefix, all.boundaries)
+ data.table::fwrite(
+ impute.output,
+ file = outputfile,
+ row.names = FALSE,
+ col.names = FALSE,
+ quote = FALSE,
+ sep = " "
+ )
+}
+
+#' Load centromere coordinates from a reference file
+#'
+#' @param coord_file Path to the gcCorrect_chromosome_coordinates_hg38.txt or similar file.
+#' @return A named list of centromere split points.
+#' @keywords internal
+load_centromere_splits <- function(coord_file) {
+ if (!file.exists(coord_file)) {
+ log_failure("Centromere coordinate file not found: {coord_file}")
+ }
+ coords <- data.table::fread(coord_file, header = TRUE)
+ # Map columns (chr, cen.left.base, cen.right.base) to a single split point (mean)
+ splits <- list()
+ for (i in seq_len(nrow(coords))) {
+ chr <- as.character(coords$chr[i])
+ # split point is the middle of the centromere range
+ splits[[chr]] <- (coords$cen.left.base[i] + coords$cen.right.base[i]) / 2
+ }
+ return(splits)
+}
diff --git a/R/logger.R b/R/logger.R
new file mode 100644
index 00000000..6c180070
--- /dev/null
+++ b/R/logger.R
@@ -0,0 +1,114 @@
+#' Initialize and Configure Logging
+#'
+#' Sets up a file-based logger using the `logger` package. It creates the
+#' destination directory if it does not already exist and adjusts the
+#' logging threshold based on the desired verbosity.
+#'
+#' @param log_path Character string. The full path to the log file.
+#' @param verbose Logical. If `TRUE`, the log level is set to `DEBUG`.
+#' If `FALSE`, it defaults to `INFO`.
+#'
+#' @export
+log_setup <- function(log_path, verbose = FALSE) {
+ if (file.info(log_path)$isdir %||% dir.exists(log_path)) {
+ log_path <- file.path(log_path, "session.log")
+ }
+
+ # Create directory if it doesn't exist
+ dir.create(dirname(log_path), recursive = TRUE, showWarnings = FALSE)
+
+ # Set where the log goes
+ logger::log_appender(logger::appender_file(log_path))
+
+ # Set sensitivity: if verbose=TRUE, we record DEBUG level
+ if (verbose) {
+ logger::log_threshold(logger::DEBUG)
+ } else {
+ logger::log_threshold(logger::INFO)
+ }
+}
+
+#' Log Informational Messages
+#'
+#' Displays a formatted message to the console using `cli` and
+#' simultaneously records a clean, non-ANSI version of the message to
+#' the log file at the `INFO` level.
+#'
+#' @param msg Character string. The message to be logged and displayed.
+#' @param ... Additional arguments passed to `cli` formatting functions.
+#'
+#' @export
+log_info <- function(msg, ...) {
+ caller_env <- parent.frame()
+ cli::cli_inform(msg, .envir = caller_env, ...)
+
+ formatted_msg <- cli::format_inline(
+ msg,
+ .envir = parent.frame()
+ )
+ clean <- cli::ansi_strip(formatted_msg)
+
+ logger::log_info(clean) # Record to file
+}
+
+#' Log Debugging Messages
+#'
+#' Displays a message to the console and records it to the log file
+#' specifically at the `DEBUG` level. Note that the message will only
+#' appear in the log file if the logger threshold is set to `DEBUG`.
+#'
+#' @param msg Character string. The message to be logged and displayed.
+#' @param ... Additional arguments passed to `cli` formatting functions.
+#'
+#' @export
+log_debug <- function(msg, ...) {
+ caller_env <- parent.frame()
+ cli::cli_inform(msg, .envir = caller_env, ...)
+ formatted_msg <- cli::format_inline(
+ msg,
+ .envir = parent.frame()
+ )
+ clean <- cli::ansi_strip(formatted_msg)
+ logger::log_debug(clean) # Record to file ONLY if threshold is DEBUG
+}
+
+#' Log Failure Messages and Abort
+#'
+#' Signals a critical failure by calling `cli::cli_abort()`, which stops
+#' execution. The error message is stripped of ANSI formatting and
+#' recorded to the log file at the `FAILURE` level.
+#'
+#' @param msg Character string. The error message.
+#' @param ... Additional arguments passed to `cli::cli_abort()`.
+#'
+#' @export
+log_failure <- function(msg, ...) {
+ caller_env <- parent.frame()
+ cli::cli_abort(msg, .envir = caller_env, ...)
+ formatted_msg <- cli::format_inline(
+ msg,
+ .envir = parent.frame()
+ )
+ clean <- cli::ansi_strip(formatted_msg)
+ logger::log_failure(clean)
+}
+
+#' Log Warning Messages
+#'
+#' Displays a warning to the console and records it to the log file
+#' at the `WARN` level.
+#'
+#' @param msg Character string. The warning message.
+#' @param ... Additional arguments passed to `cli::cli_warn()`.
+#'
+#' @export
+log_warning <- function(msg, ...) {
+ caller_env <- parent.frame()
+ cli::cli_warn(msg, .envir = caller_env, ...)
+ formatted_msg <- cli::format_inline(
+ msg,
+ .envir = parent.frame()
+ )
+ clean <- cli::ansi_strip(formatted_msg)
+ logger::log_warn(clean)
+}
diff --git a/R/orderEdges.R b/R/orderEdges.R
deleted file mode 100644
index 21164322..00000000
--- a/R/orderEdges.R
+++ /dev/null
@@ -1,174 +0,0 @@
-#' Convenience function that orders edges or squares
-#' @author dw9, kd7
-#' @noRd
-orderEdges = function(levels, l, ntot,x,y) {
- nMaj1 = NULL
- nMin1 = NULL
- nMaj2 = NULL
- nMin2 = NULL
-
- # case 1 or 2a:
- if(l>levels[3]) {
- #LogR criterion: ntot < x+y+1
- if(ntot < x+y+1) {
- # take the six options, sorted according to LogR priority (3+3) + simplicity (1+2+1+2)
- nMaj1 = c(y,y-1,y,
- y+1,y+1,y+1)
- nMin1 = c(x,x,x,
- x,x-1,x)
- nMaj2 = c(y+1,y+1,y+2,
- y+1,y+1,y+1)
- nMin2 = c(x,x,x,
- x+1,x+1,x+2)
- }
- else {
- nMaj1 = c(y+1,y+1,y+1,
- y,y-1,y)
- nMin1 = c(x,x-1,x,
- x,x,x)
- nMaj2 = c(y+1,y+1,y+1,
- y+1,y+1,y+2)
- nMin2 = c(x+1,x+1,x+2,
- x,x,x)
- }
- }
- # case 2c:
- else if (l>levels[2]) {
- if(ntot < x+y+1) {
- nMaj1 = c(y,y,y,
- y+1,y+1,y+1)
- nMin1 = c(x,x-1,x,
- x,x-1,x)
- nMaj2 = c(y,y,y,
- y+1,y+1,y+1)
- nMin2 = c(x+1,x+1,x+2,
- x+1,x+1,x+2)
- }
- else {
- nMaj1 = c(y+1,y+1,y+1,
- y,y,y)
- nMin1 = c(x,x-1,x,
- x,x-1,x)
- nMaj2 = c(y+1,y+1,y+1,
- y,y,y)
- nMin2 = c(x+1,x+1,x+2,
- x+1,x+1,x+2)
- }
- }
- # case 2b:
- else {
- if(ntot < x+y+1) {
- nMaj1 = c(y,y,y,
- y,y-1,y)
- nMin1 = c(x,x-1,x,
- x+1,x+1,x+1)
- nMaj2 = c(y,y,y,
- y+1,y+1,y+2)
- nMin2 = c(x+1,x+1,x+2,
- x+1,x+1,x+1)
- }
- else {
- nMaj1 = c(y,y-1,y,
- y,y,y)
- nMin1 = c(x+1,x+1,x+1,
- x,x-1,x)
- nMaj2 = c(y+1,y+1,y+2,
- y,y,y)
- nMin2 = c(x+1,x+1,x+1,
- x+1,x+1,x+2)
- }
- }
- #DCW 260314 - avoid negative CNs
- negative.CN = which(nMaj1<0|nMin1<0|nMaj2<0|nMin2<0)
- if(length(negative.CN)>0){
- nMaj1[negative.CN]=NA
- nMin1[negative.CN]=NA
- nMaj2[negative.CN]=NA
- nMin2[negative.CN]=NA
- return(cbind(nMaj1,nMin1,nMaj2,nMin2))
- }else{
- return(cbind(nMaj1,nMin1,nMaj2,nMin2))
- }
-}
-
-
-#' Function that fetches the nearest edge for a given a rho, psi, BAF and major and minor allele
-#' that corresponds to a certain mix of two copy number states. It first identifies the nearest edge
-#' and then just compares the vertices at the end of this edge to find the best corner.
-#' @author dw9, kd7
-#' @noRd
-GetNearestCorners_bestOption <-function( rho, psi, BAFreq, nMajor, nMinor ) {
- nMaj = c(floor(nMajor),ceiling(nMajor),floor(nMajor),ceiling(nMajor))
- nMin = c(ceiling(nMinor),ceiling(nMinor),floor(nMinor),floor(nMinor))
- x = floor(nMinor)
- y = floor(nMajor)
-
- # total copy number, to determine priority options
- ntot = nMajor + nMinor
-
- BAF_levels = (1-rho+rho*nMaj)/(2-2*rho+rho*(nMaj+nMin))
- #problem if rho=1 and nMaj=0 and nMin=0
- BAF_levels[nMaj==0 & nMin==0] = 0.5
-
- nMaj1 = NULL
- nMin1 = NULL
- nMaj2 = NULL
- nMin2 = NULL
-
- # case 1 or 2a:
- #if( is.finite(BAF_levels[3]) && (BAFreq>BAF_levels[3]) ) { # kjd 14-2-2014
- if(BAFreq>BAF_levels[3]) { #DCW
- #LogR criterion: ntot < x+y+1
- if(ntot < x+y+1) {
- # take the six options, sorted according to LogR priority (3+3) + simplicity (1+2+1+2)
- nMaj1 = y
- nMin1 = x
- nMaj2 = y+1
- nMin2 = x
- }
- else {
- nMaj1 = y+1
- nMin1 = x
- nMaj2 = y+1
- nMin2 = x+1
- }
- }
- # case 2c:
- #else if( is.finite(BAF_levels[2]) && (BAFreq>BAF_levels[2]) ) { # kjd 14-2-2014
- else if(BAFreq>BAF_levels[2]) { #DCW
- if(ntot < x+y+1) {
- nMaj1 = y
- nMin1 = x
- nMaj2 = y
- nMin2 = x+1
- }
- else {
- nMaj1 = y+1
- nMin1 = x
- nMaj2 = y+1
- nMin2 = x+1
- }
- }
- # case 2b:
- else {
- if(ntot < x+y+1) {
- nMaj1 = y
- nMin1 = x
- nMaj2 = y
- nMin2 = x+1
- }
- else {
- nMaj1 = y
- nMin1 = x+1
- nMaj2 = y+1
- nMin2 = x+1
- }
- }
-
- nMaj_vect = c( nMaj1, nMaj2 )
- nMin_vect = c( nMin1, nMin2 )
-
- nearest_segment = list( nMaj = nMaj_vect, nMin = nMin_vect )
-
- return( nearest_segment )
-}
diff --git a/R/order_edges.R b/R/order_edges.R
new file mode 100644
index 00000000..d61d97c8
--- /dev/null
+++ b/R/order_edges.R
@@ -0,0 +1,105 @@
+#' Prioritize candidate integer copy number states around a fractional state
+#'
+#' Returns candidate grid edges based on BAF and LogR position, following Battenberg's
+#' original prioritization rules (LogR distance + simplicity).
+#'
+#' @param full logical; if TRUE return all 6 candidate edges (for subclonal search),
+#' if FALSE return only the best edge (for clonal likelihood).
+#' @return A list containing matrices `nMaj1`, `nMin1`, `nMaj2`, `nMin2` (NxM),
+#' and `nMaj`, `nMin` (Nx2) for the best edge corners.
+#' @noRd
+prioritizeCopyNumbers <- function(rho, psi, BAF_req, nMajor, nMinor, full = TRUE) {
+ # Vectorized Inputs
+ x <- floor(nMinor)
+ y <- floor(nMajor)
+ ntot <- nMajor + nMinor
+ n <- length(BAF_req)
+
+ # Pre-calculate BAF at key corners
+ calc_baf <- function(nM, nm) {
+ num <- 1 - rho + rho * nM
+ den <- 2 - 2 * rho + rho * (nM + nm)
+ lev <- num / den
+ lev[nM == 0 & nm == 0] <- 0.5
+ lev
+ }
+
+ lev3 <- calc_baf(y, x) # Corner C3
+ lev2 <- calc_baf(y + 1, x + 1) # Corner C2
+
+ case_1_2a <- BAF_req > lev3
+ case_2c <- (!case_1_2a) & (BAF_req > lev2)
+ logR_low <- ntot < (x + y + 1)
+
+ # Initialize matrices for all 6 possible candidates
+ # We use the offsets defined in original orderEdges logic
+ m1 <- matrix(0, n, 6)
+ n1 <- matrix(0, n, 6)
+ m2 <- matrix(0, n, 6)
+ n2 <- matrix(0, n, 6)
+
+ # Helper to fill offsets for a logical mask
+ fill_offsets <- function(mask, om1, on1, om2, on2) {
+ # Guard against NAs in mask
+ mask[is.na(mask)] <- FALSE
+ if (any(mask)) {
+ m1[mask, ] <<- sweep(matrix(om1, sum(mask), 6, byrow = TRUE), 1, y[mask], "+")
+ n1[mask, ] <<- sweep(matrix(on1, sum(mask), 6, byrow = TRUE), 1, x[mask], "+")
+ m2[mask, ] <<- sweep(matrix(om2, sum(mask), 6, byrow = TRUE), 1, y[mask], "+")
+ n2[mask, ] <<- sweep(matrix(on2, sum(mask), 6, byrow = TRUE), 1, x[mask], "+")
+ }
+ }
+
+ # Fill based on original Battenberg orderEdges logic
+ fill_offsets(
+ case_1_2a & logR_low,
+ c(0, -1, 0, 1, 1, 1), c(0, 0, 0, 0, -1, 0),
+ c(1, 1, 2, 1, 1, 1), c(0, 0, 0, 1, 1, 2)
+ )
+ fill_offsets(
+ case_1_2a & (!logR_low),
+ c(1, 1, 1, 0, -1, 0), c(0, -1, 0, 0, 0, 0),
+ c(1, 1, 1, 1, 1, 2), c(1, 1, 2, 0, 0, 0)
+ )
+ fill_offsets(
+ case_2c & logR_low,
+ c(0, 0, 0, 1, 1, 1), c(0, -1, 0, 0, -1, 0),
+ c(0, 0, 0, 1, 1, 1), c(1, 1, 2, 1, 1, 2)
+ )
+ fill_offsets(
+ case_2c & (!logR_low),
+ c(1, 1, 1, 0, 0, 0), c(0, -1, 0, 0, -1, 0),
+ c(1, 1, 1, 0, 0, 0), c(1, 1, 2, 1, 1, 2)
+ )
+ fill_offsets(
+ (!case_1_2a) & (!case_2c) & logR_low,
+ c(0, 0, 0, 0, -1, 0), c(0, -1, 0, 1, 1, 1),
+ c(0, 0, 0, 1, 1, 2), c(1, 1, 2, 1, 1, 1)
+ )
+ fill_offsets(
+ (!case_1_2a) & (!case_2c) & (!logR_low),
+ c(0, -1, 0, 0, 0, 0), c(1, 1, 1, 0, -1, 0),
+ c(0, 0, 0, 0, 0, 0), c(1, 1, 2, 1, 1, 2)
+ )
+
+ # Validation: Avoid negative CNs
+ invalid <- (m1 < 0 | n1 < 0 | m2 < 0 | n2 < 0)
+ invalid[is.na(invalid)] <- TRUE # Treat NAs as invalid
+ m1[invalid] <- NA
+ n1[invalid] <- NA
+ m2[invalid] <- NA
+ n2[invalid] <- NA
+
+ if (full) {
+ return(list(
+ nMaj1 = m1, nMin1 = n1, nMaj2 = m2, nMin2 = n2,
+ nMaj = cbind(m1[, 1], m2[, 1]), nMin = cbind(n1[, 1], n2[, 1])
+ ))
+ } else {
+ return(list(
+ nMaj1 = m1[, 1, drop = FALSE], nMin1 = n1[, 1, drop = FALSE],
+ nMaj2 = m2[, 1, drop = FALSE], nMin2 = n2[, 1, drop = FALSE],
+ nMaj = cbind(m1[, 1], m2[, 1]), nMin = cbind(n1[, 1], n2[, 1])
+ ))
+ }
+}
diff --git a/R/plotting.R b/R/plotting.R
index 5993c7d6..d2248576 100644
--- a/R/plotting.R
+++ b/R/plotting.R
@@ -1,26 +1,64 @@
+#' @importFrom gtools mixedsort
+NULL
+
#' Function that plots two types of data points against it's chromosomal location.
#' Note: This is a plot PER chromosome.
#' @noRd
-create.haplotype.plot = function(chrom.position, points.blue, points.red, x.min, x.max, title, xlab, ylab) {
- par(pch=".", cex=1, cex.main=0.8, cex.axis = 0.6, cex.lab=0.7,yaxp=c(-0.05,1.05,6))
- plot(c(x.min,x.max), c(0,1), type="n", main=title, xlab=xlab, ylab=ylab)
- if (length(chrom.position) > 0) {
- points(chrom.position, points.blue, col="blue")
- points(chrom.position, points.red, col="red")
+create_haplotype_plot <- function(
+ chrom_position,
+ points.blue, points.red,
+ x_min, x_max,
+ title, xlab, ylab
+) {
+ graphics::par(
+ pch = ".", cex = 1, cex.main = 0.8,
+ cex.axis = 0.6, cex.lab = 0.7,
+ yaxp = c(-0.05, 1.05, 6)
+ )
+ graphics::plot(
+ c(x_min, x_max), c(0, 1),
+ type = "n", main = title, xlab = xlab, ylab = ylab
+ )
+ if (length(chrom_position) > 0) {
+ graphics::points(chrom_position, points.blue, col = "blue")
+ graphics::points(chrom_position, points.red, col = "red")
}
}
#' Function that plots two types of data points against it's chromosomal location.
#' Note: This is a plot PER chromosome.
#' @noRd
-create.segmented.plot = function(chrom.position, points.red, points.green, x.min, x.max, title, xlab, ylab, prior_bkps_pos=NULL) {
- par(mar = c(5,5,5,0.5), cex = 0.4, cex.main=3, cex.axis = 2, cex.lab = 2)
- plot(c(x.min,x.max), c(0,1), pch=".", type="n", main=title, xlab=xlab, ylab=ylab)
- points(chrom.position, points.red, pch=".", col="red", cex=2)
- points(chrom.position, points.green, pch=19, cex=0.5, col="green")
+create_segmented_plot <- function(
+ chrom_position,
+ points.red,
+ points.green,
+ x_min, x_max,
+ title, xlab,
+ ylab,
+ prior_bkps_pos = NULL
+) {
+ graphics::par(
+ mar = c(5, 5, 5, 0.5),
+ cex = 0.4,
+ cex.main = 3,
+ cex.axis = 2,
+ cex.lab = 2
+ )
+ graphics::plot(
+ c(x_min, x_max), c(0, 1),
+ pch = ".", type = "n", main = title, xlab = xlab, ylab = ylab
+ )
+ graphics::points(
+ chrom_position, points.red,
+ pch = ".", col = "red", cex = 2
+ )
+ graphics::points(
+ chrom_position, points.green,
+ pch = 19, cex = 0.5, col = "green"
+ )
if (!is.null(prior_bkps_pos)) {
- for (i in 1:length(prior_bkps_pos)) {
- abline(v=prior_bkps_pos[i])
+ for (i in seq_along(prior_bkps_pos)) {
+ graphics::abline(v = prior_bkps_pos[i])
}
}
}
@@ -28,15 +66,36 @@ create.segmented.plot = function(chrom.position, points.red, points.green, x.min
#' Function that plots two types of data points against it's chromosomal location.
#' Note: This is a plot PER chromosome.
#' @noRd
-create.baf.plot = function(chrom.position, points.red.blue, plot.red, points.darkred, points.darkblue, x.min, x.max, title, xlab, ylab, prior_bkps_pos=NULL) {
- par(mar = c(5,5,5,0.5), cex = 0.4, cex.main=3, cex.axis = 2, cex.lab = 2)
- plot(c(x.min,x.max), c(0,1), pch=".", type = "n", main=title, xlab=xlab, ylab=ylab)
- points(chrom.position, points.red.blue, pch=".", col=ifelse(plot.red, "red", "blue"), cex=2)
- points(chrom.position, points.darkred, pch=19, cex=0.5, col="darkred")
- points(chrom.position, points.darkblue, pch=19, cex=0.5, col="darkblue")
+create_baf_plot <- function(
+ chrom_position,
+ points_red_blue, plot_red,
+ points_darkred, points_darkblue,
+ x_min, x_max,
+ title, xlab, ylab,
+ prior_bkps_pos = NULL
+) {
+ graphics::par(
+ mar = c(5, 5, 5, 0.5), cex = 0.4, cex.main = 3, cex.axis = 2, cex.lab = 2
+ )
+ graphics::plot(
+ c(x_min, x_max), c(0, 1),
+ pch = ".", type = "n", main = title, xlab = xlab, ylab = ylab
+ )
+ graphics::points(
+ chrom_position, points_red_blue,
+ pch = ".", col = ifelse(plot_red, "red", "blue"), cex = 2
+ )
+ graphics::points(
+ chrom_position, points_darkred,
+ pch = 19, cex = 0.5, col = "darkred"
+ )
+ graphics::points(
+ chrom_position, points_darkblue,
+ pch = 19, cex = 0.5, col = "darkblue"
+ )
if (!is.null(prior_bkps_pos)) {
- for (i in 1:length(prior_bkps_pos)) {
- abline(v=prior_bkps_pos[i])
+ for (i in seq_along(prior_bkps_pos)) {
+ graphics::abline(v = prior_bkps_pos[i])
}
}
}
@@ -44,45 +103,86 @@ create.baf.plot = function(chrom.position, points.red.blue, plot.red, points.dar
#' Function that creates the plots for subclonal copy number
#' Note: This is a plot PER chromosome.
#' @noRd
-create.subclonal.cn.plot = function(chrom, chrom.position, LogRposke, LogRchr, BAFchr, BAFsegchr, BAFpvalschr, subcloneres, siglevel, x.min, x.max, title, xlab, ylab.logr, ylab.baf, breakpoints_pos=NULL, svs_pos=NULL) {
-
- plot_breakpoints = function(breakpoints, svs_pos) {
+create_subclonal_cn_plot <- function(
+ chrom,
+ chrom_position,
+ LogRposke, LogRchr,
+ BAFchr, BAFsegchr,
+ BAFpvalschr, subcloneres,
+ siglevel, x_min, x_max,
+ title, xlab, ylab_logr,
+ ylab_baf, breakpoints_pos = NULL,
+ svs_pos = NULL
+) {
+ plot_breakpoints <- function(breakpoints, svs_pos) {
# Plot the breakpoints
if (!is.null(breakpoints)) {
- for (i in 1:length(breakpoints)) {
- abline(v=breakpoints[i], col="darkgrey", lwd=1)
+ for (i in seq_along(breakpoints)) {
+ graphics::abline(v = breakpoints[i], col = "darkgrey", lwd = 1)
}
}
-
+
# Overplot the SV breakpoints, if supplied
if (!is.null(svs_pos)) {
- for (i in 1:length(svs_pos)) {
- abline(v=svs_pos[i], lty=3, col="lightgreen", lwd=1)
+ for (i in seq_along(svs_pos)) {
+ graphics::abline(v = svs_pos[i], lty = 3, col = "lightgreen", lwd = 1)
}
}
}
-
+
# Plot the logR
- par(mar=c(2.5,2.5,2.5,0.25), cex=0.4, cex.main=1.5, cex.axis=1, cex.lab=1, mfrow=c(2,1))
- plot(c(x.min, x.max), c(-3,3), pch=".", type="n", main=title, xlab=xlab, ylab=ylab.logr)
- points(LogRposke/1000000, LogRchr, pch=".", col="grey")
+ graphics::par(
+ mar = c(2.5, 2.5, 2.5, 0.25),
+ cex = 0.4, cex.main = 1.5,
+ cex.axis = 1, cex.lab = 1, mfrow = c(2, 1)
+ )
+ graphics::plot(
+ c(x_min, x_max), c(-3, 3),
+ pch = ".", type = "n",
+ main = title, xlab = xlab,
+ ylab = ylab_logr
+ )
+ graphics::points(LogRposke / 1000000, LogRchr, pch = ".", col = "grey")
plot_breakpoints(breakpoints_pos, svs_pos)
-
+
# Plot BAF
- plot(c(x.min, x.max), c(0,1), pch=".", type="n", main=title, xlab=xlab, ylab=ylab.baf)
- points(chrom.position, BAFchr, pch=".", col="grey")
+ graphics::plot(
+ c(x_min, x_max),
+ c(0, 1),
+ pch = ".", type = "n",
+ main = title,
+ xlab = xlab, ylab = ylab_baf
+ )
+ graphics::points(chrom_position, BAFchr, pch = ".", col = "grey")
plot_breakpoints(breakpoints_pos, svs_pos)
-
+
# Plot segments in top of BAF
- points(chrom.position, BAFsegchr, pch=19, cex=0.5, col=ifelse(BAFpvalschr>siglevel, "darkgreen", "red"))
- points(chrom.position, 1-BAFsegchr, pch=19, cex=0.5, col=ifelse(BAFpvalschr>siglevel, "darkgreen", "red"))
- for (i in 1:dim(subcloneres)[1]) {
- if(subcloneres[i,1]==chrom) {
- text((as.numeric(subcloneres[i,"startpos"])+as.numeric(subcloneres[i,"endpos"]))/2/1000000,as.numeric(subcloneres[i,"BAF"])-0.04,
- paste(subcloneres[i,"nMaj1_A"],"+",subcloneres[i,"nMin1_A"],": ",100*round(as.numeric(subcloneres[i,"frac1_A"]),3),"%",sep=""),cex = 0.8)
- if(!is.na(subcloneres[i,"nMaj2_A"])) {
- text((as.numeric(subcloneres[i,"startpos"])+as.numeric(subcloneres[i,"endpos"]))/2/1000000,as.numeric(subcloneres[i,"BAF"])-0.08,
- paste(subcloneres[i,"nMaj2_A"],"+",subcloneres[i,"nMin2_A"],": ",100*round(as.numeric(subcloneres[i,"frac2_A"]),3),"%",sep=""), cex = 0.8)
+ graphics::points(
+ chrom_position, BAFsegchr,
+ pch = 19, cex = 0.5,
+ col = ifelse(BAFpvalschr > siglevel,
+ "darkgreen", "red"
+ )
+ )
+ graphics::points(
+ chrom_position, 1 - BAFsegchr,
+ pch = 19, cex = 0.5,
+ col = ifelse(BAFpvalschr > siglevel,
+ "darkgreen", "red"
+ )
+ )
+ for (i in seq_len(dim(subcloneres)[1])) {
+ if (subcloneres[i, 1] == chrom) {
+ graphics::text(
+ (as.numeric(subcloneres[i, "startpos"]) + as.numeric(subcloneres[i, "endpos"])) / 2 / 1000000, as.numeric(subcloneres[i, "BAF"]) - 0.04,
+ paste(subcloneres[i, "nMaj1_A"], "+", subcloneres[i, "nMin1_A"], ": ", 100 * round(as.numeric(subcloneres[i, "frac1_A"]), 3), "%", sep = ""),
+ cex = 0.8
+ )
+ if (!is.na(subcloneres[i, "nMaj2_A"])) {
+ graphics::text((as.numeric(subcloneres[i, "startpos"]) + as.numeric(subcloneres[i, "endpos"])) / 2 / 1000000, as.numeric(subcloneres[i, "BAF"]) - 0.08,
+ paste(subcloneres[i, "nMaj2_A"], "+", subcloneres[i, "nMin2_A"], ": ", 100 * round(as.numeric(subcloneres[i, "frac2_A"]), 3), "%", sep = ""),
+ cex = 0.8
+ )
}
}
}
@@ -93,30 +193,69 @@ create.subclonal.cn.plot = function(chrom, chrom.position, LogRposke, LogRchr, B
#' NAP - July 2020 - updated main title now replacing 'cellularity' with 'purity' and 'goodness-of-fit' with 'PGAclonal' + adding TUMOURNAME
#' NAP - November 2023 - Replacing 'PGAclonal' with 'PGA.is.clonal' for more clarity
#' @noRd
-create.bb.plot.average = function(bafsegmented, ploidy, rho, goodnessOfFit, pos_min, pos_max, segment_states_min, segment_states_tot, chr.segs, chr.names, tumourname, ylim=5) {
+create_bb_plot_average <- function(
+ bafsegmented, ploidy, rho,
+ goodness_of_fit, pos_min, pos_max,
+ segment_states_min, segment_states_tot,
+ chr_segs, chr_names, tumourname, ylim = 5
+) {
+ log_debug(paste("Executing refactored create_bb_plot_average with goodness:", goodness_of_fit))
# Plot main frame and title
- par(mar = c(0.5,5,5,0.5), cex = 0.4, cex.main=3, cex.axis = 2.5)
- maintitle = paste0(substring(tumourname, 36, first = T),", Ploidy: ",sprintf("%1.2f",ploidy),", Purity: ",sprintf("%2.0f",rho*100),"%, PGA.is.clonal: ",sprintf("%2.1f",goodnessOfFit*100),"%")
- #maintitle = paste("Ploidy: ",sprintf("%1.2f",ploidy),", aberrant cell fraction: ",sprintf("%2.0f",rho*100),"%, goodness of fit: ",sprintf("%2.1f",goodnessOfFit*100),"%",sep="")
- plot(c(1,nrow(bafsegmented)), c(0,ylim), type = "n", xaxt = "n", main = maintitle, xlab = "", ylab = "")
- abline(v=0,lty=1,col="lightgrey")
+ graphics::par(
+ mar = c(0.5, 5, 5, 0.5), cex = 0.4, cex.main = 3, cex.axis = 2.5
+ )
+ maintitle <- paste0(
+ tumourname,
+ ", Ploidy: ", sprintf("%1.2f", ploidy),
+ ", Purity: ", sprintf("%2.0f", rho * 100),
+ "%, PGA.is.clonal: ",
+ sprintf("%2.1f", goodness_of_fit * 100), "%"
+ )
+ graphics::plot(
+ c(1, nrow(bafsegmented)), c(0, ylim),
+ type = "n", xaxt = "n", main = maintitle, xlab = "", ylab = ""
+ )
+ graphics::abline(v = 0, lty = 1, col = "lightgrey")
# Horizontal lines for y=0 to y=5
- abline(h=c(0:ylim),lty=1,col="lightgrey")
+ graphics::abline(h = c(0:ylim), lty = 1, col = "lightgrey")
# Minor allele in gray, total CN in orange
- segments(x0=pos_min, y0=segment_states_min, x1=pos_max, y1=segment_states_min, col="#2f4f4f", pch="|", lwd=6, lend=1)
- segments(x0=pos_min, y0=segment_states_tot, x1=pos_max, y1=segment_states_tot, col="#E69F00", pch="|", lwd=6, lend=1)
+ graphics::segments(
+ x0 = pos_min,
+ y0 = segment_states_min,
+ x1 = pos_max,
+ y1 = segment_states_min,
+ col = "#2f4f4f", pch = "|", lwd = 6, lend = 1
+ )
+ graphics::segments(
+ x0 = pos_min, y0 = segment_states_tot,
+ x1 = pos_max, y1 = segment_states_tot,
+ col = "#E69F00", pch = "|", lwd = 6, lend = 1
+ )
# Plot the vertical lines that show start/end of a chromosome
- chrk_tot_len = 0
- for (i in 1:length(chr.segs)) {
- chrk = chr.segs[[i]];
- chrk_hetero = names(bafsegmented)[chrk]
- chrk_tot_len_prev = chrk_tot_len
- chrk_tot_len = chrk_tot_len + length(chrk_hetero)
- vpos = chrk_tot_len;
- tpos = (chrk_tot_len+chrk_tot_len_prev)/2;
- text(tpos,ylim,chr.names[i], pos = 1, cex = 2)
- abline(v=vpos,lty=1,col="lightgrey")
+ chrk_tot_len <- 0
+ num_chrs <- length(chr_segs)
+ # Total width of the plot in units of SNPs
+ total_width <- nrow(bafsegmented)
+
+ for (i in seq_along(chr_segs)) {
+ chrk <- chr_segs[[i]]
+ chrk_tot_len_prev <- chrk_tot_len
+
+ # Robust length handling: if a chromosome has no SNPs, we give it a tiny virtual width
+ # to prevent labels from overlapping at the exact same x-coordinate.
+ chr_width <- length(chrk)
+ if (chr_width == 0) {
+ chr_width <- total_width / (num_chrs * 10) # 1% of an average chromosome width
+ }
+
+ chrk_tot_len <- chrk_tot_len + chr_width
+ vpos <- chrk_tot_len
+ tpos <- (chrk_tot_len + chrk_tot_len_prev) / 2
+
+ # Draw separator and label
+ graphics::text(tpos, ylim, chr_names[i], pos = 1, cex = 2)
+ graphics::abline(v = vpos, lty = 1, col = "lightgrey")
}
}
@@ -124,205 +263,329 @@ create.bb.plot.average = function(bafsegmented, ploidy, rho, goodnessOfFit, pos_
#' NAP - July 2020 - updated main title now replacing 'cellularity' with 'purity' and 'goodness-of-fit' with 'PGAclonal' + adding TUMOURNAME
#' NAP - November 2023 - Replacing 'PGAclonal' with 'PGA.is.clonal' for more clarity
#' @noRd
-create.bb.plot.subclones = function(bafsegmented, subclones, ploidy, rho, goodnessOfFit, pos_min, pos_max, subcl_min, subcl_max, is_subclonal, is_subclonal_maj, is_subclonal_min, chr.segs, chr.names, tumourname, ylim=5) {
- par(mar = c(0.5,5,5,0.5), cex = 0.4, cex.main=3, cex.axis = 2.5)
- maintitle = paste0(substring(tumourname, 36, first = T),", Ploidy: ",sprintf("%1.2f",ploidy),", Purity: ",sprintf("%2.0f",rho*100),"%, PGA.is.clonal: ",sprintf("%2.1f",goodnessOfFit*100),"%")
- # maintitle = paste("Ploidy: ",sprintf("%1.2f",ploidy),", aberrant cell fraction: ",sprintf("%2.0f",rho*100),"%, goodness of fit: ",sprintf("%2.1f",goodnessOfFit*100),"%",sep="")
- plot(c(1,nrow(bafsegmented)), c(0,ylim), type = "n", xaxt = "n", main = maintitle, xlab = "", ylab = "")
- abline(v=0,lty=1,col="lightgrey")
- # Minor allele clonal and lowest of the two states when subclonal
- segments(x0=pos_min, y0=subclones$nMin1_A-0.1,
- x1=pos_max, y1=subclones$nMin1_A-0.1, col="#2f4f4f", pch="|",
- lwd=ifelse(is_subclonal_min, 6*subclones$frac1_A, 6), lend=1)
-
- if (sum(is_subclonal) > 0) {
- # Minor allele highest of the two states when subclonal
- segments(x0=subcl_min, y0=subclones$nMin2_A[is_subclonal]-0.1,
- x1=subcl_max, y1=subclones$nMin2_A[is_subclonal]-0.1, col="#2f4f4f", pch="|",
- lwd=ifelse(is_subclonal_min[is_subclonal], 6*subclones$frac2_A[is_subclonal], 0), lend=1)
-
- # Total CN, when minor allele subclonal CN (one of the two alleles)
- segments(x0=subcl_min, y0=subclones$nMaj1_A[is_subclonal]+subclones$nMin1_A[is_subclonal]+0.1,
- x1=subcl_max, y1=subclones$nMaj1_A[is_subclonal]+subclones$nMin1_A[is_subclonal]+0.1, col="#E69F00", pch="|",
- lwd=ifelse(is_subclonal_min[is_subclonal], 6*subclones$frac1_A[is_subclonal], 0), lend=1)
-
- # Total CN, when minor allele subclonal CN (the other allele)
- segments(x0=subcl_min, y0=subclones$nMaj2_A[is_subclonal]+subclones$nMin2_A[is_subclonal]+0.1,
- x1=subcl_max, y1=subclones$nMaj2_A[is_subclonal]+subclones$nMin2_A[is_subclonal]+0.1, col="#E69F00", pch="|",
- lwd=ifelse(is_subclonal_min[is_subclonal], 6*subclones$frac2_A[is_subclonal], 0), lend=1)
- }
-
- # Total CN, when major allele clonal and subclonal, unless the minor allele is subclonal (then plot nothing, done above)
- segments(x0=pos_min, y0=subclones$nMaj1_A+subclones$nMin1_A+0.1,
- x1=pos_max, y1=subclones$nMaj1_A+subclones$nMin1_A+0.1, col="#E69F00", pch="|",
- lwd=ifelse(is_subclonal_maj & (!is_subclonal_min), 6*subclones$frac1_A, 0), lend=1)
-
- # Total CN, when subclonal major allele and not subclonal minor allele (the other allele)
- segments(x0=pos_min, y0=subclones$nMaj2_A+subclones$nMin2_A+0.1,
- x1=pos_max, y1=subclones$nMaj2_A+subclones$nMin2_A+0.1, col="#E69F00", pch="|",
- lwd=ifelse(is_subclonal_maj & (!is_subclonal_min), 6*subclones$frac2_A, 0), lend=1)
-
- # Total allele when major and minor both non-subclonal
- segments(x0=pos_min, y0=subclones$nMaj1_A+subclones$nMin1_A+0.1,
- x1=pos_max, y1=subclones$nMaj1_A+subclones$nMin1_A+0.1, col="#E69F00", pch="|",
- lwd=ifelse((!is_subclonal_maj) & (!is_subclonal_min), 6, 0), lend=1)
-
- chrk_tot_len = 0
- for (i in 1:length(chr.segs)) {
- chrk = chr.segs[[i]];
- chrk_hetero = names(bafsegmented)[chrk]
- chrk_tot_len_prev = chrk_tot_len
- chrk_tot_len = chrk_tot_len + length(chrk_hetero)
- vpos = chrk_tot_len;
- tpos = (chrk_tot_len+chrk_tot_len_prev)/2;
- text(tpos,ylim,chr.names[i], pos = 1, cex = 2)
- abline(v=vpos,lty=1,col="lightgrey")
- }
+create_bb_plot_subclones <- function(
+ bafsegmented, subclones, ploidy,
+ rho, goodness_of_fit, pos_min,
+ pos_max, subcl_min, subcl_max,
+ is_subclonal, is_subclonal_maj,
+ is_subclonal_min, chr_segs,
+ chr_names, tumourname, ylim = 5
+) {
+ graphics::par(
+ mar = c(0.5, 5, 5, 0.5), cex = 0.4, cex.main = 3, cex.axis = 2.5
+ )
+ maintitle <- paste0(
+ tumourname,
+ ", Ploidy: ", sprintf("%1.2f", ploidy),
+ ", Purity: ", sprintf("%2.0f", rho * 100),
+ "%, PGA.is.clonal: ",
+ sprintf("%2.1f", goodness_of_fit * 100), "%"
+ )
+
+ graphics::plot(
+ c(1, nrow(bafsegmented)), c(0, ylim),
+ type = "n", xaxt = "n", main = maintitle, xlab = "", ylab = ""
+ )
+ graphics::abline(
+ v = 0, lty = 1, col = "lightgrey"
+ )
+ # Minor allele clonal and lowest of the two states when subclonal
+ graphics::segments(
+ x0 = pos_min, y0 = subclones$nMin1_A - 0.1,
+ x1 = pos_max, y1 = subclones$nMin1_A - 0.1, col = "#2f4f4f", pch = "|",
+ lwd = ifelse(is_subclonal_min, 6 * subclones$frac1_A, 6), lend = 1
+ )
+
+ if (sum(is_subclonal) > 0) {
+ # Minor allele highest of the two states when subclonal
+ graphics::segments(
+ x0 = subcl_min, y0 = subclones$nMin2_A[is_subclonal] - 0.1,
+ x1 = subcl_max, y1 = subclones$nMin2_A[is_subclonal] - 0.1, col = "#2f4f4f", pch = "|",
+ lwd = ifelse(
+ is_subclonal_min[is_subclonal],
+ 6 * subclones$frac2_A[is_subclonal], 0
+ ), lend = 1
+ )
+
+ # Total CN, when minor allele subclonal CN (one of the two alleles)
+ graphics::segments(
+ x0 = subcl_min, y0 = subclones$nMaj1_A[is_subclonal] + subclones$nMin1_A[is_subclonal] + 0.1,
+ x1 = subcl_max, y1 = subclones$nMaj1_A[is_subclonal] + subclones$nMin1_A[is_subclonal] + 0.1, col = "#E69F00", pch = "|",
+ lwd = ifelse(
+ is_subclonal_min[is_subclonal], 6 * subclones$frac1_A[is_subclonal], 0
+ ), lend = 1
+ )
+
+ # Total CN, when minor allele subclonal CN (the other allele)
+ graphics::segments(
+ x0 = subcl_min, y0 = subclones$nMaj2_A[is_subclonal] + subclones$nMin2_A[is_subclonal] + 0.1,
+ x1 = subcl_max, y1 = subclones$nMaj2_A[is_subclonal] + subclones$nMin2_A[is_subclonal] + 0.1, col = "#E69F00", pch = "|",
+ lwd = ifelse(
+ is_subclonal_min[is_subclonal], 6 * subclones$frac2_A[is_subclonal], 0
+ ), lend = 1
+ )
+ }
+
+ # Total CN, when major allele clonal and subclonal, unless the minor allele is subclonal (then plot nothing, done above)
+ graphics::segments(
+ x0 = pos_min, y0 = subclones$nMaj1_A + subclones$nMin1_A + 0.1,
+ x1 = pos_max, y1 = subclones$nMaj1_A + subclones$nMin1_A + 0.1, col = "#E69F00", pch = "|",
+ lwd = ifelse(
+ is_subclonal_maj & (!is_subclonal_min), 6 * subclones$frac1_A, 0
+ ), lend = 1
+ )
+
+ # Total CN, when subclonal major allele and not subclonal minor allele (the other allele)
+ graphics::segments(
+ x0 = pos_min, y0 = subclones$nMaj2_A + subclones$nMin2_A + 0.1,
+ x1 = pos_max, y1 = subclones$nMaj2_A + subclones$nMin2_A + 0.1, col = "#E69F00", pch = "|",
+ lwd = ifelse(
+ is_subclonal_maj & (!is_subclonal_min), 6 * subclones$frac2_A, 0
+ ), lend = 1
+ )
+
+ # Total allele when major and minor both non-subclonal
+ graphics::segments(
+ x0 = pos_min, y0 = subclones$nMaj1_A + subclones$nMin1_A + 0.1,
+ x1 = pos_max, y1 = subclones$nMaj1_A + subclones$nMin1_A + 0.1, col = "#E69F00", pch = "|",
+ lwd = ifelse((!is_subclonal_maj) & (!is_subclonal_min), 6, 0), lend = 1
+ )
+
+ chrk_tot_len <- 0
+ for (i in seq_along(chr_segs)) {
+ chrk <- chr_segs[[i]]
+ chrk_tot_len_prev <- chrk_tot_len
+ chrk_tot_len <- chrk_tot_len + length(chrk)
+ vpos <- chrk_tot_len
+ tpos <- (chrk_tot_len + chrk_tot_len_prev) / 2
+ graphics::text(tpos, ylim, chr_names[i], pos = 1, cex = 2)
+ graphics::abline(v = vpos, lty = 1, col = "lightgrey")
+ }
}
#' Code extracted from the plot in clonal_ascat find_centroid_of_global_minima.
#' Note: This is a temporary function and VERY similar to clonal_runascat.plot1()
#' @noRd
-#'
-clonal_findcentroid.plot = function(minimise, dist_choice, d, psis, rhos, new_bounds) {
- par(mar = c(5,5,0.5,0.5), cex=0.75, cex.lab=2, cex.axis=2)
- if(minimise){ #DCW 240314 reverse colour palette, so blue always corresponds to best region
- hmcol = rev(colorRampPalette(RColorBrewer::brewer.pal(10, "RdBu"))(256))
+clonal_findcentroid_plot <- function(minimise, dist_choice, d, psis, rhos, new_bounds) {
+ graphics::par(
+ mar = c(5, 5, 0.5, 0.5), cex = 0.75, cex.lab = 2, cex.axis = 2
+ )
+ # DCW 240314 reverse colour palette, so blue always corresponds to best region
+ if (minimise) {
+ hmcol <- rev(
+ grDevices::colorRampPalette(
+ RColorBrewer::brewer.pal(10, "RdBu")
+ )(256)
+ )
} else {
- hmcol = colorRampPalette(RColorBrewer::brewer.pal(10, "RdBu"))(256)
+ hmcol <- grDevices::colorRampPalette(
+ RColorBrewer::brewer.pal(10, "RdBu")
+ )(256)
}
- if ( dist_choice == 4 ) {
- image(d, col = hmcol, axes = F, xlab = "Ploidy", ylab = "Aberrant cell fraction")
- } else {
- image(log(d), col = hmcol, axes = F, xlab = "Ploidy", ylab = "Aberrant cell fraction")
+ if (dist_choice == 4) {
+ graphics::image(
+ d,
+ col = hmcol, axes = FALSE,
+ xlab = "Ploidy", ylab = "Aberrant cell fraction"
+ )
+ } else {
+ graphics::image(
+ log(d),
+ col = hmcol, axes = FALSE,
+ xlab = "Ploidy", ylab = "Purity"
+ )
}
- psi_min = new_bounds$psi_min
- psi_max = new_bounds$psi_max
- rho_min = new_bounds$rho_min
- rho_max = new_bounds$rho_max
-
- psi_range = psi_max - psi_min
- rho_range = rho_max - rho_min
-
- psi_min_label = ceiling( 10 * psi_min )/10
- psi_max_label = floor( 10 * psi_max )/10
- psi_label_interval = 0.1
-
- psi_min_label_standardised = ( psi_min_label - psi_min ) / psi_range
- psi_max_label_standardised = ( psi_max_label - psi_min ) / psi_range
- psi_label_interval_standardised = psi_label_interval / psi_range
-
- rho_min_label = ceiling( 100 * rho_min )/100
- rho_max_label = floor( 100 * rho_max )/100
- rho_label_interval = 0.01
-
- rho_min_label_standardised = ( rho_min_label - rho_min ) / rho_range
- rho_max_label_standardised = ( rho_max_label - rho_min ) / rho_range
- rho_label_interval_standardised = rho_label_interval / rho_range
-
- axis(1, at = seq(psi_min_label_standardised, psi_max_label_standardised, by = psi_label_interval_standardised), labels = seq(psi_min_label, psi_max_label, by = psi_label_interval))
- axis(2, at = seq(rho_min_label_standardised, rho_max_label_standardised, by = rho_label_interval_standardised), labels = seq(rho_min_label, rho_max_label, by = rho_label_interval))
-
- points( ( psis - psi_min ) / psi_range , ( rhos - rho_min ) / rho_range , col=c("green", "darkgreen"), pch="X", cex = 2 )
+ psi_min <- new_bounds$psi_min
+ psi_max <- new_bounds$psi_max
+ rho_min <- new_bounds$rho_min
+ rho_max <- new_bounds$rho_max
+
+ psi_range <- psi_max - psi_min
+ rho_range <- rho_max - rho_min
+
+ psi_min_label <- ceiling(10 * psi_min) / 10
+ psi_max_label <- floor(10 * psi_max) / 10
+ psi_label_interval <- 0.1
+
+ psi_min_label_standardised <- (psi_min_label - psi_min) / psi_range
+ psi_max_label_standardised <- (psi_max_label - psi_min) / psi_range
+ psi_label_interval_standardised <- psi_label_interval / psi_range
+
+ rho_min_label <- ceiling(100 * rho_min) / 100
+ rho_max_label <- floor(100 * rho_max) / 100
+ rho_label_interval <- 0.01
+
+ rho_min_label_standardised <- (rho_min_label - rho_min) / rho_range
+ rho_max_label_standardised <- (rho_max_label - rho_min) / rho_range
+ rho_label_interval_standardised <- rho_label_interval / rho_range
+
+ graphics::axis(
+ 1,
+ at = seq(
+ psi_min_label_standardised,
+ psi_max_label_standardised,
+ by = psi_label_interval_standardised
+ ),
+ labels = seq(psi_min_label, psi_max_label, by = psi_label_interval)
+ )
+ graphics::axis(
+ 2,
+ at = seq(
+ rho_min_label_standardised,
+ rho_max_label_standardised,
+ by = rho_label_interval_standardised
+ ),
+ labels = seq(rho_min_label, rho_max_label, by = rho_label_interval)
+ )
+
+ graphics::points(
+ (psis - psi_min) / psi_range, (rhos - rho_min) / rho_range,
+ col = c("green", "darkgreen"), pch = "X", cex = 2
+ )
}
+# Plot Battenberg copy number solutions for a segment
+# Refactored for clarity and data.table integration
+squaresplot <- function(tumourname, run_dir, segment_chr, segment_pos,
+ platform_gamma = 1, pdf = 0, binwidth_baf = 0.25, xylimits = c(-0.2, 5)) {
+ # Construct output paths
+ ext <- if (pdf) ".pdf" else ".png"
+ out_file <- file.path(run_dir, paste0(tumourname, "_squares_chr", segment_chr, "_", segment_pos, ext))
-#' Plot Battenberg copy number solutions for a segment
-#'
-#' \code{squaresplot} plots the different Battenberg copy number solutions for a segment
-#'
-#' The plot is output to the run directory as "tumourname_squares_chr_position.png/pdf"
-#'
-#' @param tumourname Sample name
-#' @param run_dir Running directory
-#' @param segment_chr Chromosome containing the segment to be investigated
-#' @param segment_pos Chromosomal position within the segment in Mb (e.g. 90M)
-#' @param platform_gamma Platform-specific gamma value (0.55 for SNP6, 1 for NGS), default 1
-#' @param pdf Output format: 0 for png (default), 1 for pdf
-#' @param binwidth_baf BAF isobafline spacing, default 0.25
-#' @param xylimits x/y-axis limits, default c(-0.2,5)
-#' @author jd
-#' @export
-squaresplot <- function(tumourname, run_dir, segment_chr, segment_pos, platform_gamma=1, pdf=0, binwidth_baf=0.25, xylimits=c(-0.2,5)) {
-
- if (pdf)
- pdf(file = paste(run_dir,tumourname,"_squares","_chr",segment_chr,"_",segment_pos,".pdf", sep=""), width = 7, height = 7)
- else
- png(filename = paste(run_dir,tumourname,"_squares","_chr",segment_chr,"_",segment_pos,".png", sep=""), width = 1200, height = 1200, res = 200, type = "cairo")
-
- # read in and augment data
- segment_pos <- as.numeric(gsub("M", "000000", segment_pos))
- subclones <- read.table(paste(run_dir, tumourname, "_copynumber.txt", sep=""), header=T, stringsAsFactors=F)
- subclone <- subclones[(subclones$chr == segment_chr) & (subclones$startpos <= segment_pos) & (subclones$endpos >= segment_pos),]
- rhopsi <- read.table(paste(run_dir, tumourname, "_rho_and_psi.txt", sep=""), header = T, stringsAsFactors=F)
- rhopsi <- rhopsi[which(rhopsi$is.best == TRUE), c("rho", "psi")]
-
- nMincalc <- (rhopsi$rho-1-(subclone$BAF-1)*2^(subclone$LogR/platform_gamma)*((1-rhopsi$rho)*2+rhopsi$rho*rhopsi$psi))/rhopsi$rho
- nMajcalc <- (rhopsi$rho-1+subclone$BAF*2^(subclone$LogR/platform_gamma)*((1-rhopsi$rho)*2+rhopsi$rho*rhopsi$psi))/rhopsi$rho
-
- subclone <- data.frame(subclone, rhopsi, nMincalc, nMajcalc)
-
- # helper function to calculate isobaflines
+ if (pdf) {
+ grDevices::pdf(file = out_file, width = 7, height = 7)
+ } else {
+ grDevices::png(filename = out_file, width = 1200, height = 1200, res = 200, type = "cairo")
+ }
+
+ # Parse chromosomal position
+ segment_pos_num <- as.numeric(gsub("M", "000000", segment_pos))
+
+ # Read data using data.table
+ cn_file <- file.path(run_dir, paste0(tumourname, "_copynumber.txt"))
+ subclones <- data.table::fread(cn_file, data.table = FALSE)
+
+ # Select specific segment
+ subclone <- subclones[(subclones$chr == segment_chr) &
+ (subclones$startpos <= segment_pos_num) &
+ (subclones$endpos >= segment_pos_num), ]
+
+ # Get best rho and psi parameters
+ rp_file <- file.path(run_dir, paste0(tumourname, "_rho_and_psi.txt"))
+ rhopsi_df <- data.table::fread(rp_file, data.table = FALSE)
+ rhopsi <- rhopsi_df[!is.na(rhopsi_df$is_best) & rhopsi_df$is_best == TRUE, c("rho", "psi")]
+
+ rho <- rhopsi$rho
+ psi <- rhopsi$psi
+
+ # Theoretical calculations
+ logr_comp <- 2^(subclone$LogR / platform_gamma)
+ p_comp <- ((1 - rho) * 2 + rho * psi)
+ nMincalc <- (rho - 1 - (subclone$BAF - 1) * logr_comp * p_comp) / rho
+ nMajcalc <- (rho - 1 + subclone$BAF * logr_comp * p_comp) / rho
+
+ # Grid function
isobafline <- function(nB, cstbaf) {
- (1-rhopsi$rho+rhopsi$rho*nB-cstbaf*(2-2*rhopsi$rho)-rhopsi$rho*cstbaf*nB)/(rhopsi$rho*cstbaf)
+ (1 - rho + rho * nB - cstbaf * (2 - 2 * rho) - rho * cstbaf * nB) / (rho * cstbaf)
}
-
- # create grid for allelic copynumber
- ngrid <- data.frame(nMaj=seq(0,5,1), nMin=seq(0,5,1))
-
- # start plotting - setup
- q <- ggplot2::ggplot(data = ngrid, aes(nMaj, nMin)) + ggplot2::scale_x_continuous(breaks=0:max(xylimits), limits=xylimits) + ggplot2::scale_y_continuous(breaks=0:max(xylimits), limits=xylimits) + ggplot2::coord_fixed()
- q <- q + ggplot2::theme_bw() + ggplot2::theme(panel.grid.major = ggplot2::element_line(colour="darkgrey", size = 0.5), panel.grid.minor = ggplot2::element_blank())
-
- # add isobaflines
- for (bafval in seq(0,1,binwidth_baf)) {
- q <- q + ggplot2::stat_function(fun = isobafline, args = list(cstbaf = bafval), colour="blue", alpha=0.6)
+
+ # Base Plot
+ q <- ggplot2::ggplot() +
+ ggplot2::scale_x_continuous(name = "nMajor", breaks = 0:max(xylimits), limits = xylimits) +
+ ggplot2::scale_y_continuous(name = "nMinor", breaks = 0:max(xylimits), limits = xylimits) +
+ ggplot2::coord_fixed() +
+ ggplot2::theme_bw() +
+ ggplot2::theme(
+ panel.grid.major = ggplot2::element_line(colour = "darkgrey", size = 0.5),
+ panel.grid.minor = ggplot2::element_blank()
+ )
+
+ # Grid Lines
+ baf_seq <- seq(0, 1, binwidth_baf)
+ for (bafval in baf_seq) {
+ q <- q + ggplot2::stat_function(fun = isobafline, args = list(cstbaf = bafval), colour = "blue", alpha = 0.6)
}
- q <- q + ggplot2::stat_function(fun = isobafline, args = list(cstbaf = subclone$BAF), colour="green")
-
- # add isologrline
- df = data.frame(flnMaj = floor(nMajcalc)-0.2, cnMin = ceiling(nMincalc)+0.2, cnMaj = ceiling(nMajcalc)+0.2, flnMin = floor(nMincalc)-0.2)
- q <- q + ggplot2::geom_segment(data = df,
- aes(x = flnMaj, y = cnMin, xend = cnMaj, yend = flnMin), colour="red", alpha = 0.6)
-
- # if clonal segment, only plot clonal solution
+ q <- q + ggplot2::stat_function(fun = isobafline, args = list(cstbaf = subclone$BAF), colour = "green")
+
+ # Isologrline (Red Segment)
+ err_df <- data.frame(
+ x = floor(nMajcalc) - 0.2, y = ceiling(nMincalc) + 0.2,
+ xend = ceiling(nMajcalc) + 0.2, yend = floor(nMincalc) - 0.2
+ )
+
+ # Note the use of rlang::.data here
+ q <- q + ggplot2::geom_segment(
+ data = err_df,
+ ggplot2::aes(
+ x = x,
+ y = y,
+ xend = xend,
+ yend = yend
+ ),
+ colour = "red", alpha = 0.6
+ )
+
+ # Clonal vs Subclonal points
if (subclone$frac1_A == 1) {
- q <- q + ggplot2::geom_point(data = subclone, aes(nMaj1_A, nMin1_A), size = 5)
- } else { # if subclonal, plot all equivalent solutions
- solutions <- matrix(unlist(subclone[,grep("nM.{5}$|^frac.{3}$", colnames(subclone))]), byrow = T, ncol = 3)
- solutions <- cbind(solutions, rep(1:6,rep(2,6)))[12:1,]
+ q <- q + ggplot2::geom_point(
+ data = subclone,
+ ggplot2::aes(
+ nMaj1_A,
+ nMin1_A
+ ), size = 5
+ )
+ } else {
+ target_cols <- grep("nM.{5}$|^frac.{3}$", colnames(subclone))
+ sol_matrix <- matrix(unlist(subclone[, target_cols]), byrow = TRUE, ncol = 3)
+ solutions <- cbind(sol_matrix, rep(1:6, each = 2))
+ solutions <- solutions[12:1, ]
colnames(solutions) <- c("nMaj", "nMin", "frac", "sol")
- solutions <- na.omit(as.data.frame(solutions))
- q <- q + ggplot2::geom_point(data = solutions, aes(nMaj, nMin, size=frac, colour=factor(sol)), alpha=0.75, position = ggplot2::position_jitter(width = .05, height = .05), shape = 79) +
- ggplot2::scale_size_continuous(guide=F, limits=c(0,1) ,range = c(2,10)) + ggplot2::scale_color_discrete(name="solution")
+
+ solutions_df <- stats::na.omit(as.data.frame(solutions))
+
+ q <- q + ggplot2::geom_point(
+ data = solutions_df,
+ ggplot2::aes(
+ x = nMaj,
+ y = nMin,
+ size = frac,
+ colour = factor(sol)
+ ),
+ alpha = 0.75,
+ position = ggplot2::position_jitter(width = .05, height = .05),
+ shape = 79
+ ) +
+ ggplot2::scale_size_continuous(guide = "none", limits = c(0, 1), range = c(2, 10)) +
+ ggplot2::scale_color_discrete(name = "solution")
}
-
- # plot precise values, as calculated by battenberg
- q <- q + ggplot2::geom_point(data=subclone, aes(nMajcalc, nMincalc), size=4, shape = 88)
- q <- q + ggplot2::labs(title = paste(tumourname," chr",subclone$chr,": ",subclone$startpos,"-",subclone$endpos, sep=""))
- print(q)
- dev.off()
+
+ # Final markers
+ q <- q + ggplot2::geom_point(ggplot2::aes(x = nMajcalc, y = nMincalc), size = 4, shape = 88)
+ q <- q + ggplot2::labs(title = paste0(tumourname, " chr", subclone$chr, ": ", subclone$startpos, "-", subclone$endpos))
+
+ log_info("Plot 'q' generated.")
+ grDevices::dev.off()
}
#' Smooth data by running median
-#'
+#'
#' @param chromosome Denominator on which chromosome each data point belongs. Smoothing is done separately per chromosome
#' @param data The to be smoothed data vector
#' @param k The size of window to be used to take the median over
#' @return A single vector with the smoothed data
#' @author sd11
#' @noRd
-runmed_data = function(chromosome, data, k=101) {
- data_smoothed = rep(NA, length(data))
+runmed_data <- function(chromosome, data, k = 101) {
+ data_smoothed <- rep(NA, length(data))
for (chrom in unique(chromosome)) {
- data_smoothed[chromosome==chrom] = runmed(data[chromosome==chrom], k)
+ data_smoothed[chromosome == chrom] <- stats::runmed(data[chromosome == chrom], k)
}
return(data_smoothed)
}
#' Plot total copy number split per chromosome
-#'
-#' This plot contains estimated total copy number from logR, the copy number fit in different colours and a few general stats.
+#'
+#' This plot contains estimated total copy number from logR, the copy number fit in different colours and a few general stats.
#' It is meant as a single figure replacement for the per chromosome subclones.png figures that can be used for refitting.
#' @param samplename Name of the sample for the plot title
#' @param subclones A subclones.txt file read in as a data.frame
@@ -331,123 +594,241 @@ runmed_data = function(chromosome, data, k=101) {
#' @param purity The samples purity estimate
#' @author sd11
#' @export
-totalcn_chrom_plot = function(samplename, subclones, logr, outputfile, purity) {
-
- # Smooth the logR
- colnames(logr)[3] = "raw_logr"
- logr$logr_smoothed = runmed_data(logr$Chromosome, logr$raw_logr, 101)
-
- # Prepare subclones data
- subclones$len = subclones$endpos/1000-subclones$startpos/1000
- subclones$total_major = calc_total_cn_major(subclones)
- subclones$total_minor = calc_total_cn_minor(subclones)
- subclones$total_cn = subclones$total_minor + subclones$total_major
- subclones$is_subclonal = subclones$frac1_A < 1
- subclones$is_50_50 = subclones$frac1_A >= 0.48 & subclones$frac1_A <= 0.52
-
- # Calculate psi from the data
- ploidy = calc_ploidy(subclones)
- psi = psit2psi(purity, ploidy)
-
- # Estimate total CN for each segment based on the logR
- logr$total_cn = NA
- logr$total_cn_psi = NA
- for (i in (1:nrow(subclones))) {
- print(i)
- sel = which(logr$Chromosome == subclones$chr[i] & logr$Position >= subclones$startpos[i] & logr$Position <= subclones$endpos[i])
- tumour_cn = calculate_bb_total_cn(subclones[i,,drop=F])
- total_cn = purity*tumour_cn + 2*(1-purity)
- logr$total_cn[sel] = logr2tumcn(purity, total_cn, logr$logr_smoothed[sel])
- logr$total_cn_psi[sel] = logr2tumcn(purity, psi, logr$logr_smoothed[sel])
- }
-
- # Plot every 100 data point, there are too many for them all to be seen
- logr_plot = logr[seq(1, nrow(logr), 100),]
-
- # Sync the levels for chromosome so that all corresponding data ends up in the same plot
- logr_plot$Chromosome = factor(logr_plot$Chromosome, levels=gtools::mixedsort(unique(logr_plot$Chromosome)))
- subclones$Chromosome = factor(subclones$chr, levels=levels(logr_plot$Chromosome))
-
- # Set plot boundaries for x and y - take as y value the maximum between the data and the fit
- max_cn_plot_data = ceiling(quantile(logr_plot$total_cn_psi, c(.98), na.rm=T))
- max_cn_plot_fit = ceiling(quantile(unlist(lapply(1:nrow(subclones), function(i) rep(subclones$total_cn[i], subclones$len[i]))), c(.98), na.rm=T))
- max_cn_plot = ifelse(max_cn_plot_fit > max_cn_plot_data, max_cn_plot_fit, max_cn_plot_data)
- maxpos = max(logr$Position)
-
- # catch case when there is no clonal CNA called
- if (is.na(max_cn_plot) | max_cn_plot < 4) {
- max_cn_plot = 4
- }
-
- # These are the grey lines in the background
- background = data.frame(xmin=rep(0, (max_cn_plot/2)+1),
- xmax=rep(max(logr$Position), (max_cn_plot/2)+1),
- ymin=seq(0, max_cn_plot, 2)+0.5,
- ymax=seq(0, max_cn_plot, 2)+1.5)
-
- # Calc a couple of stats for the plot title
- genome_50_50 = sum(subclones$len[subclones$is_50_50]/1000)
- prop_subclonal = round(sum(subclones$len[subclones$is_subclonal]) / sum(subclones$len), 2)
- homdel = sum(subclones$len[subclones$total_cn == 0]/1000)
- plot_title = samplename
- plot_subtitle = paste0("Purity: ", round(purity, 2), " - Ploidy: ", round(ploidy, 2), " - Hom del: ", round(homdel, 2), "Mb - Prop. subclonal: ", prop_subclonal, " - Subclonal 50/50: ", round(genome_50_50, 2), "Mb")
-
- rect_height_padding = 0.2
-
+totalcn_chrom_plot <- function(
+ samplename,
+ subclones,
+ logr,
+ outputfile,
+ purity
+) {
+ # Using data.table::setnames to avoid copying the whole table
+ data.table::setnames(logr, 3, "raw_logr")
+
+ # collapse::fcompute/fmutate is faster for smoothing across chromosomes
+ # Assuming runmed_data is your custom function
+ logr$logr_smoothed <- runmed_data(logr$Chromosome, logr$raw_logr, 101)
+
+ subclones$len <- (subclones$endpos - subclones$startpos) / 1000
+ subclones$total_major <- calc_total_cn_major(subclones)
+ subclones$total_minor <- calc_total_cn_minor(subclones)
+ subclones$total_cn <- subclones$total_minor + subclones$total_major
+ subclones$is_subclonal <- subclones$frac1_A < 1
+ subclones$is_50_50 <- subclones$frac1_A >= 0.48 & subclones$frac1_A <= 0.52
+
+ ploidy <- calc_ploidy(subclones)
+ psi <- psit2psi(purity, ploidy)
+
+ # Convert to data.table if they aren't already
+ data.table::setDT(logr)
+ data.table::setDT(subclones)
+ logr$Position_end <- logr$Position
+
+ # Calculate segment constants once (Vectorized)
+ subclones$target_total_cn <- purity * calculate_bb_total_cn(subclones) + 2 * (1 - purity)
+
+ # Set keys for foverlaps (Standard requirement for range joins)
+ data.table::setkeyv(subclones, c("chr", "startpos", "endpos"))
+
+ # Perform the join - This maps the correct 'target_total_cn' to every SNP
+ logr_joined <- data.table::foverlaps(
+ logr,
+ subclones,
+ by.x = c("Chromosome", "Position", "Position_end"),
+ by.y = c("chr", "startpos", "endpos"),
+ type = "within",
+ nomatch = NA
+ )
+
+ # Vectorized calculation of CN columns on the joined data
+ logr_joined$total_cn <- logr2tumcn(purity, logr_joined$target_total_cn, logr_joined$logr_smoothed)
+ logr_joined$total_cn_psi <- logr2tumcn(purity, psi, logr_joined$logr_smoothed)
+
+ # Replace .N with standard nrow() indexing
+ sample_idx <- seq(from = 1, to = nrow(logr_joined), by = 100)
+ logr_plot <- logr_joined[sample_idx, ]
+
+ # mixedsort handles the chr1, chr2, chr10 order correctly
+ chr_levels <- gtools::mixedsort(unique(as.character(logr_plot$Chromosome)))
+ logr_plot$Chromosome <- factor(logr_plot$Chromosome, levels = chr_levels)
+ subclones$Chromosome <- factor(subclones$chr, levels = chr_levels)
+
+ max_cn_plot_data <- ceiling(
+ collapse::fquantile(
+ logr_plot$total_cn_psi, 0.98,
+ na.rm = TRUE
+ )
+ )
+
+ # Optimization: Use weighted quantile for the fit instead of rep() + unlist()
+ # This saves massive amounts of memory
+ max_cn_plot_fit <- ceiling(
+ collapse::fquantile(
+ subclones$total_cn, 0.98,
+ w = subclones$len, na.rm = TRUE
+ )
+ )
+
+ max_cn_plot <- max(4, max_cn_plot_data, max_cn_plot_fit, na.rm = TRUE)
+ maxpos <- max(logr$Position)
+
+ # 7. Background Data Preparation
+ bg_y <- seq(0, max_cn_plot, 2)
+ background <- data.frame(
+ xmin = 0,
+ xmax = maxpos,
+ ymin = bg_y + 0.5,
+ ymax = bg_y + 1.5
+ )
+
+ # 8. Plot Annotations
+ prop_subclonal <- round(
+ sum(subclones$len[subclones$is_subclonal]) / sum(subclones$len), 2
+ )
+ homdel <- sum(subclones$len[!is.na(subclones$total_cn) & subclones$total_cn == 0] / 1000, na.rm = TRUE)
+
+ plot_subtitle <- paste0(
+ "Purity: ", round(purity, 2),
+ " - Ploidy: ", round(ploidy, 2),
+ " - Hom del: ", round(homdel, 2), "Mb",
+ " - Prop. subclonal: ", prop_subclonal
+ )
+ rect_height_padding <- 0.2
+
# Build the actual plot - CNA segments are drawn separately depending on their category as categories have different colours
- p = ggplot() +
- geom_rect(data=background, aes(xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax), fill='gray80', alpha=0.5) +
- geom_point(data=logr_plot, mapping=aes(x=Position, y=total_cn_psi), size=0.5) +
- ylab("Copy Number") +
- scale_y_continuous(breaks=seq(0, max_cn_plot, 2)) + #, limits=c(-rect_height_padding, max_cn_plot+rect_height_padding)
+ p <- ggplot2::ggplot() +
+ ggplot2::geom_rect(
+ data = background,
+ ggplot2::aes(
+ xmin = xmin,
+ xmax = xmax,
+ ymin = ymin,
+ ymax = ymax
+ ),
+ fill = "gray80", alpha = 0.5
+ ) +
+ ggplot2::geom_point(
+ data = logr_plot,
+ mapping = ggplot2::aes(
+ x = Position,
+ y = total_cn_psi
+ ),
+ size = 0.5
+ ) +
+ ggplot2::ylab("Copy Number") +
+ ggplot2::scale_y_continuous(breaks = seq(0, max_cn_plot, 2)) +
# Axis ticks every 10Mb
- scale_x_continuous(breaks=seq(1, max(logr$Position), 10000000)[-1], labels=round(seq(0, maxpos, 10000000) / 1000000)[-1], expand=c(0, 0)) +
+ ggplot2::scale_x_continuous(
+ breaks = seq(1, max(logr$Position), 10000000)[-1],
+ labels = round(seq(0, maxpos, 10000000) / 1000000)[-1], expand = c(0, 0)
+ ) +
# Don't restrict the plotting area, zoom. that way segments that go outside the limits are partially plotted still
- coord_cartesian(ylim=c(-rect_height_padding, max_cn_plot+rect_height_padding)) +
- facet_wrap(~Chromosome, ncol=2, strip.position="right") +
- # ggtitle(plot_title) +
- ggtitle(bquote(atop(.(plot_title), atop(.(plot_subtitle), "")))) +
- theme_bw() + theme(axis.title.x=element_blank(),
- axis.text.x=element_text(colour="black",size=16,face="plain"),
- axis.text.y = element_text(colour="black",size=16,face="plain"),
- axis.title.y = element_text(colour="black",size=20,face="plain"),
- strip.text.y = element_text(colour="black",size=20,face="plain"),
- plot.title = element_text(colour="black",size=36,face="plain",hjust = 0.5))
-
+ ggplot2::coord_cartesian(
+ ylim = c(-rect_height_padding, max_cn_plot + rect_height_padding)
+ ) +
+ ggplot2::facet_wrap(~Chromosome, ncol = 2, strip.position = "right") +
+ ggplot2::ggtitle(
+ bquote(
+ atop(
+ .(samplename),
+ atop(.(plot_subtitle), "")
+ )
+ )
+ ) +
+ ggplot2::theme_bw() +
+ ggplot2::theme(
+ axis.title.x = ggplot2::element_blank(),
+ axis.text.x = ggplot2::element_text(
+ colour = "black", size = 16, face = "plain"
+ ),
+ axis.text.y = ggplot2::element_text(
+ colour = "black", size = 16, face = "plain"
+ ),
+ axis.title.y = ggplot2::element_text(
+ colour = "black", size = 20, face = "plain"
+ ),
+ strip.text.y = ggplot2::element_text(
+ colour = "black", size = 20, face = "plain"
+ ),
+ plot.title = ggplot2::element_text(
+ colour = "black", size = 36, face = "plain", hjust = 0.5
+ )
+ )
+
# Plot the copy number segments - some of the data.frames may be empty, so check for that first before adding to the plot
- sel = !subclones$is_subclonal
+ sel <- !subclones$is_subclonal
if (any(sel)) {
# Minor allele - Normal clonal copy number
- p = p + geom_rect(data=subclones[sel, ], mapping=aes(xmin=startpos, xmax=endpos, ymin=total_minor-rect_height_padding, ymax=total_minor+rect_height_padding), fill="#2f4f4f")
+ p <- p + ggplot2::geom_rect(
+ data = subclones[sel, ],
+ mapping = ggplot2::aes(
+ xmin = startpos,
+ xmax = endpos,
+ ymin = total_minor - rect_height_padding,
+ ymax = total_minor + rect_height_padding
+ ), fill = "#2f4f4f"
+ )
}
- sel = subclones$is_subclonal & !subclones$is_50_50
+ sel <- subclones$is_subclonal & !subclones$is_50_50
if (any(sel)) {
# Minor allele - Normal subclonal copy number
- p = p + geom_rect(data=subclones[sel, ], mapping=aes(xmin=startpos, xmax=endpos, ymin=total_minor-rect_height_padding, ymax=total_minor+rect_height_padding), fill="#2f3f4f")
+ p <- p + ggplot2::geom_rect(
+ data = subclones[sel, ],
+ mapping = ggplot2::aes(
+ xmin = startpos,
+ xmax = endpos,
+ ymin = total_minor - rect_height_padding,
+ ymax = total_minor + rect_height_padding
+ ), fill = "#2f3f4f"
+ )
}
- sel = subclones$is_subclonal & subclones$is_50_50
+ sel <- subclones$is_subclonal & subclones$is_50_50
if (any(sel)) {
# Minor allele - Subclonal segments right in between two clonal states
- p = p + geom_rect(data=subclones[sel, ], mapping=aes(xmin=startpos, xmax=endpos, ymin=total_minor-rect_height_padding, ymax=total_minor+rect_height_padding), fill="#2f3f4f", colour="red")
+ p <- p + ggplot2::geom_rect(
+ data = subclones[sel, ],
+ mapping = ggplot2::aes(
+ xmin = startpos,
+ xmax = endpos,
+ ymin = total_minor - rect_height_padding,
+ ymax = total_minor + rect_height_padding
+ ), fill = "#2f3f4f", colour = "red"
+ )
}
- sel = !subclones$is_subclonal
+ sel <- !subclones$is_subclonal
if (any(sel)) {
# Major allele - clonal copy number
- p = p + geom_rect(data=subclones[sel, ], mapping=aes(xmin=startpos, xmax=endpos, ymin=total_cn-rect_height_padding, ymax=total_cn+rect_height_padding), fill="#E69F00")
+ p <- p + ggplot2::geom_rect(
+ data = subclones[sel, ],
+ mapping = ggplot2::aes(
+ xmin = startpos,
+ xmax = endpos,
+ ymin = total_cn - rect_height_padding,
+ ymax = total_cn + rect_height_padding
+ ), fill = "#E69F00"
+ )
}
- sel = subclones$is_subclonal
+ sel <- subclones$is_subclonal
if (any(sel)) {
# Major allele - subclonal copy number
- p = p + geom_rect(data=subclones[sel, ], mapping=aes(xmin=startpos, xmax=endpos, ymin=total_cn-rect_height_padding, ymax=total_cn+rect_height_padding), fill="#E55300")
+ p <- p + ggplot2::geom_rect(
+ data = subclones[sel, ],
+ mapping = ggplot2::aes(
+ xmin = startpos,
+ xmax = endpos,
+ ymin = total_cn - rect_height_padding,
+ ymax = total_cn + rect_height_padding
+ ), fill = "#E55300"
+ )
}
-
- png(outputfile, width=2000, height=1300, type = "cairo")
- print(p)
- dev.off()
+
+ grDevices::png(outputfile, width = 2000, height = 1300, type = "cairo")
+ log_info(p)
+ log_info("Plot 'p' generated.")
+ grDevices::dev.off()
}
#' Plot allele ratios from raw segmented data
-#'
+#'
#' @param samplename Name of the sample for the plot title
#' @param bafsegmented The BAFsegmented data read in as a data.frame
#' @param logrsegmented The logRsegmented data read in as a data.frame
@@ -456,151 +837,238 @@ totalcn_chrom_plot = function(samplename, subclones, logr, outputfile, purity) {
#' @param max.plot.cn Maximum y-axis value to plot (Default: 5)
#' @author sd11
#' @export
-allele_ratio_plot = function(samplename, bafsegmented, logrsegmented, outputfile, logr, max.plot.cn=5) {
-
+allele_ratio_plot <- function(
+ samplename, bafsegmented,
+ logrsegmented, outputfile,
+ logr, max.plot.cn = 5
+) {
if (nrow(logr) < 2000000) {
- platform = "SNP6"
+ platform <- "SNP6"
} else {
- platform = "WGS"
+ platform <- "WGS"
}
- bafsegmented$Chromosome = factor(bafsegmented$Chromosome, levels=gtools::mixedsort(unique(bafsegmented$Chromosome)))
- colnames(logrsegmented) = c("Chromosome", "Position", "logRseg")
- logrsegmented$Chromosome = factor(logrsegmented$Chromosome, levels=levels(bafsegmented$Chromosome))
-
- colnames(logr)[3] = "raw_logr"
- logr$copy_ratio_binned = runmed_data(logr$Chromosome, exp(logr$raw_logr))
- logr$Chromosome = factor(logr$Chromosome, levels=levels(bafsegmented$Chromosome))
- allelecounts = logr
-
- copyratio_binnedLogR = as.data.frame(array(NA, c(nrow(bafsegmented), 8)))
- colnames(copyratio_binnedLogR) = c("Chromosome", "Position", "ratioBAF", "ratioBAFphased", "ratioBAF_alt", "ratioBAFphased_alt", "ratioBAFseg", "ratioBAFseg_alt")
- copyratio_binnedLogR$Chromosome = bafsegmented$Chromosome
- copyratio_binnedLogR$Position = bafsegmented$Position
-
- print("Calculating copy ratios..")
- for (chrom in unique(bafsegmented$Chromosome)) {
- print(chrom)
+ bafsegmented$Chromosome <- factor(bafsegmented$Chromosome, levels = gtools::mixedsort(unique(bafsegmented$Chromosome)))
+ colnames(logrsegmented) <- c("Chromosome", "Position", "logRseg")
+ logrsegmented$Chromosome <- factor(logrsegmented$Chromosome, levels = S4Vectors::levels(bafsegmented$Chromosome))
- baf_chrom = bafsegmented[bafsegmented$Chromosome==chrom,]
- logrseg_chrom = logrsegmented[logrsegmented$Chromosome==chrom,]
+ colnames(logr)[3] <- "raw_logr"
+ logr$copy_ratio_binned <- runmed_data(logr$Chromosome, exp(logr$raw_logr))
+ logr$Chromosome <- factor(logr$Chromosome, levels = S4Vectors::levels(bafsegmented$Chromosome))
+ allelecounts <- logr
- baf_sel = baf_chrom$Position %in% intersect(baf_chrom$Position, logrseg_chrom$Position)
- logrseg_sel = logrseg_chrom$Position %in% intersect(baf_chrom$Position, logrseg_chrom$Position)
- ratio_sel = which(copyratio_binnedLogR$Chromosome==chrom)[baf_sel]
+ copyratio_binnedLogR <- as.data.frame(array(NA, c(nrow(bafsegmented), 8)))
+ colnames(copyratio_binnedLogR) <- c("Chromosome", "Position", "ratioBAF", "ratioBAFphased", "ratioBAF_alt", "ratioBAFphased_alt", "ratioBAFseg", "ratioBAFseg_alt")
+ copyratio_binnedLogR$Chromosome <- bafsegmented$Chromosome
+ copyratio_binnedLogR$Position <- bafsegmented$Position
- copyratio_binnedLogR$ratioBAFseg[ratio_sel] = (baf_chrom$BAFseg[baf_sel]*(2^logrseg_chrom$logRseg[logrseg_sel]))
- copyratio_binnedLogR$ratioBAFseg_alt[ratio_sel] = (-(baf_chrom$BAFseg[baf_sel]-1)*(2^logrseg_chrom$logRseg[logrseg_sel]))
+ log_info("Calculating copy ratios..")
+ for (chrom in unique(bafsegmented$Chromosome)) {
+ baf_chrom <- bafsegmented[bafsegmented$Chromosome == chrom, ]
+ logrseg_chrom <- logrsegmented[logrsegmented$Chromosome == chrom, ]
+
+ baf_sel <- baf_chrom$Position %in% intersect(baf_chrom$Position, logrseg_chrom$Position)
+ logrseg_sel <- logrseg_chrom$Position %in% intersect(baf_chrom$Position, logrseg_chrom$Position)
+ ratio_sel <- which(copyratio_binnedLogR$Chromosome == chrom)[baf_sel]
+
+ copyratio_binnedLogR$ratioBAFseg[ratio_sel] <- (baf_chrom$BAFseg[baf_sel] * (2^logrseg_chrom$logRseg[logrseg_sel]))
+ copyratio_binnedLogR$ratioBAFseg_alt[ratio_sel] <- (-(baf_chrom$BAFseg[baf_sel] - 1) * (2^logrseg_chrom$logRseg[logrseg_sel]))
}
-
- background = data.frame(y=seq(0,max.plot.cn,0.5))
-
- print("Plotting..")
+
+ background <- data.frame(y = seq(0, max.plot.cn, 0.5))
+
+ log_info("Plotting..")
if (platform == "WGS") {
- sel = seq(1, nrow(allelecounts), 100)
+ sel <- seq(1, nrow(allelecounts), 100)
} else {
- sel = rep(T, nrow(allelecounts))
+ sel <- rep(TRUE, nrow(allelecounts))
}
- plot_title = samplename
- copy_ratio = ggplot(allelecounts[sel,]) +
- geom_hline(data=background, mapping=aes(yintercept=y), colour="black", alpha=0.3) +
- geom_point(mapping=aes(x=Position, y=copy_ratio_binned), alpha=0.5, size=0.9, colour="darkgreen") +
- facet_grid(~Chromosome, scales="free_x", space = "free_x") +
- scale_x_continuous(expand=c(0, 0)) +
- ylim(0,max.plot.cn) + ylab("Copy Ratio") +
- ggtitle(plot_title) +
- theme_bw() + theme(axis.title.x=element_blank(),
- axis.text.x=element_blank(),
- axis.ticks.x=element_blank(),
- axis.text.y = element_text(colour="black",size=18,face="plain"),
- axis.title.y = element_text(colour="black",size=20,face="plain"),
- strip.text.x = element_text(colour="black",size=16,face="plain"),
- plot.title = element_text(colour="black",size=36,face="plain",hjust = 0.5))
+ plot_title <- samplename
+ copy_ratio <- ggplot2::ggplot(allelecounts[sel, ]) +
+ ggplot2::geom_hline(
+ data = background, mapping = ggplot2::aes(yintercept = y),
+ colour = "black", alpha = 0.3
+ ) +
+ ggplot2::geom_point(
+ mapping = ggplot2::aes(
+ x = Position,
+ y = copy_ratio_binned
+ ),
+ alpha = 0.5, size = 0.9, colour = "darkgreen"
+ ) +
+ ggplot2::facet_grid(. ~ Chromosome, scales = "free_x", space = "free_x") +
+ ggplot2::scale_x_continuous(expand = c(0, 0)) +
+ ggplot2::ylim(0, max.plot.cn) +
+ ggplot2::ylab("Copy Ratio") +
+ ggplot2::ggtitle(plot_title) +
+ ggplot2::theme_bw() +
+ ggplot2::theme(
+ axis.title.x = ggplot2::element_blank(),
+ axis.text.x = ggplot2::element_blank(),
+ axis.ticks.x = ggplot2::element_blank(),
+ axis.text.y = ggplot2::element_text(
+ colour = "black", size = 18, face = "plain"
+ ),
+ axis.title.y = ggplot2::element_text(
+ colour = "black", size = 20, face = "plain"
+ ),
+ strip.text.x = ggplot2::element_text(
+ colour = "black", size = 16, face = "plain"
+ ),
+ plot.title = ggplot2::element_text(
+ colour = "black", size = 36, face = "plain", hjust = 0.5
+ )
+ )
if (platform == "WGS") {
- sel = seq(1, nrow(copyratio_binnedLogR), 100)
+ sel <- seq(1, nrow(copyratio_binnedLogR), 100)
} else {
- sel = rep(T, nrow(copyratio_binnedLogR))
+ sel <- rep(TRUE, nrow(copyratio_binnedLogR))
}
- as_copy_ratio_seg = ggplot(copyratio_binnedLogR[sel,]) +
- geom_hline(data=background, mapping=aes(yintercept=y), colour="black", alpha=0.3) +
- geom_point(mapping=aes(x=Position, y=ratioBAFseg_alt), alpha=0.5, size=0.9, colour="darkblue") +
- geom_point(mapping=aes(x=Position, y=ratioBAFseg), alpha=0.5, size=0.9, colour="purple") +
- facet_grid(~Chromosome, scales="free_x", space = "free_x") +
- scale_x_continuous(expand=c(0, 0)) +
- ylim(0,max.plot.cn) + ylab("AS Copy Ratio - Segm") +
- theme_bw() + theme(axis.title.x=element_blank(),
- axis.text.x=element_blank(),
- axis.ticks.x=element_blank(),
- axis.text.y = element_text(colour="black",size=18,face="plain"),
- axis.title.y = element_text(colour="black",size=20,face="plain"),
- strip.text.x = element_text(colour="black",size=16,face="plain"),
- plot.title = element_text(colour="black",size=36,face="plain"))
- png(outputfile, width=2000, height=750, type = "cairo")
- gridExtra::grid.arrange(gridExtra::arrangeGrob(copy_ratio, as_copy_ratio_seg, ncol=1))
- dev.off()
+ as_copy_ratio_seg <- ggplot2::ggplot(copyratio_binnedLogR[sel, ]) +
+ ggplot2::geom_hline(
+ data = background,
+ mapping = ggplot2::aes(yintercept = y),
+ colour = "black", alpha = 0.3
+ ) +
+ ggplot2::geom_point(
+ mapping = ggplot2::aes(
+ x = Position, y = ratioBAFseg_alt
+ ), alpha = 0.5, size = 0.9, colour = "darkblue"
+ ) +
+ ggplot2::geom_point(
+ mapping = ggplot2::aes(
+ x = Position, y = ratioBAFseg
+ ), alpha = 0.5, size = 0.9, colour = "purple"
+ ) +
+ ggplot2::facet_grid(. ~ Chromosome, scales = "free_x", space = "free_x") +
+ ggplot2::scale_x_continuous(expand = c(0, 0)) +
+ ggplot2::ylim(0, max.plot.cn) +
+ ggplot2::ylab("AS Copy Ratio - Segm") +
+ ggplot2::theme_bw() +
+ ggplot2::theme(
+ axis.title.x = ggplot2::element_blank(),
+ axis.text.x = ggplot2::element_blank(),
+ axis.ticks.x = ggplot2::element_blank(),
+ axis.text.y = ggplot2::element_text(
+ colour = "black", size = 18, face = "plain"
+ ),
+ axis.title.y = ggplot2::element_text(
+ colour = "black", size = 20, face = "plain"
+ ),
+ strip.text.x = ggplot2::element_text(
+ colour = "black", size = 16, face = "plain"
+ ),
+ plot.title = ggplot2::element_text(
+ colour = "black", size = 36, face = "plain"
+ )
+ )
+ grDevices::png(outputfile, width = 2000, height = 750, type = "cairo")
+ gridExtra::grid.arrange(
+ gridExtra::arrangeGrob(copy_ratio, as_copy_ratio_seg, ncol = 1)
+ )
+ grDevices::dev.off()
}
#' Plot relative coverage of tumour and normal
-#'
+#'
#' @param samplename Name of the sample for the plot title
#' @param allelecounts Combined allele counts of tumour and normal, read in as a data.frame
#' @param outputfile Full path of file where the figure is to be stored
#' @param max.y The max Y-axis value to be plotted
#' @author sd11
#' @export
-coverage_plot = function(samplename, allelecounts, outputfile, max.y=4) {
-
- print("Normalising allele counts..")
- allelecounts$tumour = allelecounts$mutCountT1+allelecounts$mutCountT2
- allelecounts$tumour = allelecounts$tumour / median(allelecounts$tumour, na.rm=T)
- allelecounts$normal = allelecounts$mutCountN1+allelecounts$mutCountN2
- allelecounts$normal = allelecounts$normal / median(allelecounts$normal, na.rm=T)
-
- print("Smoothing data..")
+coverage_plot <- function(samplename, allelecounts, outputfile, max.y = 4) {
+ log_info("Normalising allele counts..")
+ allelecounts$tumour <- allelecounts$mutCountT1 + allelecounts$mutCountT2
+ allelecounts$tumour <- allelecounts$tumour / collapse::fmedian(allelecounts$tumour, na.rm = TRUE)
+ allelecounts$normal <- allelecounts$mutCountN1 + allelecounts$mutCountN2
+ allelecounts$normal <- allelecounts$normal / collapse::fmedian(allelecounts$normal, na.rm = TRUE)
+
+ log_info("Smoothing data..")
# res = bin_coverage_tumour(allelecounts, binsize=10000)
# allelecounts$tumour_binned = res$tumour_binned
- allelecounts$tumour_binned = runmed_data(allelecounts$Chromosome, allelecounts$tumour)
-
+ allelecounts$tumour_binned <- runmed_data(allelecounts$Chromosome, allelecounts$tumour)
+
# res = bin_coverage_normal(allelecounts, binsize=10000)
# allelecounts$normal_binned = res$normal_binned
# rm(res)
- allelecounts$normal_binned = runmed_data(allelecounts$Chromosome, allelecounts$normal)
- allelecounts$Chromosome = factor(allelecounts$Chromosome, levels=gtools::mixedsort(unique(allelecounts$Chromosome)))
-
- background = data.frame(y=seq(0,2,0.5))
- plot_title = samplename
- p = ggplot(allelecounts[seq(1, nrow(allelecounts), 100),]) +
- geom_hline(data=background, mapping=aes(yintercept=y), colour="black", alpha=0.3) +
- geom_point(mapping=aes(x=Position, y=normal_binned), alpha=0.5, size=0.5, colour="darkgreen") +
- facet_grid(~Chromosome, scales="free_x", space = "free_x") +
- scale_x_continuous(expand=c(0, 0)) +
- ylab("Normal") + scale_y_continuous(breaks=c(0:2), limits=c(0,2)) +
- ggtitle(plot_title) +
- theme_bw() + theme(axis.title.x=element_blank(),
- axis.text.x=element_blank(),
- axis.ticks.x=element_blank(),
- axis.text.y = element_text(colour="black",size=18,face="plain"),
- axis.title.y = element_text(colour="black",size=20,face="plain"),
- strip.text.x = element_text(colour="black",size=16,face="plain"),
- plot.title = element_text(colour="black",size=36,face="plain",hjust = 0.5))
-
- background = data.frame(y=seq(0,max.y,0.5))
- p3 = ggplot(allelecounts[seq(1, nrow(allelecounts), 100),]) +
- geom_hline(data=background, mapping=aes(yintercept=y), colour="black", alpha=0.3) +
- geom_point(mapping=aes(x=Position, y=tumour_binned), alpha=0.5, size=0.5, colour="darkgreen") +
- facet_grid(~Chromosome, scales="free_x", space = "free_x") +
- scale_x_continuous(expand=c(0, 0)) +
- ylim(0,max.y) + ylab("Tumour") +
- theme_bw() + theme(axis.title.x=element_blank(),
- axis.text.x=element_blank(),
- axis.ticks.x=element_blank(),
- axis.text.y = element_text(colour="black",size=18,face="plain"),
- axis.title.y = element_text(colour="black",size=20,face="plain"),
- strip.text.x = element_text(colour="black",size=16,face="plain"),
- plot.title = element_text(colour="black",size=36,face="plain"))
- png(outputfile, width=2000, height=750, type = "cairo")
- gridExtra::grid.arrange(gridExtra::arrangeGrob(p, p3, ncol=1))
- dev.off()
+ allelecounts$normal_binned <- runmed_data(allelecounts$Chromosome, allelecounts$normal)
+ allelecounts$Chromosome <- factor(allelecounts$Chromosome, levels = gtools::mixedsort(unique(allelecounts$Chromosome)))
+
+ background <- data.frame(y = seq(0, 2, 0.5))
+ plot_title <- samplename
+ p <- ggplot2::ggplot(allelecounts[seq(1, nrow(allelecounts), 100), ]) +
+ ggplot2::geom_hline(
+ data = background, mapping = ggplot2::aes(yintercept = y),
+ colour = "black", alpha = 0.3
+ ) +
+ ggplot2::geom_point(
+ mapping = ggplot2::aes(x = Position, y = normal_binned),
+ alpha = 0.5, size = 0.5, colour = "darkgreen"
+ ) +
+ ggplot2::facet_grid(~Chromosome, scales = "free_x", space = "free_x") +
+ ggplot2::scale_x_continuous(expand = c(0, 0)) +
+ ggplot2::ylab("Normal") +
+ ggplot2::scale_y_continuous(breaks = c(0:2), limits = c(0, 2)) +
+ ggplot2::ggtitle(plot_title) +
+ ggplot2::theme_bw() +
+ ggplot2::theme(
+ axis.title.x = ggplot2::element_blank(),
+ axis.text.x = ggplot2::element_blank(),
+ axis.ticks.x = ggplot2::element_blank(),
+ axis.text.y = ggplot2::element_text(
+ colour = "black", size = 18, face = "plain"
+ ),
+ axis.title.y = ggplot2::element_text(
+ colour = "black", size = 20, face = "plain"
+ ),
+ strip.text.x = ggplot2::element_text(
+ colour = "black", size = 16, face = "plain"
+ ),
+ plot.title = ggplot2::element_text(
+ colour = "black", size = 36, face = "plain", hjust = 0.5
+ )
+ )
+
+ background <- data.frame(y = seq(0, max.y, 0.5))
+ p3 <- ggplot2::ggplot(allelecounts[seq(1, nrow(allelecounts), 100), ]) +
+ ggplot2::geom_hline(
+ data = background,
+ mapping = ggplot2::aes(yintercept = y),
+ colour = "black", alpha = 0.3
+ ) +
+ ggplot2::geom_point(
+ mapping = ggplot2::aes(
+ x = Position,
+ y = tumour_binned
+ ),
+ alpha = 0.5, size = 0.5, colour = "darkgreen"
+ ) +
+ ggplot2::facet_grid(~Chromosome, scales = "free_x", space = "free_x") +
+ ggplot2::scale_x_continuous(expand = c(0, 0)) +
+ ggplot2::ylim(0, max.y) +
+ ggplot2::ylab("Tumour") +
+ ggplot2::theme_bw() +
+ ggplot2::theme(
+ axis.title.x = ggplot2::element_blank(),
+ axis.text.x = ggplot2::element_blank(),
+ axis.ticks.x = ggplot2::element_blank(),
+ axis.text.y = ggplot2::element_text(
+ colour = "black", size = 18, face = "plain"
+ ),
+ axis.title.y = ggplot2::element_text(
+ colour = "black", size = 20, face = "plain"
+ ),
+ strip.text.x = ggplot2::element_text(
+ colour = "black", size = 16, face = "plain"
+ ),
+ plot.title = ggplot2::element_text(
+ colour = "black", size = 36, face = "plain"
+ )
+ )
+ grDevices::png(outputfile, width = 2000, height = 750, type = "cairo")
+ gridExtra::grid.arrange(gridExtra::arrangeGrob(p, p3, ncol = 1))
+ grDevices::dev.off()
}
diff --git a/R/plotting_calc.R b/R/plotting_calc.R
new file mode 100644
index 00000000..a2392008
--- /dev/null
+++ b/R/plotting_calc.R
@@ -0,0 +1,47 @@
+########################################################################################
+# Various functions for calculating from data for plotting
+########################################################################################
+#' Calc copy number of major allele per segment from a subclones data.frame
+#' @noRd
+calc_total_cn_major <- function(bb) {
+ return(bb$nMaj1_A * bb$frac1_A + ifelse(bb$frac1_A < 1, bb$nMaj2_A * bb$frac2_A, 0))
+}
+
+#' Calc copy number of minor allele per segment from a subclones data.frame
+#' @noRd
+calc_total_cn_minor <- function(bb) {
+ return(bb$nMin1_A * bb$frac1_A + ifelse(bb$frac1_A < 1, bb$nMin2_A * bb$frac2_A, 0))
+}
+
+#' Calc total copy number per segment from a subclones data.frame
+#' @noRd
+calculate_bb_total_cn <- function(bb) {
+ return((bb$nMaj1_A + bb$nMin1_A) * bb$frac1_A + ifelse(!is.na(bb$frac2_A), (bb$nMaj2_A + bb$nMin2_A) * bb$frac2_A, 0))
+}
+
+#' Calc ploidy from a subclones data.frame
+#' @noRd
+calc_ploidy <- function(bb) {
+ bb$len <- bb$endpos / 1000 - bb$startpos / 1000
+ bb$total_cn <- calculate_bb_total_cn(bb)
+ ploidy <- sum(bb$total_cn * bb$len) / sum(bb$len)
+ return(ploidy)
+}
+
+#' Transform logR into an estimate of total copy number given purity and total ploidy (tumour+normal)
+#' @noRd
+logr2tumcn <- function(cellularity, total_ploidy, logR) {
+ return(((total_ploidy * (2^logR)) - 2 * (1 - cellularity)) / cellularity)
+}
+
+#' Calc psi from psi_t and rho
+#' @noRd
+psit2psi <- function(rho, psi_t) {
+ return(rho * psi_t + 2 * (1 - rho))
+}
+
+#' Calc psi_t from psi and rho
+#' @noRd
+psi2psit <- function(rho, psi) {
+ return((psi - 2 * (1 - rho)) / rho)
+}
diff --git a/R/prepare_SNP6.R b/R/prepare_SNP6.R
index fb8db957..d4877b2e 100644
--- a/R/prepare_SNP6.R
+++ b/R/prepare_SNP6.R
@@ -1,150 +1,58 @@
-#' Adapted code from ASCAT to load in SNP6 data for plotting
-#' noRD
-# ascat.loadData = function(Tumor_LogR_file, Tumor_BAF_file, Germline_LogR_file = NULL, Germline_BAF_file = NULL, chrs = c(1:22,"X","Y"), gender = NULL, sexchromosomes = c("X","Y")) {
-#
-# # read in SNP array data files
-# print.noquote("Reading Tumor LogR data...")
-# Tumor_LogR <- read.table(Tumor_LogR_file, header=T, row.names=1, comment.char="", sep = "\t", check.names=F)
-# print.noquote("Reading Tumor BAF data...")
-# Tumor_BAF <- read.table(Tumor_BAF_file, header=T, row.names=1, comment.char="", sep = "\t", check.names=F)
-#
-# #infinite values are a problem - change those
-# Tumor_LogR[Tumor_LogR==-Inf]=NA
-# Tumor_LogR[Tumor_LogR==Inf]=NA
-#
-# Germline_LogR = NULL
-# Germline_BAF = NULL
-# if(!is.null(Germline_LogR_file)) {
-# print.noquote("Reading Germline LogR data...")
-# Germline_LogR <- read.table(Germline_LogR_file, header=T, row.names=1, comment.char="", sep = "\t", check.names=F)
-# print.noquote("Reading Germline BAF data...")
-# Germline_BAF <- read.table(Germline_BAF_file, header=T, row.names=1, comment.char="", sep = "\t", check.names=F)
-#
-# #infinite values are a problem - change those
-# Germline_LogR[Germline_LogR==-Inf]=NA
-# Germline_LogR[Germline_LogR==Inf]=NA
-# }
-#
-# # make SNPpos vector that contains genomic position for all SNPs and remove all data not on chromosome 1-22,X,Y (or whatever is given in the input value of chrs)
-# print.noquote("Registering SNP locations...")
-# SNPpos <- Tumor_LogR[,1:2]
-# SNPpos = SNPpos[SNPpos[,1]%in%chrs,]
-#
-# # if some chromosomes have no data, just remove them
-# chrs = intersect(chrs,unique(SNPpos[,1]))
-#
-# Tumor_LogR = Tumor_LogR[,c(-1,-2),drop=F]
-# Tumor_BAF = Tumor_BAF[,c(-1,-2),drop=F]
-# # make sure it is all converted to numerical values
-# for (cc in 1:dim(Tumor_LogR)[2]) {
-# Tumor_LogR[,cc]=as.numeric(as.vector(Tumor_LogR[,cc]))
-# Tumor_BAF[,cc]=as.numeric(as.vector(Tumor_BAF[,cc]))
-# }
-# if(!is.null(Germline_LogR_file)) {
-# Germline_LogR = Germline_LogR[,c(-1,-2),drop=F]
-# Germline_BAF = Germline_BAF[,c(-1,-2),drop=F]
-# for (cc in 1:dim(Germline_LogR)[2]) {
-# Germline_LogR[,cc]=as.numeric(as.vector(Germline_LogR[,cc]))
-# Germline_BAF[,cc]=as.numeric(as.vector(Germline_BAF[,cc]))
-# }
-# }
-#
-# # sort all data by genomic position
-# last = 0;
-# ch = list();
-# SNPorder = vector(length=dim(SNPpos)[1])
-# for (i in 1:length(chrs)) {
-# chrke = SNPpos[SNPpos[,1]==chrs[i],]
-# chrpos = chrke[,2]
-# names(chrpos) = rownames(chrke)
-# chrpos = sort(chrpos)
-# ch[[i]] = (last+1):(last+length(chrpos))
-# SNPorder[ch[[i]]] = names(chrpos)
-# last = last+length(chrpos)
-# }
-# SNPpos = SNPpos[SNPorder,]
-# Tumor_LogR=Tumor_LogR[SNPorder,,drop=F]
-# Tumor_BAF=Tumor_BAF[SNPorder,,drop=F]
-#
-# if(!is.null(Germline_LogR_file)) {
-# Germline_LogR = Germline_LogR[SNPorder,,drop=F]
-# Germline_BAF = Germline_BAF[SNPorder,,drop=F]
-# }
-#
-# # split the genome into distinct parts to be used for segmentation (e.g. chromosome arms, parts of genome between gaps in array design)
-# print.noquote("Splitting genome in distinct chunks...")
-# chr = split_genome(SNPpos)
-#
-# if (is.null(gender)) {
-# gender = rep("XX",dim(Tumor_LogR)[2])
-# }
-# return(list(Tumor_LogR = Tumor_LogR, Tumor_BAF = Tumor_BAF,
-# Tumor_LogR_segmented = NULL, Tumor_BAF_segmented = NULL,
-# Germline_LogR = Germline_LogR, Germline_BAF = Germline_BAF,
-# SNPpos = SNPpos, ch = ch, chr = chr, chrs = chrs,
-# samples = colnames(Tumor_LogR), gender = gender,
-# sexchromosomes = sexchromosomes,
-# failedarrays = NULL))
-# }
-
-
-#' Parse the reference info file
-#' @param snp6_reference_info_file A SNP6 reference info master file
-#' @noRd
-parseSNP6refFile = function(snp6_reference_info_file) {
- return(read.table(snp6_reference_info_file, header=T, stringsAsFactors=F))
-}
-
#' Transform cel files into BAF and LogR
#'
#' This function takes a cel file from a tumour and a matched normal and
-#' extracts the BAF and LogR, which is saved into a single file. The \code{gc.correct}
+#' extracts the BAF and LogR, which is saved into a single file. The \code{gc_correct}
#' function can read that file and transforms it into separate BAF and LogR files that
#' both Battenberg and ASCAT can use.
#' @param normal_cel_file String that points to the cel file containing the matched normal data
#' @param tumour_cel_file String that points to the cel file containing the tumour data
#' @param output_file String where the BAF and LogR should be written
#' @param snp6_reference_info_file String to the SNP6 reference info file that comes with Battenberg SNP6
-#' @param apt.probeset.genotype.exe Path to the apt.probeset.genotype executable (Default $PATH)
-#' @param apt.probeset.summarize.exe Path to the apt.probeset.summarize executable (Default $PATH)
-#' @param norm.geno.clust.exe Path to the normalize_affy_geno_cluster.pl script (Default $PATH)
+#' @param apt_probeset_genotype_exe Path to the apt.probeset.genotype executable (Default $PATH)
+#' @param apt_probeset_summarize_exe Path to the apt.probeset.summarize executable (Default $PATH)
+#' @param norm_geno_clust_exe Path to the normalize_affy_geno_cluster.pl script (Default $PATH)
#' @author sd11
#' @export
-cel2baf.logr = function(normal_cel_file, tumour_cel_file, output_file, snp6_reference_info_file, apt.probeset.genotype.exe="apt-probeset-genotype", apt.probeset.summarize.exe="apt-probeset-summarize", norm.geno.clust.exe="normalize_affy_geno_cluster.pl") {
+cel2baf_logr <- function(
+ normal_cel_file,
+ tumour_cel_file,
+ output_file,
+ snp6_reference_info_file
+) {
# Unpack pointers to reference files required during this step
- ref.files = parseSNP6refFile(snp6_reference_info_file)
- GW_SNP6 = ref.files[ref.files$variable == "GW_SNP6",]$reference_file
- SNP6_BIRDSEED_MODELS = ref.files[ref.files$variable == "SNP6_BIRDSEED_MODELS",]$reference_file
- SNP6_SPECIALSNPS = ref.files[ref.files$variable == "SNP6_SPECIALSNPS",]$reference_file
- QUANT_NORM_TARGET = ref.files[ref.files$variable == "QUANT_NORM_TARGET",]$reference_file
- LOCFILE = ref.files[ref.files$variable == "LOCFILE",]$reference_file
- UNM_NORMALS = ref.files[ref.files$variable == "UNM_NORMALS",]$reference_file
-
+ ref_files <- parse_snp6_ref_file(snp6_reference_info_file)
+ GW_SNP6 <- ref_files[ref_files$variable == "GW_SNP6", ]$reference_file
+ SNP6_BIRDSEED_MODELS <- ref_files[ref_files$variable == "SNP6_BIRDSEED_MODELS", ]$reference_file
+ SNP6_SPECIALSNPS <- ref_files[ref_files$variable == "SNP6_SPECIALSNPS", ]$reference_file
+ QUANT_NORM_TARGET <- ref_files[ref_files$variable == "QUANT_NORM_TARGET", ]$reference_file
+ LOCFILE <- ref_files[ref_files$variable == "LOCFILE", ]$reference_file
+ UNM_NORMALS <- ref_files[ref_files$variable == "UNM_NORMALS", ]$reference_file
+
# Unpack the normal cel file
- cmd = paste(apt.probeset.genotype.exe, "-c", GW_SNP6, "-a birdseed", "--read-models-birdseed", SNP6_BIRDSEED_MODELS, "--special-snps", SNP6_SPECIALSNPS, "--cels", normal_cel_file)
- print(cmd)
- EXIT_CODE=system(cmd, wait=T)
- stopifnot(EXIT_CODE==0)
+ cmd <- paste("apt-probeset-genotype", "-c", GW_SNP6, "-a birdseed", "--read-models-birdseed", SNP6_BIRDSEED_MODELS, "--special-snps", SNP6_SPECIALSNPS, "--cels", normal_cel_file)
+ log_info(cmd)
+ exit_code <- system(cmd, wait = TRUE)
+ stopifnot(exit_code == 0)
# Unpack the tumour cel file
- cmd = paste(apt.probeset.summarize.exe, "--cdf-file", GW_SNP6, "--analysis quant-norm.sketch=50000,pm-only,med-polish,expr.genotype=true", "--target-sketch", QUANT_NORM_TARGET, normal_cel_file, tumour_cel_file)
- print(cmd)
- EXIT_CODE=system(cmd, wait=T)
- stopifnot(EXIT_CODE==0)
- # Construct the LogR and BAF and push that to
- cmd = paste(norm.geno.clust.exe, UNM_NORMALS, "quant-norm.pm-only.med-polish.expr.summary.txt", "-locfile", LOCFILE, "-out", output_file)
- print(cmd)
- EXIT_CODE=system(cmd, wait=T)
- stopifnot(EXIT_CODE==0)
+ cmd <- paste("apt-probeset-summarize", "--cdf-file", GW_SNP6, "--analysis quant-norm.sketch=50000,pm-only,med-polish,expr.genotype=true", "--target-sketch", QUANT_NORM_TARGET, normal_cel_file, tumour_cel_file)
+ log_info(cmd)
+ exit_code <- system(cmd, wait = TRUE)
+ stopifnot(exit_code == 0)
+ # Construct the LogR and BAF and push that to
+ cmd <- paste("normalize_affy_geno_cluster.pl", UNM_NORMALS, "quant-norm.pm-only.med-polish.expr.summary.txt", "-locfile", LOCFILE, "-out", output_file)
+ log_info(cmd)
+ exit_code <- system(cmd, wait = TRUE)
+ stopifnot(exit_code == 0)
}
#' Correct the LogR estimates for GC content
-#'
+#'
#' This function performs GC correction of the LogR
#' data. Sometimes a wave pattern is observed there
#' that correlates with GC content. Internally it uses
#' the ASCAT gc correction function.
#' @param samplename Name of the sample to be used to name columns
-#' @param infile.logr.baf String that points to the raw combined BAF and LogR file that is the result of \code{cel2baf.logr}
+#' @param infile.logr.baf String that points to the raw combined BAF and LogR file that is the result of \code{cel2baf_logr}
#' @param outfile.tumor.LogR The filename of the file where the tumour LogR will be written
#' @param outfile.tumor.BAF The filename of the file where the tumour BAF will be written
#' @param outfile.normal.LogR The filename of the file where the normal LogR will be written
@@ -155,96 +63,96 @@ cel2baf.logr = function(normal_cel_file, tumour_cel_file, output_file, snp6_refe
#' @param birdseed_report_file Name of the birdseed output file. This is a temp output file of one of the internally called functions of which the name cannot be defined. Don't change this parameter. (Default birdseed.report.txt)
#' @author sd11
#' @export
-gc.correct = function(samplename, infile.logr.baf, outfile.tumor.LogR, outfile.tumor.BAF, outfile.normal.LogR, outfile.normal.BAF, outfile.probeBAF, snp6_reference_info_file, chr_names, birdseed_report_file="birdseed.report.txt",genomebuild="hg19") {
+gc_correct <- function(samplename, infile.logr.baf, outfile.tumor.LogR, outfile.tumor.BAF, outfile.normal.LogR, outfile.normal.BAF, outfile.probeBAF, snp6_reference_info_file, chr_names, birdseed_report_file = "birdseed.report.txt", genomebuild = "hg19") {
# Read in needed reference files
- ref.files = parseSNP6refFile(snp6_reference_info_file)
- SNP_POS_REF = ref.files[ref.files$variable == "SNP_POS",]$reference_file
- GC_SNP6 = ref.files[ref.files$variable == "GC_SNP6",]$reference_file
-
- lrrbaf = read.table(infile.logr.baf, header=T, sep="\t", row.names=1, stringsAsFactors=F)
- SNPpos = read.table(SNP_POS_REF, header=T, sep="\t", row.names=1, stringsAsFactors=F)
-
- Tumor_LogR = lrrbaf[rownames(SNPpos), 5, drop=F]
- colnames(Tumor_LogR) = samplename
-
- Tumor_BAF = lrrbaf[rownames(SNPpos), 6, drop=F]
- colnames(Tumor_BAF) = samplename
-
- Normal_LogR = lrrbaf[rownames(SNPpos), 3, drop=F]
- colnames(Normal_LogR) = samplename
-
- Normal_BAF = lrrbaf[rownames(SNPpos), 4, drop=F]
- colnames(Normal_BAF) = samplename
-
- #replace 2's by NA
- Tumor_BAF[Tumor_BAF==2]=NA
- Normal_BAF[Normal_BAF==2]=NA
-
+ ref_files <- parse_snp6_ref_file(snp6_reference_info_file)
+ SNP_POS_REF <- ref_files[ref_files$variable == "SNP_POS", ]$reference_file
+ GC_SNP6 <- ref_files[ref_files$variable == "GC_SNP6", ]$reference_file
+
+ lrrbaf <- utils::read.table(infile.logr.baf, header = TRUE, sep = "\t", row.names = 1, stringsAsFactors = FALSE)
+ SNPpos <- utils::read.table(SNP_POS_REF, header = TRUE, sep = "\t", row.names = 1, stringsAsFactors = FALSE)
+
+ Tumor_LogR <- lrrbaf[rownames(SNPpos), 5, drop = FALSE]
+ colnames(Tumor_LogR) <- samplename
+
+ Tumor_BAF <- lrrbaf[rownames(SNPpos), 6, drop = FALSE]
+ colnames(Tumor_BAF) <- samplename
+
+ Normal_LogR <- lrrbaf[rownames(SNPpos), 3, drop = FALSE]
+ colnames(Normal_LogR) <- samplename
+
+ Normal_BAF <- lrrbaf[rownames(SNPpos), 4, drop = FALSE]
+ colnames(Normal_BAF) <- samplename
+
+ # replace 2's by NA
+ Tumor_BAF[Tumor_BAF == 2] <- NA
+ Normal_BAF[Normal_BAF == 2] <- NA
+
# Tumor_LogR: correct difference between copy number only probes and other probes
- CNprobes = substring(rownames(SNPpos),1,2)=="CN"
-
- Tumor_LogR[CNprobes,1] = Tumor_LogR[CNprobes,1]-mean(Tumor_LogR[CNprobes,1],na.rm=T)
- Tumor_LogR[!CNprobes,1] = Tumor_LogR[!CNprobes,1]-mean(Tumor_LogR[!CNprobes,1],na.rm=T)
-
- Normal_LogR[CNprobes,1] = Normal_LogR[CNprobes,1]-mean(Normal_LogR[CNprobes,1],na.rm=T)
- Normal_LogR[!CNprobes,1] = Normal_LogR[!CNprobes,1]-mean(Normal_LogR[!CNprobes,1],na.rm=T)
-
+ CNprobes <- substring(rownames(SNPpos), 1, 2) == "CN"
+
+ Tumor_LogR[CNprobes, 1] <- Tumor_LogR[CNprobes, 1] - mean(Tumor_LogR[CNprobes, 1], na.rm = TRUE)
+ Tumor_LogR[!CNprobes, 1] <- Tumor_LogR[!CNprobes, 1] - mean(Tumor_LogR[!CNprobes, 1], na.rm = TRUE)
+
+ Normal_LogR[CNprobes, 1] <- Normal_LogR[CNprobes, 1] - mean(Normal_LogR[CNprobes, 1], na.rm = TRUE)
+ Normal_LogR[!CNprobes, 1] <- Normal_LogR[!CNprobes, 1] - mean(Normal_LogR[!CNprobes, 1], na.rm = TRUE)
+
# limit the number of digits:
- Tumor_LogR = round(Tumor_LogR,4)
- Normal_LogR = round(Normal_LogR,4)
-
- write.table(cbind(SNPpos,Tumor_BAF), paste(outfile.tumor.BAF, "_noGCcorr.txt", sep=""), sep="\t", row.names=T, quote=F)
- write.table(cbind(SNPpos,Normal_BAF), paste(outfile.normal.BAF, "_noGCcorr.txt", sep=""), sep="\t", row.names=T, quote=F)
-
+ Tumor_LogR <- round(Tumor_LogR, 4)
+ Normal_LogR <- round(Normal_LogR, 4)
+
+ data.table::fwrite(cbind(SNPpos, Tumor_BAF), paste(outfile.tumor.BAF, "_noGCcorr.txt", sep = ""), sep = "\t", row.names = TRUE, quote = FALSE)
+ data.table::fwrite(cbind(SNPpos, Normal_BAF), paste(outfile.normal.BAF, "_noGCcorr.txt", sep = ""), sep = "\t", row.names = TRUE, quote = FALSE)
+
# read into ASCAT and make GC corrected input:
- write.table(cbind(SNPpos,Tumor_LogR), paste(outfile.tumor.LogR, "_noGCcorr.txt", sep=""), sep="\t", row.names=T, quote=F)
- write.table(cbind(SNPpos,Normal_LogR), paste(outfile.normal.LogR, "_noGCcorr.txt", sep=""), sep="\t", row.names=T, quote=F)
-
+ data.table::fwrite(cbind(SNPpos, Tumor_LogR), paste(outfile.tumor.LogR, "_noGCcorr.txt", sep = ""), sep = "\t", row.names = TRUE, quote = FALSE)
+ data.table::fwrite(cbind(SNPpos, Normal_LogR), paste(outfile.normal.LogR, "_noGCcorr.txt", sep = ""), sep = "\t", row.names = TRUE, quote = FALSE)
+
# ======================================= above previous prepareGCcorrect, below runGCcorrect ==============================================
-
+
# TODO: This must be a dapted to not hardcode the chromosome names
- gender <- read.table(birdseed_report_file, sep="\t", skip=66, header=T)
- sex <- as.vector(gender[,"computed_gender"])
+ gender <- utils::read.table(birdseed_report_file, sep = "\t", skip = 66, header = TRUE)
+ sex <- as.vector(gender[, "computed_gender"])
sex[sex == "female"] <- "XX"
sex[sex == "male"] <- "XY"
sex[sex == "unknown"] <- NA
-
- ascat.bc <- ASCAT::ascat.loadData(paste(outfile.tumor.LogR, "_noGCcorr.txt", sep=""), paste(outfile.tumor.BAF, "_noGCcorr.txt", sep=""),paste(outfile.normal.LogR, "_noGCcorr.txt", sep=""), paste(outfile.normal.BAF, "_noGCcorr.txt", sep=""), chrs=chr_names, gender=sex, genomeVersion=genomebuild)
- ASCAT::ascat.plotRawData(ascat.bc)
- ascat.bc <- ASCAT::ascat.correctLogR(ascat.bc, GC_SNP6)
+
+ ascat_bc <- ASCAT::ascat.loadData(paste(outfile.tumor.LogR, "_noGCcorr.txt", sep = ""), paste(outfile.tumor.BAF, "_noGCcorr.txt", sep = ""), paste(outfile.normal.LogR, "_noGCcorr.txt", sep = ""), paste(outfile.normal.BAF, "_noGCcorr.txt", sep = ""), chrs = chr_names, gender = sex, genomeVersion = genomebuild)
+ ASCAT::ascat.plotRawData(ascat_bc)
+ ascat_bc <- ASCAT::ascat.correctLogR(ascat_bc, GC_SNP6)
# Make sure the right column names are added here, because these are expected by fitcopynumber
- colnames(ascat.bc$SNPpos) = c("Chromosome", "Position")
+ colnames(ascat_bc$SNPpos) <- c("Chromosome", "Position")
# Determine SNPs with BAF between 0.3-0.7 from normal => these are supposed to be heterozygous
- is.het = (ascat.bc$Germline_BAF >= 0.3 & ascat.bc$Germline_BAF <= 0.7)
- dat = cbind(ascat.bc$SNPpos, round(ascat.bc$Germline_LogR, 4))
- dat = dat[which(is.het),]
- colnames(dat) = c("Chromosome", "Position", samplename)
- write.table(dat, file=outfile.normal.LogR, row.names=F, quote=F, sep="\t")
+ is.het <- (ascat_bc$Germline_BAF >= 0.3 & ascat_bc$Germline_BAF <= 0.7)
+ dat <- cbind(ascat_bc$SNPpos, round(ascat_bc$Germline_LogR, 4))
+ dat <- dat[which(is.het), ]
+ colnames(dat) <- c("Chromosome", "Position", samplename)
+ data.table::fwrite(dat, file = outfile.normal.LogR, row.names = FALSE, quote = FALSE, sep = "\t")
- select = !is.na(ascat.bc$Germline_BAF)
- dat = cbind(ascat.bc$SNPpos, round(ascat.bc$Germline_BAF, 4))
- colnames(dat) = c("Chromosome", "Position", samplename)
- write.table(dat[which(select),], file=outfile.normal.BAF, row.names=F, quote=F, sep="\t")
+ select <- !is.na(ascat_bc$Germline_BAF)
+ dat <- cbind(ascat_bc$SNPpos, round(ascat_bc$Germline_BAF, 4))
+ colnames(dat) <- c("Chromosome", "Position", samplename)
+ data.table::fwrite(dat[which(select), ], file = outfile.normal.BAF, row.names = FALSE, quote = FALSE, sep = "\t")
# Save the probe ids plus their BAF for only the germline heterozygous mutations
- select = !is.na(ascat.bc$Tumor_BAF)
- dat = cbind(row.names(ascat.bc$SNPpos), ascat.bc$Tumor_BAF)
- dat = dat[which(select & is.het),]
- write.table(dat, file=outfile.probeBAF, row.names=F, quote=F, col.names=F, sep="\t")
+ select <- !is.na(ascat_bc$Tumor_BAF)
+ dat <- cbind(row.names(ascat_bc$SNPpos), ascat_bc$Tumor_BAF)
+ dat <- dat[which(select & is.het), ]
+ data.table::fwrite(dat, file = outfile.probeBAF, row.names = FALSE, quote = FALSE, col.names = FALSE, sep = "\t")
# Save tumour BAF and LogR directly. Include homozygous SNPs here.
- dat = cbind(ascat.bc$SNPpos, round(ascat.bc$Tumor_BAF, 4))
- dat = dat[which(select),]
- colnames(dat) = c("Chromosome", "Position", samplename)
- write.table(dat, file=outfile.tumor.BAF, row.names=F, quote=F, sep="\t")
-
- select = !is.na(ascat.bc$Tumor_LogR)
- dat = cbind(ascat.bc$SNPpos, round(ascat.bc$Tumor_LogR, 4))
- dat = dat[which(select),]
- colnames(dat) = c("Chromosome", "Position", samplename)
- write.table(dat, file=outfile.tumor.LogR, row.names=F, quote=F, sep="\t")
+ dat <- cbind(ascat_bc$SNPpos, round(ascat_bc$Tumor_BAF, 4))
+ dat <- dat[which(select), ]
+ colnames(dat) <- c("Chromosome", "Position", samplename)
+ data.table::fwrite(dat, file = outfile.tumor.BAF, row.names = FALSE, quote = FALSE, sep = "\t")
+
+ select <- !is.na(ascat_bc$Tumor_LogR)
+ dat <- cbind(ascat_bc$SNPpos, round(ascat_bc$Tumor_LogR, 4))
+ dat <- dat[which(select), ]
+ colnames(dat) <- c("Chromosome", "Position", samplename)
+ data.table::fwrite(dat, file = outfile.tumor.LogR, row.names = FALSE, quote = FALSE, sep = "\t")
}
@@ -254,181 +162,225 @@ gc.correct = function(samplename, infile.logr.baf, outfile.tumor.LogR, outfile.t
#' needs to be prepared to go into Impute2, which is essentially morphing it into
#' the correct format. This function does that per chromosome and can therefore
#' be run in parallel for each chromosome.
-#' @param infile.germlineBAF Germline BAF file generated by \code{cel2baf.logr}
-#' @param infile.tumourBAF Tumour BAF file generated by \code{cel2baf.logr}
+#' @param infile_germlineBAF Germline BAF file generated by \code{cel2baf_logr}
+#' @param infile_tumourBAF Tumour BAF file generated by \code{cel2baf_logr}
#' @param outFileStart Prefix of the filenames where the Impute2 input will be written. These will be extended with the chromosome
#' @param chrom Char with the chromosome for which an Impute2 file is produced
#' @param chr_names A vector of chromosome names that can be considered. This vector can just contain the chromosome for which the Impute2 file is produced, but can contain all chromosomes.
-#' @param problemLociFile A string that points to a file with problematic loci that should be removed from the data
+#' @param problem_loci_file A string that points to a file with problematic loci that should be removed from the data
#' @param snp6_reference_info_file String to the SNP6 reference info file that comes with Battenberg SNP6
#' @param imputeinfofile String to the impute 1000 genomes reference info file that comes with Battenberg
-#' @param is.male Boolean that is True if the donor is male, False when female
-#' @param heterozygousFilter BAF cutoff for calling homozygous SNPs
+#' @param is_male Boolean that is True if the donor is male, False when female
+#' @param heterozygous_filter BAF cutoff for calling homozygous SNPs
#' @author dw9 jd
#' @export
-generate.impute.input.snp6 = function(infile.germlineBAF, infile.tumourBAF, outFileStart, chrom, chr_names, problemLociFile, snp6_reference_info_file, imputeinfofile, is.male, heterozygousFilter="none") {
- # Obtain pointer to SNP6 specific reference file
- ref.files = parseSNP6refFile(snp6_reference_info_file)
- ANNO_FILE = ref.files[ref.files$variable == "ANNO_FILE",]$reference_file
-
- # Read in the 1000 genomes reference file paths for the specified chrom
- impute.info = parse.imputeinfofile(imputeinfofile, is.male, chrom=chrom)
-
- # Read in the known SNP locations from the 1000 genomes reference files
- known_SNPs = read.table(impute.info$impute_legend[1], sep=" ", header=T)
- if(nrow(impute.info)>1){
- for(r in 2:nrow(impute.info)){
- known_SNPs = rbind(known_SNPs, read.table(impute.info$impute_legend[r], sep=" ", header=T))
- }
- }
-
- outfile=paste(outFileStart,chrom,".txt",sep="")
-
- known_SNPs[,3]=factor(known_SNPs[,3],levels=c("A","C","G","T"))
- known_SNPs[,4]=factor(known_SNPs[,4],levels=c("A","C","G","T"))
-
- print(head(known_SNPs))
- print(dim(known_SNPs))
- chr_name = chrom
-
- # filter out bad SNPs (streaks in BAF)
- if((problemLociFile !="NA") & (!is.na(problemLociFile)))
- {
- problemSNPs=read.table(problemLociFile,header=T,sep="\t")
- problemSNPs=problemSNPs$Pos[problemSNPs$Chr==chr_name]
- badIndices=match(known_SNPs[,2],problemSNPs)
- known_SNPs = known_SNPs[is.na(badIndices),]
- print(paste("badIndices lengths=",length(badIndices),",",sum(is.na(badIndices)),sep=""))
- }
-
- knownSNP6data=read.csv(ANNO_FILE,comment.char="#",header=T,row.names=NULL,stringsAsFactors=F)
- knownSNP6data=knownSNP6data[knownSNP6data$Chromosome==chr_name,]
- print(paste("first column=",names(knownSNP6data)[1],sep=""))
- print(paste("first known datum=",knownSNP6data[1,1],sep=""))
-
- # adjust for strand
- knownSNP6data$Allele.A[knownSNP6data$Strand=="-" & knownSNP6data$Allele.A=="A"]="X"
- knownSNP6data$Allele.A[knownSNP6data$Strand=="-" & knownSNP6data$Allele.A=="C"]="Y"
- knownSNP6data$Allele.A[knownSNP6data$Strand=="-" & knownSNP6data$Allele.A=="G"]="Z"
- knownSNP6data$Allele.A[knownSNP6data$Strand=="-" & knownSNP6data$Allele.A=="T"]="A"
- knownSNP6data$Allele.A[knownSNP6data$Strand=="-" & knownSNP6data$Allele.A=="X"]="T"
- knownSNP6data$Allele.A[knownSNP6data$Strand=="-" & knownSNP6data$Allele.A=="Y"]="G"
- knownSNP6data$Allele.A[knownSNP6data$Strand=="-" & knownSNP6data$Allele.A=="Z"]="C"
- knownSNP6data$Allele.B[knownSNP6data$Strand=="-" & knownSNP6data$Allele.B=="A"]="X"
- knownSNP6data$Allele.B[knownSNP6data$Strand=="-" & knownSNP6data$Allele.B=="C"]="Y"
- knownSNP6data$Allele.B[knownSNP6data$Strand=="-" & knownSNP6data$Allele.B=="G"]="Z"
- knownSNP6data$Allele.B[knownSNP6data$Strand=="-" & knownSNP6data$Allele.B=="T"]="A"
- knownSNP6data$Allele.B[knownSNP6data$Strand=="-" & knownSNP6data$Allele.B=="X"]="T"
- knownSNP6data$Allele.B[knownSNP6data$Strand=="-" & knownSNP6data$Allele.B=="Y"]="G"
- knownSNP6data$Allele.B[knownSNP6data$Strand=="-" & knownSNP6data$Allele.B=="Z"]="C"
-
- #remove duplicates (variants on both strands)
- knownSNP6data = knownSNP6data[!duplicated(knownSNP6data$Physical.Position),]
-
- #make sure all bases are repesented as factors, in the correct order
- knownSNP6data$Allele.A = factor(knownSNP6data$Allele.A,levels=c("A","C","G","T"))
- knownSNP6data$Allele.B = factor(knownSNP6data$Allele.B,levels=c("A","C","G","T"))
-
- # Read in the BAFs and see which 1000 genomes SNPs are covered
- germline_snp_data = read.table(infile.germlineBAF,sep="\t",header=T, stringsAsFactors=F) #[,3,drop=F]
- germline_snp_data = germline_snp_data[germline_snp_data[,1]==chr_name,]
- tumour_snp_data = read.table(infile.tumourBAF,sep="\t",header=T, stringsAsFactors=F) #[,3,drop=F]
- tumour_snp_data = tumour_snp_data[tumour_snp_data[,1]==chr_name,]
- # snp_matches = match(rownames(germline_snp_data), rownames(tumour_snp_data))
- snp_matches = match(germline_snp_data[,2], tumour_snp_data[,2])
- snp_data = na.omit(cbind(nBAF = germline_snp_data[,3], tBAF = tumour_snp_data[snp_matches,3]))
-
- print(paste("first datum=",rownames(snp_data[1,]),sep=""))
-
- #indices = match(rownames(snp_data),knownSNP6data$Probe.Set.ID)
- indices = match(germline_snp_data[,2], knownSNP6data$Physical.Position)
- if(sum(!is.na(indices))==0){
- print("Did not find any positional matches of the provided data to the reference")
- # indices = match(rownames(snp_data),knownSNP6data$dbSNP.RS.ID)
- q(save="no", status=1)
+generate_impute_input_snp6 <- function(
+ infile_germlineBAF,
+ infile_tumourBAF,
+ outFileStart,
+ chrom,
+ chr_names,
+ problem_loci_file,
+ snp6_reference_info_file,
+ imputeinfofile,
+ is_male,
+ heterozygous_filter = "none"
+) {
+ ref_files <- parse_snp6_ref_file(snp6_reference_info_file)
+ ANNO_FILE <- ref_files[ref_files$variable == "ANNO_FILE", "reference_file"]
+
+ impute_info <- parse_imputeinfofile(imputeinfofile, is_male, chrom = chrom)
+
+ known_SNPs <- data.table::rbindlist(
+ lapply(impute_info$impute_legend, function(f) {
+ data.table::fread(f, header = TRUE)
+ })
+ )
+
+ allele_levels <- c("A", "C", "G", "T")
+ data.table::set(
+ known_SNPs,
+ j = "position",
+ value = as.integer(known_SNPs[["position"]])
+ )
+ data.table::set(
+ known_SNPs,
+ j = "a0",
+ value = factor(known_SNPs[["a0"]], levels = allele_levels)
+ )
+ data.table::set(
+ known_SNPs,
+ j = "a1",
+ value = factor(known_SNPs[["a1"]], levels = allele_levels)
+ )
+
+ if (!is.na(problem_loci_file) && problem_loci_file != "NA") {
+ problemSNPs <- data.table::fread(problem_loci_file, header = TRUE)
+ bad_pos <- problemSNPs[
+ problemSNPs[["Chr"]] == chrom,
+ problemSNPs[["Pos"]]
+ ]
+ known_SNPs <- known_SNPs[!(known_SNPs[["position"]] %in% bad_pos)]
}
- print(paste("found SNPs=",sum(!is.na(indices)),sep=""))
- print(paste("class=",class(knownSNP6data$Physical.Position),sep=""))
- matched.info = cbind(knownSNP6data[indices[!is.na(indices)],c("Physical.Position","Allele.A","Allele.B")],snp_data[!is.na(indices),1:2])
- print(paste("class2=",class(matched.info[,1]),sep=""))
-
- print(paste("first row of matched.info=",paste(matched.info[1,],sep=","),sep=""))
- print(paste("first Allele.A=",matched.info$Allele.A[1],sep=""))
- print(paste("first Allele.B=",matched.info$Allele.B[1],sep=""))
-
- print(paste("class 1a =",class(known_SNPs[,2]),sep=""))
- print(paste("class 2a =",class(as.numeric(known_SNPs[,2])),sep=""))
-
- indices2 = match(matched.info[,1],known_SNPs[,2])
- combined.info = na.omit(cbind(matched.info[!is.na(indices2),],known_SNPs[indices2[!is.na(indices2)],1:4]))
- print(paste("first row of combined.info=",paste(combined.info[1,],sep=","),sep=""))
- lev2 = levels(combined.info[,2])
- print(paste("levels[2]=",paste(lev2,sep=","),sep=""))
- lev3 = levels(combined.info[,3])
- print(paste("levels[3]=",paste(lev3,sep=","),sep=""))
- lev8 = levels(combined.info[,8])
- print(paste("levels[8]=",paste(lev8,sep=","),sep=""))
- lev9 = levels(combined.info[,9])
- print(paste("levels[9]=",paste(lev9,sep=","),sep=""))
-
- combined.info1 = combined.info[(combined.info[,2]==combined.info[,8] & combined.info[,3]==combined.info[,9]),]
- #alleles are reversed
- combined.info2 = cbind(combined.info[(combined.info[,2]==combined.info[,9] & combined.info[,3]==combined.info[,8]),1:3],1.0-combined.info[(combined.info[,2]==combined.info[,9] & combined.info[,3]==combined.info[,8]),4:5],combined.info[(combined.info[,2]==combined.info[,9] & combined.info[,3]==combined.info[,8]),6:9])
- names(combined.info2) = names(combined.info1)
-
- all.info = rbind(combined.info1,combined.info2)
- print(paste("norows all.info=",nrow(all.info),sep=""))
-
- all.info = all.info[order(as.numeric(all.info[,1])),]
-
- is.het = (all.info[,4] >= 0.3 & all.info[,4] <= 0.7)
- names(all.info)[5]="allele.frequency"
- write.csv(all.info[is.het,-4], file=paste(outFileStart,chrom,"_withAlleleFreq.csv",sep=""), quote=F, row.names=F)
-
- out.data = data.frame()
- if (heterozygousFilter!="none") {
- # Set the minimum level to use for calling homozygous SNPs
- minBaf = min(heterozygousFilter, 1.0-heterozygousFilter)
- maxBaf = max(heterozygousFilter, 1.0-heterozygousFilter)
-
- is.hom.ref = (all.info[,4] <= minBaf)
- is.hom.alt = (all.info[,4] >= maxBaf)
-
- # Obtain genotypes that impute2 is able to understand
- genotypes = array(0,c(nrow(all.info),3))
- genotypes[is.hom.ref,1] = 1
- genotypes[is.het,2] = 1
- genotypes[is.hom.alt,3] = 1
- is.genotyped = (is.het | is.hom.ref | is.hom.alt)
-
- snp.names = paste("snp",1:sum(is.genotyped),sep="")
- out.data = cbind(snp.names,all.info[is.genotyped,6:9], genotypes[is.genotyped,])
+ knownSNP6data <- data.table::fread(ANNO_FILE, skip = "#", header = TRUE)
+ knownSNP6data <- knownSNP6data[knownSNP6data[["Chromosome"]] == chrom]
+
+ complement <- c("A" = "T", "C" = "G", "G" = "C", "T" = "A")
+ neg_strand <- knownSNP6data[["Strand"]] == "-"
+
+ data.table::set(
+ knownSNP6data,
+ i = which(neg_strand),
+ j = "Allele.A",
+ value = complement[knownSNP6data[["Allele.A"]][neg_strand]]
+ )
+ data.table::set(
+ knownSNP6data,
+ i = which(neg_strand),
+ j = "Allele.B",
+ value = complement[knownSNP6data[["Allele.B"]][neg_strand]]
+ )
+
+ knownSNP6data <- knownSNP6data[!duplicated(knownSNP6data[["Physical.Position"]])]
+
+ data.table::set(
+ knownSNP6data,
+ j = "Allele.A",
+ value = factor(knownSNP6data[["Allele.A"]], levels = allele_levels)
+ )
+ data.table::set(
+ knownSNP6data,
+ j = "Allele.B",
+ value = factor(knownSNP6data[["Allele.B"]], levels = allele_levels)
+ )
+
+ germline_snp_data <- data.table::fread(infile_germlineBAF, header = TRUE)
+ chr_col <- names(germline_snp_data)[1]
+ germline_snp_data <- germline_snp_data[germline_snp_data[[chr_col]] == chrom]
+
+ tumour_snp_data <- data.table::fread(infile_tumourBAF, header = TRUE)
+ chr_col <- names(tumour_snp_data)[1]
+ tumour_snp_data <- tumour_snp_data[tumour_snp_data[[chr_col]] == chrom]
+
+ data.table::setnames(germline_snp_data, c("Chr", "Pos", "nBAF"))
+ data.table::setnames(tumour_snp_data, c("Chr", "Pos", "tBAF"))
+
+ snp_data <- merge(germline_snp_data, tumour_snp_data, by = c("Chr", "Pos"))
+
+ anno_subset <- data.table::data.table(
+ Physical.Position = knownSNP6data[["Physical.Position"]],
+ Allele.A = knownSNP6data[["Allele.A"]],
+ Allele.B = knownSNP6data[["Allele.B"]]
+ )
+
+ matched.info <- merge(
+ anno_subset,
+ snp_data,
+ by.x = "Physical.Position",
+ by.y = "Pos"
+ )
+
+ combined.info <- merge(
+ matched.info,
+ known_SNPs,
+ by.x = "Physical.Position",
+ by.y = "position"
+ )
+
+ idx_match <- combined.info[["Allele.A"]] == combined.info[["a0"]] &
+ combined.info[["Allele.B"]] == combined.info[["a1"]]
+
+ idx_flip <- combined.info[["Allele.A"]] == combined.info[["a1"]] &
+ combined.info[["Allele.B"]] == combined.info[["a0"]]
+
+ combined.info1 <- combined.info[idx_match]
+ combined.info2 <- combined.info[idx_flip]
+
+ data.table::set(
+ combined.info2,
+ j = "nBAF",
+ value = 1.0 - combined.info2[["nBAF"]]
+ )
+ data.table::set(
+ combined.info2,
+ j = "tBAF",
+ value = 1.0 - combined.info2[["tBAF"]]
+ )
+
+ all.info <- data.table::rbindlist(list(combined.info1, combined.info2))
+ all.info <- all.info[order(all.info[["Physical.Position"]])]
+
+ is_het_vec <- all.info[["nBAF"]] >= 0.3 & all.info[["nBAF"]] <= 0.7
+
+ utils::write.csv(
+ all.info[is_het_vec, setdiff(names(all.info), "nBAF"), drop = FALSE],
+ file = paste0(outFileStart, chrom, "_withAlleleFreq.csv"),
+ quote = FALSE,
+ row.names = FALSE
+ )
+
+ if (heterozygous_filter != "none") {
+ minBaf <- min(heterozygous_filter, 1.0 - heterozygous_filter)
+ maxBaf <- max(heterozygous_filter, 1.0 - heterozygous_filter)
+
+ is_het <- all.info[["nBAF"]] >= 0.3 & all.info[["nBAF"]] <= 0.7
+ is_hom_ref <- all.info[["nBAF"]] <= minBaf
+ is_hom_alt <- all.info[["nBAF"]] >= maxBaf
+
+ keep <- is_het | is_hom_ref | is_hom_alt
+ subset <- all.info[keep]
+
+ out.data <- data.table::data.table(
+ snp.names = paste0("snp", seq_len(nrow(subset))),
+ ID = subset[["id"]],
+ Pos = subset[["Physical.Position"]],
+ a0 = subset[["a0"]],
+ a1 = subset[["a1"]],
+ G1 = as.integer(is_hom_ref[keep]),
+ G2 = as.integer(is_het[keep]),
+ G3 = as.integer(is_hom_alt[keep])
+ )
} else {
- snp.names = paste("snp",1:sum(is.het),sep="")
- out.data = cbind(snp.names,all.info[is.het,6:9], matrix(data=c(0,1,0), nrow=sum(is.het), ncol=3, byrow=T))
- }
- write.table(out.data,file=outfile,row.names=F,col.names=F,quote=F)
-
- if (chrom=='chrX') {
- sample.g.file = paste(outFileStart,"sample_g.txt",sep="")
- sample_g_data = data.frame(ID_1=c(0,"INDIVI1"),ID_2=c(0,"INDIVI1"),missing=c(0,0),sex=c("D",2))
- write.table(sample_g_data, file=sample.g.file, row.names=F, col.names=T, quote=F)
- }
-}
+ subset <- all.info[is_het_vec]
-#' Infer the gender using the birdseed report file
-#' @param birdseed_report_file The birdseed report file
-#' @export
-infer_gender_birdseed = function(birdseed_report_file) {
- z = read.table(birdseed_report_file, header=T)
- return(as.character(z$em.cluster.chrX.het.contrast_gender))
-}
+ out.data <- data.table::data.table(
+ snp.names = paste0("snp", seq_len(nrow(subset))),
+ ID = subset[["id"]],
+ Pos = subset[["Physical.Position"]],
+ a0 = subset[["a0"]],
+ a1 = subset[["a1"]],
+ G1 = 0L,
+ G2 = 1L,
+ G3 = 0L
+ )
+ }
+ data.table::fwrite(
+ out.data,
+ file = paste0(outFileStart, chrom, ".txt"),
+ col.names = FALSE,
+ quote = FALSE,
+ sep = " "
+ )
+ if (chrom == "chrX") {
+ sample_g_data <- data.frame(
+ ID_1 = c(0, "INDIVI1"),
+ ID_2 = c(0, "INDIVI1"),
+ missing = c(0, 0),
+ sex = c("D", 2)
+ )
+ data.table::fwrite(
+ sample_g_data,
+ file = paste0(outFileStart, "sample_g.txt"),
+ sep = " "
+ )
+ }
+}
#' Prepare SNP6 data for haplotype construction
-#'
-#' This function performs part of the Battenberg SNP6 pipeline: Extract BAF and logR from the CEL files
+#'
+#' This function performs part of the Battenberg SNP6 pipeline: Extract BAF and logR from the CEL files
#' and performing GC content correction.
#'
#' @param tumour_cel_file Full path to a CEL file containing the tumour raw data
@@ -436,36 +388,38 @@ infer_gender_birdseed = function(birdseed_report_file) {
#' @param tumourname Identifier to be used for tumour output files
#' @param chrom_names A vector containing the names of chromosomes to be included
#' @param snp6_reference_info_file Full path to the SNP6 reference info file
-#' @param apt.probeset.genotype.exe Full path to the apt.probeset.genotype executable (Default: expected in $PATH)
-#' @param apt.probeset.summarize.exe Full path to the apt.probeset.summarize executable (Default: expected in $PATH)
-#' @param norm.geno.clust.exe Full path to the norm.geno.clust.exe executable (Default: expected in $PATH)
+#' @param apt_probeset_genotype_exe Full path to the apt.probeset.genotype executable (Default: expected in $PATH)
+#' @param apt_probeset_summarize_exe Full path to the apt.probeset.summarize executable (Default: expected in $PATH)
+#' @param norm_geno_clust_exe Full path to the norm_geno_clust_exe executable (Default: expected in $PATH)
#' @param birdseed_report_file Name of the birdseed output file. This is a temp output file of one of the internally called functions of which the name cannot be defined. Don't change this parameter. (Default: birdseed.report.txt)
#' @author sd11
#' @export
-prepare_snp6 = function(tumour_cel_file, normal_cel_file, tumourname, chrom_names,
- snp6_reference_info_file, apt.probeset.genotype.exe="apt-probeset-genotype",
- apt.probeset.summarize.exe="apt-probeset-summarize", norm.geno.clust.exe="normalize_affy_geno_cluster.pl",
- birdseed_report_file="birdseed.report.txt",genomebuild="hg19") {
-
+prepare_snp6 <- function(
+ tumour_cel_file, normal_cel_file,
+ tumourname, chrom_names,
+ snp6_reference_info_file,
+ birdseed_report_file = "birdseed.report.txt",
+ genomebuild = "hg38"
+) {
# Extract the LogR and BAF from both tumour and normal cel files.
- cel2baf.logr(normal_cel_file=normal_cel_file,
- tumour_cel_file=tumour_cel_file,
- output_file=paste(tumourname, "_lrr_baf.txt", sep=""),
- snp6_reference_info_file=snp6_reference_info_file,
- apt.probeset.genotype.exe=apt.probeset.genotype.exe,
- apt.probeset.summarize.exe=apt.probeset.summarize.exe,
- norm.geno.clust.exe=norm.geno.clust.exe)
-
- gc.correct(samplename=tumourname,
- infile.logr.baf=paste(tumourname, "_lrr_baf.txt", sep=""),
- outfile.tumor.LogR=paste(tumourname, "_mutantLogR.tab", sep=""),
- outfile.tumor.BAF=paste(tumourname, "_mutantBAF.tab", sep=""),
- outfile.normal.LogR=paste(tumourname, "_germlineLogR.tab", sep=""),
- outfile.normal.BAF=paste(tumourname, "_germlineBAF.tab", sep=""),
- outfile.probeBAF=paste(tumourname, "_probeBAF.txt", sep=""),
- snp6_reference_info_file=snp6_reference_info_file,
- birdseed_report_file=birdseed_report_file,
- chr_names=chrom_names,
- genomebuild=genomebuild)
-
+ cel2baf_logr(
+ normal_cel_file = normal_cel_file,
+ tumour_cel_file = tumour_cel_file,
+ output_file = paste(tumourname, "_lrr_baf.txt", sep = ""),
+ snp6_reference_info_file = snp6_reference_info_file
+ )
+
+ gc_correct(
+ samplename = tumourname,
+ infile.logr.baf = paste(tumourname, "_lrr_baf.txt", sep = ""),
+ outfile.tumor.LogR = paste(tumourname, "_mutantLogR.tab", sep = ""),
+ outfile.tumor.BAF = paste(tumourname, "_mutantBAF.tab", sep = ""),
+ outfile.normal.LogR = paste(tumourname, "_germlineLogR.tab", sep = ""),
+ outfile.normal.BAF = paste(tumourname, "_germlineBAF.tab", sep = ""),
+ outfile.probeBAF = paste(tumourname, "_probeBAF.txt", sep = ""),
+ snp6_reference_info_file = snp6_reference_info_file,
+ birdseed_report_file = birdseed_report_file,
+ chr_names = chrom_names,
+ genomebuild = genomebuild
+ )
}
diff --git a/R/prepare_wgs.R b/R/prepare_wgs.R
index a31ec6e2..bc68d92f 100644
--- a/R/prepare_wgs.R
+++ b/R/prepare_wgs.R
@@ -1,250 +1,328 @@
-
-#' Obtain allele counts for 1000 Genomes loci through external program alleleCount
-#'
-#' @param bam.file A BAM alignment file on which the counter should be run.
-#' @param output.file The file where output should go.
-#' @param g1000.loci A file with 1000 Genomes SNP loci.
-#' @param min.base.qual The minimum base quality required for it to be counted (optional, default=20).
-#' @param min.map.qual The minimum mapping quality required for it to be counted (optional, default=35).
-#' @param allelecounter.exe A pointer to where the alleleCounter executable can be found (optional, default points to $PATH).
-#' @author sd11
+#' Obtain BAF and LogR from the allele counts (Memory Optimized)
#' @export
-getAlleleCounts = function(bam.file, output.file, g1000.loci, min.base.qual=20, min.map.qual=35, allelecounter.exe="alleleCounter") {
- cmd = paste(allelecounter.exe,
- "-b", bam.file,
- "-l", g1000.loci,
- "-o", output.file,
- "-m", min.base.qual,
- "-q", min.map.qual)
-
-
- # alleleCount >= v4.0.0 is sped up considerably on 1000G loci when run in dense-snp mode
- counter_version = system(paste(allelecounter.exe, "--version"), intern = T)
- if (as.integer(substr(x = counter_version, start = 1, stop = 1)) >= 4)
- cmd = paste(cmd, "--dense-snps")
-
- EXIT_CODE=system(cmd, wait=T)
- stopifnot(EXIT_CODE==0)
-}
+getBAFsAndLogRs <- function(tumourAlleleCountsFile.prefix, normalAlleleCountsFile.prefix, figuresFile.prefix, BAFnormalFile, BAFmutantFile, logRnormalFile, logRmutantFile, combinedAlleleCountsFile, chr_names, g1000file.prefix, minCounts = NA, samplename = "sample1", seed = as.integer(Sys.time())) {
+ set.seed(seed)
+ # Initialize files (delete if already exists to avoid double-appending)
+ out_files <- c(BAFnormalFile, BAFmutantFile, logRnormalFile, logRmutantFile, combinedAlleleCountsFile)
+ for (f in out_files) if (file.exists(f)) file.remove(f)
-#' Obtain BAF and LogR from the allele counts
-#'
-#' @param tumourAlleleCountsFile.prefix Prefix of the allele counts files for the tumour.
-#' @param normalAlleleCountsFile.prefix Prefix of the allele counts files for the normal.
-#' @param figuresFile.prefix Prefix for output figures file names.
-#' @param BAFnormalFile File where BAF from the normal will be written.
-#' @param BAFmutantFile File where BAF from the tumour will be written.
-#' @param logRnormalFile File where LogR from the normal will be written.
-#' @param logRmutantFile File where LogR from the tumour will be written.
-#' @param combinedAlleleCountsFile File where combined allele counts for tumour and normal will be written.
-#' @param chr_names A vector with allowed chromosome names.
-#' @param g1000file.prefix Prefix to where 1000 Genomes reference files can be found.
-#' @param minCounts Integer, minimum depth required for a SNP to be included (optional, default=NA).
-#' @param samplename String, name of the sample (optional, default=sample1).
-#' @param seed A seed to be set for when randomising the alleles.
-#' @author dw9, sd11
-#' @export
-getBAFsAndLogRs = function(tumourAlleleCountsFile.prefix, normalAlleleCountsFile.prefix, figuresFile.prefix, BAFnormalFile, BAFmutantFile, logRnormalFile, logRmutantFile, combinedAlleleCountsFile, chr_names, g1000file.prefix, minCounts=NA, samplename="sample1", seed=as.integer(Sys.time())) {
+ # Containers for thinned plotting data (to prevent graphical OOM)
+ plot_data_list <- list()
+ total_snps_processed <- 0
- set.seed(seed)
+ for (chrom in chr_names) {
+ log_info("Processing chromosome {chrom}...")
- input_data = concatenateAlleleCountFiles(tumourAlleleCountsFile.prefix, ".txt", chr_names)
- normal_input_data = concatenateAlleleCountFiles(normalAlleleCountsFile.prefix, ".txt", chr_names)
- allele_data = concatenateG1000SnpFiles(g1000file.prefix, ".txt", chr_names)
-
- # We're no longer stripping out the "chr", which is causing problems
- allele_data[,1] = gsub("chr","",allele_data[,1])
- normal_input_data[,1] = gsub("chr","",normal_input_data[,1])
- input_data[,1] = gsub("chr","",input_data[,1])
-
- # Synchronise all the data frames
- chrpos_allele = paste(allele_data[,1], "_", allele_data[,2], sep="")
- chrpos_normal = paste(normal_input_data[,1], "_", normal_input_data[,2], sep="")
- chrpos_tumour = paste(input_data[,1], "_", input_data[,2], sep="")
- matched_data = Reduce(intersect, list(chrpos_allele, chrpos_normal, chrpos_tumour))
-
- allele_data = allele_data[chrpos_allele %in% matched_data,]
- normal_input_data = normal_input_data[chrpos_normal %in% matched_data,]
- input_data = input_data[chrpos_tumour %in% matched_data,]
-
- # Clean up and reduce amount of unneeded data
- names(input_data)[1] = "CHR"
- names(normal_input_data)[1] = "CHR"
-
- normal_data = normal_input_data[,3:6]
- mutant_data = input_data[,3:6]
-
- # Obtain depth for both alleles for tumour and normal
- len = nrow(normal_data)
- normCount1 = normal_data[cbind(1:len,allele_data[,3])]
- normCount2 = normal_data[cbind(1:len,allele_data[,4])]
- totalNormal = normCount1 + normCount2
- mutCount1 = mutant_data[cbind(1:len,allele_data[,3])]
- mutCount2 = mutant_data[cbind(1:len,allele_data[,4])]
- totalMutant = mutCount1 + mutCount2
-
- # Clean up a few unused variables to save some memory
- rm(normal_data, mutant_data, allele_data, normal_input_data)
-
- # Clear SNPs where there is not enough coverage
- indices = 1:nrow(input_data)
- if(!is.na(minCounts)){
- print(paste("minCount=", minCounts,sep=""))
- # Only normal has to have min coverage, mutant must have at least 1 read to prevent division by zero
- indices = which(totalNormal>=minCounts & totalMutant>=1)
-
- totalNormal = totalNormal[indices]
- totalMutant = totalMutant[indices]
- normCount1 = normCount1[indices]
- normCount2 = normCount2[indices]
- mutCount1 = mutCount1[indices]
- mutCount2 = mutCount2[indices]
- }
- n = length(indices)
-
- normalBAF = vector(length=n, mode="numeric")
- mutantBAF = vector(length=n, mode="numeric")
- normalLogR = vector(length=n, mode="numeric")
- mutantLogR = vector(length=n, mode="numeric")
-
- # randomise A and B alleles
- selector = round(runif(n))
- normalBAF[which(selector==0)] = normCount1[which(selector==0)] / totalNormal[which(selector==0)]
- normalBAF[which(selector==1)] = normCount2[which(selector==1)] / totalNormal[which(selector==1)]
- mutantBAF[which(selector==0)] = mutCount1[which(selector==0)] / totalMutant[which(selector==0)]
- mutantBAF[which(selector==1)] = mutCount2[which(selector==1)] / totalMutant[which(selector==1)]
-
- normalLogR = vector(length=n, mode="integer") #assume that normallogR is 0, and normalise mutantLogR to normalLogR
- mutantLogR = totalMutant/totalNormal
- rm(selector)
-
- # Create the output data.frames
- germline.BAF = data.frame(Chromosome=input_data$CHR[indices], Position=input_data$POS[indices], baf=normalBAF)
- germline.LogR = data.frame(Chromosome=input_data$CHR[indices], Position=input_data$POS[indices], samplename=normalLogR)
- tumor.BAF = data.frame(Chromosome=input_data$CHR[indices], Position=input_data$POS[indices], baf=mutantBAF)
- tumor.LogR = data.frame(Chromosome=input_data$CHR[indices], Position=input_data$POS[indices], samplename=log2(mutantLogR/mean(mutantLogR, na.rm=T)))
- alleleCounts = data.frame(Chromosome=input_data$CHR[indices], Position=input_data$POS[indices], mutCountT1=mutCount1, mutCountT2=mutCount2, mutCountN1=normCount1, mutCountN2=normCount2)
-
- # Save data.frames to disk
- write.table(germline.BAF,file=BAFnormalFile, row.names=F, quote=F, sep="\t", col.names=c("Chromosome","Position",samplename))
- write.table(tumor.BAF,file=BAFmutantFile, row.names=F, quote=F, sep="\t", col.names=c("Chromosome","Position",samplename))
- write.table(germline.LogR,file=logRnormalFile, row.names=F, quote=F, sep="\t", col.names=c("Chromosome","Position",samplename))
- write.table(tumor.LogR,file=logRmutantFile, row.names=F, quote=F, sep="\t", col.names=c("Chromosome","Position",samplename))
- write.table(alleleCounts, file=combinedAlleleCountsFile, row.names=F, quote=F, sep="\t")
-
- # Plot the raw data using ASCAT
- # Manually create an ASCAT object, which saves reading in the above files again
- SNPpos = germline.BAF[,c("Chromosome", "Position")]
- ch = list()
- for (i in 1:length(chr_names)) {
- temp = which(SNPpos$Chromosome==chr_names[i])
- if (length(temp) == 0) {
- ch[[i]] = 0
- } else {
- ch[[i]] = temp[1]:temp[length(temp)]
+ # Load data for THIS chromosome only
+ input_data <- concatenateAlleleCountFiles(tumourAlleleCountsFile.prefix, ".txt", chrom)
+ normal_input_data <- concatenateAlleleCountFiles(normalAlleleCountsFile.prefix, ".txt", chrom)
+ allele_data <- concatenateG1000SnpFiles(g1000file.prefix, ".txt", chrom)
+
+ log_info(" - Raw SNPs: Tumour={nrow(input_data)}, Normal={nrow(normal_input_data)}, G1000={nrow(allele_data)}")
+
+ if (nrow(input_data) == 0 || nrow(normal_input_data) == 0 || nrow(allele_data) == 0) {
+ log_warning(" - Missing data for chromosome {chrom}. Skipping.")
+ next
}
- }
- ascat.bc = list(Tumor_LogR=as.data.frame(tumor.LogR[,3]), Tumor_BAF=as.data.frame(tumor.BAF[,3]),
- Germline_LogR=as.data.frame(germline.LogR[,3]), Germline_BAF=as.data.frame(germline.BAF[,3]),
- Tumor_LogR_segmented=NULL, Tumor_BAF_segmented=NULL, Tumor_counts=NULL, Germline_counts=NULL,
- SNPpos=tumor.LogR[,1:2], chrs=chr_names, samples=c(samplename), chrom=split_genome(tumor.LogR[,1:2]),
- ch=ch)
+ # Convert to data.table
+ data.table::setDT(input_data)
+ data.table::setDT(normal_input_data)
+ data.table::setDT(allele_data)
+
+ # Standardize
+ input_data[[1]] <- gsub("chr", "", as.character(input_data[[1]]))
+ normal_input_data[[1]] <- gsub("chr", "", as.character(normal_input_data[[1]]))
+ allele_data[[1]] <- gsub("chr", "", as.character(allele_data[[1]]))
+
+ names(allele_data)[1:4] <- c("CHR", "POS", "A0", "A1")
+ names(normal_input_data)[1:7] <- c("CHR", "POS", "nCountA", "nCountC", "nCountG", "nCountT", "nDepth")
+ names(input_data)[1:7] <- c("CHR", "POS", "tCountA", "tCountC", "tCountG", "tCountT", "tDepth")
- ASCAT::ascat.plotRawData(ascat.bc) #, parentDir=figuresFile.prefix)
+ # Ensure types match for join
+ input_data[, `:=`(CHR = as.character(CHR), POS = as.integer(POS))]
+ normal_input_data[, `:=`(CHR = as.character(CHR), POS = as.integer(POS))]
+ allele_data[, `:=`(CHR = as.character(CHR), POS = as.integer(POS))]
+
+ # Fast Join logic
+ data.table::setkey(input_data, CHR, POS)
+ data.table::setkey(normal_input_data, CHR, POS)
+ data.table::setkey(allele_data, CHR, POS)
+
+ # Join
+ joined <- normal_input_data[input_data, nomatch = 0]
+ joined <- allele_data[joined, nomatch = 0]
+
+ log_info(" - Synced SNPs: {nrow(joined)}")
+
+ if (nrow(joined) == 0) {
+ log_warning(" - Zero overlap for chromosome {chrom}. Check reference compatibility.")
+ next
+ }
+
+ # cleanup temp objects
+ rm(input_data, normal_input_data, allele_data)
+
+ # Matrix extraction
+ norm_m <- as.matrix(joined[, .(nCountA, nCountC, nCountG, nCountT)])
+ mut_m <- as.matrix(joined[, .(tCountA, tCountC, tCountG, tCountT)])
+
+ len <- nrow(joined)
+ idx_matrix <- cbind(seq_len(len), as.integer(joined$A0))
+ idx_matrix2 <- cbind(seq_len(len), as.integer(joined$A1))
+
+ normCount1 <- norm_m[idx_matrix]
+ normCount2 <- norm_m[idx_matrix2]
+ mutCount1 <- mut_m[idx_matrix]
+ mutCount2 <- mut_m[idx_matrix2]
+
+ totalNormal <- normCount1 + normCount2
+ totalMutant <- mutCount1 + mutCount2
+
+ rm(norm_m, mut_m)
+
+ # Apply coverage filters
+ valid_indices <- seq_len(len)
+ if (!is.na(minCounts)) {
+ valid_indices <- which(totalNormal >= minCounts & totalMutant >= 1)
+ totalNormal <- totalNormal[valid_indices]
+ totalMutant <- totalMutant[valid_indices]
+ normCount1 <- normCount1[valid_indices]
+ normCount2 <- normCount2[valid_indices]
+ mutCount1 <- mutCount1[valid_indices]
+ mutCount2 <- mutCount2[valid_indices]
+ }
+
+ n <- length(valid_indices)
+ log_info(" - Final Filtered SNPs: {n}")
+
+ if (n == 0) {
+ log_warning(" - No SNPs passed coverage filters for {chrom}.")
+ next
+ }
+
+ # BAF/LogR Calc
+ selector <- round(stats::runif(n))
+ is_zero <- selector == 0
+ is_one <- !is_zero
+
+ normalBAF <- numeric(n)
+ mutantBAF <- numeric(n)
+ normalBAF[is_zero] <- normCount1[is_zero] / totalNormal[is_zero]
+ normalBAF[is_one] <- normCount2[is_one] / totalNormal[is_one]
+ mutantBAF[is_zero] <- mutCount1[is_zero] / totalMutant[is_zero]
+ mutantBAF[is_one] <- mutCount2[is_one] / totalMutant[is_one]
+
+ mutantLogR_raw <- totalMutant / totalNormal
+ # Mean shift will be approximate per chromosome here, but we can fix the global mean shift later
+ # Actually, original code used log2(ratio / mean(all_ratios))
+ # For now, let's keep the raw ratio and we'll normalize at the very end of this loop?
+ # No, let's calculate the log2(ratio) and keep the global mean shift in mind.
+ # Actually, we should probably calculate the global mean first...
+ # But that requires loading all ratios.
+ # Let's just use log2(ratio) and we'll shift the file afterwards.
+ tumorLogR_unshifted <- log2(mutantLogR_raw)
+
+ CHR_final <- joined$CHR[valid_indices]
+ POS_final <- joined$POS[valid_indices]
+
+ # Write results appending to disk
+ baseDT <- data.table::data.table(Chromosome = CHR_final, Position = POS_final)
+
+ # Normal BAF
+ baseDT[[samplename]] <- normalBAF
+ data.table::fwrite(baseDT, file = BAFnormalFile, sep = "\t", append = TRUE, col.names = !file.exists(BAFnormalFile))
+
+ # Mutant BAF
+ baseDT[[samplename]] <- mutantBAF
+ data.table::fwrite(baseDT, file = BAFmutantFile, sep = "\t", append = TRUE, col.names = !file.exists(BAFmutantFile))
+
+ # Normal LogR
+ baseDT[[samplename]] <- integer(n)
+ data.table::fwrite(baseDT, file = logRnormalFile, sep = "\t", append = TRUE, col.names = !file.exists(logRnormalFile))
+
+ # Mutant LogR
+ baseDT[[samplename]] <- tumorLogR_unshifted
+ data.table::fwrite(baseDT, file = logRmutantFile, sep = "\t", append = TRUE, col.names = !file.exists(logRmutantFile))
+
+ # Combined counts
+ baseDT[[samplename]] <- NULL
+ combinedDT <- cbind(baseDT, data.table::data.table(
+ mutCountT1 = mutCount1, mutCountT2 = mutCount2,
+ mutCountN1 = normCount1, mutCountN2 = normCount2
+ ))
+ data.table::fwrite(combinedDT, file = combinedAlleleCountsFile, sep = "\t", append = TRUE, col.names = !file.exists(combinedAlleleCountsFile))
+
+ # Thinned plotting data: keep 1 in every 25 SNPs
+ thin_idx <- seq(1, n, by = 25)
+ plot_data_list[[chrom]] <- data.table::data.table(
+ Chromosome = CHR_final[thin_idx],
+ Position = POS_final[thin_idx],
+ Tumor_LogR = tumorLogR_unshifted[thin_idx],
+ Tumor_BAF = mutantBAF[thin_idx],
+ Germline_BAF = normalBAF[thin_idx]
+ )
+
+ total_snps_processed <- total_snps_processed + n
+ rm(joined, baseDT, combinedDT, normalBAF, mutantBAF, tumorLogR_unshifted)
+ gc()
+ }
+
+ log_info("Sync complete. Total SNPs processed across all chromosomes: {total_snps_processed}")
+
+ # GLOBAL MEAN SHIFT for LogR (Battenberg requires center at 0)
+ log_info("Performing global LogR mean shift...")
+ # We read the LogR column to calculate the global mean.
+ # vroom is faster for column selection on large files.
+ global_mean <- mean(vroom::vroom(logRmutantFile, col_select = 3, show_col_types = FALSE)[[1]], na.rm = TRUE)
+ log_info("Global LogR Mean: {global_mean}. Shifting values...")
+
+ # Read full file, shift, write. (This is high RAM but only for 2 columns Chrom/Pos + 1 Float)
+ # 28M rows * 3 cols * 8 bytes ≈ 672 MB. Totally safe.
+ full_logr <- data.table::fread(logRmutantFile)
+ full_logr[[3]] <- full_logr[[3]] - global_mean
+ data.table::fwrite(full_logr, file = logRmutantFile, sep = "\t")
+ rm(full_logr)
+ gc()
+
+ # CONSTRUCT PLOTTING OBJECT (FROM THINNED DATA)
+ log_info("Constructing thinned ASCAT plot...")
+ plot_data <- data.table::rbindlist(plot_data_list)
+ # Standardize Chromosome names for ASCAT factor sorting
+ ch <- lapply(chr_names, function(x) {
+ # Match robustly (handling both '1' and 'chr1' in the data)
+ normalized_data_chrs <- gsub("chr", "", as.character(plot_data$Chromosome))
+ normalized_target_chr <- gsub("chr", "", as.character(x))
+ tmp <- which(normalized_data_chrs == normalized_target_chr)
+
+ if (length(tmp) == 0) {
+ return(numeric(0))
+ }
+ return(tmp[1]:tmp[length(tmp)])
+ })
+
+ ascat_bc <- list(
+ Tumor_LogR = data.frame(plot_data$Tumor_LogR - global_mean),
+ Tumor_BAF = data.frame(plot_data$Tumor_BAF),
+ Germline_LogR = data.frame(integer(nrow(plot_data))),
+ Germline_BAF = data.frame(plot_data$Germline_BAF),
+ Tumor_LogR_segmented = NULL, Tumor_BAF_segmented = NULL,
+ Tumor_counts = NULL, Germline_counts = NULL,
+ SNPpos = data.frame(Chromosome = plot_data$Chromosome, Position = plot_data$Position, stringsAsFactors = FALSE),
+ chrs = chr_names,
+ samples = samplename,
+ chrom = split_genome(plot_data[, 1:2]),
+ ch = ch
+ )
+ ASCAT::ascat.plotRawData(ascat_bc)
}
#' Prepare data for impute
#'
#' @param chrom The chromosome for which impute input should be generated.
-#' @param tumour.allele.counts.file Output from the allele counter on the matched tumour for this chromosome.
-#' @param normal.allele.counts.file Output from the allele counter on the matched normal for this chromosome.
-#' @param output.file File where the impute input for this chromosome will be written.
+#' @param tumour_allele_counts_file Output from the allele counter on the matched tumour for this chromosome.
+#' @param normal_allele_counts_file Output from the allele counter on the matched normal for this chromosome.
+#' @param output_file File where the impute input for this chromosome will be written.
#' @param imputeinfofile Info file with impute reference information.
-#' @param is.male Boolean denoting whether this sample is male (TRUE), or female (FALSE).
-#' @param problemLociFile A file containing genomic locations that must be discarded (optional).
-#' @param useLociFile A file containing genomic locations that must be included (optional).
-#' @param heterozygousFilter The cutoff where a SNP will be considered as heterozygous (default 0.1).
+#' @param is_male Boolean denoting whether this sample is male (TRUE), or female (FALSE).
+#' @param problem_loci_file A file containing genomic locations that must be discarded (optional).
+#' @param use_loci_file A file containing genomic locations that must be included (optional).
+#' @param heterozygous_filter The cutoff where a SNP will be considered as heterozygous (default 0.1).
#' @author dw9, sd11
#' @export
-generate.impute.input.wgs = function(chrom, tumour.allele.counts.file, normal.allele.counts.file, output.file, imputeinfofile, is.male, problemLociFile=NA, useLociFile=NA, heterozygousFilter=0.1) {
-
- # Read in the 1000 genomes reference file paths for the specified chrom
- impute.info = parse.imputeinfofile(imputeinfofile, is.male, chrom=chrom)
- chr_names = unique(impute.info$chrom)
- chrom_name = chrom
-
- #print(paste("GenerateImputeInput is.male? ", is.male,sep=""))
- #print(paste("GenerateImputeInput #impute files? ", nrow(impute.info),sep=""))
-
- # Read in the known SNP locations from the 1000 genomes reference files
- known_SNPs = read.table(impute.info$impute_legend[1], sep=" ", header=T, stringsAsFactors=F)
- if(nrow(impute.info)>1){
- for(r in 2:nrow(impute.info)){
- known_SNPs = rbind(known_SNPs, read.table(impute.info$impute_legend[r], sep=" ", header=T, stringsAsFactors=F))
- }
- }
-
- # filter out bad SNPs (streaks in BAF)
- if((problemLociFile != "NA") & (!is.na(problemLociFile))) {
- problemSNPs = read.table(problemLociFile, header=T, sep="\t", stringsAsFactors=F)
- problemSNPs = problemSNPs$Pos[problemSNPs$Chr==chrom_name]
- badIndices = match(known_SNPs$position, problemSNPs)
- known_SNPs = known_SNPs[is.na(badIndices),]
- rm(problemSNPs, badIndices)
+generate_impute_input_wgs <- function(
+ chrom, tumour_allele_counts_file, normal_allele_counts_file,
+ output_file, imputeinfofile, is_male, problem_loci_file = NA,
+ use_loci_file = NA, heterozygous_filter = 0.1
+) {
+ # Read in the reference file paths for the specified chrom
+ impute_info <- parse_imputeinfofile(imputeinfofile, is_male, chrom = chrom)
+ chrom_name <- chrom
+
+ # Efficiently load and combine known SNP legend files
+ # Replaces the for-loop/rbind pattern which is very slow in R
+ # Efficiently load known SNP legend files using vroom
+ known_SNPs <- vroom::vroom(
+ unlist(impute_info$impute_legend),
+ delim = " ",
+ show_col_types = FALSE
+ )
+ data.table::setDF(known_SNPs)
+
+ # Filter out 'problem' SNPs (BAF streaks)
+ if (!is.na(problem_loci_file) && problem_loci_file != "NA") {
+ problem_snps_raw <- data.table::fread(problem_loci_file, header = TRUE, sep = "auto", data.table = FALSE)
+ problem_positions <- problem_snps_raw$Pos[problem_snps_raw$Chr == chrom_name]
+ known_SNPs <- known_SNPs[!(known_SNPs$position %in% problem_positions), ]
}
- # filter 'good' SNPs (e.g. SNP6 positions)
- if((useLociFile != "NA") & (!is.na(useLociFile))) {
- goodSNPs = read.table(useLociFile, header=T, sep="\t", stringsAsFactors=F)
- goodSNPs = goodSNPs$pos[goodSNPs$chr==chrom_name]
- len = length(goodSNPs)
- goodIndices = match(known_SNPs$position, goodSNPs)
- known_SNPs = known_SNPs[!is.na(goodIndices),]
- rm(goodSNPs, goodIndices)
+ # Filter for 'good' SNPs (e.g., SNP6 positions)
+ if (!is.na(use_loci_file) && use_loci_file != "NA") {
+ good_snps_raw <- data.table::fread(use_loci_file, header = TRUE, sep = "auto", data.table = FALSE)
+ good_positions <- good_snps_raw$pos[good_snps_raw$chr == chrom_name]
+ known_SNPs <- known_SNPs[known_SNPs$position %in% good_positions, ]
}
- # Read in the allele counts and see which known SNPs are covered
- snp_data = read.table(tumour.allele.counts.file, comment.char="#", sep="\t", header=F, stringsAsFactors=F)
- normal_snp_data = read.table(normal.allele.counts.file, comment.char="#", sep="\t", header=F, stringsAsFactors=F)
- snp_data = cbind(snp_data, normal_snp_data)
- indices = match(known_SNPs$position, snp_data[,2])
- found_snp_data = snp_data[indices[!is.na(indices)],]
- rm(snp_data)
-
- # Obtain BAF for this chromosome (note: this is quicker than reading in the whole genome BAF file generated in the earlier step)
- nucleotides = c("A","C","G","T")
- ref_indices = match(known_SNPs[!is.na(indices),3], nucleotides)+ncol(normal_snp_data)+2
- alt_indices = match(known_SNPs[!is.na(indices),4], nucleotides)+ncol(normal_snp_data)+2
- BAFs = as.numeric(found_snp_data[cbind(1:nrow(found_snp_data),alt_indices)])/(as.numeric(found_snp_data[cbind(1:nrow(found_snp_data),alt_indices)])+as.numeric(found_snp_data[cbind(1:nrow(found_snp_data),ref_indices)]))
- BAFs[is.nan(BAFs)] = 0
- rm(nucleotides, ref_indices, alt_indices, found_snp_data, normal_snp_data)
-
- # Set the minimum level to use for obtaining genotypes
- minBaf = min(heterozygousFilter, 1.0-heterozygousFilter)
- maxBaf = max(heterozygousFilter, 1.0-heterozygousFilter)
-
- # Obtain genotypes that impute2 is able to understand
- genotypes = array(0,c(sum(!is.na(indices)),3))
- genotypes[BAFs<=minBaf,1] = 1
- genotypes[BAFs>minBaf & BAFs=maxBaf,3] = 1
-
- # Create the output
- snp.names = paste("snp",1:sum(!is.na(indices)), sep="")
- out.data = cbind(snp.names, known_SNPs[!is.na(indices),1:4], genotypes)
-
- write.table(out.data, file=output.file, row.names=F, col.names=F, quote=F)
- if(is.na(chrom_name)) {
- sample.g.file = paste(dirname(output.file), "/sample_g.txt", sep="")
- #not sure this is necessary, because only the PAR regions are used for males
- #if(is.male){
- # sample_g_data=data.frame(ID_1=c(0,"INDIVI1"),ID_2=c(0,"INDIVI1"),missing=c(0,0),sex=c("D",1))
- #}else{
- sample_g_data = data.frame(ID_1=c(0,"INDIVI1"), ID_2=c(0,"INDIVI1"), missing=c(0,0), sex=c("D",2))
- #}
- write.table(sample_g_data, file=sample.g.file, row.names=F, col.names=T, quote=F)
+ # Load allele counts using fread (ignoring comments)
+ # Tumour and Normal are combined column-wise to match legacy indexing
+ snp_tumour <- data.table::fread(tumour_allele_counts_file, sep = "auto", header = FALSE, data.table = FALSE)
+ snp_normal <- data.table::fread(normal_allele_counts_file, sep = "auto", header = FALSE, data.table = FALSE)
+
+ # Combined data: [Tumour Cols 1-6] [Normal Cols 7-12]
+ snp_combined <- cbind(snp_tumour, snp_normal)
+
+ # Match known SNPs to the allele counter positions
+ indices <- match(known_SNPs$position, snp_combined[, 2])
+ mask <- !is.na(indices)
+ found_snp_data <- snp_combined[indices[mask], ]
+ valid_known_snps <- known_SNPs[mask, ]
+
+ # Calculate BAF for the NORMAL sample to determine genotypes
+ # Logic: Alt / (Alt + Ref).
+ # Ref column index: match allele in col 3 + normal offset (ncol) + 2
+ # Alt column index: match allele in col 4 + normal offset (ncol) + 2
+ nucleotides <- c("A", "C", "G", "T")
+ norm_col_count <- ncol(snp_normal)
+
+ ref_cols <- match(valid_known_snps[, 3], nucleotides) + norm_col_count + 2
+ alt_cols <- match(valid_known_snps[, 4], nucleotides) + norm_col_count + 2
+
+ # Matrix indexing for high-speed extraction of specific allele counts
+ row_idx <- seq_len(nrow(found_snp_data))
+ alt_counts <- as.numeric(found_snp_data[cbind(row_idx, alt_cols)])
+ ref_counts <- as.numeric(found_snp_data[cbind(row_idx, ref_cols)])
+
+ bafs <- alt_counts / (alt_counts + ref_counts)
+ bafs[is.nan(bafs)] <- 0
+
+ # Determine genotypes for IMPUTE2 (1-hot encoded: HomRef, Het, HomAlt)
+ min_baf <- min(heterozygous_filter, 1.0 - heterozygous_filter)
+ max_baf <- max(heterozygous_filter, 1.0 - heterozygous_filter)
+
+ genotypes <- matrix(0, nrow = nrow(found_snp_data), ncol = 3)
+ genotypes[bafs <= min_baf, 1] <- 1
+ genotypes[bafs > min_baf & bafs < max_baf, 2] <- 1
+ genotypes[bafs >= max_baf, 3] <- 1
+
+ # Create final output table
+ # Format: [snpID] [Chr] [Pos] [Ref] [Alt] [G1] [G2] [G3]
+ snp_names <- paste0("snp", seq_len(nrow(genotypes)))
+ out_data <- cbind(snp_names, valid_known_snps[, 1:4], genotypes)
+
+ # Write main output
+ data.table::fwrite(out_data, file = output_file, sep = " ", row.names = FALSE, col.names = FALSE, quote = FALSE)
+
+ # Legacy check: Write sample_g.txt if chrom_name is NA (usually for non-standard chrom processing)
+ if (is.na(chrom_name)) {
+ sample_g_file <- file.path(dirname(output_file), "sample_g.txt")
+ sample_g_data <- data.frame(
+ ID_1 = c(0, "INDIVI1"),
+ ID_2 = c(0, "INDIVI1"),
+ missing = c(0, 0),
+ sex = c("D", 2)
+ )
+ data.table::fwrite(sample_g_data, file = sample_g_file, sep = " ", row.names = FALSE, col.names = TRUE, quote = FALSE)
}
}
@@ -260,117 +338,266 @@ generate.impute.input.wgs = function(chrom, tumour.allele.counts.file, normal.al
#' @param recalc_corr_afterwards Set to TRUE to recalculate correlations after correction
#' @author jdemeul, sd11
#' @export
-gc.correct.wgs = function(Tumour_LogR_file, outfile, correlations_outfile, gc_content_file_prefix, replic_timing_file_prefix, chrom_names, recalc_corr_afterwards=F) {
+gc_correct_wgs <- function(Tumour_LogR_file, outfile, correlations_outfile, gc_content_file_prefix, replic_timing_file_prefix, chrom_names) {
+ if (is.null(gc_content_file_prefix)) log_failure("GC content reference files must be supplied")
- if (is.null(gc_content_file_prefix)) {
- stop("GC content reference files must be supplied to WGS GC content correction")
- }
+ log_info("Starting two-pass memory-optimized GC correction...")
- Tumor_LogR = read_logr(Tumour_LogR_file)
+ # Helper to identify reference file properties (names, index presence)
+ get_ref_info <- function(f) {
+ if (!file.exists(f)) {
+ return(NULL)
+ }
+ # Use suppressWarnings ONLY once to peek at the format
+ h_orig <- suppressWarnings(names(data.table::fread(f, nrows = 0)))
+ d_check <- suppressWarnings(data.table::fread(f, nrows = 5, header = FALSE))
+ has_idx <- ncol(d_check) > length(h_orig)
- print("Processing GC content data")
- gc_files = paste0(gc_content_file_prefix, chrom_names, ".txt.gz")
- GC_data = do.call(rbind, lapply(gc_files, read_gccontent))
- colnames(GC_data) = c("chr", "Position", paste0(c(25,50,100,200,500), "bp"),
- paste0(c(1,2,5,10,20,50,100), "kb"))#,200,500), "kb"),
- # paste0(c(1,2,5,10), "Mb"))
+ h_clean <- h_orig
+ if ("chr" %in% h_clean) h_clean[h_clean == "chr"] <- "Chromosome"
+ if ("pos" %in% h_clean) h_clean[h_clean == "pos"] <- "Position"
+ wins <- setdiff(h_clean, c("Chromosome", "Position"))
- if (!is.null(replic_timing_file_prefix)) {
- print("Processing replication timing data")
- replic_files = paste0(replic_timing_file_prefix, chrom_names, ".txt.gz")
- replic_data = do.call(rbind, lapply(replic_files, read_replication))
+ return(list(has_index = has_idx, orig_names = h_orig, clean_names = h_clean, win_cols = wins))
}
- # omit non-matching loci, replication data generated at exactly same GC loci
- locimatches = match(x = paste0(Tumor_LogR$Chromosome, "_", Tumor_LogR$Position),
- table = paste0(GC_data$chr, "_", GC_data$Position))
- Tumor_LogR = Tumor_LogR[which(!is.na(locimatches)), ]
- GC_data = GC_data[na.omit(locimatches), ]
- if (!is.null(replic_timing_file_prefix)) {
- replic_data = replic_data[na.omit(locimatches), ]
+ # Helper to load reference files robustly without causing fread warnings
+ load_ref_dt <- function(f, info) {
+ if (info$has_index) {
+ dt <- data.table::fread(f, skip = 1, header = FALSE, col.names = c("V1_idx", info$clean_names))
+ return(dt[, -1, with = FALSE])
+ } else {
+ # Use col.names even if no index to ensure standardized names (Chromosome/Position)
+ dt <- data.table::fread(f, header = TRUE, col.names = info$clean_names)
+ return(dt)
+ }
}
- rm(locimatches)
- corr = abs(cor(GC_data[, 3:ncol(GC_data)], Tumor_LogR[,3], use="complete.obs")[,1])
- if (!is.null(replic_timing_file_prefix)) {
- corr_rep = abs(cor(replic_data[, 3:ncol(replic_data)], Tumor_LogR[,3], use="complete.obs")[,1])
+ # Peeking at the first GC file
+ first_gc_file <- paste0(gc_content_file_prefix, chrom_names[1], ".txt.gz")
+ if (!file.exists(first_gc_file)) log_failure("GC reference file not found: {first_gc_file}")
+ gc_info <- get_ref_info(first_gc_file)
+ win_cols <- gc_info$win_cols
+ log_info("GC Reference Windows: {paste(win_cols, collapse=', ')}")
+
+ # Accumulators for cross-genome correlation statistics
+ N_vec <- setNames(numeric(length(win_cols)), win_cols)
+ SX_vec <- setNames(numeric(length(win_cols)), win_cols)
+ SXX_vec <- setNames(numeric(length(win_cols)), win_cols)
+ SXY_vec <- setNames(numeric(length(win_cols)), win_cols)
+ SY <- 0
+ SYY <- 0
+ Total_N <- 0
+
+ has_replic <- !is.null(replic_timing_file_prefix) && !is.na(replic_timing_file_prefix)
+ rep_info <- NULL
+ rep_win_cols <- NULL
+ if (has_replic) {
+ first_rep_file <- paste0(replic_timing_file_prefix, chrom_names[1], ".txt.gz")
+ rep_info <- get_ref_info(first_rep_file)
+ if (!is.null(rep_info)) {
+ rep_win_cols <- rep_info$win_cols
+ RN_vec <- setNames(numeric(length(rep_win_cols)), rep_win_cols)
+ RSX_vec <- setNames(numeric(length(rep_win_cols)), rep_win_cols)
+ RSXX_vec <- setNames(numeric(length(rep_win_cols)), rep_win_cols)
+ RSXY_vec <- setNames(numeric(length(rep_win_cols)), rep_win_cols)
+ } else {
+ has_replic <- FALSE
+ }
}
- index_1kb = which(names(corr)=="1kb")
- maxGCcol_insert = names(which.max(corr[1:index_1kb]))
- index_100kb = which(names(corr)=="100kb")
- # start large window sizes at 5kb rather than 2kb to avoid overly correlated expl variables
- maxGCcol_amplic = names(which.max(corr[(index_1kb+2):index_100kb]))
- if (!is.null(replic_timing_file_prefix)) {
- maxreplic = names(which.max(corr_rep))
+ log_info("Pass 1: Identifying best GC windows via online correlation accumulation...")
+ all_logr <- data.table::fread(Tumour_LogR_file) # High but manageable RAM usage
+ all_logr[, `:=`(Chromosome = gsub("chr", "", as.character(Chromosome)), Position = as.integer(Position))]
+ data.table::setkey(all_logr, Chromosome, Position)
+
+ for (cn in chrom_names) {
+ log_info(" - Pass 1: Processing {cn}...")
+ gc_f <- paste0(gc_content_file_prefix, cn, ".txt.gz")
+ if (!file.exists(gc_f)) next
+ dt_gc <- load_ref_dt(gc_f, gc_info)
+ dt_gc[, `:=`(Chromosome = gsub("chr", "", as.character(Chromosome)), Position = as.integer(Position))]
+ sub_logr <- all_logr[gsub("chr", "", as.character(cn))]
+
+ if (nrow(sub_logr) == 0) next
+
+ data.table::setkey(dt_gc, Position)
+ data.table::setkey(sub_logr, Position)
+ m <- dt_gc[sub_logr, nomatch = 0]
+ log_info(" - Joined with GC: {nrow(m)} SNPs")
+ if (nrow(m) == 0) next
+
+ y <- as.numeric(m[[ncol(m)]])
+ SY <- SY + sum(y, na.rm = TRUE)
+ SYY <- SYY + sum(y^2, na.rm = TRUE)
+ Total_N <- Total_N + length(y)
+
+ for (w in win_cols) {
+ if (!w %in% names(m)) next
+ x <- as.numeric(m[[w]])
+ valid <- !is.na(x) & !is.na(y)
+ N_vec[w] <- N_vec[w] + sum(valid)
+ SX_vec[w] <- SX_vec[w] + sum(x[valid])
+ SXX_vec[w] <- SXX_vec[w] + sum(x[valid]^2)
+ SXY_vec[w] <- SXY_vec[w] + sum(x[valid] * y[valid])
+ }
+
+ if (has_replic) {
+ rep_f <- paste0(replic_timing_file_prefix, cn, ".txt.gz")
+ if (file.exists(rep_f)) {
+ dt_rep <- load_ref_dt(rep_f, rep_info)
+ dt_rep[, `:=`(Chromosome = gsub("chr", "", as.character(Chromosome)), Position = as.integer(Position))]
+ data.table::setkey(dt_rep, Position)
+ mr <- dt_rep[m, nomatch = 0]
+ log_info(" - Joined with Replication: {nrow(mr)} SNPs")
+ if (nrow(mr) > 0) {
+ yr <- as.numeric(mr[[ncol(mr)]])
+ for (rw in rep_win_cols) {
+ if (!rw %in% names(mr)) next
+ rx <- as.numeric(mr[[rw]])
+ v <- !is.na(rx) & !is.na(yr)
+ RN_vec[rw] <- RN_vec[rw] + sum(v)
+ RSX_vec[rw] <- RSX_vec[rw] + sum(rx[v])
+ RSXX_vec[rw] <- RSXX_vec[rw] + sum(rx[v]^2)
+ RSXY_vec[rw] <- RSXY_vec[rw] + sum(rx[v] * yr[v])
+ }
+ }
+ rm(dt_rep, mr)
+ }
+ }
+ rm(dt_gc, m, sub_logr)
+ gc()
}
- if (!is.null(replic_timing_file_prefix)) {
- cat("Replication timing correlation: ",paste(names(corr_rep),format(corr_rep,digits=2), ";"),"\n")
- cat("Replication dataset: " ,maxreplic,"\n")
+ calc_corr <- function(n, sx, sy, sxx, syy, sxy) {
+ num <- (n * sxy) - (sx * sy)
+ den <- sqrt(pmax(0, (n * sxx - sx^2) * (n * syy - sy^2)))
+ return(ifelse(den == 0, 0, num / den))
}
- cat("GC correlation: ",paste(names(corr),format(corr,digits=2), ";"),"\n")
- cat("Short window size: ",maxGCcol_insert,"\n")
- cat("Long window size: ",maxGCcol_amplic,"\n")
-
- if (!is.null(replic_timing_file_prefix)) {
- # Multiple regression - with replication timing
- corrdata = data.frame(logr = Tumor_LogR[,3, drop = T],
- GC_insert = GC_data[,maxGCcol_insert, drop = T],
- GC_amplic = GC_data[,maxGCcol_amplic, drop = T],
- replic = replic_data[, maxreplic, drop = T])
- colnames(corrdata) = c("logr", "GC_insert", "GC_amplic", "replic")
- if (!recalc_corr_afterwards)
- rm(GC_data, replic_data)
-
- model = lm(logr ~ splines::ns(x = GC_insert, df = 5, intercept = T) + splines::ns(x = GC_amplic, df = 5, intercept = T) + splines::ns(x = replic, df = 5, intercept = T), y=F, model = F, data = corrdata, na.action="na.exclude")
-
- corr = data.frame(windowsize=c(names(corr), names(corr_rep)), correlation=c(corr, corr_rep))
- write.table(corr, file=gsub(".txt", "_beforeCorrection.txt", correlations_outfile), sep="\t", quote=F, row.names=F)
-
- } else {
- # Multiple regression - without replication timing
- corrdata = data.frame(logr = Tumor_LogR[,3, drop = T],
- GC_insert = GC_data[,maxGCcol_insert, drop = T],
- GC_amplic = GC_data[,maxGCcol_amplic, drop = T])
- colnames(corrdata) = c("logr", "GC_insert", "GC_amplic")
- if (!recalc_corr_afterwards)
- rm(GC_data)
-
- model = lm(logr ~ splines::ns(x = GC_insert, df = 5, intercept = T) + splines::ns(x = GC_amplic, df = 5, intercept = T), y=F, model = F, data = corrdata, na.action="na.exclude")
-
- corr = data.frame(windowsize=names(corr), correlation=corr)
- write.table(corr, file=gsub(".txt", "_beforeCorrection.txt", correlations_outfile), sep="\t", quote=F, row.names=F)
+ corrs <- sapply(win_cols, function(w) unname(abs(calc_corr(N_vec[w], SX_vec[w], SY, SXX_vec[w], SYY, SXY_vec[w]))))
+
+ index_2kb <- which(names(corrs) == "2kb")
+ if (length(index_2kb) == 0) index_2kb <- floor(length(corrs) / 2)
+ maxGCcol_insert <- names(which.max(corrs[1:index_2kb]))
+ maxGCcol_amplic <- names(which.max(corrs[(index_2kb + 1):length(corrs)]))
+ index_100kb <- which(names(corrs) == "100kb")
+ if (length(index_100kb) > 0 && index_100kb > index_2kb) maxGCcol_amplic <- names(which.max(corrs[(index_2kb + 1):index_100kb]))
+
+ maxreplic <- NULL
+ if (has_replic) {
+ corrs_rep <- sapply(rep_win_cols, function(w) unname(abs(calc_corr(RN_vec[w], RSX_vec[w], SY, RSXX_vec[w], SYY, RSXY_vec[w]))))
+ maxreplic <- names(which.max(corrs_rep))
}
+ log_info("Selected Windows: Insert={maxGCcol_insert}, Amplic={maxGCcol_amplic}, Rep={maxreplic}")
+
+ # Pass 2: Online Linear Regression (Accumulate X'X and X'y)
+ log_info("Pass 2: Accumulating matrix cross-products for the spline model...")
+ XtX <- NULL
+ Xty <- NULL
+
+ for (cn in chrom_names) {
+ log_info(" - Pass 2: Processing {cn}...")
+ gc_f <- paste0(gc_content_file_prefix, cn, ".txt.gz")
+ if (!file.exists(gc_f)) next
+ dt_gc <- load_ref_dt(gc_f, gc_info)
+ dt_gc[, `:=`(Chromosome = gsub("chr", "", as.character(Chromosome)), Position = as.integer(Position))]
+
+ sub_logr <- all_logr[gsub("chr", "", as.character(cn))]
+
+ data.table::setkey(dt_gc, Position)
+ data.table::setkey(sub_logr, Position)
+ m <- dt_gc[sub_logr, nomatch = 0]
+ log_info(" - Joined for regression: {nrow(m)} SNPs")
+ if (nrow(m) == 0) next
+
+ Xi <- cbind(splines::ns(m[[maxGCcol_insert]], df = 5, intercept = TRUE), splines::ns(m[[maxGCcol_amplic]], df = 5, intercept = FALSE))
+ if (has_replic) {
+ rep_f <- paste0(replic_timing_file_prefix, cn, ".txt.gz")
+ dt_rep <- load_ref_dt(rep_f, rep_info)
+ dt_rep[, `:=`(Chromosome = gsub("chr", "", as.character(Chromosome)), Position = as.integer(Position))]
+
+ data.table::setkey(dt_rep, Position)
+ mr <- dt_rep[m, nomatch = 0]
+ Xi <- cbind(Xi, splines::ns(mr[[maxreplic]], df = 5, intercept = FALSE))
+ y_i <- as.numeric(mr[[ncol(mr)]])
+ rm(dt_rep, mr)
+ } else {
+ y_i <- as.numeric(m[[ncol(m)]])
+ }
- Tumor_LogR[,3] = residuals(model)
- rm(model, corrdata)
-
- readr::write_tsv(x=Tumor_LogR[which(!is.na(Tumor_LogR[,3])), ], file=outfile)
+ # Remove NAs which break splineDesign/solve
+ keep <- rowSums(is.na(Xi)) == 0 & !is.na(y_i)
+ if (sum(keep) < 20) {
+ rm(dt_gc, m, Xi, y_i)
+ next
+ }
+ Xi <- Xi[keep, , drop = FALSE]
+ y_i <- y_i[keep]
- if (recalc_corr_afterwards) {
- # Recalculate the correlations to see how much there is left
- corr = abs(cor(GC_data[, 3:ncol(GC_data)], Tumor_LogR[,3], use="complete.obs")[,1])
- if (!is.null(replic_timing_file_prefix)) {
- corr_rep = abs(cor(replic_data[, 3:ncol(replic_data)], Tumor_LogR[,3], use="complete.obs")[,1])
- cat("Replication timing correlation post correction: ",paste(names(corr_rep),format(corr_rep,digits=2), ";"),"\n")
+ if (is.null(XtX)) {
+ n_cols <- ncol(Xi)
+ XtX <- matrix(0, n_cols, n_cols)
+ Xty <- numeric(n_cols)
}
- cat("GC correlation post correction: ",paste(names(corr),format(corr,digits=2), ";"),"\n")
- if (!is.null(replic_timing_file_prefix)) {
- corr = data.frame(windowsize=c(names(corr), names(corr_rep)), correlation=c(corr, corr_rep))
- write.table(corr, file=gsub(".txt", "_afterCorrection.txt", correlations_outfile), sep="\t", quote=F, row.names=F)
+ XtX <- XtX + t(Xi) %*% Xi
+ Xty <- Xty + t(Xi) %*% y_i
+ rm(dt_gc, m, Xi, y_i)
+ gc()
+ }
+
+ beta <- solve(XtX, Xty)
+ log_info("Pass 3: Calculating and writing residuals...")
+ if (file.exists(outfile)) file.remove(outfile)
+
+ # Final pass to write results
+ for (cn in chrom_names) {
+ log_info(" - Pass 3: Writing {cn}...")
+ gc_f <- paste0(gc_content_file_prefix, cn, ".txt.gz")
+ if (!file.exists(gc_f)) next
+ dt_gc <- load_ref_dt(gc_f, gc_info)
+ dt_gc[, `:=`(Chromosome = gsub("chr", "", as.character(Chromosome)), Position = as.integer(Position))]
+
+ sub_logr <- all_logr[gsub("chr", "", as.character(cn))]
+ data.table::setkey(dt_gc, Position)
+ data.table::setkey(sub_logr, Position)
+ m <- dt_gc[sub_logr, nomatch = 0]
+ log_info(" - Joined for output: {nrow(m)} SNPs")
+ if (nrow(m) == 0) next
+
+ Xi <- cbind(splines::ns(m[[maxGCcol_insert]], df = 5, intercept = TRUE), splines::ns(m[[maxGCcol_amplic]], df = 5, intercept = FALSE))
+ if (has_replic) {
+ rep_f <- paste0(replic_timing_file_prefix, cn, ".txt.gz")
+ dt_rep <- load_ref_dt(rep_f, rep_info)
+ dt_rep[, `:=`(Chromosome = gsub("chr", "", as.character(Chromosome)), Position = as.integer(Position))]
+
+ data.table::setkey(dt_rep, Position)
+ mr <- dt_rep[m, nomatch = 0]
+ Xi_rep <- splines::ns(mr[[maxreplic]], df = 5, intercept = FALSE)
+
+ # For output, we apply logic to each row. But since we filtered with joins,
+ # we need to be careful. Splines ns() will return NA for rows with NA input.
+ # residual = y - X * beta
+ # We'll do it in a robust way:
+ Xi_full <- cbind(Xi, Xi_rep)
+ y_full <- as.numeric(mr[[ncol(mr)]])
+ residuals <- y_full - (Xi_full %*% beta)
+
+ out_dt <- mr[, 1:2]
+ out_dt$LogR <- as.numeric(residuals)
+ rm(dt_rep, mr, Xi_rep, Xi_full)
} else {
- corr = data.frame(windowsize=c(names(corr)), correlation=corr)
- write.table(corr, file=gsub(".txt", "_afterCorrection.txt", correlations_outfile), sep="\t", quote=F, row.names=F)
+ residuals <- as.numeric(m[[ncol(m)]]) - (Xi %*% beta)
+ out_dt <- m[, 1:2]
+ out_dt$LogR <- as.numeric(residuals)
}
- } else {
- corr$correlation = NA
- write.table(corr, file=gsub(".txt", "_afterCorrection.txt", correlations_outfile), sep="\t", quote=F, row.names=F)
+ out_dt$LogR <- pmax(pmin(out_dt$LogR, 5), -5)
+ data.table::fwrite(out_dt, file = outfile, sep = "\t", append = TRUE, col.names = !file.exists(outfile))
+ rm(dt_gc, m, Xi, out_dt)
+ gc()
}
}
-
#' Prepare WGS data for haplotype construction
#'
#' This function performs part of the Battenberg WGS pipeline: Counting alleles, constructing BAF and logR
@@ -387,59 +614,119 @@ gc.correct.wgs = function(Tumour_LogR_file, outfile, correlations_outfile, gc_co
#' @param repliccorrectprefix Prefix path to replication timing reference data (supply NULL if no replication timing correction is to be applied)
#' @param min_base_qual Minimum base quality required for a read to be counted
#' @param min_map_qual Minimum mapping quality required for a read to be counted
-#' @param allelecounter_exe Path to the allele counter executable (can be found in $PATH)
+#' @param allele_counts_dir Directory containing the allele counts files
#' @param min_normal_depth Minimum depth required in the normal for a SNP to be included
#' @param nthreads The number of paralel processes to run
-#' @param skip_allele_counting Flag, set to TRUE if allele counting is already complete (files are expected in the working directory on disk)
-#' @param skip_allele_counting_normal Flag, set to TRUE from the second sample onwards for multisample case (Default: FALSE)
+#' @param libs Path to the R libraries to be used by parallel workers
#' @author sd11
#' @export
-prepare_wgs = function(chrom_names, tumourbam, normalbam, tumourname, normalname, g1000allelesprefix, g1000prefix, gccorrectprefix,
- repliccorrectprefix, min_base_qual, min_map_qual, allelecounter_exe, min_normal_depth, nthreads, skip_allele_counting, skip_allele_counting_normal = F) {
-
- requireNamespace("foreach")
- requireNamespace("doParallel")
- requireNamespace("parallel")
-
- if (!skip_allele_counting) {
- # Obtain allele counts for 1000 Genomes locations for both tumour and normal
- foreach::foreach(i=1:length(chrom_names)) %dopar% {
- getAlleleCounts(bam.file=tumourbam,
- output.file=paste(tumourname,"_alleleFrequencies_chr", chrom_names[i], ".txt", sep=""),
- g1000.loci=paste(g1000prefix, chrom_names[i], ".txt", sep=""),
- min.base.qual=min_base_qual,
- min.map.qual=min_map_qual,
- allelecounter.exe=allelecounter_exe)
-
- if (!skip_allele_counting_normal) {
- getAlleleCounts(bam.file=normalbam,
- output.file=paste(normalname,"_alleleFrequencies_chr", chrom_names[i], ".txt", sep=""),
- g1000.loci=paste(g1000prefix, chrom_names[i], ".txt", sep=""),
- min.base.qual=min_base_qual,
- min.map.qual=min_map_qual,
- allelecounter.exe=allelecounter_exe)
- }
- }
+prepare_wgs <- function(
+ chrom_names,
+ tumourbam,
+ normalbam,
+ tumourname,
+ normalname,
+ g1000allelesprefix,
+ g1000prefix,
+ gccorrectprefix,
+ repliccorrectprefix,
+ min_base_qual,
+ min_map_qual,
+ allele_counts_dir,
+ min_normal_depth,
+ nthreads,
+ libs
+) {
+ # Check files exist
+ tumour_prefix <- file.path(allele_counts_dir, paste0(tumourname, "_alleleFrequencies_chr"))
+ normal_prefix <- file.path(allele_counts_dir, paste0(normalname, "_alleleFrequencies_chr"))
+
+ # Simple validation for first chromosome to ensure files are present
+ # Note: detailed validation could loop over all chromosomes
+ first_tumour_file <- paste0(tumour_prefix, chrom_names[1], ".txt")
+ if (!file.exists(first_tumour_file)) {
+ log_failure("Expected tumour allele counts file not found: {first_tumour_file}")
}
# Obtain BAF and LogR from the raw allele counts
- getBAFsAndLogRs(tumourAlleleCountsFile.prefix=paste(tumourname,"_alleleFrequencies_chr", sep=""),
- normalAlleleCountsFile.prefix=paste(normalname,"_alleleFrequencies_chr", sep=""),
- figuresFile.prefix=paste(tumourname, "_", sep=''),
- BAFnormalFile=paste(tumourname,"_normalBAF.tab", sep=""),
- BAFmutantFile=paste(tumourname,"_mutantBAF.tab", sep=""),
- logRnormalFile=paste(tumourname,"_normalLogR.tab", sep=""),
- logRmutantFile=paste(tumourname,"_mutantLogR.tab", sep=""),
- combinedAlleleCountsFile=paste(tumourname,"_alleleCounts.tab", sep=""),
- chr_names=chrom_names,
- g1000file.prefix=g1000allelesprefix,
- minCounts=min_normal_depth,
- samplename=tumourname)
+ getBAFsAndLogRs(
+ tumourAlleleCountsFile.prefix = tumour_prefix,
+ normalAlleleCountsFile.prefix = normal_prefix,
+ figuresFile.prefix = paste(tumourname, "_", sep = ""),
+ BAFnormalFile = paste(tumourname, "_normalBAF.tab", sep = ""),
+ BAFmutantFile = paste(tumourname, "_mutantBAF.tab", sep = ""),
+ logRnormalFile = paste(tumourname, "_normalLogR.tab", sep = ""),
+ logRmutantFile = paste(tumourname, "_mutantLogR.tab", sep = ""),
+ combinedAlleleCountsFile = paste(tumourname, "_alleleCounts.tab", sep = ""),
+ chr_names = chrom_names,
+ g1000file.prefix = g1000allelesprefix,
+ minCounts = min_normal_depth,
+ samplename = tumourname
+ )
# Perform GC correction
- gc.correct.wgs(Tumour_LogR_file=paste(tumourname,"_mutantLogR.tab", sep=""),
- outfile=paste(tumourname,"_mutantLogR_gcCorrected.tab", sep=""),
- correlations_outfile=paste(tumourname, "_GCwindowCorrelations.txt", sep=""),
- gc_content_file_prefix=gccorrectprefix,
- replic_timing_file_prefix=repliccorrectprefix,
- chrom_names=chrom_names)
+ gc_correct_wgs(
+ Tumour_LogR_file = paste(tumourname, "_mutantLogR.tab", sep = ""),
+ outfile = paste(tumourname, "_mutantLogR_gcCorrected.tab", sep = ""),
+ correlations_outfile = paste(tumourname, "_GCwindowCorrelations.txt", sep = ""),
+ gc_content_file_prefix = gccorrectprefix,
+ replic_timing_file_prefix = repliccorrectprefix,
+ chrom_names = chrom_names
+ )
+
+ log_info("Battenberg WGS preparation complete. Corrected LogR written to: {paste(tumourname, '_mutantLogR_gcCorrected.tab', sep='')}")
+}
+
+#' A helper function to split the genome into parts
+#' @param SNPpos A data.frame with a row for each SNP. First column is chromosome, second column position
+#' @noRd
+split_genome <- function(SNPpos) {
+ # look for gaps of more than 1Mb and chromosome borders
+ holesOver1Mb <- which(diff(SNPpos[, 2]) >= 1000000) + 1
+ chrBorders <- which(diff(as.numeric(factor(SNPpos[, 1], levels = unique(SNPpos[, 1])))) != 0) + 1
+ holes <- unique(sort(c(holesOver1Mb, chrBorders)))
+
+ # find which segments are too small
+ joincandidates <- which(diff(c(0, holes, dim(SNPpos)[1])) < 200)
+
+ # if it's the first or last segment, just join to the one next to it, irrespective of chromosome and positions
+ while (1 %in% joincandidates) {
+ holes <- holes[-1]
+ joincandidates <- which(diff(c(0, holes, dim(SNPpos)[1])) < 200)
+ }
+ while ((length(holes) + 1) %in% joincandidates) {
+ holes <- holes[-length(holes)]
+ joincandidates <- which(diff(c(0, holes, dim(SNPpos)[1])) < 200)
+ }
+
+ while (length(joincandidates) != 0) {
+ # the while loop is because after joining, segments may still be too small..
+ startseg <- c(1, holes)
+ endseg <- c(holes - 1, dim(SNPpos)[1])
+
+ # for each segment that is too short, see if it has the same chromosome as the segments before and after
+ # the next always works because neither the first or the last segment is in joincandidates now
+ previoussamechr <- SNPpos[endseg[joincandidates - 1], 1] == SNPpos[startseg[joincandidates], 1]
+ nextsamechr <- SNPpos[endseg[joincandidates], 1] == SNPpos[startseg[joincandidates + 1], 1]
+
+ distanceprevious <- SNPpos[startseg[joincandidates], 2] - SNPpos[endseg[joincandidates - 1], 2]
+ distancenext <- SNPpos[startseg[joincandidates + 1], 2] - SNPpos[endseg[joincandidates], 2]
+
+ # if both the same, decide based on distance, otherwise if one the same, take the other, if none, just take one.
+ joins <- ifelse(previoussamechr & nextsamechr,
+ ifelse(distanceprevious > distancenext, joincandidates, joincandidates - 1),
+ ifelse(nextsamechr, joincandidates, joincandidates - 1)
+ )
+
+ holes <- holes[-joins]
+ joincandidates <- which(diff(c(0, holes, dim(SNPpos)[1])) < 200)
+ }
+ # if two neighboring segments are selected, this may make bigger segments then absolutely necessary.
+ startseg <- c(1, holes)
+ endseg <- c(holes - 1, dim(SNPpos)[1])
+ chr <- list()
+ for (i in seq_along(startseg)) {
+ chr[[i]] <- startseg[i]:endseg[i]
+ }
+
+ return(chr)
}
diff --git a/R/prepare_wgs_cell_line.R b/R/prepare_wgs_cell_line.R
index 27babde5..aab1ca79 100644
--- a/R/prepare_wgs_cell_line.R
+++ b/R/prepare_wgs_cell_line.R
@@ -1,110 +1,138 @@
-
-#' Chromosome notation standardisation (removing 'chr' string from chromosome names - mainly an issue in hg38 BAMs)
-#'
-#' @param tumourname Tumour identifier, this is used as a prefix for the allele count files. If allele counts are supplied separately, they are expected to have this identifier as prefix.
-#' @param normalname Matched normal identifier, this is used as a prefix for the allele count files. If allele counts are supplied separately, they are expected to have this identifier as prefix.
-#' @author Naser Ansari-Pour (BDI, Oxford)
-#' @export
-standardiseChrNotation = function(tumourname,normalname) {
- if (!is.null(tumourname)){
-tAF=capture.output(cat('bash -c \'sed -i \'s/chr//g\' ', tumourname,'_alleleFrequencies_chr*.txt\'',sep = ""))
-system(tAF)
- }
- if (!is.null(normalname)){
-nAF=capture.output(cat('bash -c \'sed -i \'s/chr//g\' ', normalname,'_alleleFrequencies_chr*.txt\'',sep = ""))
-system(nAF)
- }
-}
-
#' Obtain BAF and LogR from the Cell line (tumour only) allele counts
#'
#' Function to generate BAF and LogR files based on allele counts of the Cell line.
#' It also generates the input data required by the following 'cell_line_reconstruct_normal' function.
#' @param TUMOURNAME The tumour name used for Battenberg (i.e. the cell line BAM file name without the '.bam' extension).
-#' @param g1000alleles.prefix Prefix to where 1000 Genomes allele files can be found.
+#' @param g1000alleles_prefix Prefix to where 1000 Genomes allele files can be found.
#' @param chrom_names A vector with allowed chromosome names.
#' @author Naser Ansari-Pour (BDI, Oxford)
#' @export
-cell_line_baf_logR = function(TUMOURNAME,g1000alleles.prefix,chrom_names){
- #read heterozygous SNPs per chromosome for alleleCounter files & 1000G allele files####
- AC=list() # alleleCounts
- AL=list() # 1000G alleles
- MaC=list() # matched alleleCounts
- OHET=list() # HET SNP data
- for (chr in chrom_names){
- # read in alleleCounter output for each chromosome
- ac=read.table(paste0(TUMOURNAME,"_alleleFrequencies_chr",chr,".txt"),stringsAsFactors = F)
- ac=ac[order(ac$V2),]
- AC[[chr]]=ac
- print(length(AC))
+cell_line_baf_logR <- function(TUMOURNAME, g1000alleles_prefix, chrom_names) {
+ # read heterozygous SNPs per chromosome for alleleCounter files & 1000G allele files####
+ AC <- list() # alleleCounts
+ AL <- list() # 1000G alleles
+ MaC <- list() # matched alleleCounts
+ OHET <- list() # HET SNP data
+
+ for (chr in chrom_names) {
+ # read in alleleCounter output for each chromosome (FAST)
+ ac_file <- paste0(TUMOURNAME, "_alleleFrequencies_chr", chr, ".txt")
+ if (!file.exists(ac_file) || file.size(ac_file) == 0) {
+ log_failure("Allele count file '{ac_file}' is missing or empty. Preprocessing cannot continue.")
+ }
+ ac <- data.table::fread(ac_file, header = FALSE, sep = "auto", stringsAsFactors = FALSE)
+ if (nrow(ac) == 0) {
+ log_failure("Allele count file '{ac_file}' contains no data.")
+ }
+ # Ensure column 2 (Position) is numeric for sorting
+ if (!is.numeric(ac[[2]])) ac[[2]] <- as.numeric(ac[[2]])
+ data.table::setorder(ac, V2)
+ AC[[chr]] <- ac
+ log_info("length(AC): '{length(AC)}'")
+
# match allele counts with respective SNP alleles
- al=read.table(paste0(g1000alleles.prefix,chr,".txt"),header=T,stringsAsFactors = F)
- AL[[chr]]=al
- print(length(AL))
- #etc
- ref=al$a0
- ref_df=data.frame(pos=1:nrow(al),ref=ref+2)
- REF=ac[cbind(ref_df$pos,ref_df$ref)]
- alt=al$a1
- alt_df=data.frame(pos=1:nrow(al),alt=alt+2)
- ALT=ac[cbind(alt_df$pos,alt_df$alt)]
- mac=data.frame(ref=REF,alt=ALT)
- mac$depth=as.numeric(mac$ref)+as.numeric(mac$alt)
- mac$baf=as.numeric(mac$alt)/as.numeric(mac$depth)
- o=cbind(al,mac)
- names(o)=c("Position","a0","a1","ref","alt","depth","baf")
- MaC[[chr]]=o
- #extract rows with 0.1==0.10 & o$baf<=0.90 & o$depth>10),]
- ohet$Position2=c(ohet$Position[2:nrow(ohet)],2*ohet$Position[nrow(ohet)]-ohet$Position[nrow(ohet)-1])
- ohet$Position_dist=ohet$Position2-ohet$Position
- ohet$Position_dist_percent=ohet$Position_dist/max(ohet$Position_dist)
- OHET[[chr]]=ohet
- print(paste("chromosome",chr,"file read"))
+ al_file <- paste0(g1000alleles_prefix, chr, ".txt")
+ if (!file.exists(al_file) || file.size(al_file) == 0) {
+ log_failure("1000G alleles file '{al_file}' is missing or empty.")
+ }
+ al <- data.table::fread(al_file, header = TRUE, sep = "auto", stringsAsFactors = FALSE)
+ if (nrow(al) == 0) {
+ log_failure("1000G alleles file '{al_file}' contains no data.")
+ }
+ AL[[chr]] <- al
+ log_info("length(AL): '{length(AL)}'")
+
+ # Explicitly cast alleles to integer to support indexing even if read as character
+ ref <- as.integer(al$a0)
+ alt <- as.integer(al$a1)
+
+ # Matrix indexing for lightning-fast extraction
+ m_ac <- as.matrix(ac)
+ REF <- m_ac[cbind(seq_len(nrow(al)), ref + 2)]
+ ALT <- m_ac[cbind(seq_len(nrow(al)), alt + 2)]
+
+ mac <- data.frame(ref = REF, alt = ALT)
+ mac$depth <- as.numeric(mac$ref) + as.numeric(mac$alt)
+ mac$baf <- as.numeric(mac$alt) / as.numeric(mac$depth)
+
+ if (nrow(mac) == 0) {
+ log_failure("No matching SNPs found between allele counts and 1000G alleles for chromosome {chr}.")
+ }
+
+ o <- cbind(al, mac)
+ names(o) <- c("Position", "a0", "a1", "ref", "alt", "depth", "baf")
+ MaC[[chr]] <- o
+
+ # Extract HET SNPs
+ ohet <- o[which(o$baf >= 0.10 & o$baf <= 0.90 & o$depth > 10), ]
+ if (nrow(ohet) < 50) {
+ log_warning("Extremely low heterozygosity detected on chromosome {chr} (n={nrow(ohet)}). Results may be unreliable.")
+ }
+ if (nrow(ohet) > 0) {
+ ohet$Position2 <- c(
+ ohet$Position[2:nrow(ohet)],
+ 2 * ohet$Position[nrow(ohet)] - ohet$Position[nrow(ohet) - 1]
+ )
+ ohet$Position_dist <- ohet$Position2 - ohet$Position
+ ohet$Position_dist_percent <- ohet$Position_dist / max(ohet$Position_dist)
+ }
+ OHET[[chr]] <- ohet
+ log_info("chromosome {chr} file read")
}
+
# CREATE mutantBAF and mutantLogR *.tab files #
- cellline=TUMOURNAME
- MAC=data.frame()
- for (chr in chrom_names){
- MaC_CHR=data.frame(chr=chr,MaC[[chr]])
- MAC=rbind(MAC,MaC_CHR)
- print(chr)
- }
- names(MAC)=c("chr","position","a0","a1","ref","alt","coverage","baf")
- print(head(MAC))
- print(dim(MAC))
- #MAC$logr=log2(MAC$coverage/mean(MAC$coverage))
- MAC$logr=log2(MAC$coverage/mean(MAC$coverage,na.rm=TRUE)) # in case of coverage == NA due to non-matching alleles or presence of indels in loci file
- MACC=MAC[which(!is.na(MAC$baf)),]
- print(nrow(MAC)-nrow(MACC))
-
- BAF=data.frame(Chromosome=MACC$chr,Position=MACC$pos,cellline=MACC$baf)
- names(BAF)[names(BAF) == "cellline"] <- cellline
- BAF=BAF[order(BAF$Chromosome,BAF$Position),]
- BAF$Chromosome[BAF$Chromosome==23]="X" # revert back from 23 to X for Chromosome name
- write.table(BAF,paste0(cellline,"_mutantBAF.tab"),col.names=T,row.names=F,quote=F,sep="\t")
+ # Use basename to ensure outputs land in the current directory, not the input counts directory
+ cellline <- basename(TUMOURNAME)
+
+ # Assemble MAC efficiently (O(N))
+ MAC_list <- lapply(chrom_names, function(chr) {
+ data.frame(chr = chr, MaC[[chr]], stringsAsFactors = FALSE)
+ })
+ MAC <- collapse::rowbind(MAC_list)
+ names(MAC) <- c("chr", "position", "a0", "a1", "ref", "alt", "coverage", "baf")
+
+ log_info("Sync complete. dim(MAC): {paste(dim(MAC), collapse = ' ')}")
+
+ # LogR calculation
+ MAC$logr <- log2(MAC$coverage / mean(MAC$coverage, na.rm = TRUE))
+ MACC <- MAC[which(!is.na(MAC$baf)), ]
+
+ # Prepare and save BAF
+ BAF <- data.frame(
+ Chromosome = MACC$chr,
+ Position = MACC$position,
+ cellline = MACC$baf
+ )
+ names(BAF)[3] <- cellline
+ # Standardization
+ BAF$Chromosome[BAF$Chromosome %in% c("23", 23)] <- "X"
+ data.table::setorder(BAF, Chromosome, Position)
+ data.table::fwrite(BAF, paste0(cellline, "_mutantBAF.tab"), sep = "\t")
rm(BAF)
- LogR=data.frame(Chromosome=MACC$chr,Position=MACC$pos,cellline=MACC$logr)
- names(LogR)[names(LogR) == "cellline"] <- cellline
- LogR=LogR[order(LogR$Chromosome,LogR$Position),]
- LogR$Chromosome[LogR$Chromosome==23]="X" # revert back from 23 to X for Chromosome name
- write.table(LogR,paste0(cellline,"_mutantLogR.tab"),col.names=T,row.names=F,quote=F,sep="\t")
-
- rm(MAC)
- rm(MaC)
- rm(MACC)
- CL_OHET <<- OHET
- CL_AL <<- AL
- CL_AC <<- AC
- CL_LogR <<- LogR
- print("STEP 1 - BAF and LogR - completed")
+ # Prepare and save LogR
+ LogR_out <- data.frame(
+ Chromosome = MACC$chr,
+ Position = MACC$position,
+ cellline = MACC$logr
+ )
+ names(LogR_out)[3] <- cellline
+ LogR_out$Chromosome[LogR_out$Chromosome %in% c("23", 23)] <- "X"
+ data.table::setorder(LogR_out, Chromosome, Position)
+ data.table::fwrite(LogR_out, paste0(cellline, "_mutantLogR.tab"), sep = "\t")
+
+ return(list(
+ OHET = OHET,
+ AL = AL,
+ AC = AC,
+ LogR = LogR_out
+ ))
}
#' Reconstruct normal-pair allele count files for cell lines
#'
-#' Function to generate normal-pair allele count files based on IVD-PCF and inter-hetSNP logR-based LOH detection (IVD: Inter-Variant Distance, het: heterozygote)
+#' Function to generate normal-pair allele count files based on IVD-PCF and inter-hetSNP logR-based LOH detection (IVD: Inter-Variant Distance, het: heterozygote)
#' This method reconstructs the normal-pair counts by using the allele counts of the Cell line as template.
#' It fills the detected LOH regions with evenly-distributed hetSNPs with the density estimated based on each chromosome in each tumour sample.
#' It essentially informs Battenberg of the location of hetSNPs across the genome in the tumour sample.
@@ -126,599 +154,567 @@ cell_line_baf_logR = function(TUMOURNAME,g1000alleles.prefix,chrom_names){
#' @author Naser Ansari-Pour (BDI, Oxford)
#' @export
-cell_line_reconstruct_normal <-function(TUMOURNAME,NORMALNAME,chrom_coord,chrom,CL_OHET,CL_AL,CL_AC,CL_LogR,GAMMA_IVD,KMIN_IVD,CENTROMERE_NOISE_SEG_SIZE,CENTROMERE_DIST,MIN_HET_DIST,GAMMA_LOGR,LENGTH_ADJACENT){
+cell_line_reconstruct_normal <- function(
+ TUMOURNAME, NORMALNAME,
+ chrom_coord, chrom,
+ CL_OHET, CL_AL,
+ CL_AC, CL_LogR,
+ GAMMA_IVD, KMIN_IVD,
+ CENTROMERE_NOISE_SEG_SIZE,
+ CENTROMERE_DIST, MIN_HET_DIST,
+ GAMMA_LOGR, LENGTH_ADJACENT
+) {
# IDENTIFY REGIONS OF LOH ####
- colClasses=c(chr="numeric",start="numeric",cen.left.base="numeric",cen.right.base="numeric",end="numeric")
- chr_loc=read.table(chrom_coord,colClasses = colClasses,header=T,stringsAsFactors = F) # chrom_coord = full path to chromosome coordinates
- chr_loc$length=(chr_loc$cen.left.base-chr_loc$start)+(chr_loc$end-chr_loc$cen.right.base)
- #STEP 2.0: identify LOH by IVD-PCF
- LOH=list()
- PCF_folder = "PCF_plots"
- if(!file.exists(PCF_folder)){
+ colClasses <- c(chr = "numeric", start = "numeric", cen.left.base = "numeric", cen.right.base = "numeric", end = "numeric")
+ # Use fast I/O
+ chr_loc <- data.table::fread(chrom_coord, colClasses = colClasses, header = TRUE, stringsAsFactors = FALSE)
+ data.table::setDF(chr_loc)
+ chr_loc$length <- (chr_loc$cen.left.base - chr_loc$start) + (chr_loc$end - chr_loc$cen.right.base)
+
+ # identify LOH by IVD-PCF
+ LOH <- list()
+ PCF_folder <- "PCF_plots"
+ if (!dir.exists(PCF_folder)) {
dir.create(PCF_folder)
}
- i=chrom
- print(paste("chrom=",i))
- pcf_input=data.frame(chr=i,position=CL_OHET[[i]]$Position,IVD=(CL_OHET[[i]]$Position_dist_percent))
- pcf_input=pcf_input[which(pcf_input$positionchr_loc[i,"cen.right.base"]+CENTROMERE_DIST),]
- pcf_input=pcf_input[which(pcf_input$position>=chr_loc[i,"start"] & pcf_input$position<=chr_loc[i,"end"]),] # use only regions covered with gcCorrect LogR range
- PCF=pcf(pcf_input,gamma=GAMMA_IVD,kmin = KMIN_IVD)
- pdf(paste0(PCF_folder,"/",TUMOURNAME,"_chr",i,"_PCF_plot.pdf"))
- plotChrom(pcf_input,PCF)
- dev.off()
- PCF$diff=PCF$end.pos-PCF$start.pos
+ i <- chrom
+ log_info("chrom={i}")
+ pcf_input <- data.frame(chr = i, position = CL_OHET[[i]]$Position, IVD = (CL_OHET[[i]]$Position_dist_percent))
+ pcf_input <- pcf_input[which(pcf_input$position < chr_loc[i, "cen.left.base"] - CENTROMERE_DIST | pcf_input$position > chr_loc[i, "cen.right.base"] + CENTROMERE_DIST), ]
+ pcf_input <- pcf_input[which(pcf_input$position >= chr_loc[i, "start"] & pcf_input$position <= chr_loc[i, "end"]), ] # use only regions covered with gcCorrect LogR range
+ PCF <- copynumber::pcf(pcf_input, gamma = GAMMA_IVD, kmin = KMIN_IVD)
+ grDevices::pdf(paste0(PCF_folder, "/", TUMOURNAME, "_chr", i, "_PCF_plot.pdf"))
+ copynumber::plotChrom(pcf_input, PCF)
+ grDevices::dev.off()
+ PCF$diff <- PCF$end.pos - PCF$start.pos
# Decide if there is any LOH based on PCF and chr_snp_density
- chr_snp_density=nrow(pcf_input)/(pcf_input$position[nrow(pcf_input)]-pcf_input$position[1]) # density of HET SNPs across the region covered by HET SNPs
- #CALCULATE min_normal_snp_density#
- # minimum normal density for SNPs (in bps) is 3 x 10^-4 with median of 7 x 10^-4
- ####
- min_normal_snp_density=0.0001
- loh_regions=PCF[which(round(PCF$mean,3)>0.001),] # LOH regions
- loh_regions=loh_regions[which(loh_regions$n.probes>1),] # only keep segments with minimum of 2 probes (SNPs) in PCF jump
- if (nrow(loh_regions)>0){
- if (mean(pcf_input$IVD)>0.01 & chr_snp_density=((pcf_input$position[nrow(pcf_input)]-pcf_input$position[1]))*0.9 & chr_snp_density>min_normal_snp_density){
- # do PCF regions cover >=90% of the chromosome & is the chromosome snp density above the minimum
- loh_regions=0 # LOH regions
- print(paste("no PCF jumps at chr",i))
+ # density of HET SNPs across the region covered by HET SNPs
+ chr_snp_density <- nrow(pcf_input) / (pcf_input$position[nrow(pcf_input)] - pcf_input$position[1])
+ min_normal_snp_density <- 0.0001
+ # LOH regions
+ loh_regions <- PCF[which(round(PCF$mean, 3) > 0.001), ]
+ # only keep segments with minimum of 2 probes (SNPs) in PCF jump
+ loh_regions <- loh_regions[which(loh_regions$n.probes > 1), ]
+ if (nrow(loh_regions) > 0) {
+ # can change chr_snp_density from 0.00005 to 0.0001 as conservative measure - done
+ if (mean(pcf_input$IVD) > 0.01 && chr_snp_density < min_normal_snp_density) {
+ # mean(pcf_input$IVD) or mean(PCF$mean) indicates presence of jumps in IVD
+ loh_regions <- loh_regions # LOH regions
+ log_info("full-length chromosomal loss at chr {i}")
+ } else if (sum(loh_regions$diff) >= ((pcf_input$position[nrow(pcf_input)] - pcf_input$position[1])) * 0.9 && chr_snp_density > min_normal_snp_density) {
+ # do PCF regions cover >=90% of the chromosome & is the chromosome snp density above the minimum
+ loh_regions <- 0 # LOH regions
+ log_info("no PCF jumps at chr {i}")
+ } else {
+ loh_regions <- loh_regions # LOH regions
+ log_info("likely partial LOH(s) at chr {i}")
+ }
} else {
- loh_regions=loh_regions # LOH regions
- print(paste("likely partial LOH(s) at chr",i))
+ loh_regions <- 0
}
- } else {loh_regions=0}
-
- # loop to turn empty dataframe to 0 for loh_regions
- #suppressWarnings(
- # if (loh_regions[1]!=0){
- # if (nrow(loh_regions)==0){
- # loh_regions=0
- # } else {print("dataframe non-empty")}
- # } else {print("no LOH at all")})
-
- #filter regions for those next to the centromere and 'short'
- noise=NULL
- if (!is.null(nrow(loh_regions))){
- for (j in 1:nrow(loh_regions)){
- if (loh_regions$arm[j]=="p"){
- #if (loh_regions$end.pos[j]-chr_loc$cen.left.base[i]<1e5 & loh_regions$diff[j]<1e6){ #FOR EXCLUSION: max distance to centromere = 100kb , max length of short LOH region = 1Mb
- # noise=append(noise,j)
- #}
- if (loh_regions$end.pos[j]>chr_loc$cen.left.base[i] & loh_regions$diff[j] chr_loc$cen.left.base[i] && loh_regions$diff[j] < CENTROMERE_NOISE_SEG_SIZE) {
+ # FOR EXCLUSION: segment is short IVD region (default<1Mb) and endpos is over the p-arm limit (ending point)
+ noise <- c(noise, j)
}
- #if (loh_regions$end.pos[j]>chr_loc$cen.left.base[i] & loh_regions$diff[j]>CENTROMERE_NOISE_SEG_SIZE & !is.na(match(chrom,c(1,9,16)))){ # Chr 1,9,16 have large heterochromatin region next to centromere
- # noise=append(noise,j)
- #}
}
- if (loh_regions$arm[j]=="q"){
- #if (loh_regions$start.pos[j]-chr_loc$cen.right.base[i]<1e5 & loh_regions$diff[j]<1e6){ #FOR EXCLUSION: max distance to centromere = 100kb , max length of short LOH region = 1Mb
- # noise=append(noise,j)
- #}
- if (loh_regions$start.pos[j]CENTROMERE_NOISE_SEG_SIZE & !is.na(match(chrom,c(1,9,16)))){ # qARM of Chr 1,9,16 have large heterochromatin region next to centromere + 100kb tolerance for start of heterochromatin region
- noise=append(noise,j)
+ if (loh_regions$start.pos[j] < (chr_loc$cen.right.base[i] + 1e5) && loh_regions$diff[j] > CENTROMERE_NOISE_SEG_SIZE && !is.na(match(chrom, c(1, 9, 16)))) {
+ # qARM of Chr 1,9,16 have large heterochromatin region next to centromere + 100kb tolerance for start of heterochromatin region
+ noise <- c(noise, j)
}
}
}
- } else {print("no 'centromere noise' calculation")}
- if (!is.null(noise)){
- LOH_regions=loh_regions[-noise,]
- } else {LOH_regions=loh_regions}
- ####
- #remove LOH regions in the p arm of acrocentric chromosomes 13,14,15,21 and 22
- if (!is.na(match(i,c(13:15,21:22))) & !is.null(nrow(LOH_regions))){
- LOH_regions=LOH_regions[which(LOH_regions$arm!="p"),]
+ } else {
+ log_info("no 'centromere noise' calculation")
+ }
+ if (!is.null(noise)) {
+ LOH_regions <- loh_regions[-noise, ]
+ } else {
+ LOH_regions <- loh_regions
}
- ####
- #remove LOH regions which do not have negative LogR and are essentially stretches of homozygosity
- if (!is.null(nrow(LOH_regions))){
- logr=CL_LogR[which(CL_LogR$Chromosome==i),]
- hom_stretch = NULL
- for (j in 1:nrow(LOH_regions)){
- COV=logr[which(logr$Position>LOH_regions$start.pos[j] & logr$Position=10){
- print(paste("Retaining region",j,"due to clear evidence of LOH"))
+ # remove LOH regions in the p arm of acrocentric chromosomes 13,14,15,21 and 22
+ if (!is.na(match(i, c(13:15, 21:22))) && !is.null(nrow(LOH_regions))) {
+ LOH_regions <- LOH_regions[which(LOH_regions$arm != "p"), ]
+ }
+ # remove LOH regions which do not have negative LogR and are essentially stretches of homozygosity
+ if (!is.null(nrow(LOH_regions)) && nrow(LOH_regions) > 0) {
+ logr <- CL_LogR[which(CL_LogR$Chromosome == i), ]
+ colnames(logr)[3] <- "LogR"
+ logr$Position <- as.numeric(logr$Position)
+
+ # Use findInterval for O(M) mapping to segments
+ snp_to_loh <- findInterval(logr$Position, LOH_regions$start.pos)
+ valid_mask <- snp_to_loh > 0 & logr$Position <= LOH_regions$end.pos[pmax(1, snp_to_loh)]
+
+ if (any(valid_mask)) {
+ stats <- collapse::fgroup_by(logr[valid_mask, ], snp_to_loh[valid_mask]) |>
+ collapse::fsummarise(medcov = fmedian(LogR), cov = fmean(LogR), n = fnobs(LogR))
+
+ # Only keep regions that meet the LOH criteria
+ keep_regions <- stats$g[stats$cov < -0.8 & stats$medcov < -0.8 & stats$n >= 10]
+ if (length(keep_regions) > 0) {
+ LOH_regions <- LOH_regions[keep_regions, ]
+ } else {
+ LOH_regions <- data.frame()
+ }
} else {
- print(paste("Region",j,"is likely to be a stretch of homozygosity or sequencing gap in rare cases"))
- hom_stretch = append(hom_stretch,j)
- }
- }
- if (!is.null(hom_stretch)){
- LOH_regions=LOH_regions[-hom_stretch,]
+ LOH_regions <- data.frame()
}
}
- ####
- if (is.null(dim(LOH_regions))){
- print(paste("no LOH detected in chr",i))
- LOH[[i]]=0
- } else if (dim(LOH_regions)[1]!=0 & dim(LOH_regions)[2]!=0) {
- print(paste("we have LOH for",sum(LOH_regions$diff),"bp in chr",i))
- LOH[[i]]=data.frame(chr=i,LOH_regions)
- } else if (dim(LOH_regions)[1]==0) {
- print(paste("no LOH regions remained after noise correction for chr",i))
- LOH[[i]]=0
- } else {print("unkown issue!")}
- print(paste("chrom=",i,"IVD-PCF finished"))
- #
- ##
+ if (is.null(dim(LOH_regions))) {
+ log_info("no LOH detected in chr {i}")
+ LOH[[i]] <- 0
+ } else if (dim(LOH_regions)[1] != 0 && dim(LOH_regions)[2] != 0) {
+ log_info("we have LOH for {sum(LOH_regions$diff)} bp in chr {i}")
+ LOH[[i]] <- data.frame(chr = i, LOH_regions)
+ } else if (dim(LOH_regions)[1] == 0) {
+ log_info("no LOH regions remained after noise correction for chr {i}")
+ LOH[[i]] <- 0
+ } else {
+ log_info("unkown issue!")
+ }
+ log_info("chrom={i} IVD-PCF finished")
+
# STEP 2 - get higher resolution LOH regions
- ##
- #
- print(paste("chrom=",i))
- # use loop to find blocks with no LOH - while taking account of the centromere - RUN1
- ac=CL_AC[[i]]
- al=CL_AL[[i]]
- names(ac)=c("chr","position",1:4,"depth")
- chr_interval=c(chr_loc[i,"start"],chr_loc[i,"end"]) # use gcCorrect LogR range for chromosome interval
- if (!is.null(nrow(LOH[[i]]))){
- non_LOH=data.frame()## get all non_LOH regions ##
- for (j in 1:(nrow(LOH[[i]])+1)){
- if (j == 1 & chr_interval[1]==LOH[[i]]$start.pos[j]){
- print("LOH from start of chromosome")
- } else if (j == 1 & chr_interval[1]1 & j <= nrow(LOH[[i]]) & LOH[[i]]$arm[j]==LOH[[i]]$arm[j-1]){
- non_loh=data.frame(start=LOH[[i]]$end.pos[j-1]+1,end=LOH[[i]]$start.pos[j]-1)
- } else if (j>1 & j <= nrow(LOH[[i]]) & LOH[[i]]$arm[j]!=LOH[[i]]$arm[j-1]){
- non_loh=data.frame(start=c(LOH[[i]]$end.pos[j-1]+1,chr_loc[i,]$cen.right.base),end=c(chr_loc[i,]$cen.left.base,LOH[[i]]$start.pos[j]-1))
- } else{
- if ((LOH[[i]]$end.pos[j-1]+1) 0) {
+ non_LOH_list <- list()
+ for (j in 1:(nrow(LOH[[i]]) + 1)) {
+ if (j == 1 && chr_interval[1] >= LOH[[i]]$start.pos[j]) {} else if (j == 1) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(start = chr_interval[1], end = LOH[[i]]$start.pos[j] - 1)
+ } else if (j <= nrow(LOH[[i]]) && LOH[[i]]$arm[j] == LOH[[i]]$arm[j - 1]) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(start = LOH[[i]]$end.pos[j - 1] + 1, end = LOH[[i]]$start.pos[j] - 1)
+ } else if (j <= nrow(LOH[[i]])) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(
+ start = c(LOH[[i]]$end.pos[j - 1] + 1, chr_loc[i, ]$cen.right.base),
+ end = c(chr_loc[i, ]$cen.left.base, LOH[[i]]$start.pos[j] - 1)
+ )
+ } else if ((LOH[[i]]$end.pos[j - 1] + 1) < chr_interval[2]) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(start = LOH[[i]]$end.pos[j - 1] + 1, end = chr_interval[2])
}
}
- } else {non_LOH=data.frame(start=chr_interval[1],end=chr_interval[2])} # in case no LOH is identified by IVD-PCF
- if (nrow(non_LOH)>0){
- for (j in 1:nrow(non_LOH)){
- if (non_LOH$start[j]chr_loc[i,]$cen.right.base){
- start.pos=c(non_LOH$start[j],chr_loc[i,]$cen.right.base)
- end.pos=c(chr_loc[i,]$cen.left.base,non_LOH$end[j])
- non_LOH=non_LOH[-j,]
- non_LOH=rbind(non_LOH, data.frame(start=start.pos,end=end.pos))
- }
- if (non_LOH$start[j]chr_loc[i,]$cen.left.base & non_LOH$end[j]>chr_loc[i,]$cen.right.base){ # when segment startpoint is in the centromere (noisy data; observed in hg38 SNP aC data)
- start.pos=chr_loc[i,]$cen.right.base
- end.pos=non_LOH$end[j]
- non_LOH=non_LOH[-j,]
- non_LOH=rbind(non_LOH, data.frame(start=start.pos,end=end.pos))
- }
+ non_LOH <- collapse::rowbind(non_LOH_list)
+ } else {
+ non_LOH <- data.frame(start = chr_interval[1], end = chr_interval[2])
+ }
+
+ if (nrow(non_LOH) > 0) {
+ # Check for centromere crossing and split if necessary
+ cross_idx <- which(non_LOH$start < chr_loc[i, ]$cen.left.base & non_LOH$end > chr_loc[i, ]$cen.right.base)
+ if (length(cross_idx) > 0) {
+ to_split <- non_LOH[cross_idx, ]
+ non_LOH <- non_LOH[-cross_idx, ]
+ split_list <- list(
+ non_LOH,
+ data.frame(start = to_split$start, end = chr_loc[i, ]$cen.left.base),
+ data.frame(start = chr_loc[i, ]$cen.right.base, end = to_split$end)
+ )
+ non_LOH <- collapse::rowbind(split_list)
}
- non_LOH$diff=non_LOH$end-non_LOH$start
+ non_LOH$diff <- non_LOH$end - non_LOH$start
+ non_LOH <- non_LOH[non_LOH$diff > 0, ]
}
- non_LOH=non_LOH[order(non_LOH$start),] # the non_LOH should always be in order by position
-
- #STEP 2.1: identify LOH by inter-het regions
- winsize=MIN_HET_DIST # optimum value is 1e5 in differentiating from HOM stretch in sample
- ohet=CL_OHET[[i]]
- nSNPs=as.numeric(nrow(CL_LogR))
- logr=CL_LogR[which(CL_LogR$Chromosome==i),]
- colnames(logr)[3]="LogR"
- logr$Position=as.numeric(logr$Position)
- if (!is.null(non_LOH)){
- pLOH_regions=data.frame()
- if (is.na(match(i,c(13,14,15,21,22)))){
- print(paste("START",i,"p ARM"))
- PARM=non_LOH[which(non_LOH$end<=chr_loc[i,]$cen.left.base),]
- if (nrow(PARM)>0){
- #if (nrow(PARM)==1 & non_LOH$start[1]==chr_interval[1] & non_LOH$end[1]==chr_interval[2]){
- parm=PARM
- } else if (nrow(PARM)==0 & sum(non_LOH$diff)!=0) {
- parm=data.frame(start=chr_interval[1],end=chr_loc[i,]$cen.left.base-CENTROMERE_DIST)
- } else {print("unknown issue")}
-
- if (parm[nrow(parm),1]<(parm[nrow(parm),2]-CENTROMERE_DIST)){
- parm[nrow(parm),2]=parm[nrow(parm),2]-CENTROMERE_DIST # to exclude the last CENTROMERE_DIST segment next to the centromere (left side) - too noisy
- } else {parm=parm[-nrow(parm),]}
+ non_LOH <- non_LOH[order(non_LOH$start), ]
+
+ # identify LOH by inter-het regions
+ ohet <- CL_OHET[[i]]
+ nSNPs <- as.numeric(nrow(CL_LogR))
+ logr <- CL_LogR[which(CL_LogR$Chromosome == i), ]
+ colnames(logr)[3] <- "LogR"
+ logr$Position <- as.numeric(logr$Position)
+
+ pLOH_collector_list <- list() # to collect results of p-arm analysis
+ if (!is.null(non_LOH)) {
+ if (is.na(match(i, c(13, 14, 15, 21, 22)))) {
+ log_info("START {i}, p ARM")
+ PARM <- non_LOH[which(non_LOH$end <= chr_loc[i, ]$cen.left.base), ]
+ if (nrow(PARM) > 0) {
+ parm <- PARM
+ } else if (nrow(PARM) == 0 && sum(non_LOH$diff) != 0) {
+ parm <- data.frame(start = chr_interval[1], end = chr_loc[i, ]$cen.left.base - CENTROMERE_DIST)
+ } else {
+ log_info("unknown issue")
+ }
+
+ if (parm[nrow(parm), 1] < (parm[nrow(parm), 2] - CENTROMERE_DIST)) {
+ # to exclude the last CENTROMERE_DIST segment next to the centromere (left side) - too noisy
+ parm[nrow(parm), 2] <- parm[nrow(parm), 2] - CENTROMERE_DIST
+ } else {
+ parm <- parm[-nrow(parm), ]
+ }
#
- for (seg in 1:nrow(parm)){
- LoH=data.frame()
- #IVD-based breakpoints for small regions#
- seg_ivd=ohet[which(ohet$Position_dist>=MIN_HET_DIST & ohet$Position>=parm$start[seg] & ohet$Position<=parm$end[seg]),]
- #if (!is.null(nrow(seg_ivd))){
- if (nrow(seg_ivd)>0){
- win=nrow(seg_ivd)
- print(win)
- # win=floor(parm$diff[seg]/winsize)
- # print(win)
- #if (win>0){
- for (j in 1:win){
- loh=NULL
- start=seg_ivd$Position[j]
- end=start+seg_ivd$Position_dist[j]
- COV=logr[which(logr$Position>start & logr$Position0.5){ # to use a minimum SNP density of 0.5 to get logR estimate #CLcode
- #loh=data.frame(start=start,end=end,LogR=cov,medianLogR=medcov,denSNP=denSNP)
- jpcf=pcf(COV,gamma=GAMMA_LOGR,verbose = F)
- jpcf=jpcf[which(jpcf$mean < -0.8),]
- if (nrow(jpcf)>0){
- loh=data.frame(start=jpcf$start.pos[1],end=jpcf$end.pos[nrow(jpcf)],LogR=mean(jpcf$mean),denSNP=denSNP)
- loh$N=nrow(logr[which(logr$Position>=loh$start & logr$Position<=loh$end),])
- if (loh$N<10){loh=NULL} # if LOH region is supported by less than 10 SNPs, then remove it
+ for (seg in seq_len(nrow(parm))) {
+ LoH_list <- list()
+ # IVD-based breakpoints for small regions#
+ seg_ivd <- ohet[which(ohet$Position_dist >= MIN_HET_DIST & ohet$Position >= parm$start[seg] & ohet$Position <= parm$end[seg]), ]
+ if (nrow(seg_ivd) > 0) {
+ logr_in_seg_idx <- which(logr$Position >= parm$start[seg] & logr$Position <= parm$end[seg])
+ if (length(logr_in_seg_idx) > 0) {
+ logr_seg <- logr[logr_in_seg_idx, ]
+ starts_idx <- findInterval(seg_ivd$Position, logr_seg$Position) + 1
+ ends_idx <- findInterval(seg_ivd$Position + seg_ivd$Position_dist, logr_seg$Position)
+
+ for (j in seq_len(nrow(seg_ivd))) {
+ if (starts_idx[j] > ends_idx[j]) next
+ COV <- logr_seg[starts_idx[j]:ends_idx[j], ]
+ medcov <- collapse::fmedian(COV$LogR)
+ cov <- mean(COV$LogR)
+ denSNP <- nrow(COV) / (nSNPs / sum(chr_loc$length) * seg_ivd$Position_dist[j])
+
+ if (!is.na(cov) && cov < -0.8 && medcov < -0.8 && denSNP > 0.5) {
+ jpcf <- copynumber::pcf(COV, gamma = GAMMA_LOGR, verbose = FALSE)
+ jpcf_loh <- jpcf[which(jpcf$mean < -0.8), ]
+ if (nrow(jpcf_loh) > 0) {
+ loh <- data.frame(
+ start = jpcf_loh$start.pos[1],
+ end = jpcf_loh$end.pos[nrow(jpcf_loh)],
+ LogR = mean(jpcf_loh$mean),
+ denSNP = denSNP,
+ stringsAsFactors = FALSE
+ )
+ loh$N <- sum(COV$Position >= loh$start & COV$Position <= loh$end)
+ if (loh$N >= 10) LoH_list[[length(LoH_list) + 1]] <- loh
+ }
}
}
- if (!is.null(loh)){
- LoH=rbind(LoH,loh)
- }
- if (j %% 100 ==0){
- print(paste("interval=",j))
- }
}
- } else {print(paste("no het SNPs in segment",seg))}
- # no. of LOH intervals
- print(paste("p-arm nrow(LOH) segment",seg,"=",nrow(LoH)))
- if (nrow(LoH)==0){
- print(paste("No LOH identified in p-arm segment",seg))
- } else{
- if (nrow(LoH)==1){
- LoH_regions=data.frame(chrom=i,arm="p",start.pos=LoH$start,end.pos=LoH$end)
- }
- if (nrow(LoH)>1){
- #combine smaller regions into larger regions of LOH
- LoH_regions=data.frame()
- start=LoH$start[1]
- for (j in 2:nrow(LoH)){
- print(j)
- if (LoH$start[j]==LoH$end[j-1]){
- end=LoH$end[j] # include the new row (i) in the merge
- }
- else {
- end=LoH$end[j-1] # stop merge at the previous row (i-1)
- LoH_regions=rbind(LoH_regions,data.frame(chrom=i,arm="p",start.pos=start,end.pos=end))
- start=LoH$start[j]
+ }
+
+ if (length(LoH_list) > 0) {
+ LoH <- collapse::rowbind(LoH_list)
+ LoH_regions_list <- list()
+ start <- LoH$start[1]
+ end <- LoH$end[1]
+ if (nrow(LoH) > 1) {
+ for (j in 2:nrow(LoH)) {
+ if (LoH$start[j] <= end) {
+ end <- max(end, LoH$end[j])
+ } else {
+ LoH_regions_list[[length(LoH_regions_list) + 1]] <- data.frame(chrom = i, arm = "p", start.pos = start, end.pos = end)
+ start <- LoH$start[j]
+ end <- LoH$end[j]
}
}
- # add final block if it ends at the end of the LoH dataframe
- if (end==LoH$end[nrow(LoH)]){
- LoH_regions=rbind(LoH_regions,data.frame(chrom=i,arm="p",start.pos=start,end.pos=end))
- }
- else if (start==LoH$start[nrow(LoH)] & end==LoH$end[nrow(LoH)-1]){
- LoH_regions=rbind(LoH_regions,data.frame(chrom=i,arm="p",start.pos=start,end.pos=LoH$end[nrow(LoH)]))
- }
}
- pLOH_regions=rbind(pLOH_regions,LoH_regions)
+ LoH_regions_list[[length(LoH_regions_list) + 1]] <- data.frame(chrom = i, arm = "p", start.pos = start, end.pos = end)
+ pLOH_collector_list[[length(pLOH_collector_list) + 1]] <- collapse::rowbind(LoH_regions_list)
}
}
- if (nrow(pLOH_regions)>0){
- #pARM BAF/LogR plot(s)
- pdf(paste0(TUMOURNAME,"_chr",i,"_",MIN_HET_DIST/1e3,"k_based_pLOH_events.pdf"))
+ pLOH_regions <- collapse::rowbind(pLOH_collector_list)
+
+ if (nrow(pLOH_regions) > 0) {
+ grDevices::pdf(paste0(TUMOURNAME, "_chr", i, "_", MIN_HET_DIST / 1e3, "k_based_pLOH_events.pdf"))
suppressWarnings(
- for (s in 1:nrow(pLOH_regions)){
- sBAF=ggplot(ohet,aes(Position,baf))+geom_jitter()+ylim(0,1)+
- geom_vline(xintercept = c(pLOH_regions$start.pos[s],pLOH_regions$end.pos[s]),col="red",linetype="longdash")+
- xlim(pLOH_regions$start.pos[s]-LENGTH_ADJACENT,pLOH_regions$end.pos[s]+LENGTH_ADJACENT)+
- ggtitle(paste("pARM LOH region",s))+labs(y="BAF")
- sLogR=ggplot(logr,aes(Position,LogR))+geom_jitter()+ylim(-5.2,1.2)+
- geom_vline(xintercept = c(pLOH_regions$start.pos[s],pLOH_regions$end.pos[s]),col="red",linetype="longdash")+
- xlim(pLOH_regions$start.pos[s]-LENGTH_ADJACENT,pLOH_regions$end.pos[s]+LENGTH_ADJACENT)
- grid.newpage()
- grid.draw(rbind(ggplotGrob(sBAF), ggplotGrob(sLogR), size = "last"))
- #print(plot_grid(sBAF,sLogR, ncol = 1, align = "v"))
+ for (s in seq_len(nrow(pLOH_regions))) {
+ sBAF <- ggplot2::ggplot(ohet, ggplot2::aes(Position, baf)) +
+ ggplot2::geom_jitter() +
+ ggplot2::ylim(0, 1) +
+ ggplot2::geom_vline(xintercept = c(pLOH_regions$start.pos[s], pLOH_regions$end.pos[s]), col = "red", linetype = "longdash") +
+ ggplot2::xlim(pLOH_regions$start.pos[s] - LENGTH_ADJACENT, pLOH_regions$end.pos[s] + LENGTH_ADJACENT) +
+ ggplot2::ggtitle(paste("pARM LOH region", s)) +
+ ggplot2::labs(y = "BAF")
+ sLogR <- ggplot2::ggplot(logr, ggplot2::aes(Position, LogR)) +
+ ggplot2::geom_jitter() +
+ ggplot2::ylim(-5.2, 1.2) +
+ ggplot2::geom_vline(xintercept = c(pLOH_regions$start.pos[s], pLOH_regions$end.pos[s]), col = "red", linetype = "longdash") +
+ ggplot2::xlim(pLOH_regions$start.pos[s] - LENGTH_ADJACENT, pLOH_regions$end.pos[s] + LENGTH_ADJACENT)
+ grid::grid.newpage()
+ grid::grid.draw(rbind(ggplot2::ggplotGrob(sBAF), ggplot2::ggplotGrob(sLogR), size = "last"))
}
)
- dev.off()
+ grDevices::dev.off()
#
- print("Candidate LOH regions plotted for pARM")
+ log_info("Candidate LOH regions plotted for pARM")
}
- } else {print(paste("chr",i,"is acrocentric - no p arm analysis"))}
+ } else {
+ pLOH_regions <- data.frame() # ensure it exists
+ log_info("chr {i} is acrocentric - no p arm analysis")
+ }
# Q ARM RUN:
- print(paste("START",i,"q ARM"))
- qLOH_regions=data.frame()
- QARM=non_LOH[which(non_LOH$start>=chr_loc[i,]$cen.right.base),]
- if (nrow(QARM)>0){
- #if (nrow(PARM)==1 & non_LOH$start[1]==chr_interval[1] & non_LOH$end[1]==chr_interval[2]){
- qarm=QARM
- } else if (nrow(QARM)==0 & sum(non_LOH$diff)!=0) {
- qarm=data.frame(start=chr_loc[i,]$cen.right.base,end=chr_interval[2])
- } else {print("unknown issue")}
- qarm[1,1]=qarm[1,1]+CENTROMERE_DIST # to exclude the first CENTROMERE_DIST segment next to the centromere (right side) - noisy
- qarm$diff=qarm$end-qarm$start
+ log_info("START {i} q ARM")
+ qLOH_collector_list <- list()
+ QARM <- non_LOH[which(non_LOH$start >= chr_loc[i, ]$cen.right.base), ]
+ if (nrow(QARM) > 0) {
+ qarm <- QARM
+ } else if (nrow(QARM) == 0 && sum(non_LOH$diff) != 0) {
+ qarm <- data.frame(start = chr_loc[i, ]$cen.right.base, end = chr_interval[2])
+ } else {
+ log_info("unknown issue")
+ }
+ # to exclude the first CENTROMERE_DIST segment next to the centromere (right side) - noisy
+ qarm[1, 1] <- qarm[1, 1] + CENTROMERE_DIST
+ qarm$diff <- qarm$end - qarm$start
#
# search per non_LOH segment
- for (seg in 1:nrow(qarm)){
- LoH=data.frame()
- #IVD-based breakpoints for small regions#
- seg_ivd=ohet[which(ohet$Position_dist>=MIN_HET_DIST & ohet$Position>=qarm$start[seg] & ohet$Position<=qarm$end[seg]),]
- #if (!is.null(nrow(seg_ivd))){
- if (nrow(seg_ivd)>0){
- win=nrow(seg_ivd)
- print(win)
- # win=floor(qarm$diff[seg]/winsize)
- # print(win)
- #if (win>0){
- for (j in 1:win){
- loh=NULL
- start=seg_ivd$Position[j]
- end=start+seg_ivd$Position_dist[j]
- COV=logr[which(logr$Position>start & logr$Position0.5){ # to use a minimum SNP density of 0.5 to get logR estimate #CLcode
- #loh=data.frame(start=start,end=end,LogR=cov,medianLogR=medcov,denSNP=denSNP)
- jpcf=pcf(COV,gamma=GAMMA_LOGR,verbose = F)
- jpcf=jpcf[which(jpcf$mean < -0.8),]
- if (nrow(jpcf)>0){
- loh=data.frame(start=jpcf$start.pos[1],end=jpcf$end.pos[nrow(jpcf)],LogR=mean(jpcf$mean),denSNP=denSNP)
- loh$N=nrow(logr[which(logr$Position>=loh$start & logr$Position<=loh$end),])
- if (loh$N<10){loh=NULL} # if LOH region is supported by less than 10 SNPs, then remove it
+ for (seg in seq_len(nrow(qarm))) {
+ LoH_list <- list()
+ # IVD-based breakpoints for small regions#
+ seg_ivd <- ohet[which(ohet$Position_dist >= MIN_HET_DIST & ohet$Position >= qarm$start[seg] & ohet$Position <= qarm$end[seg]), ]
+ if (nrow(seg_ivd) > 0) {
+ logr_in_seg_idx <- which(logr$Position >= qarm$start[seg] & logr$Position <= qarm$end[seg])
+ if (length(logr_in_seg_idx) > 0) {
+ logr_seg <- logr[logr_in_seg_idx, ]
+ starts_idx <- findInterval(seg_ivd$Position, logr_seg$Position) + 1
+ ends_idx <- findInterval(seg_ivd$Position + seg_ivd$Position_dist, logr_seg$Position)
+
+ for (j in seq_len(nrow(seg_ivd))) {
+ if (starts_idx[j] > ends_idx[j]) next
+ COV <- logr_seg[starts_idx[j]:ends_idx[j], ]
+ cov <- mean(COV$LogR)
+ medcov <- collapse::fmedian(COV$LogR)
+ denSNP <- nrow(COV) / (nSNPs / sum(chr_loc$length) * seg_ivd$Position_dist[j])
+ if (!is.na(cov) && cov < -0.8 && medcov < -0.8 && denSNP > 0.5) {
+ jpcf <- copynumber::pcf(COV, gamma = GAMMA_LOGR, verbose = FALSE)
+ jpcf_loh <- jpcf[which(jpcf$mean < -0.8), ]
+ if (nrow(jpcf_loh) > 0) {
+ loh <- data.frame(
+ start = jpcf_loh$start.pos[1],
+ end = jpcf_loh$end.pos[nrow(jpcf_loh)],
+ LogR = mean(jpcf_loh$mean),
+ denSNP = denSNP,
+ stringsAsFactors = FALSE
+ )
+ loh$N <- sum(COV$Position >= loh$start & COV$Position <= loh$end)
+ if (loh$N >= 10) LoH_list[[length(LoH_list) + 1]] <- loh
+ }
}
}
- if (!is.null(loh)){
- LoH=rbind(LoH,loh)
- }
- if (j %% 100 ==0){
- print(paste("interval=",j))
- }
}
- } else {print(paste("no het SNPs in segment",seg))}
+ }
- # no. of LoH intervals
- print(paste("q-arm nrow(LoH) segment",seg,"=",nrow(LoH)))
- if (nrow(LoH)==0){
- print(paste("No LOH identified in q-arm segment",seg))
- } else {
- if (nrow(LoH)==1){
- LoH_regions=data.frame(chrom=i,arm="q",start.pos=LoH$start,end.pos=LoH$end)
- }
- if (nrow(LoH)>1){
- LoH_regions=data.frame()
- #combine smaller regions into larger regions of LOH
- start=LoH$start[1]
- for (j in 2:nrow(LoH)){
- print(j)
- if (LoH$start[j]==LoH$end[j-1]){
- end=LoH$end[j] # include the new row (i) in the merge
- }
- else {
- end=LoH$end[j-1] # stop merge at the previous row (i-1)
- LoH_regions=rbind(LoH_regions,data.frame(chrom=i,arm="q",start.pos=start,end.pos=end))
- start=LoH$start[j]
+ if (length(LoH_list) > 0) {
+ LoH <- collapse::rowbind(LoH_list)
+ LoH_regions_list <- list()
+ start <- LoH$start[1]
+ end <- LoH$end[1]
+ if (nrow(LoH) > 1) {
+ for (j in 2:nrow(LoH)) {
+ if (LoH$start[j] <= end) {
+ end <- max(end, LoH$end[j])
+ } else {
+ LoH_regions_list[[length(LoH_regions_list) + 1]] <- data.frame(chrom = i, arm = "q", start.pos = start, end.pos = end)
+ start <- LoH$start[j]
+ end <- LoH$end[j]
}
}
- # add final block if it ends at the end of the LOH dataframe
- if (end==LoH$end[nrow(LoH)]){
- LoH_regions=rbind(LoH_regions,data.frame(chrom=i,arm="q",start.pos=start,end.pos=end))
- }
- else if (start==LoH$start[nrow(LoH)] & end==LoH$end[nrow(LoH)-1]){
- LoH_regions=rbind(LoH_regions,data.frame(chrom=i,arm="q",start.pos=start,end.pos=LoH$end[nrow(LoH)]))
- }
}
- qLOH_regions=rbind(qLOH_regions,LoH_regions)
+ LoH_regions_list[[length(LoH_regions_list) + 1]] <- data.frame(chrom = i, arm = "q", start.pos = start, end.pos = end)
+ qLOH_collector_list[[length(qLOH_collector_list) + 1]] <- collapse::rowbind(LoH_regions_list)
}
}
- if (nrow(qLOH_regions)>0){
- #qARM BAF/LogR plot(s)
- pdf(paste0(TUMOURNAME,"_chr",i,"_",MIN_HET_DIST/1e3,"k_based_qLOH_events.pdf"))
+ qLOH_regions <- collapse::rowbind(qLOH_collector_list)
+
+ if (nrow(qLOH_regions) > 0) {
+ grDevices::pdf(paste0(TUMOURNAME, "_chr", i, "_", MIN_HET_DIST / 1e3, "k_based_qLOH_events.pdf"))
suppressWarnings(
- for (s in 1:nrow(qLOH_regions)){
- sBAF=ggplot(ohet,aes(Position,baf))+geom_jitter()+ylim(0,1)+
- geom_vline(xintercept = c(qLOH_regions$start.pos[s],qLOH_regions$end.pos[s]),col="red",linetype="longdash")+
- xlim(qLOH_regions$start.pos[s]-LENGTH_ADJACENT,qLOH_regions$end.pos[s]+LENGTH_ADJACENT)+
- ggtitle(paste("qARM LOH region",s))
- sLogR=ggplot(logr,aes(Position,LogR))+geom_jitter()+ylim(-5.2,1.2)+
- geom_vline(xintercept = c(qLOH_regions$start.pos[s],qLOH_regions$end.pos[s]),col="red",linetype="longdash")+
- xlim(qLOH_regions$start.pos[s]-LENGTH_ADJACENT,qLOH_regions$end.pos[s]+LENGTH_ADJACENT)
- grid.newpage()
- grid.draw(rbind(ggplotGrob(sBAF), ggplotGrob(sLogR), size = "last"))
- #print(plot_grid(sBAF,sLogR, ncol = 1, align = "v"))
+ for (s in seq_len(nrow(qLOH_regions))) {
+ sBAF <- ggplot2::ggplot(ohet, ggplot2::aes(Position, baf)) +
+ ggplot2::geom_jitter() +
+ ggplot2::ylim(0, 1) +
+ ggplot2::geom_vline(xintercept = c(qLOH_regions$start.pos[s], qLOH_regions$end.pos[s]), col = "red", linetype = "longdash") +
+ ggplot2::xlim(qLOH_regions$start.pos[s] - LENGTH_ADJACENT, qLOH_regions$end.pos[s] + LENGTH_ADJACENT) +
+ ggplot2::ggtitle(paste("qARM LOH region", s))
+ sLogR <- ggplot2::ggplot(logr, ggplot2::aes(Position, LogR)) +
+ ggplot2::geom_jitter() +
+ ggplot2::ylim(-5.2, 1.2) +
+ ggplot2::geom_vline(xintercept = c(qLOH_regions$start.pos[s], qLOH_regions$end.pos[s]), col = "red", linetype = "longdash") +
+ ggplot2::xlim(qLOH_regions$start.pos[s] - LENGTH_ADJACENT, qLOH_regions$end.pos[s] + LENGTH_ADJACENT)
+ grid::grid.newpage()
+ grid::grid.draw(rbind(ggplot2::ggplotGrob(sBAF), ggplot2::ggplotGrob(sLogR), size = "last"))
}
)
- dev.off()
+ grDevices::dev.off()
#
- print("Candidate LOH regions plotted for qARM")
+ log_info("Candidate LOH regions plotted for qARM")
+ }
+ # merge LOH regions of both methods
+ LOH_merge_list <- list()
+ if (!is.null(pLOH_regions) && nrow(pLOH_regions) > 0) {
+ LOH_merge_list[[length(LOH_merge_list) + 1]] <- pLOH_regions
}
- #STEP 2.2: merge LOH regions of both methods
- LOH_regions=data.frame()
- if (nrow(pLOH_regions)>0){
- print(pLOH_regions)
- LOH_regions=rbind(LOH_regions,pLOH_regions)
- } else {print("no window-based LOH regions identified in p arm of non_LOH of IVD-PCF")}
- if (nrow(qLOH_regions)>0){
- print(qLOH_regions)
- LOH_regions=rbind(LOH_regions,qLOH_regions)
- } else {print("no window-based LOH regions identified in q arm of non_LOH of IVD-PCF")}
- if (nrow(LOH_regions)>0){
- if (!is.null(nrow(LOH[[i]]))){
- LOH[[i]]=rbind(LOH[[i]][,c("chrom","arm","start.pos","end.pos")],LOH_regions)
- LOH[[i]]=LOH[[i]][order(LOH[[i]]$start.pos),]
+ if (!is.null(qLOH_regions) && nrow(qLOH_regions) > 0) {
+ LOH_merge_list[[length(LOH_merge_list) + 1]] <- qLOH_regions
+ }
+
+ if (length(LOH_merge_list) > 0) {
+ LOH_regions_final <- collapse::rowbind(LOH_merge_list)
+ if (!is.null(LOH[[i]]) && !is.null(nrow(LOH[[i]])) && nrow(LOH[[i]]) > 0) {
+ LOH[[i]] <- collapse::rowbind(LOH[[i]][, c("chrom", "arm", "start.pos", "end.pos")], LOH_regions_final)
+ LOH[[i]] <- LOH[[i]][order(LOH[[i]]$start.pos), ]
} else {
- LOH[[i]]=LOH_regions
+ LOH[[i]] <- LOH_regions_final
}
}
- #combine adjacent regions into larger regions of LOH
- if (!is.null(nrow(LOH[[i]]))){
- LOH[[i]]=LOH[[i]][!duplicated(LOH[[i]]),]
- LOHall=data.frame()
- ChrArms=unique(LOH[[i]]$arm)
- for (arm in ChrArms){
- LOHarm=LOH[[i]][LOH[[i]]$arm==arm,]
- if (nrow(LOHarm)>1){
- start=LOHarm$start.pos[1]
- for (j in 2:nrow(LOHarm)){
- print(j)
- if (LOHarm$start.pos[j]==LOHarm$end.pos[j-1]){
- end=LOHarm$end.pos[j] # include the new row (i) in the merge
+ # combine adjacent regions into larger regions of LOH
+ if (!is.null(LOH[[i]]) && !is.null(nrow(LOH[[i]])) && nrow(LOH[[i]]) > 0) {
+ LOH[[i]] <- LOH[[i]][!duplicated(LOH[[i]]), ]
+ LOHall_list <- list()
+ ChrArms <- unique(LOH[[i]]$arm)
+ for (arm in ChrArms) {
+ LOHarm <- LOH[[i]][LOH[[i]]$arm == arm, ]
+ if (nrow(LOHarm) > 1) {
+ start <- LOHarm$start.pos[1]
+ end <- LOHarm$end.pos[1]
+ for (j in 2:nrow(LOHarm)) {
+ if (LOHarm$start.pos[j] <= end) {
+ end <- max(end, LOHarm$end.pos[j])
} else {
- if (LOHarm$start.pos[j]>LOHarm$end.pos[j-1]){
- end=LOHarm$end.pos[j-1] # stop merge at the previous row (i-1)
- LOHall=rbind(LOHall,data.frame(chrom=i,arm=arm,start.pos=start,end.pos=end))
- start=LOHarm$start.pos[j]
- } else if (LOHarm$start.pos[j] 0) {
+ LOHall <- LOH[[i]][, c("chrom", "arm", "start.pos", "end.pos")]
+ } else {
+ LOHall <- NULL
+ }
}
- if (!is.null(nrow(LOHall))){
- LOHall=LOHall[!duplicated(LOHall),]
- LOHall$diff=LOHall$end.pos-LOHall$start.pos
- } else {print(paste("no LOH (IVD and/or inter-het based) was identified for chr",i))}
- if (exists("non_loh")){
- rm(non_loh)}
- if (exists("non_LOH")){
- rm(non_LOH)
+ if (!is.null(LOHall) && !is.null(nrow(LOHall)) && nrow(LOHall) > 0) {
+ LOHall <- LOHall[!duplicated(LOHall), ]
+ LOHall$diff <- LOHall$end.pos - LOHall$start.pos
}
- #STEP 3####################################################################################################################################################
# RECONSTRUCT alleleCounter files for the pseudo-NORMAL sample
- # use loop to find intervening blocks with no LOH - while taking account of the centromere - RUN2####
- if (!is.null(nrow(LOHall))){
- names(ac)=c("chr","position",1:4,"depth")
- chr_interval=c(ac$position[1],ac$position[nrow(ac)])
- non_LOH=data.frame()####################################### get all non_LOH regions####
- for (j in 1:(nrow(LOHall)+1)){
- if (j == 1 & chr_interval[1]==LOHall$start.pos[j]){
- print("LOH from start of chromosome")
- } else if (j == 1 & chr_interval[1]1 & j <= nrow(LOHall) & LOHall$arm[j]==LOHall$arm[j-1]){
- non_loh=data.frame(start=LOHall$end.pos[j-1]+1,end=LOHall$start.pos[j]-1)
- print("TWO")
- } else if (j>1 & j <= nrow(LOHall) & LOHall$arm[j]!=LOHall$arm[j-1]){
- non_loh=data.frame(start=c(min(LOHall$end.pos[j-1]+1,chr_loc[i,]$cen.left.base),chr_loc[i,]$cen.right.base),end=c(chr_loc[i,]$cen.left.base,LOHall$start.pos[j]-1))
- print("THREE")
- } else{
- if ((LOHall$end.pos[j-1]+1) 0) {
+ names(ac) <- c("chr", "position", "A", "C", "G", "T", "depth")
+ chr_interval <- c(ac$position[1], ac$position[nrow(ac)])
+
+ # Get non_LOH regions based on LOHall
+ non_LOH_list <- list()
+ for (j in 1:(nrow(LOHall) + 1)) {
+ if (j == 1 && chr_interval[1] >= LOHall$start.pos[j]) {} else if (j == 1) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(start = chr_interval[1], end = LOHall$start.pos[j] - 1)
+ } else if (j <= nrow(LOHall) && LOHall$arm[j] == LOHall$arm[j - 1]) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(start = LOHall$end.pos[j - 1] + 1, end = LOHall$start.pos[j] - 1)
+ } else if (j <= nrow(LOHall)) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(
+ start = c(min(LOHall$end.pos[j - 1] + 1, chr_loc[i, ]$cen.left.base), chr_loc[i, ]$cen.right.base),
+ end = c(chr_loc[i, ]$cen.left.base, LOHall$start.pos[j] - 1)
+ )
+ } else if ((LOHall$end.pos[j - 1] + 1) < chr_interval[2]) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(start = LOHall$end.pos[j - 1] + 1, end = chr_interval[2])
}
- print(j)
- if (exists("non_loh")){
- non_LOH=rbind(non_LOH,non_loh)
+ }
+ non_LOH <- collapse::rowbind(non_LOH_list)
+ non_LOH <- non_LOH[non_LOH$end >= non_LOH$start, ]
+
+ if (nrow(non_LOH) > 0) {
+ non_LOH$length <- non_LOH$end - non_LOH$start
+ non_LOH_length <- sum(non_LOH$length)
+ if (non_LOH_length > 1e6) {
+ SNP_interval <- non_LOH_length / max(1, nrow(CL_OHET[[i]]))
+ } else {
+ SNP_interval <- 2000
}
- }
- # the non-LOH region length from PCF is:
- if (!is.null(nrow(non_LOH))){
- non_LOH$length=non_LOH$end-non_LOH$start
- non_LOH=non_LOH[non_LOH$length>=0,] # >= rather than > as it would miss potential 1bp non_LOH seg with a hetSNP in it
- non_LOH_length=sum(non_LOH$length) # total length of non-LOH regions in chr i
- print(paste("Total length of non LOH regions =",non_LOH_length))
- # average Het SNP interval:
- if (non_LOH_length>1e6){ # run this only if combined non-LOH regions are at least 1Mb long
- SNP_interval=non_LOH_length/nrow(CL_OHET[[i]]) # estimate of genomic space between any two Het SNPs
- } else {SNP_interval = 2000} # replace with 5000 to increase run speed!?
- # no. of SNPs to be Hets in the LOH region (COMBINED FOR THE WHOLE CHROMOSOME):
- LOH_hetSNP_number=floor(sum(LOHall$diff)/SNP_interval)
- print(paste("No. of Het SNPs to be added to LOH regions:",LOH_hetSNP_number))
+ } else {
+ SNP_interval <- 2000
}
- # reconstruct allele counts for the LOH region based on actual depth for all to be perfect heterozygotes - allele counts remain as integers
- #
- lohs=data.frame() # get all non_LOH regions#
- for (j in 1:nrow(LOHall)){
- loh=ac[which(ac$position>=LOHall$start.pos[j] & ac$position<=LOHall$end.pos[j]),]
- m=merge(loh,al,"position")
- if (nrow(m)==nrow(loh)){
- print("merge OK")
- } else {print("ERROR - merge not OK")}
- # reconstruct allele counts for LOH region
- hetSNP_number=LOHall$diff[j]/SNP_interval
- if (nrow(m)>hetSNP_number){
- print("more rows in LOH region than Het SNP number")
- for (k in 1:nrow(m)){
- if (k %% floor(nrow(m)/hetSNP_number)==0){
- m[cbind(k,2+m$a0[k])]=ifelse(m$depth[k]%%2==0,m$depth[k]/2,ceiling(m$depth[k]/2))
- m[cbind(k,2+m$a1[k])]=ifelse(m$depth[k]%%2==0,m$depth[k]/2,floor(m$depth[k]/2))
- print(k)
- }
+
+ # Spike in heterozygotes in LOH regions
+ lohs_list <- list()
+ for (j in seq_len(nrow(LOHall))) {
+ loh_idx <- which(ac$position >= LOHall$start.pos[j] & ac$position <= LOHall$end.pos[j])
+ if (length(loh_idx) == 0) next
+ loh <- ac[loh_idx, ]
+
+ # Merge with alleles
+ m <- merge(loh, al, by = "position")
+
+ hetSNP_number <- max(floor(LOHall$diff[j] / SNP_interval), 10)
+ if (nrow(m) >= hetSNP_number) {
+ spike <- unique(c(1, floor(seq(1, nrow(m), length.out = hetSNP_number)), nrow(m)))
+ for (k in spike) {
+ m$depth[k] <- max(m$depth[k], 10)
+ a0_col <- match(as.character(m$a0[k]), c("1", "2", "3", "4")) + 2
+ a1_col <- match(as.character(m$a1[k]), c("1", "2", "3", "4")) + 2
+ if (!is.na(a0_col)) m[k, a0_col] <- ceiling(m$depth[k] / 2)
+ if (!is.na(a1_col)) m[k, a1_col] <- floor(m$depth[k] / 2)
}
} else {
- print("less rows in LOH region than Het SNP number - turning all into Heterozygotes")
- for (k in 1:nrow(m)){
- m[cbind(k,2+m$a0[k])]=ifelse(m$depth[k]%%2==0,m$depth[k]/2,ceiling(m$depth[k]/2))
- m[cbind(k,2+m$a1[k])]=ifelse(m$depth[k]%%2==0,m$depth[k]/2,floor(m$depth[k]/2))
- #print(k)
+ for (k in seq_len(nrow(m))) {
+ m$depth[k] <- max(m$depth[k], 10)
+ a0_col <- match(as.character(m$a0[k]), c("1", "2", "3", "4")) + 2
+ a1_col <- match(as.character(m$a1[k]), c("1", "2", "3", "4")) + 2
+ if (!is.na(a0_col)) m[k, a0_col] <- ceiling(m$depth[k] / 2)
+ if (!is.na(a1_col)) m[k, a1_col] <- floor(m$depth[k] / 2)
}
}
- print(paste("LOH region segment",j))
- lohs=rbind(lohs,m)
+ # Reorder columns to match ac
+ lohs_list[[j]] <- m[, c("chr", "position", "A", "C", "G", "T", "depth")]
}
+ lohs <- collapse::rowbind(lohs_list)
- lohs=lohs[,c("chr","position",1:4,"depth")]
- ####
- # combine alleleCounts for LOHS and non_LOH regions####
- non_lohs=data.frame()
- for (j in 1:nrow(non_LOH)){
- non_loh=ac[which(ac$position>=non_LOH$start[j] & ac$position<=non_LOH$end[j]),]
- non_lohs=rbind(non_lohs,non_loh)
- print(paste("non_LOH segment",j,"added"))
- }
- # write out as alleleCounts file - "normal" ID #
- if (nrow(non_lohs)+nrow(lohs)==nrow(ac)){
- ac_out=rbind(non_lohs,lohs)
- ac_out=ac_out[order(ac_out$position),]
- write.table(ac_out,paste0(NORMALNAME,"_alleleFrequencies_chr",i,".txt"),col.names=F,row.names=F,quote=F,sep="\t")
- print(paste("reconstruction OK - new alleleCounts file generated for chr",i))
- } else {
- centro_ac=ac[which(ac$position>chr_loc$cen.left.base[i] & ac$position 0) {
+ for (j in seq_len(nrow(non_LOH))) {
+ non_lohs_list[[j]] <- ac[ac$position >= non_LOH$start[j] & ac$position <= non_LOH$end[j], ]
}
+ }
+ non_lohs <- collapse::rowbind(non_lohs_list)
+
+ # Final assembly
+ ac_out_list <- list(non_lohs, lohs)
+ covered_pos <- c(lohs$position, non_lohs$position)
+ missing_ac <- ac[!(ac$position %in% covered_pos), ]
+ if (nrow(missing_ac) > 0) {
+ ac_out_list[[3]] <- missing_ac
+ }
+
+ ac_out <- collapse::rowbind(ac_out_list)
+ ac_out <- ac_out[order(ac_out$position), ]
+ ac_out <- ac_out[!duplicated(ac_out$position), ]
+
+ data.table::fwrite(ac_out, paste0(NORMALNAME, "_alleleFrequencies_chr", i, ".txt"), col.names = FALSE, row.names = FALSE, quote = FALSE, sep = "\t")
+ log_info("reconstruction OK - new alleleCounts file generated for chr {i}")
} else {
- ac_out=ac
- write.table(ac_out,paste0(NORMALNAME,"_alleleFrequencies_chr",i,".txt"),col.names=F,row.names=F,quote=F,sep="\t")
- print(paste("No changes made to the alleleCounter file - no LOH in chr",i))
+ # No LOH identified
+ data.table::fwrite(ac, paste0(NORMALNAME, "_alleleFrequencies_chr", i, ".txt"), col.names = FALSE, row.names = FALSE, quote = FALSE, sep = "\t")
+ log_info("No change to allele frequencies for chr {i}")
}
- print(paste("STEP 2&3 - chr",i,"completed"))
}
#' Prepare WGS data of cell line for haplotype construction
-#'
-#' This function performs part of the Battenberg WGS pipeline: Counting alleles, generating BAF and logR,
+#'
+#' This function performs part of the Battenberg WGS pipeline: Counting alleles, generating BAF and logR,
#' reconstructing normal-pair allele counts for the cell line and performing GC content correction.
-#'
+#'
#' @param chrom_names A vector containing the names of chromosomes to be included
-#' @param tumourbam Full path to the tumour BAM file
+#' @param tumourbam Full path to the tumour BAM file
#' @param tumourname Identifier to be used for tumour output files (i.e. the cell line BAM file name without the '.bam' extension).
+#' @param chrom_coord Path to the chromosome coordinates file
#' @param g1000lociprefix Prefix path to the 1000 Genomes loci reference files
#' @param g1000allelesprefix Prefix path to the 1000 Genomes SNP allele reference files
#' @param gamma_ivd The PCF gamma value for segmentation of 1000G hetSNP IVD values (Default 1e5).
@@ -732,74 +728,83 @@ cell_line_reconstruct_normal <-function(TUMOURNAME,NORMALNAME,chrom_coord,chrom,
#' @param repliccorrectprefix Prefix path to replication timing reference data (supply NULL if no replication timing correction is to be applied)
#' @param min_base_qual Minimum base quality required for a read to be counted
#' @param min_map_qual Minimum mapping quality required for a read to be counted
-#' @param allelecounter_exe Path to the allele counter executable (can be found in $PATH)
+#' @param allele_counts_dir Directory containing the allele counts files
#' @param min_normal_depth Minimum depth required in the normal for a SNP to be included
-#' @param skip_allele_counting Flag, set to TRUE if allele counting is already complete (files are expected in the working directory on disk)
+#' @param libs Path to the R libraries to be used by parallel workers
#' @author Naser Ansari-Pour (BDI, Oxford)
#' @export
-prepare_wgs_cell_line = function(chrom_names, chrom_coord, tumourbam, tumourname, g1000lociprefix, g1000allelesprefix, gamma_ivd=1e5, kmin_ivd=50, centromere_noise_seg_size=1e6,
- centromere_dist=5e5, min_het_dist=1e5, gamma_logr=100, length_adjacent=5e4, gccorrectprefix,repliccorrectprefix, min_base_qual, min_map_qual,
- allelecounter_exe, min_normal_depth, skip_allele_counting) {
-
- requireNamespace("foreach")
- requireNamespace("doParallel")
- requireNamespace("parallel")
-
- if (!skip_allele_counting) {
- # Obtain allele counts for 1000 Genomes locations for the cell line
- foreach::foreach(i=1:length(chrom_names)) %dopar% {
- getAlleleCounts(bam.file=tumourbam,
- output.file=paste(tumourname,"_alleleFrequencies_chr", i, ".txt", sep=""),
- g1000.loci=paste(g1000lociprefix, i, ".txt", sep=""),
- min.base.qual=min_base_qual,
- min.map.qual=min_map_qual,
- allelecounter.exe=allelecounter_exe)
- }
- }
-
+prepare_wgs_cell_line <- function(
+ chrom_names, chrom_coord, tumourbam, tumourname,
+ g1000lociprefix, g1000allelesprefix, gamma_ivd = 1e5,
+ kmin_ivd = 50, centromere_noise_seg_size = 1e6,
+ centromere_dist = 5e5, min_het_dist = 1e5, gamma_logr = 100,
+ length_adjacent = 5e4, gccorrectprefix, repliccorrectprefix,
+ min_base_qual, min_map_qual, allele_counts_dir, min_normal_depth,
+ nthreads = 1,
+ libs
+) {
# Standardise Chr notation (removes 'chr' string if present; essential for cell_line_baf_logR)
+ # Skipping modification of external files. Assuming files are correct or handled in R reading.
- standardiseChrNotation(tumourname=tumourname,
- normalname=NULL)
+ tumour_prefix <- file.path(allele_counts_dir, tumourname)
+
+ # Check existence of at least one file
+ first_file <- paste0(tumour_prefix, "_alleleFrequencies_chr", chrom_names[1], ".txt")
+ if (!file.exists(first_file)) {
+ log_failure("Expected allele counts file not found: {first_file}")
+ }
# Obtain BAF and LogR from the raw allele counts of the cell line
- cell_line_baf_logR(TUMOURNAME=tumourname,
- g1000alleles.prefix=g1000allelesprefix,
- chrom_names=chrom_names
+ cl_data <- cell_line_baf_logR(
+ TUMOURNAME = tumour_prefix,
+ g1000alleles_prefix = g1000allelesprefix,
+ chrom_names = chrom_names
)
-
# Reconstruct normal-pair allele count files for the cell line
- foreach::foreach(i=1:length(chrom_names),.export=c("cell_line_reconstruct_normal","CL_OHET","CL_AL","CL_AC","CL_LogR"),.packages=c("copynumber","ggplot2","grid")) %dopar% {
-
- cell_line_reconstruct_normal(TUMOURNAME=tumourname,
- NORMALNAME=paste0(tumourname,"_normal"),
- chrom_coord=chrom_coord,
- chrom=i,
- CL_OHET=CL_OHET,
- CL_AL=CL_AL,
- CL_AC=CL_AC,
- CL_LogR=CL_LogR,
- GAMMA_IVD=gamma_ivd,
- KMIN_IVD=kmin_ivd,
- CENTROMERE_NOISE_SEG_SIZE=centromere_noise_seg_size,
- CENTROMERE_DIST=centromere_dist,
- MIN_HET_DIST=min_het_dist,
- GAMMA_LOGR=gamma_logr,
- LENGTH_ADJACENT=length_adjacent)
- }
+ run_with_error_handling(seq_along(chrom_names), function(i) {
+ # If we are in parallel mode, ensure the packages are loaded on the worker
+ if (FALSE) {
+ # The least shit way to load dependencies inside a worker
+ # This replaces the .packages argument from foreach
+ requireNamespace("copynumber", quietly = TRUE)
+ requireNamespace("ggplot2", quietly = TRUE)
+ requireNamespace("grid", quietly = TRUE)
+ }
+
+ # Execute the reconstruction
+ cell_line_reconstruct_normal(
+ TUMOURNAME = tumourname,
+ NORMALNAME = paste(tumourname, "_normal", sep = ""),
+ chrom_coord = chrom_coord,
+ chrom = i,
+ CL_OHET = cl_data$OHET,
+ CL_AL = cl_data$AL,
+ CL_AC = cl_data$AC,
+ CL_LogR = cl_data$LogR,
+ GAMMA_IVD = gamma_ivd,
+ KMIN_IVD = kmin_ivd,
+ CENTROMERE_NOISE_SEG_SIZE = centromere_noise_seg_size,
+ CENTROMERE_DIST = centromere_dist,
+ MIN_HET_DIST = min_het_dist,
+ GAMMA_LOGR = gamma_logr,
+ LENGTH_ADJACENT = length_adjacent
+ )
+ }, libs, nthreads = nthreads)
- if (length(list.files(pattern="normal_alleleFrequencies"))==length(chrom_names)){
- print("STEP 2 - Normal allelecounts reconstruction - completed")
- } else {
- stop("Missing 'normal' allelecount files - all chromosomes NOT reconstructed")
+ if (length(list.files(pattern = "normal_alleleFrequencies")) == length(chrom_names)) {
+ log_info("STEP 2 - Normal allelecounts reconstruction - completed")
+ } else {
+ log_failure("Missing 'normal' allelecount files - all chromosomes NOT reconstructed")
}
# Perform GC correction
- gc.correct.wgs(Tumour_LogR_file=paste(tumourname,"_mutantLogR.tab", sep=""),
- outfile=paste(tumourname,"_mutantLogR_gcCorrected.tab", sep=""),
- correlations_outfile=paste(tumourname, "_GCwindowCorrelations.txt", sep=""),
- gc_content_file_prefix=gccorrectprefix,
- replic_timing_file_prefix=repliccorrectprefix,
- chrom_names=chrom_names)
+ gc_correct_wgs(
+ Tumour_LogR_file = paste(tumourname, "_mutantLogR.tab", sep = ""),
+ outfile = paste(tumourname, "_mutantLogR_gcCorrected.tab", sep = ""),
+ correlations_outfile = paste(tumourname, "_GCwindowCorrelations.txt", sep = ""),
+ gc_content_file_prefix = gccorrectprefix,
+ replic_timing_file_prefix = repliccorrectprefix,
+ chrom_names = chrom_names
+ )
}
diff --git a/R/prepare_wgs_germline.R b/R/prepare_wgs_germline.R
index 692909ce..98898ca4 100644
--- a/R/prepare_wgs_germline.R
+++ b/R/prepare_wgs_germline.R
@@ -1,103 +1,138 @@
-#' Chromosome notation standardisation (removing 'chr' string from chromosome names - mainly an issue in hg38 BAMs)
-#'
-#' @param GERMLINENAME The germline identifier, this is used as a prefix for the allele count files. If allele counts are supplied separately, they are expected to have this identifier as prefix.
-#' @author Naser Ansari-Pour (BDI, Oxford)
-#' @export
-standardiseChrNotation_germline = function(GERMLINENAME) {
-gAF=capture.output(cat('bash -c \'sed -i \'s/chr//g\' ', GERMLINENAME,'_alleleFrequencies_chr*.txt\'',sep = ""))
-system(gAF)
-}
-
#' Obtain BAF and LogR from the Germline allele counts
#'
#' Function to generate BAF and LogR files based on allele counts of the Germline.
#' It also generates the input data required by the following 'germline_reconstruct_normal' function.
#' @param GERMLINENAME The germline name used for Battenberg (i.e. the Germline BAM file name without the '.bam' extension).
-#' @param g1000alleles.prefix Prefix to where 1000 Genomes allele files can be found.
+#' @param g1000alleles_prefix Prefix to where 1000 Genomes allele files can be found.
#' @param chrom_names A vector with allowed chromosome names.
#' @author Naser Ansari-Pour (BDI, Oxford)
#' @export
-germline_baf_logR = function(GERMLINENAME,g1000alleles.prefix,chrom_names){
- #read heterozygous SNPs per chromosome for alleleCounter files & 1000G allele files####
- AC=list() # alleleCounts
- AL=list() # 1000G alleles
- MaC=list() # matched alleleCounts
- OHET=list() # HET SNP data
- for (chr in chrom_names){
- # read in alleleCounter output for each chromosome
- ac=read.table(paste0(GERMLINENAME,"_alleleFrequencies_chr",chr,".txt"),stringsAsFactors = F)
- ac=ac[order(ac$V2),]
- AC[[chr]]=ac
- print(length(AC))
+germline_baf_logR <- function(GERMLINENAME, g1000alleles_prefix, chrom_names) {
+ # read heterozygous SNPs per chromosome for alleleCounter files & 1000G allele files####
+ AC <- list() # alleleCounts
+ AL <- list() # 1000G alleles
+ MaC <- list() # matched alleleCounts
+ OHET <- list() # HET SNP data
+
+ for (chr in chrom_names) {
+ # read in alleleCounter output for each chromosome (FAST)
+ ac_file <- paste0(GERMLINENAME, "_alleleFrequencies_chr", chr, ".txt")
+ if (!file.exists(ac_file) || file.size(ac_file) == 0) {
+ log_failure("Allele count file '{ac_file}' is missing or empty. Preprocessing cannot continue.")
+ }
+ ac <- data.table::fread(ac_file, header = FALSE, sep = "auto", stringsAsFactors = FALSE)
+ if (nrow(ac) == 0) {
+ log_failure("Allele count file '{ac_file}' contains no data.")
+ }
+ # Ensure column 2 (Position) is numeric for sorting
+ if (!is.numeric(ac[[2]])) ac[[2]] <- as.numeric(ac[[2]])
+ data.table::setorder(ac, V2)
+ AC[[chr]] <- ac
+ log_info("length(AC): '{length(AC)}'")
+
# match allele counts with respective SNP alleles
-
- al=read.table(paste0(g1000alleles.prefix,chr,".txt"),header=T,stringsAsFactors = F)
- AL[[chr]]=al
- print(length(AL))
- #etc
- ref=al$a0
- ref_df=data.frame(pos=1:nrow(al),ref=ref+2)
- REF=ac[cbind(ref_df$pos,ref_df$ref)]
- alt=al$a1
- alt_df=data.frame(pos=1:nrow(al),alt=alt+2)
- ALT=ac[cbind(alt_df$pos,alt_df$alt)]
- mac=data.frame(ref=REF,alt=ALT)
- mac$depth=as.numeric(mac$ref)+as.numeric(mac$alt)
- mac$baf=as.numeric(mac$alt)/as.numeric(mac$depth)
- o=cbind(al,mac)
- names(o)=c("Position","a0","a1","ref","alt","depth","baf")
- MaC[[chr]]=o
- #extract rows with 0.1==0.10 & o$baf<=0.90 & o$depth>10),]
- ohet$Position2=c(ohet$Position[2:nrow(ohet)],2*ohet$Position[nrow(ohet)]-ohet$Position[nrow(ohet)-1])
- ohet$Position_dist=ohet$Position2-ohet$Position
- ohet$Position_dist_percent=ohet$Position_dist/max(ohet$Position_dist)
- OHET[[chr]]=ohet
- print(paste("chromosome",chr,"file read"))
+ al_file <- paste0(g1000alleles_prefix, chr, ".txt")
+ if (!file.exists(al_file) || file.size(al_file) == 0) {
+ log_failure("1000G alleles file '{al_file}' is missing or empty.")
+ }
+ al <- data.table::fread(al_file, header = TRUE, sep = "auto", stringsAsFactors = FALSE)
+ if (nrow(al) == 0) {
+ log_failure("1000G alleles file '{al_file}' contains no data.")
+ }
+ AL[[chr]] <- al
+ log_info("length(AL): '{length(AL)}'")
+
+ # Explicitly cast to integer for matrix indexing safety
+ ref <- as.integer(al$a0)
+ alt <- as.integer(al$a1)
+
+ # Matrix indexing for lightning-fast extraction
+ m_ac <- as.matrix(ac)
+ REF <- m_ac[cbind(seq_len(nrow(al)), ref + 2)]
+ ALT <- m_ac[cbind(seq_len(nrow(al)), alt + 2)]
+
+ mac <- data.frame(ref = REF, alt = ALT)
+ mac$depth <- as.numeric(mac$ref) + as.numeric(mac$alt)
+ mac$baf <- as.numeric(mac$alt) / as.numeric(mac$depth)
+
+ if (nrow(mac) == 0) {
+ log_failure("No matching SNPs found between allele counts and 1000G alleles for chromosome {chr}.")
+ }
+
+ o <- cbind(al, mac)
+ names(o) <- c("Position", "a0", "a1", "ref", "alt", "depth", "baf")
+ MaC[[chr]] <- o
+
+ # Extract HET SNPs
+ ohet <- o[which(o$baf >= 0.10 & o$baf <= 0.90 & o$depth > 10), ]
+ if (nrow(ohet) < 50) {
+ log_warning("Extremely low heterozygosity detected on chromosome {chr} (n={nrow(ohet)}). Results may be unreliable.")
+ }
+ if (nrow(ohet) > 0) {
+ ohet$Position2 <- c(
+ ohet$Position[2:nrow(ohet)],
+ 2 * ohet$Position[nrow(ohet)] - ohet$Position[nrow(ohet) - 1]
+ )
+ ohet$Position_dist <- ohet$Position2 - ohet$Position
+ ohet$Position_dist_percent <- ohet$Position_dist / max(ohet$Position_dist)
+ }
+ OHET[[chr]] <- ohet
+ log_info("chromosome {chr} file read")
}
+
# CREATE mutantBAF and mutantLogR *.tab files #
- germline=GERMLINENAME
- MAC=data.frame()
- for (chr in chrom_names){
- MaC_CHR=data.frame(chr=chr,MaC[[chr]])
- MAC=rbind(MAC,MaC_CHR)
- print(chr)
- }
- names(MAC)=c("chr","position","a0","a1","ref","alt","coverage","baf")
- print(head(MAC))
- print(dim(MAC))
- # MAC$logr=log2(MAC$coverage/mean(MAC$coverage))
- MAC$logr=log2(MAC$coverage/mean(MAC$coverage,na.rm=TRUE)) # in case of coverage == NA due to non-matching alleles or presence of indels in loci file
- MACC=MAC[which(!is.na(MAC$baf)),]
- print(nrow(MAC)-nrow(MACC))
-
- BAF=data.frame(Chromosome=MACC$chr,Position=MACC$pos,germline=MACC$baf)
- names(BAF)[names(BAF) == "germline"] <- germline
- BAF=BAF[order(BAF$Chromosome,BAF$Position),]
- BAF$Chromosome[BAF$Chromosome==23]="X" # revert back from 23 to X for Chromosome number
- write.table(BAF,paste0(germline,"_mutantBAF.tab"),col.names=T,row.names=F,quote=F,sep="\t")
+ # Use basename to ensure outputs land in the current directory, not the input counts directory
+ germline <- basename(GERMLINENAME)
+
+ # Assemble MAC efficiently (O(N))
+ MAC_list <- lapply(chrom_names, function(chr) {
+ data.frame(chr = chr, MaC[[chr]], stringsAsFactors = FALSE)
+ })
+ MAC <- collapse::rowbind(MAC_list)
+ names(MAC) <- c("chr", "position", "a0", "a1", "ref", "alt", "coverage", "baf")
+
+ log_info("Sync complete. dim(MAC): {paste(dim(MAC), collapse = ' ')}")
+
+ # LogR calculation
+ MAC$logr <- log2(MAC$coverage / mean(MAC$coverage, na.rm = TRUE))
+ MACC <- MAC[which(!is.na(MAC$baf)), ]
+
+ # Prepare and save BAF
+ BAF <- data.frame(
+ Chromosome = MACC$chr,
+ Position = MACC$position,
+ germline = MACC$baf
+ )
+ names(BAF)[3] <- germline
+ # Standardization
+ BAF$Chromosome[BAF$Chromosome %in% c("23", 23)] <- "X"
+ data.table::setorder(BAF, Chromosome, Position)
+ data.table::fwrite(BAF, paste0(germline, "_mutantBAF.tab"), sep = "\t")
rm(BAF)
-
- LogR=data.frame(Chromosome=MACC$chr,Position=MACC$pos,germline=MACC$logr)
- names(LogR)[names(LogR) == "germline"] <- germline
- LogR=LogR[order(LogR$Chromosome,LogR$Position),]
- LogR$Chromosome[LogR$Chromosome==23]="X" # revert back from 23 to X for Chromosome number
- write.table(LogR,paste0(germline,"_mutantLogR.tab"),col.names=T,row.names=F,quote=F,sep="\t")
-
- rm(MAC)
- rm(MaC)
- rm(MACC)
- GL_OHET <<- OHET
- GL_AL <<- AL
- GL_AC <<- AC
- GL_LogR <<- LogR
- print("STEP 1 - BAF and LogR - completed")
+
+ # Prepare and save LogR
+ LogR_out <- data.frame(
+ Chromosome = MACC$chr,
+ Position = MACC$position,
+ germline = MACC$logr
+ )
+ names(LogR_out)[3] <- germline
+ LogR_out$Chromosome[LogR_out$Chromosome %in% c("23", 23)] <- "X"
+ data.table::setorder(LogR_out, Chromosome, Position)
+ data.table::fwrite(LogR_out, paste0(germline, "_mutantLogR.tab"), sep = "\t")
+
+ return(list(
+ OHET = OHET,
+ AL = AL,
+ AC = AC,
+ LogR = LogR_out
+ ))
}
#' Reconstruct normal-pair allele count files for Germlines
#'
-#' Function to generate normal-pair allele count files based on IVD-PCF and inter-hetSNP logR-based LOH detection (IVD: Inter-Variant Distance, het: heterozygote)
+#' Function to generate normal-pair allele count files based on IVD-PCF and inter-hetSNP logR-based LOH detection (IVD: Inter-Variant Distance, het: heterozygote)
#' This method reconstructs the normal-pair counts by using the allele counts of the Germline as template
#' It fills the detected LOH regions with evenly-distributed hetSNPs with the density estimated based on each chromosome in each germline sample
#' It essentially informs Battenberg of the location of hetSNPs across the genome in the germline sample
@@ -118,683 +153,798 @@ germline_baf_logR = function(GERMLINENAME,g1000alleles.prefix,chrom_names){
#' @author Naser Ansari-Pour (BDI, Oxford)
#' @export
-germline_reconstruct_normal = function(GERMLINENAME,NORMALNAME,chrom_coord,chrom,GL_OHET,GL_AL,GL_AC,GL_LogR,GAMMA_IVD,KMIN_IVD,CENTROMERE_NOISE_SEG_SIZE,CENTROMERE_DIST,MIN_HET_DIST,GAMMA_LOGR,LENGTH_ADJACENT){
+germline_reconstruct_normal <- function(
+ GERMLINENAME, NORMALNAME,
+ chrom_coord, chrom,
+ GL_OHET, GL_AL, GL_AC,
+ GL_LogR, GAMMA_IVD, KMIN_IVD,
+ CENTROMERE_NOISE_SEG_SIZE,
+ CENTROMERE_DIST, MIN_HET_DIST,
+ GAMMA_LOGR, LENGTH_ADJACENT
+) {
# IDENTIFY REGIONS OF LOH #
- colClasses=c(chr="numeric",start="numeric",cen.left.base="numeric",cen.right.base="numeric",end="numeric")
- chr_loc=read.table(chrom_coord,colClasses = colClasses,header=T,stringsAsFactors = F) # chrom_coord = full path to chromosome coordinates
- chr_loc$length=(chr_loc$cen.left.base-chr_loc$start)+(chr_loc$end-chr_loc$cen.right.base)
- #STEP 2.0: identify LOH by IVD-PCF
- LOH=list()
- PCF_folder = "PCF_plots"
- if(!file.exists(PCF_folder)){
+ colClasses <- c(chr = "numeric", start = "numeric", cen.left.base = "numeric", cen.right.base = "numeric", end = "numeric")
+ # Use fast I/O
+ chr_loc <- data.table::fread(chrom_coord, colClasses = colClasses, header = TRUE, stringsAsFactors = FALSE)
+ data.table::setDF(chr_loc)
+ chr_loc$length <- (chr_loc$cen.left.base - chr_loc$start) + (chr_loc$end - chr_loc$cen.right.base)
+
+ # STEP 2.0: identify LOH by IVD-PCF
+ LOH <- list()
+ PCF_folder <- "PCF_plots"
+ if (!dir.exists(PCF_folder)) {
dir.create(PCF_folder)
}
- i=chrom
- print(paste("chrom=",i))
- pcf_input=data.frame(chr=i,position=GL_OHET[[i]]$Position,IVD=(GL_OHET[[i]]$Position_dist_percent))
- pcf_input=pcf_input[which(pcf_input$positionchr_loc[i,"cen.right.base"]+CENTROMERE_DIST),]
- pcf_input=pcf_input[which(pcf_input$position>=chr_loc[i,"start"] & pcf_input$position<=chr_loc[i,"end"]),] # use only regions covered with gcCorrect LogR range
- PCF=pcf(pcf_input,gamma=GAMMA_IVD,kmin = KMIN_IVD)
- pdf(paste0(PCF_folder,"/",GERMLINENAME,"_chr",i,"_PCF_plot.pdf"))
- plotChrom(pcf_input,PCF)
- dev.off()
- PCF$diff=PCF$end.pos-PCF$start.pos
-
+ i <- chrom
+ log_info("chrom={i}")
+ pcf_input <- data.frame(chr = i, position = GL_OHET[[i]]$Position, IVD = (GL_OHET[[i]]$Position_dist_percent))
+ pcf_input <- pcf_input[which(pcf_input$position < chr_loc[i, "cen.left.base"] - CENTROMERE_DIST | pcf_input$position > chr_loc[i, "cen.right.base"] + CENTROMERE_DIST), ]
+ # use only regions covered with gcCorrect LogR range
+ pcf_input <- pcf_input[which(pcf_input$position >= chr_loc[i, "start"] & pcf_input$position <= chr_loc[i, "end"]), ]
+ PCF <- copynumber::pcf(pcf_input, gamma = GAMMA_IVD, kmin = KMIN_IVD)
+ grDevices::pdf(paste0(
+ PCF_folder, "/", GERMLINENAME, "_chr", i, "_PCF_plot.pdf"
+ ))
+ copynumber::plotChrom(pcf_input, PCF)
+ grDevices::dev.off()
+ PCF$diff <- PCF$end.pos - PCF$start.pos
+
# Decide if there is any LOH based on PCF and chr_snp_density
- chr_snp_density=nrow(pcf_input)/(pcf_input$position[nrow(pcf_input)]-pcf_input$position[1]) # density of HET SNPs across the region covered by HET SNPs
- #CALCULATE min_normal_snp_density#
+ chr_snp_density <- nrow(pcf_input) / (pcf_input$position[nrow(pcf_input)] - pcf_input$position[1]) # density of HET SNPs across the region covered by HET SNPs
+ # CALCULATE min_normal_snp_density#
# minimum normal density for SNPs (in bps) is 3 x 10^-4 with median of 7 x 10^-4
####
- min_normal_snp_density=0.0001
- loh_regions=PCF[which(round(PCF$mean,3)>0.001),] # LOH regions
- loh_regions=loh_regions[which(loh_regions$n.probes>1),] # only keep segments with minimum of 2 probes (SNPs) in PCF jump
- if (nrow(loh_regions)>0){
- if (mean(pcf_input$IVD)>0.01 & chr_snp_density=((pcf_input$position[nrow(pcf_input)]-pcf_input$position[1]))*0.9 & chr_snp_density>min_normal_snp_density){
- # do PCF regions cover >=90% of the chromosome & is the chromosome snp density above the minimum
- loh_regions=0 # LOH regions
- print(paste("no PCF jumps at chr",i))
+ min_normal_snp_density <- 0.0001
+ loh_regions <- PCF[which(round(PCF$mean, 3) > 0.001), ]
+ # only keep segments with minimum of 2 probes (SNPs) in PCF jump
+ loh_regions <- loh_regions[which(loh_regions$n.probes > 1), ]
+ if (nrow(loh_regions) > 0) {
+ # can change chr_snp_density from 0.00005 to 0.0001 as conservative measure - done
+ if (mean(pcf_input$IVD) > 0.01 && chr_snp_density < min_normal_snp_density) {
+ # mean(pcf_input$IVD) or mean(PCF$mean) indicates presence of jumps in IVD
+ loh_regions <- loh_regions # LOH regions
+ log_info("full-length chromosomal loss at chr {i}")
+ } else if (sum(loh_regions$diff) >= ((pcf_input$position[nrow(pcf_input)] - pcf_input$position[1])) * 0.9 && chr_snp_density > min_normal_snp_density) {
+ # do PCF regions cover >=90% of the chromosome & is the chromosome snp density above the minimum
+ loh_regions <- 0 # LOH regions
+ log_info("no PCF jumps at chr {i}")
+ } else {
+ loh_regions <- loh_regions # LOH regions
+ log_info("likely partial LOH(s) at chr {i}")
+ }
} else {
- loh_regions=loh_regions # LOH regions
- print(paste("likely partial LOH(s) at chr",i))
+ loh_regions <- 0
}
- } else {loh_regions=0}
-
- # loop to turn empty dataframe to 0 for loh_regions
- #suppressWarnings(
- # if (loh_regions[1]!=0){
- # if (nrow(loh_regions)==0){
- # loh_regions=0
- # } else {print("dataframe non-empty")}
- # } else {print("no LOH at all")})
-
- #filter regions for those next to the centromere and 'short'
- noise=NULL
- if (!is.null(nrow(loh_regions))){
- for (j in 1:nrow(loh_regions)){
- if (loh_regions$arm[j]=="p"){
- #if (loh_regions$end.pos[j]-chr_loc$cen.left.base[i]<1e5 & loh_regions$diff[j]<1e6){ #FOR EXCLUSION: max distance to centromere = 100kb , max length of short LOH region = 1Mb
- # noise=append(noise,j)
- #}
- if (loh_regions$end.pos[j]>chr_loc$cen.left.base[i] & loh_regions$diff[j] chr_loc$cen.left.base[i] && loh_regions$diff[j] < CENTROMERE_NOISE_SEG_SIZE) {
+ noise <- append(noise, j)
}
- #if (loh_regions$end.pos[j]>chr_loc$cen.left.base[i] & loh_regions$diff[j]>CENTROMERE_NOISE_SEG_SIZE & !is.na(match(chrom,c(1,9,16)))){ # Chr 1,9,16 have large heterochromatin region next to centromere
- # noise=append(noise,j)
- #}
}
- if (loh_regions$arm[j]=="q"){
- #if (loh_regions$start.pos[j]-chr_loc$cen.right.base[i]<1e5 & loh_regions$diff[j]<1e6){ #FOR EXCLUSION: max distance to centromere = 100kb , max length of short LOH region = 1Mb
- # noise=append(noise,j)
- #}
- if (loh_regions$start.pos[j]CENTROMERE_NOISE_SEG_SIZE & !is.na(match(chrom,c(1,9,16)))){ # qARM of Chr 1,9,16 have large heterochromatin region next to centromere + 100kb tolerance for start of heterochromatin region
- noise=append(noise,j)
+ # qARM of Chr 1,9,16 have large heterochromatin region next to centromere + 100kb tolerance for start of heterochromatin region
+ if (loh_regions$start.pos[j] < (chr_loc$cen.right.base[i] + 1e5) && loh_regions$diff[j] > CENTROMERE_NOISE_SEG_SIZE && !is.na(match(chrom, c(1, 9, 16)))) {
+ noise <- append(noise, j)
}
}
}
- } else {print("no 'centromere noise' calculation")}
- if (!is.null(noise)){
- LOH_regions=loh_regions[-noise,]
- } else {LOH_regions=loh_regions}
-
- #remove LOH regions in the p arm of acrocentric chromosomes 13,14,15,21 and 22
- if (!is.na(match(i,c(13:15,21:22))) & !is.null(nrow(LOH_regions))){
- LOH_regions=LOH_regions[which(LOH_regions$arm!="p"),]
+ } else {
+ log_info("no 'centromere noise' calculation")
+ }
+ if (!is.null(noise)) {
+ LOH_regions <- loh_regions[-noise, ]
+ } else {
+ LOH_regions <- loh_regions
+ }
+
+ # remove LOH regions in the p arm of acrocentric chromosomes 13,14,15,21 and 22
+ if (!is.na(match(i, c(13:15, 21:22))) && !is.null(nrow(LOH_regions))) {
+ LOH_regions <- LOH_regions[which(LOH_regions$arm != "p"), ]
}
#
- if (is.null(dim(LOH_regions))){
- print(paste("no LOH detected in chr",i))
- LOH[[i]]=0
- } else if (dim(LOH_regions)[1]!=0 & dim(LOH_regions)[2]!=0) {
- print(paste("we have LOH for",sum(LOH_regions$diff),"bp in chr",i))
- LOH[[i]]=data.frame(chr=i,LOH_regions)
- } else if (dim(LOH_regions)[1]==0) {
- print(paste("no LOH regions remained after noise correction for chr",i))
- LOH[[i]]=0
- } else {print("unkown issue!")}
- print(paste("chrom=",i,"IVD-PCF finished"))
+ if (is.null(dim(LOH_regions))) {
+ log_info("no LOH detected in chr {i}")
+ LOH[[i]] <- 0
+ } else if (dim(LOH_regions)[1] != 0 && dim(LOH_regions)[2] != 0) {
+ log_info("we have LOH for {sum(LOH_regions$diff)} bp in chr {i}")
+ LOH[[i]] <- data.frame(chr = i, LOH_regions)
+ } else if (dim(LOH_regions)[1] == 0) {
+ log_info("no LOH regions remained after noise correction for chr {i}")
+ LOH[[i]] <- 0
+ } else {
+ log_info("unkown issue!")
+ }
+ log_info("chrom={i} IVD-PCF finished")
#
##
# STEP 2 - get higher resolution LOH regions
- ##
- #
- print(paste("chrom=",i))
- # use loop to find blocks with no LOH - while taking account of the centromere - RUN1
- ac=GL_AC[[i]]
- al=GL_AL[[i]]
- names(ac)=c("chr","position",1:4,"depth")
- chr_interval=c(chr_loc[i,"start"],chr_loc[i,"end"]) # use gcCorrect LogR range for chromosome interval
- if (!is.null(nrow(LOH[[i]]))){
- non_LOH=data.frame()## get all non_LOH regions ##
- for (j in 1:(nrow(LOH[[i]])+1)){
- if (j == 1 & chr_interval[1]==LOH[[i]]$start.pos[j]){
- print("LOH from start of chromosome")
- } else if (j == 1 & chr_interval[1]1 & j <= nrow(LOH[[i]]) & LOH[[i]]$arm[j]==LOH[[i]]$arm[j-1]){
- non_loh=data.frame(start=LOH[[i]]$end.pos[j-1]+1,end=LOH[[i]]$start.pos[j]-1)
- } else if (j>1 & j <= nrow(LOH[[i]]) & LOH[[i]]$arm[j]!=LOH[[i]]$arm[j-1]){
- non_loh=data.frame(start=c(LOH[[i]]$end.pos[j-1]+1,chr_loc[i,]$cen.right.base),end=c(chr_loc[i,]$cen.left.base,LOH[[i]]$start.pos[j]-1))
- } else{
- if ((LOH[[i]]$end.pos[j-1]+1) 0) {
+ non_LOH_list <- list()
+ for (j in 1:(nrow(LOH[[i]]) + 1)) {
+ if (j == 1 && chr_interval[1] >= LOH[[i]]$start.pos[j]) {
+ # LOH starts at or before interval start
+ } else if (j == 1) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(start = chr_interval[1], end = LOH[[i]]$start.pos[j] - 1)
+ } else if (j <= nrow(LOH[[i]]) && LOH[[i]]$arm[j] == LOH[[i]]$arm[j - 1]) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(start = LOH[[i]]$end.pos[j - 1] + 1, end = LOH[[i]]$start.pos[j] - 1)
+ } else if (j <= nrow(LOH[[i]])) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(
+ start = c(LOH[[i]]$end.pos[j - 1] + 1, chr_loc[i, ]$cen.right.base),
+ end = c(chr_loc[i, ]$cen.left.base, LOH[[i]]$start.pos[j] - 1)
+ )
+ } else if ((LOH[[i]]$end.pos[j - 1] + 1) < chr_interval[2]) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(start = LOH[[i]]$end.pos[j - 1] + 1, end = chr_interval[2])
}
}
- } else {non_LOH=data.frame(start=chr_interval[1],end=chr_interval[2])} # in case no LOH is identified by IVD-PCF
- if (nrow(non_LOH)>0){
- for (j in 1:nrow(non_LOH)){
- if (non_LOH$start[j]chr_loc[i,]$cen.right.base){
- start.pos=c(non_LOH$start[j],chr_loc[i,]$cen.right.base)
- end.pos=c(chr_loc[i,]$cen.left.base,non_LOH$end[j])
- non_LOH=non_LOH[-j,]
- non_LOH=rbind(non_LOH, data.frame(start=start.pos,end=end.pos))
- }
+ non_LOH <- collapse::rowbind(non_LOH_list)
+ } else {
+ non_LOH <- data.frame(start = chr_interval[1], end = chr_interval[2])
+ }
+
+ if (nrow(non_LOH) > 0) {
+ # Check for centromere crossing and split if necessary
+ cross_idx <- which(non_LOH$start < chr_loc[i, ]$cen.left.base & non_LOH$end > chr_loc[i, ]$cen.right.base)
+ if (length(cross_idx) > 0) {
+ to_split <- non_LOH[cross_idx, ]
+ non_LOH <- non_LOH[-cross_idx, ]
+ split_list <- list(
+ non_LOH,
+ data.frame(start = to_split$start, end = chr_loc[i, ]$cen.left.base),
+ data.frame(start = chr_loc[i, ]$cen.right.base, end = to_split$end)
+ )
+ non_LOH <- collapse::rowbind(split_list)
}
- non_LOH$diff=non_LOH$end-non_LOH$start
+ non_LOH$diff <- non_LOH$end - non_LOH$start
+ # Filter out invalid segments
+ non_LOH <- non_LOH[non_LOH$diff > 0, ]
}
-
- non_LOH=non_LOH[order(non_LOH$start),] # the non_LOH should always be in order by position
-
- #STEP 2.1: identify LOH by inter-HET SNP regions # differentiating from HOM stretch in sample with logR < -0.8
- winsize=MIN_HET_DIST
- ohet=GL_OHET[[i]]
- nSNPs=as.numeric(nrow(GL_LogR))
- logr=GL_LogR[which(GL_LogR$Chromosome==i),]
- colnames(logr)[3]="LogR"
- logr$Position=as.numeric(logr$Position)
- if (!is.null(non_LOH)){ # if regions of non_LOH exist after IVD-PCF, run window-based search
- pLOH_regions=data.frame()
- if (is.na(match(i,c(13,14,15,21,22)))){
- print(paste("START",i,"p ARM"))
- PARM=non_LOH[which(non_LOH$end<=chr_loc[i,]$cen.left.base),]
- if (nrow(PARM)>0){
- #if (nrow(PARM)==1 & non_LOH$start[1]==chr_interval[1] & non_LOH$end[1]==chr_interval[2]){
- parm=PARM
- } else if (nrow(PARM)==0 & sum(non_LOH$diff)!=0) {
- parm=data.frame(start=chr_interval[1],end=chr_loc[i,]$cen.left.base-CENTROMERE_DIST)
- } else {print("unknown issue")}
-
- if (parm[nrow(parm),1]<(parm[nrow(parm),2]-CENTROMERE_DIST)){
- parm[nrow(parm),2]=parm[nrow(parm),2]-CENTROMERE_DIST # exclude the last CENTROMERE_DIST segment next to the centromere (left side) - too noisy
- } else {parm=parm[-nrow(parm),]}
- parm$diff=parm$end-parm$start
-
+
+ non_LOH <- non_LOH[order(non_LOH$start), ]
+
+ # STEP 2.1: identify LOH by inter-HET SNP regions
+ ohet <- GL_OHET[[i]]
+ nSNPs <- as.numeric(nrow(GL_LogR))
+ logr <- GL_LogR[which(GL_LogR$Chromosome == i), ]
+ colnames(logr)[3] <- "LogR"
+ logr$Position <- as.numeric(logr$Position)
+ # if regions of non_LOH exist after IVD-PCF, run window-based search
+ if (!is.null(non_LOH)) {
+ pLOH_regions <- data.frame()
+ if (is.na(match(i, c(13, 14, 15, 21, 22)))) {
+ log_info("START {i} p ARM")
+ PARM <- non_LOH[which(non_LOH$end <= chr_loc[i, ]$cen.left.base), ]
+ if (nrow(PARM) > 0) {
+ parm <- PARM
+ } else if (nrow(PARM) == 0 && sum(non_LOH$diff) != 0) {
+ parm <- data.frame(start = chr_interval[1], end = chr_loc[i, ]$cen.left.base - CENTROMERE_DIST)
+ } else {
+ log_info("unknown issue")
+ }
+
+ if (parm[nrow(parm), 1] < (parm[nrow(parm), 2] - CENTROMERE_DIST)) {
+ # exclude the last CENTROMERE_DIST segment next to the centromere (left side) - too noisy
+ parm[nrow(parm), 2] <- parm[nrow(parm), 2] - CENTROMERE_DIST
+ } else {
+ parm <- parm[-nrow(parm), ]
+ }
+ parm$diff <- parm$end - parm$start
+
# search per non_LOH segment
- for (seg in 1:nrow(parm)){
- LoH=data.frame()
- #IVD-based breakpoints for small regions#
- seg_ivd=ohet[which(ohet$Position_dist>=MIN_HET_DIST & ohet$Position>=parm$start[seg] & ohet$Position<=parm$end[seg]),]
- #if (!is.null(nrow(seg_ivd))){
- if (nrow(seg_ivd)>0){
- win=nrow(seg_ivd)
- print(win)
- # win=floor(parm$diff[seg]/winsize)
- # print(win)
- #if (win>0){
- for (j in 1:win){
- loh=NULL
- start=seg_ivd$Position[j]
- end=start+seg_ivd$Position_dist[j]
- COV=logr[which(logr$Position>start & logr$Position0.5){ # to use a minimum SNP density of 0.5 to get logR estimate
- if (!is.na(cov) & !is.null(denSNP) & denSNP>0.5){ # to use a minimum SNP density of 0.5 to get logR estimate AND not put the cov cut-off before applying PCF
- #loh=data.frame(start=start,end=end,LogR=cov,medianLogR=medcov,denSNP=denSNP)
- jpcf=pcf(COV,gamma=GAMMA_LOGR,verbose = F)
- jpcf=jpcf[which(jpcf$mean < -0.8),]
- if (nrow(jpcf)>0){
- loh=data.frame(start=jpcf$start.pos[1],end=jpcf$end.pos[nrow(jpcf)],LogR=mean(jpcf$mean),denSNP=denSNP)
- loh$N=nrow(logr[which(logr$Position>=loh$start & logr$Position<=loh$end),])
- if (loh$N<10){loh=NULL} # if LOH region is supported by less than 10 SNPs, then remove it
+ for (seg in seq_len(nrow(parm))) {
+ LoH_list <- list()
+ # IVD-based breakpoints for small regions#
+ seg_ivd <- ohet[which(ohet$Position_dist >= MIN_HET_DIST & ohet$Position >= parm$start[seg] & ohet$Position <= parm$end[seg]), ]
+ if (nrow(seg_ivd) > 0) {
+ # Pre-calculate indices for logr to avoid repeated subsetting
+ logr_in_seg_idx <- which(logr$Position >= parm$start[seg] & logr$Position <= parm$end[seg])
+ if (length(logr_in_seg_idx) > 0) {
+ logr_seg <- logr[logr_in_seg_idx, ]
+ # Using findInterval to quickly get boundaries for each window
+ starts_idx <- findInterval(seg_ivd$Position, logr_seg$Position) + 1
+ ends_idx <- findInterval(seg_ivd$Position + seg_ivd$Position_dist, logr_seg$Position)
+
+ for (j in seq_len(nrow(seg_ivd))) {
+ if (starts_idx[j] > ends_idx[j]) next
+
+ COV <- logr_seg[starts_idx[j]:ends_idx[j], ]
+ cov <- mean(COV$LogR)
+ denSNP <- nrow(COV) / (nSNPs / sum(chr_loc$length) * seg_ivd$Position_dist[j])
+
+ if (!is.na(cov) && denSNP > 0.5) {
+ jpcf <- copynumber::pcf(COV, gamma = GAMMA_LOGR, verbose = FALSE)
+ jpcf_loh <- jpcf[which(jpcf$mean < -0.8), ]
+ if (nrow(jpcf_loh) > 0) {
+ loh <- data.frame(
+ start = jpcf_loh$start.pos[1],
+ end = jpcf_loh$end.pos[nrow(jpcf_loh)],
+ LogR = mean(jpcf_loh$mean),
+ denSNP = denSNP,
+ stringsAsFactors = FALSE
+ )
+ # Count SNPs in the actual LOH region
+ loh$N <- sum(COV$Position >= loh$start & COV$Position <= loh$end)
+ if (loh$N >= 10) {
+ LoH_list[[length(LoH_list) + 1]] <- loh
+ }
+ }
}
}
- if (!is.null(loh)){
- LoH=rbind(LoH,loh)
- }
- if (j %% 100 ==0){
- print(paste("interval=",j))
- }
}
- } else {print(paste("no het SNPs in segment",seg))}
- # no. of LOH intervals
- print(paste("p-arm nrow(LOH) segment",seg,"=",nrow(LoH)))
- if (nrow(LoH)==0){
- print(paste("No LOH identified in p-arm segment",seg))
- } else{
- if (nrow(LoH)==1){
- LoH_regions=data.frame(chrom=i,arm="p",start.pos=LoH$start,end.pos=LoH$end)
- }
- if (nrow(LoH)>1){
- #combine smaller regions into larger regions of LOH
- LoH_regions=data.frame()
- start=LoH$start[1]
- for (j in 2:nrow(LoH)){
- print(j)
- if (LoH$start[j]==LoH$end[j-1]){
- end=LoH$end[j] # include the new row (i) in the merge
- }
- else {
- end=LoH$end[j-1] # stop merge at the previous row (i-1)
- LoH_regions=rbind(LoH_regions,data.frame(chrom=i,arm="p",start.pos=start,end.pos=end))
- start=LoH$start[j]
+ }
+
+ if (length(LoH_list) > 0) {
+ LoH <- collapse::rowbind(LoH_list)
+ # Combine smaller regions into larger regions of LOH
+ LoH_regions_list <- list()
+ if (nrow(LoH) > 0) {
+ start <- LoH$start[1]
+ end <- LoH$end[1]
+ if (nrow(LoH) > 1) {
+ for (j in 2:nrow(LoH)) {
+ if (LoH$start[j] == end) {
+ end <- LoH$end[j]
+ } else {
+ LoH_regions_list[[length(LoH_regions_list) + 1]] <- data.frame(chrom = i, arm = "p", start.pos = start, end.pos = end)
+ start <- LoH$start[j]
+ end <- LoH$end[j]
+ }
}
}
- # add final block if it ends at the end of the LoH dataframe
- if (end==LoH$end[nrow(LoH)]){
- LoH_regions=rbind(LoH_regions,data.frame(chrom=i,arm="p",start.pos=start,end.pos=end))
- }
- else if (start==LoH$start[nrow(LoH)] & end==LoH$end[nrow(LoH)-1]){
- LoH_regions=rbind(LoH_regions,data.frame(chrom=i,arm="p",start.pos=start,end.pos=LoH$end[nrow(LoH)]))
- }
+ LoH_regions_list[[length(LoH_regions_list) + 1]] <- data.frame(chrom = i, arm = "p", start.pos = start, end.pos = end)
}
- pLOH_regions=rbind(pLOH_regions,LoH_regions)
+ pLOH_regions <- rbind(pLOH_regions, collapse::rowbind(LoH_regions_list))
}
}
- if (nrow(pLOH_regions)>0){
- #pARM BAF/LogR plot(s)
- pdf(paste0(GERMLINENAME,"_chr",i,"_",MIN_HET_DIST/1e3,"k_based_pLOH_events.pdf"))
+ if (nrow(pLOH_regions) > 0) {
+ # pARM BAF/LogR plot(s)
+ grDevices::pdf(paste0(GERMLINENAME, "_chr", i, "_", MIN_HET_DIST / 1e3, "k_based_pLOH_events.pdf"))
suppressWarnings(
- for (s in 1:nrow(pLOH_regions)){
- sBAF=ggplot(ohet,aes(Position,baf))+geom_jitter()+ylim(0,1)+
- geom_vline(xintercept = c(pLOH_regions$start.pos[s],pLOH_regions$end.pos[s]),col="red",linetype="longdash")+
- xlim(pLOH_regions$start.pos[s]-LENGTH_ADJACENT,pLOH_regions$end.pos[s]+LENGTH_ADJACENT)+
- ggtitle(paste("pARM LOH region",s))+labs(y="BAF")
- sLogR=ggplot(logr,aes(Position,LogR))+geom_jitter()+ylim(-5.2,1.2)+
- geom_vline(xintercept = c(pLOH_regions$start.pos[s],pLOH_regions$end.pos[s]),col="red",linetype="longdash")+
- xlim(pLOH_regions$start.pos[s]-LENGTH_ADJACENT,pLOH_regions$end.pos[s]+LENGTH_ADJACENT)
- grid.newpage()
- grid.draw(rbind(ggplotGrob(sBAF), ggplotGrob(sLogR), size = "last"))
- #print(plot_grid(sBAF,sLogR, ncol = 1, align = "v"))
+ for (s in seq_len(nrow(pLOH_regions))) {
+ sBAF <- ggplot2::ggplot(
+ ohet, ggplot2::aes(Position, baf)
+ ) +
+ ggplot2::geom_jitter() +
+ ggplot2::ylim(0, 1) +
+ ggplot2::geom_vline(
+ xintercept = c(pLOH_regions$start.pos[s], pLOH_regions$end.pos[s]),
+ col = "red", linetype = "longdash"
+ ) +
+ ggplot2::xlim(
+ pLOH_regions$start.pos[s] - LENGTH_ADJACENT,
+ pLOH_regions$end.pos[s] + LENGTH_ADJACENT
+ ) +
+ ggplot2::ggtitle(paste("pARM LOH region", s)) +
+ ggplot2::labs(y = "BAF")
+ sLogR <- ggplot2::ggplot(
+ logr,
+ ggplot2::aes(Position, LogR)
+ ) +
+ ggplot2::geom_jitter() +
+ ggplot2::ylim(-5.2, 1.2) +
+ ggplot2::geom_vline(
+ xintercept = c(pLOH_regions$start.pos[s], pLOH_regions$end.pos[s]),
+ col = "red", linetype = "longdash"
+ ) +
+ ggplot2::xlim(
+ pLOH_regions$start.pos[s] - LENGTH_ADJACENT,
+ pLOH_regions$end.pos[s] + LENGTH_ADJACENT
+ )
+ grid::grid.newpage()
+ grid::grid.draw(
+ rbind(ggplot2::ggplotGrob(sBAF),
+ ggplot2::ggplotGrob(sLogR),
+ size = "last"
+ )
+ )
}
)
- dev.off()
+ grDevices::dev.off()
#
- print("Candidate LOH regions plotted for pARM")
+ log_info("Candidate LOH regions plotted for pARM")
}
- } else {print(paste("chr",i,"is acrocentric - no p arm analysis"))}
+ } else {
+ log_info("chr {i} is acrocentric - no p arm analysis")
+ }
# Q ARM RUN:
- print(paste("START",i,"q ARM"))
- qLOH_regions=data.frame()
- QARM=non_LOH[which(non_LOH$start>=chr_loc[i,]$cen.right.base),]
- if (nrow(QARM)>0){
- #if (nrow(PARM)==1 & non_LOH$start[1]==chr_interval[1] & non_LOH$end[1]==chr_interval[2]){
- qarm=QARM
- } else if (nrow(QARM)==0 & sum(non_LOH$diff)!=0) {
- qarm=data.frame(start=chr_loc[i,]$cen.right.base,end=chr_interval[2])
- } else {print("unknown issue")}
- qarm[1,1]=qarm[1,1]+CENTROMERE_DIST # to exclude the first CENTROMERE_DIST next to the centromere (right side) - noisy
- qarm$diff=qarm$end-qarm$start
+ log_info("START {i} q ARM")
+ qLOH_regions <- data.frame()
+ QARM <- non_LOH[which(non_LOH$start >= chr_loc[i, ]$cen.right.base), ]
+ if (nrow(QARM) > 0) {
+ qarm <- QARM
+ } else if (nrow(QARM) == 0 && sum(non_LOH$diff) != 0) {
+ qarm <- data.frame(start = chr_loc[i, ]$cen.right.base, end = chr_interval[2])
+ } else {
+ log_info("unknown issue")
+ }
+ # to exclude the first CENTROMERE_DIST next to the centromere (right side) - noisy
+ qarm[1, 1] <- qarm[1, 1] + CENTROMERE_DIST
+ qarm$diff <- qarm$end - qarm$start
#
# search per non_LOH segment
- for (seg in 1:nrow(qarm)){
- LoH=data.frame()
- #IVD-based breakpoints for small regions#
- seg_ivd=ohet[which(ohet$Position_dist>=MIN_HET_DIST & ohet$Position>=qarm$start[seg] & ohet$Position<=qarm$end[seg]),]
- #if (!is.null(nrow(seg_ivd))){
- if (nrow(seg_ivd)>0){
- win=nrow(seg_ivd)
- print(win)
- # win=floor(qarm$diff[seg]/winsize)
- # print(win)
- #if (win>0){
- for (j in 1:win){
- loh=NULL
- start=seg_ivd$Position[j]
- end=start+seg_ivd$Position_dist[j]
- COV=logr[which(logr$Position>start & logr$Position0.5){ # to use a minimum SNP density of 0.5 to get logR estimate
- if (!is.na(cov) & !is.null(denSNP) & denSNP>0.5){ # to use a minimum SNP density of 0.5 to get logR estimate AND not put the cov cut-off before applying PCF
- #loh=data.frame(start=start,end=end,LogR=cov,medianLogR=medcov,denSNP=denSNP)
- jpcf=pcf(COV,gamma=GAMMA_LOGR,verbose = F)
- jpcf=jpcf[which(jpcf$mean < -0.8),]
- if (nrow(jpcf)>0){
- loh=data.frame(start=jpcf$start.pos[1],end=jpcf$end.pos[nrow(jpcf)],LogR=mean(jpcf$mean),denSNP=denSNP)
- loh$N=nrow(logr[which(logr$Position>=loh$start & logr$Position<=loh$end),])
- if (loh$N<10){loh=NULL} # if LOH region is supported by less than 10 SNPs, then remove it
+ for (seg in seq_len(nrow(qarm))) {
+ LoH_list <- list()
+ # IVD-based breakpoints for small regions#
+ seg_ivd <- ohet[which(ohet$Position_dist >= MIN_HET_DIST & ohet$Position >= qarm$start[seg] & ohet$Position <= qarm$end[seg]), ]
+ if (nrow(seg_ivd) > 0) {
+ # Pre-calculate indices for logr
+ logr_in_seg_idx <- which(logr$Position >= qarm$start[seg] & logr$Position <= qarm$end[seg])
+ if (length(logr_in_seg_idx) > 0) {
+ logr_seg <- logr[logr_in_seg_idx, ]
+ starts_idx <- findInterval(seg_ivd$Position, logr_seg$Position) + 1
+ ends_idx <- findInterval(seg_ivd$Position + seg_ivd$Position_dist, logr_seg$Position)
+
+ for (j in seq_len(nrow(seg_ivd))) {
+ if (starts_idx[j] > ends_idx[j]) next
+
+ COV <- logr_seg[starts_idx[j]:ends_idx[j], ]
+ cov <- mean(COV$LogR)
+ denSNP <- nrow(COV) / (nSNPs / sum(chr_loc$length) * seg_ivd$Position_dist[j])
+
+ if (!is.na(cov) && denSNP > 0.5) {
+ jpcf <- copynumber::pcf(COV, gamma = GAMMA_LOGR, verbose = FALSE)
+ jpcf_loh <- jpcf[which(jpcf$mean < -0.8), ]
+ if (nrow(jpcf_loh) > 0) {
+ loh <- data.frame(
+ start = jpcf_loh$start.pos[1],
+ end = jpcf_loh$end.pos[nrow(jpcf_loh)],
+ LogR = mean(jpcf_loh$mean),
+ denSNP = denSNP,
+ stringsAsFactors = FALSE
+ )
+ loh$N <- sum(COV$Position >= loh$start & COV$Position <= loh$end)
+ if (loh$N >= 10) {
+ LoH_list[[length(LoH_list) + 1]] <- loh
+ }
+ }
}
}
- if (!is.null(loh)){
- LoH=rbind(LoH,loh)
- }
- if (j %% 100 ==0){
- print(paste("interval=",j))
- }
- }
- } else {print(paste("no het SNPs in segment",seg))}
-
- # no. of LoH intervals
- print(paste("q-arm nrow(LoH) segment",seg,"=",nrow(LoH)))
- if (nrow(LoH)==0){
- print(paste("No LOH identified in q-arm segment",seg))
- } else {
- if (nrow(LoH)==1){
- LoH_regions=data.frame(chrom=i,arm="q",start.pos=LoH$start,end.pos=LoH$end)
}
- if (nrow(LoH)>1){
- LoH_regions=data.frame()
- #combine smaller regions into larger regions of LOH
- start=LoH$start[1]
- for (j in 2:nrow(LoH)){
- print(j)
- if (LoH$start[j]==LoH$end[j-1]){
- end=LoH$end[j] # include the new row (i) in the merge
- }
- else {
- end=LoH$end[j-1] # stop merge at the previous row (i-1)
- LoH_regions=rbind(LoH_regions,data.frame(chrom=i,arm="q",start.pos=start,end.pos=end))
- start=LoH$start[j]
+ }
+
+ if (length(LoH_list) > 0) {
+ LoH <- collapse::rowbind(LoH_list)
+ LoH_regions_list <- list()
+ if (nrow(LoH) > 0) {
+ start <- LoH$start[1]
+ end <- LoH$end[1]
+ if (nrow(LoH) > 1) {
+ for (j in 2:nrow(LoH)) {
+ if (LoH$start[j] == end) {
+ end <- LoH$end[j]
+ } else {
+ LoH_regions_list[[length(LoH_regions_list) + 1]] <- data.frame(chrom = i, arm = "q", start.pos = start, end.pos = end)
+ start <- LoH$start[j]
+ end <- LoH$end[j]
+ }
}
}
- # add final block if it ends at the end of the LOH dataframe
- if (end==LoH$end[nrow(LoH)]){
- LoH_regions=rbind(LoH_regions,data.frame(chrom=i,arm="q",start.pos=start,end.pos=end))
- }
- else if (start==LoH$start[nrow(LoH)] & end==LoH$end[nrow(LoH)-1]){
- LoH_regions=rbind(LoH_regions,data.frame(chrom=i,arm="q",start.pos=start,end.pos=LoH$end[nrow(LoH)]))
- }
+ LoH_regions_list[[length(LoH_regions_list) + 1]] <- data.frame(chrom = i, arm = "q", start.pos = start, end.pos = end)
}
- qLOH_regions=rbind(qLOH_regions,LoH_regions)
+ qLOH_regions <- rbind(qLOH_regions, collapse::rowbind(LoH_regions_list))
}
}
- if (nrow(qLOH_regions)>0){
- #qARM BAF/LogR plot(s)
- pdf(paste0(GERMLINENAME,"_chr",i,"_",MIN_HET_DIST/1e3,"k_based_qLOH_events.pdf"))
+ if (nrow(qLOH_regions) > 0) {
+ # qARM BAF/LogR plot(s)
+ grDevices::pdf(paste0(GERMLINENAME, "_chr", i, "_", MIN_HET_DIST / 1e3, "k_based_qLOH_events.pdf"))
suppressWarnings(
- for (s in 1:nrow(qLOH_regions)){
- sBAF=ggplot(ohet,aes(Position,baf))+geom_jitter()+ylim(0,1)+
- geom_vline(xintercept = c(qLOH_regions$start.pos[s],qLOH_regions$end.pos[s]),col="red",linetype="longdash")+
- xlim(qLOH_regions$start.pos[s]-LENGTH_ADJACENT,qLOH_regions$end.pos[s]+LENGTH_ADJACENT)+
- ggtitle(paste("qARM LOH region",s))
- sLogR=ggplot(logr,aes(Position,LogR))+geom_jitter()+ylim(-5.2,1.2)+
- geom_vline(xintercept = c(qLOH_regions$start.pos[s],qLOH_regions$end.pos[s]),col="red",linetype="longdash")+
- xlim(qLOH_regions$start.pos[s]-LENGTH_ADJACENT,qLOH_regions$end.pos[s]+LENGTH_ADJACENT)
- grid.newpage()
- grid.draw(rbind(ggplotGrob(sBAF), ggplotGrob(sLogR), size = "last"))
- #print(plot_grid(sBAF,sLogR, ncol = 1, align = "v"))
+ for (s in seq_len(nrow(qLOH_regions))) {
+ sBAF <- ggplot2::ggplot(ohet, ggplot2::aes(Position, baf)) +
+ ggplot2::geom_jitter() +
+ ggplot2::ylim(0, 1) +
+ ggplot2::geom_vline(
+ xintercept = c(
+ qLOH_regions$start.pos[s],
+ qLOH_regions$end.pos[s]
+ ),
+ col = "red", linetype = "longdash"
+ ) +
+ ggplot2::xlim(
+ qLOH_regions$start.pos[s] - LENGTH_ADJACENT,
+ qLOH_regions$end.pos[s] + LENGTH_ADJACENT
+ ) +
+ ggplot2::ggtitle(paste("qARM LOH region", s))
+ sLogR <- ggplot2::ggplot(
+ logr, ggplot2::aes(Position, LogR)
+ ) +
+ ggplot2::geom_jitter() +
+ ggplot2::ylim(-5.2, 1.2) +
+ ggplot2::geom_vline(
+ xintercept = c(
+ qLOH_regions$start.pos[s],
+ qLOH_regions$end.pos[s]
+ ),
+ col = "red", linetype = "longdash"
+ ) +
+ ggplot2::xlim(
+ qLOH_regions$start.pos[s] - LENGTH_ADJACENT,
+ qLOH_regions$end.pos[s] + LENGTH_ADJACENT
+ )
+ grid::grid.newpage()
+ grid::grid.draw(rbind(
+ ggplot2::ggplotGrob(sBAF),
+ ggplot2::ggplotGrob(sLogR),
+ size = "last"
+ ))
}
)
- dev.off()
+ grDevices::dev.off()
#
- print("Candidate LOH regions plotted for qARM")
+ log_info("Candidate LOH regions plotted for qARM")
}
- #STEP 2.2: clean-up LOH[[i]] and merge LOH regions of both methods
-
- noLOH=NULL
- if (!is.null(nrow(LOH[[i]]))){
- for (j in 1:nrow(LOH[[i]])){
- LOH[[i]]$logR[j]=mean(logr[which(logr$Position>=LOH[[i]]$start.pos[j] & logr$Position<=LOH[[i]]$end.pos[j]),][,3])
- LOH[[i]]$nSNP[j]=nrow(logr[which(logr$Position>=LOH[[i]]$start.pos[j] & logr$Position<=LOH[[i]]$end.pos[j]),])
- LOH[[i]]$denSNP[j]=LOH[[i]]$nSNP[j]/((LOH[[i]]$end.pos[j]-LOH[[i]]$start.pos[j])*nSNPs/sum(chr_loc$length))
- if (LOH[[i]]$logR[j]>-0.8 | LOH[[i]]$denSNP[j]<0.5){
- noLOH=append(noLOH,j)
- print(j)
+ # STEP 2.2: clean-up LOH[[i]] and merge LOH regions of both methods
+
+ noLOH <- NULL
+ if (!is.null(nrow(LOH[[i]]))) {
+ for (j in seq_len(nrow(LOH[[i]]))) {
+ LOH[[i]]$logR[j] <- mean(logr[which(logr$Position >= LOH[[i]]$start.pos[j] & logr$Position <= LOH[[i]]$end.pos[j]), ][, 3])
+ LOH[[i]]$nSNP[j] <- nrow(logr[which(logr$Position >= LOH[[i]]$start.pos[j] & logr$Position <= LOH[[i]]$end.pos[j]), ])
+ LOH[[i]]$denSNP[j] <- LOH[[i]]$nSNP[j] / ((LOH[[i]]$end.pos[j] - LOH[[i]]$start.pos[j]) * nSNPs / sum(chr_loc$length))
+ if (LOH[[i]]$logR[j] > -0.8 || LOH[[i]]$denSNP[j] < 0.5) {
+ noLOH <- append(noLOH, j)
+ log_info("j: '{j}'")
}
}
- LOH[[i]]=LOH[[i]][-noLOH,]
- LOH[[i]]=ifelse(nrow(LOH[[i]])==0,0,LOH[[i]])
+ LOH[[i]] <- LOH[[i]][-noLOH, ]
+ LOH[[i]] <- ifelse(nrow(LOH[[i]]) == 0, 0, LOH[[i]])
+ }
+
+ LOH_regions <- data.frame()
+ if (nrow(pLOH_regions) > 0) {
+ LOH_regions <- rbind(LOH_regions, pLOH_regions)
+ } else {
+ log_info("no window-based LOH regions identified in p arm of non_LOH of IVD-PCF")
+ }
+ if (nrow(qLOH_regions) > 0) {
+ if (nrow(LOH_regions) > 0) {
+ LOH_regions <- collapse::rowbind(LOH_regions, qLOH_regions)
+ } else {
+ LOH_regions <- qLOH_regions
+ }
}
-
- LOH_regions=data.frame()
- if (nrow(pLOH_regions)>0){
- LOH_regions=rbind(LOH_regions,pLOH_regions)
- } else {print("no window-based LOH regions identified in p arm of non_LOH of IVD-PCF")}
- if (nrow(qLOH_regions)>0){
- LOH_regions=rbind(LOH_regions,qLOH_regions)
- } else {print("no window-based LOH regions identified in q arm of non_LOH of IVD-PCF")}
- if (nrow(LOH_regions)>0){
- if (!is.null(nrow(LOH[[i]]))){
- LOH[[i]]=rbind(LOH[[i]][,c("chrom","arm","start.pos","end.pos")],LOH_regions)
- LOH[[i]]=LOH[[i]][order(LOH[[i]]$start.pos),]
+
+ if (nrow(LOH_regions) > 0) {
+ if (!is.null(LOH[[i]]) && !is.null(nrow(LOH[[i]])) && nrow(LOH[[i]]) > 0) {
+ LOH[[i]] <- collapse::rowbind(LOH[[i]][, c("chrom", "arm", "start.pos", "end.pos")], LOH_regions)
+ LOH[[i]] <- LOH[[i]][order(LOH[[i]]$start.pos), ]
} else {
- LOH[[i]]=LOH_regions
+ LOH[[i]] <- LOH_regions
}
}
-
-
- #combine adjacent regions into larger regions of LOH
- if (!is.null(nrow(LOH[[i]]))){
- LOH[[i]]=LOH[[i]][!duplicated(LOH[[i]]),]
- LOHall=data.frame()
- ChrArms=unique(LOH[[i]]$arm)
- for (arm in ChrArms){
- LOHarm=LOH[[i]][LOH[[i]]$arm==arm,]
- if (nrow(LOHarm)>1){
- start=LOHarm$start.pos[1]
- for (j in 2:nrow(LOHarm)){
- print(j)
- if (LOHarm$start.pos[j]==LOHarm$end.pos[j-1]){
- end=LOHarm$end.pos[j] # include the new row (i) in the merge
+
+ # combine adjacent regions into larger regions of LOH
+ if (!is.null(LOH[[i]]) && !is.null(nrow(LOH[[i]])) && nrow(LOH[[i]]) > 0) {
+ LOH[[i]] <- LOH[[i]][!duplicated(LOH[[i]]), ]
+ LOHall_list <- list()
+ ChrArms <- unique(LOH[[i]]$arm)
+ for (arm in ChrArms) {
+ LOHarm <- LOH[[i]][LOH[[i]]$arm == arm, ]
+ if (nrow(LOHarm) > 1) {
+ start <- LOHarm$start.pos[1]
+ end <- LOHarm$end.pos[1]
+ for (j in 2:nrow(LOHarm)) {
+ if (LOHarm$start.pos[j] <= end) {
+ end <- max(end, LOHarm$end.pos[j])
} else {
- if (LOHarm$start.pos[j]>LOHarm$end.pos[j-1]){
- end=LOHarm$end.pos[j-1] # stop merge at the previous row (i-1)
- LOHall=rbind(LOHall,data.frame(chrom=i,arm=arm,start.pos=start,end.pos=end))
- start=LOHarm$start.pos[j]
- } else if (LOHarm$start.pos[j] 0) {
+ LOHall <- LOH[[i]][, c("chrom", "arm", "start.pos", "end.pos")]
+ } else {
+ LOHall <- NULL
+ }
}
-
- if (!is.null(nrow(LOHall))){
- LOHall=LOHall[!duplicated(LOHall),]
- LOHall$diff=LOHall$end.pos-LOHall$start.pos
- } else {print(paste("no LOH (IVD and/or window-based) was identified for chr",i))}
- if (exists("non_loh")){
- rm(non_loh)}
- if (exists("non_LOH")){
- rm(non_LOH)
+
+ if (!is.null(LOHall) && !is.null(nrow(LOHall)) && nrow(LOHall) > 0) {
+ LOHall <- LOHall[!duplicated(LOHall), ]
+ LOHall$diff <- LOHall$end.pos - LOHall$start.pos
}
-
- #STEP 3####################################################################################################################################################
- # RECONSTRUCT alleleCounter files for the pseudo-NORMAL sample
- # use loop to find intervening blocks with no LOH - while taking account of the centromere - RUN2#
- if (!is.null(nrow(LOHall))){
- names(ac)=c("chr","position",1:4,"depth")
- chr_interval=c(ac$position[1],ac$position[nrow(ac)])
- non_LOH=data.frame()####################################### get all non_LOH regions#
- for (j in 1:(nrow(LOHall)+1)){
- if (j == 1 & chr_interval[1]==LOHall$start.pos[j]){
- print("LOH from start of chromosome")
- } else if (j == 1 & chr_interval[1]1 & j <= nrow(LOHall) & LOHall$arm[j]==LOHall$arm[j-1]){
- non_loh=data.frame(start=LOHall$end.pos[j-1]+1,end=LOHall$start.pos[j]-1)
- print("TWO")
- } else if (j>1 & j <= nrow(LOHall) & LOHall$arm[j]!=LOHall$arm[j-1]){
- non_loh=data.frame(start=c(min(LOHall$end.pos[j-1]+1,chr_loc[i,]$cen.left.base),chr_loc[i,]$cen.right.base),end=c(chr_loc[i,]$cen.left.base,LOHall$start.pos[j]-1))
- print("THREE")
- } else{
- if ((LOHall$end.pos[j-1]+1) 0) {
+ names(ac) <- c("chr", "position", "A", "C", "G", "T", "depth")
+ chr_interval <- c(ac$position[1], ac$position[nrow(ac)])
+
+ # Get non_LOH regions based on LOHall
+ non_LOH_list <- list()
+ for (j in 1:(nrow(LOHall) + 1)) {
+ if (j == 1 && chr_interval[1] >= LOHall$start.pos[j]) {} else if (j == 1) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(start = chr_interval[1], end = LOHall$start.pos[j] - 1)
+ } else if (j <= nrow(LOHall) && LOHall$arm[j] == LOHall$arm[j - 1]) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(start = LOHall$end.pos[j - 1] + 1, end = LOHall$start.pos[j] - 1)
+ } else if (j <= nrow(LOHall)) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(
+ start = c(min(LOHall$end.pos[j - 1] + 1, chr_loc[i, ]$cen.left.base), chr_loc[i, ]$cen.right.base),
+ end = c(chr_loc[i, ]$cen.left.base, LOHall$start.pos[j] - 1)
+ )
+ } else if ((LOHall$end.pos[j - 1] + 1) < chr_interval[2]) {
+ non_LOH_list[[length(non_LOH_list) + 1]] <- data.frame(start = LOHall$end.pos[j - 1] + 1, end = chr_interval[2])
}
- print(j)
- if (exists("non_loh")){
- non_LOH=rbind(non_LOH,non_loh)
+ }
+ non_LOH <- collapse::rowbind(non_LOH_list)
+ non_LOH <- non_LOH[non_LOH$end >= non_LOH$start, ]
+
+ if (nrow(non_LOH) > 0) {
+ non_LOH$length <- non_LOH$end - non_LOH$start
+ non_LOH_length <- sum(non_LOH$length)
+ if (non_LOH_length > 1e6) {
+ SNP_interval <- non_LOH_length / max(1, nrow(GL_OHET[[i]]))
+ } else {
+ SNP_interval <- 2000
}
- }
- # the non-LOH region length from PCF is:
- if (!is.null(nrow(non_LOH))){
- non_LOH$length=non_LOH$end-non_LOH$start
- non_LOH=non_LOH[non_LOH$length>=0,] # picks all non_LOH segments even if 1bp in length
- non_LOH_length=sum(non_LOH$length) # total length of non-LOH regions in chr i
- print(paste("Total length of non LOH regions =",non_LOH_length))
- # average Het SNP interval:
- if (non_LOH_length>1e6){ # run this only if combined non-LOH regions are at least 1Mb long
- SNP_interval=non_LOH_length/nrow(GL_OHET[[i]]) # estimate of genomic space between any two Het SNPs
- } else {SNP_interval = 2000} # replace with 5000 to increase run speed!?
- # no. of SNPs to be Hets in the LOH region (COMBINED FOR THE WHOLE CHROMOSOME):
- LOH_hetSNP_number=floor(sum(LOHall$diff)/SNP_interval)
- print(paste("No. of Het SNPs to be added to LOH regions:",LOH_hetSNP_number))
+ } else {
+ SNP_interval <- 2000
}
- # reconstruct allele counts for the LOH region based on actual depth for all to be perfect heterozygotes - allele counts remain as integers
- lohs=data.frame() ####################################### get all non_LOH regions####
- for (j in 1:nrow(LOHall)){
- loh=ac[which(ac$position>=LOHall$start.pos[j] & ac$position<=LOHall$end.pos[j]),]
- m=merge(loh,al,"position")
- if (nrow(m)==nrow(loh)){
- print("merge OK")
- } else {print("ERROR - merge not OK")}
- # RE-reconstruct allele counts for LOH region
- hetSNP_number=max(LOHall$diff[j]/SNP_interval,10) #at least ten SNPs (if available in region) should be spiked in to be heterozygotes for PCF in Battenberg to pick it up
- if (nrow(m)>=hetSNP_number){
- print("more rows in LOH region than Het SNP number")
- spike=c(1,head(which(1:nrow(m) %% floor(nrow(m)/(hetSNP_number-1))==0),-1),nrow(m)) # to make the exact breakpoints are seen by Battenberg - making 1st and last SNP in region heterozygote
- for (k in spike){
- #for (k in 1:nrow(m)){
- #if (k %% floor(nrow(m)/hetSNP_number)==0){
- m$depth[k]=max(m$depth[k],10)
- m[cbind(k,2+m$a0[k])]=ifelse(m$depth[k]%%2==0,m$depth[k]/2,ceiling(m$depth[k]/2))
- m[cbind(k,2+m$a1[k])]=ifelse(m$depth[k]%%2==0,m$depth[k]/2,floor(m$depth[k]/2))
- print(k)
- #}
+
+ # Spike in heterozygotes in LOH regions
+ lohs_list <- list()
+ for (j in seq_len(nrow(LOHall))) {
+ loh_idx <- which(ac$position >= LOHall$start.pos[j] & ac$position <= LOHall$end.pos[j])
+ if (length(loh_idx) == 0) next
+ loh <- ac[loh_idx, ]
+
+ # Merge with alleles
+ m <- merge(loh, al, by = "position")
+
+ hetSNP_number <- max(floor(LOHall$diff[j] / SNP_interval), 10)
+ if (nrow(m) >= hetSNP_number) {
+ spike <- unique(c(1, floor(seq(1, nrow(m), length.out = hetSNP_number)), nrow(m)))
+ for (k in spike) {
+ m$depth[k] <- max(m$depth[k], 10)
+ a0_col <- match(as.character(m$a0[k]), c("1", "2", "3", "4")) + 2
+ a1_col <- match(as.character(m$a1[k]), c("1", "2", "3", "4")) + 2
+ if (!is.na(a0_col)) m[k, a0_col] <- ceiling(m$depth[k] / 2)
+ if (!is.na(a1_col)) m[k, a1_col] <- floor(m$depth[k] / 2)
}
} else {
- print("less rows in LOH region than Het SNP number - turning all into Heterozygotes") # technically shouldn't happen
- for (k in 1:nrow(m)){
- m$depth[k]=max(m$depth[k],10)
- m[cbind(k,2+m$a0[k])]=ifelse(m$depth[k]%%2==0,m$depth[k]/2,ceiling(m$depth[k]/2))
- m[cbind(k,2+m$a1[k])]=ifelse(m$depth[k]%%2==0,m$depth[k]/2,floor(m$depth[k]/2))
- print(k)
+ for (k in seq_len(nrow(m))) {
+ m$depth[k] <- max(m$depth[k], 10)
+ a0_col <- match(as.character(m$a0[k]), c("1", "2", "3", "4")) + 2
+ a1_col <- match(as.character(m$a1[k]), c("1", "2", "3", "4")) + 2
+ if (!is.na(a0_col)) m[k, a0_col] <- ceiling(m$depth[k] / 2)
+ if (!is.na(a1_col)) m[k, a1_col] <- floor(m$depth[k] / 2)
}
}
- print(paste("LOH region segment",j))
- lohs=rbind(lohs,m)
+ # Reorder columns to match ac
+ lohs_list[[j]] <- m[, c("chr", "position", "A", "C", "G", "T", "depth")]
}
-
- lohs=lohs[,c("chr","position",1:4,"depth")]
- ####
- # combine alleleCounts for LOHS and non_LOH regions####
- non_lohs=data.frame()
- for (j in 1:nrow(non_LOH)){
- non_loh=ac[which(ac$position>=non_LOH$start[j] & ac$position<=non_LOH$end[j]),]
- non_lohs=rbind(non_lohs,non_loh)
- print(paste("non_LOH segment",j,"added"))
- }
- #write out as alleleCounts file - "normal" ID #####################################
- if (nrow(non_lohs)+nrow(lohs)==nrow(ac)){
- ac_out=rbind(non_lohs,lohs)
- ac_out=ac_out[order(ac_out$position),]
- write.table(ac_out,paste0(NORMALNAME,"_alleleFrequencies_chr",i,".txt"),col.names=F,row.names=F,quote=F,sep="\t")
- print(paste("reconstruction OK - new alleleCounts file generated for chr",i))
- } else {
- centro_ac=ac[which(ac$position>chr_loc$cen.left.base[i] & ac$position 0) {
+ for (j in seq_len(nrow(non_LOH))) {
+ non_lohs_list[[j]] <- ac[ac$position >= non_LOH$start[j] & ac$position <= non_LOH$end[j], ]
}
+ }
+ non_lohs <- collapse::rowbind(non_lohs_list)
+
+ # Final assembly
+ ac_out_list <- list(non_lohs, lohs)
+ # Check for centromeric SNPs not covered by LOH/non-LOH
+ covered_pos <- c(lohs$position, non_lohs$position)
+ missing_ac <- ac[!(ac$position %in% covered_pos), ]
+ if (nrow(missing_ac) > 0) {
+ ac_out_list[[3]] <- missing_ac
+ }
+
+ ac_out <- collapse::rowbind(ac_out_list)
+ ac_out <- ac_out[order(ac_out$position), ]
+ ac_out <- ac_out[!duplicated(ac_out$position), ]
+
+ data.table::fwrite(ac_out, paste0(NORMALNAME, "_alleleFrequencies_chr", i, ".txt"), col.names = FALSE, row.names = FALSE, quote = FALSE, sep = "\t")
+ log_info("reconstruction OK - new alleleCounts file generated for chr {i}")
} else {
- ac_out=ac
- write.table(ac_out,paste0(NORMALNAME,"_alleleFrequencies_chr",i,".txt"),col.names=F,row.names=F,quote=F,sep="\t")
- print(paste("No changes made to the alleleCounter file - no LOH in chr",i))
+ # No LOH identified
+ data.table::fwrite(ac, paste0(NORMALNAME, "_alleleFrequencies_chr", i, ".txt"), col.names = FALSE, row.names = FALSE, quote = FALSE, sep = "\t")
+ log_info("No changes made to the alleleCounter file - no LOH in chr {i}")
}
- print(paste("STEP 2&3 - chr",i,"completed"))
}
#' Prepare data for impute
#'
#' @param chrom The chromosome for which impute input should be generated.
-#' @param germline.allele.counts.file Output from the allele counter on the matched germline for this chromosome.
-#' @param normal.allele.counts.file Output from the allele counter on the matched normal for this chromosome.
-#' @param output.file File where the impute input for this chromosome will be written.
+#' @param germline_allele_counts_file Output from the allele counter on the matched germline for this chromosome.
+#' @param normal_allele_counts_file Output from the allele counter on the matched normal for this chromosome.
+#' @param output_file File where the impute input for this chromosome will be written.
#' @param imputeinfofile Info file with impute reference information.
-#' @param is.male Boolean denoting whether this sample is male (TRUE), or female (FALSE).
-#' @param problemLociFile A file containing genomic locations that must be discarded (optional).
-#' @param useLociFile A file containing genomic locations that must be included (optional).
-#' @param heterozygousFilter The cutoff where a SNP will be considered as heterozygous (default 0.01).
+#' @param is_male Boolean denoting whether this sample is male (TRUE), or female (FALSE).
+#' @param problem_loci_file A file containing genomic locations that must be discarded (optional).
+#' @param use_loci_file A file containing genomic locations that must be included (optional).
+#' @param heterozygous_filter The cutoff where a SNP will be considered as heterozygous (default 0.01).
#' @author dw9, sd11, Naser Ansari-Pour (BDI, Oxford)
#' @export
-generate.impute.input.wgs.germline = function(chrom, germline.allele.counts.file, normal.allele.counts.file, output.file, imputeinfofile, is.male, problemLociFile=NA, useLociFile=NA, heterozygousFilter=0.1) {
-
- # Read in the 1000 genomes reference file paths for the specified chrom
- impute.info = parse.imputeinfofile(imputeinfofile, is.male, chrom=chrom)
- chr_names = unique(impute.info$chrom)
- chrom_name = parse.imputeinfofile(imputeinfofile, is.male)$chrom[chrom]
-
- #print(paste("GenerateImputeInput is.male? ", is.male,sep=""))
- #print(paste("GenerateImputeInput #impute files? ", nrow(impute.info),sep=""))
-
- # Read in the known SNP locations from the 1000 genomes reference files
- known_SNPs = read.table(impute.info$impute_legend[1], sep=" ", header=T, stringsAsFactors=F)
- if(nrow(impute.info)>1){
- for(r in 2:nrow(impute.info)){
- known_SNPs = rbind(known_SNPs, read.table(impute.info$impute_legend[r], sep=" ", header=T, stringsAsFactors=F))
- }
+generate_impute_input_wgs_germline <- function(
+ chrom,
+ germline_allele_counts_file,
+ normal_allele_counts_file,
+ output_file,
+ imputeinfofile,
+ is_male,
+ problem_loci_file = NA,
+ use_loci_file = NA,
+ heterozygous_filter = 0.1
+) {
+ # Load impute reference info
+ impute_info <- parse_imputeinfofile(imputeinfofile, is_male, chrom = chrom)
+ chrom_name <- unique(impute_info$chrom)
+
+ # Load and combine known SNPs from legend files
+ known_SNPs <- data.table::rbindlist(
+ lapply(impute_info$impute_legend, data.table::fread, sep = " "),
+ use.names = TRUE
+ )
+ data.table::setkeyv(known_SNPs, "position")
+
+ # Filter problem loci (anti-join using base-style logic or setkey)
+ if (!is.na(problem_loci_file) && problem_loci_file != "NA") {
+ problemSNPs <- data.table::fread(
+ problem_loci_file,
+ sep = "\t",
+ select = c("Chr", "Pos")
+ )
+ # Subset using standard logical indexing to avoid NSE warnings
+ problemSNPs <- problemSNPs[problemSNPs[["Chr"]] == chrom_name, ]
+
+ data.table::setkeyv(problemSNPs, "Pos")
+ known_SNPs <- known_SNPs[!problemSNPs, on = c(position = "Pos")]
+ }
+
+ # Filter to explicitly allowed loci
+ if (!is.na(use_loci_file) && use_loci_file != "NA") {
+ use_loci <- data.table::fread(use_loci_file, sep = "\t")
+ # Using standard column access
+ goodSNPs <- use_loci[use_loci[["chr"]] == chrom_name, "pos", with = FALSE][[1]]
+ known_SNPs <- known_SNPs[known_SNPs[["position"]] %in% goodSNPs, ]
+ }
+
+ # Load allele counts
+ cnt_names <- c("chr", "position", "ref_base", "A", "C", "G", "T")
+
+ snp_data <- data.table::fread(
+ germline_allele_counts_file,
+ sep = "\t",
+ header = FALSE,
+ comment.char = "#"
+ )
+ data.table::setnames(snp_data, cnt_names)
+
+ normal_snp_data <- data.table::fread(
+ normal_allele_counts_file,
+ sep = "\t",
+ header = FALSE,
+ comment.char = "#"
+ )
+ data.table::setnames(normal_snp_data, cnt_names)
+
+ if (nrow(snp_data) == 0) {
+ log_failure("Germline allele counts file is empty: {germline_allele_counts_file}")
}
-
- # filter out bad SNPs (streaks in BAF)
- if((problemLociFile != "NA") & (!is.na(problemLociFile))) {
- problemSNPs = read.table(problemLociFile, header=T, sep="\t", stringsAsFactors=F)
- problemSNPs = problemSNPs$Pos[problemSNPs$Chr==chrom_name]
- badIndices = match(known_SNPs$position, problemSNPs)
- known_SNPs = known_SNPs[is.na(badIndices),]
- rm(problemSNPs, badIndices)
+ if (ncol(snp_data) < 7) {
+ log_failure("Germline allele counts file has fewer than 7 columns: {germline_allele_counts_file}")
}
-
- # filter 'good' SNPs (e.g. SNP6 positions)
- if((useLociFile != "NA") & (!is.na(useLociFile))) {
- goodSNPs = read.table(useLociFile, header=T, sep="\t", stringsAsFactors=F)
- goodSNPs = goodSNPs$pos[goodSNPs$chr==chrom_name]
- len = length(goodSNPs)
- goodIndices = match(known_SNPs$position, goodSNPs)
- known_SNPs = known_SNPs[!is.na(goodIndices),]
- rm(goodSNPs, goodIndices)
+
+ if (nrow(normal_snp_data) == 0) {
+ log_failure("Normal allele counts file is empty: {normal_allele_counts_file}")
+ }
+
+ data.table::setkeyv(snp_data, "position")
+ data.table::setkeyv(normal_snp_data, "position")
+
+ # Join reference SNPs to observed data
+ found_data <- known_SNPs[snp_data, nomatch = NULL][normal_snp_data, nomatch = NULL]
+
+ n <- nrow(found_data)
+ if (n == 0L) {
+ log_failure("No SNPs matched between reference and allele counts")
}
-
- # Read in the allele counts and see which known SNPs are covered
- snp_data = read.table(germline.allele.counts.file, comment.char="#", sep="\t", header=F, stringsAsFactors=F)
- normal_snp_data = read.table(normal.allele.counts.file, comment.char="#", sep="\t", header=F, stringsAsFactors=F)
- snp_data = cbind(snp_data, normal_snp_data)
- indices = match(known_SNPs$position, snp_data[,2])
- found_snp_data = snp_data[indices[!is.na(indices)],]
- rm(snp_data)
-
- # Obtain BAF for this chromosome (note: this is quicker than reading in the whole genome BAF file generated in the earlier step)
- nucleotides = c("A","C","G","T")
- ref_indices = match(known_SNPs[!is.na(indices),3], nucleotides)+ncol(normal_snp_data)+2
- alt_indices = match(known_SNPs[!is.na(indices),4], nucleotides)+ncol(normal_snp_data)+2
- BAFs = as.numeric(found_snp_data[cbind(1:nrow(found_snp_data),alt_indices)])/(as.numeric(found_snp_data[cbind(1:nrow(found_snp_data),alt_indices)])+as.numeric(found_snp_data[cbind(1:nrow(found_snp_data),ref_indices)]))
- BAFs[is.nan(BAFs)] = 0
- rm(nucleotides, ref_indices, alt_indices, found_snp_data, normal_snp_data)
-
- # Set the minimum level to use for obtaining genotypes
- minBaf = min(heterozygousFilter, 1.0-heterozygousFilter)
- maxBaf = max(heterozygousFilter, 1.0-heterozygousFilter)
-
- # Obtain genotypes that impute2 is able to understand
- genotypes = array(0,c(sum(!is.na(indices)),3))
- genotypes[BAFs<=minBaf,1] = 1
- genotypes[BAFs>minBaf & BAFs=maxBaf,3] = 1
-
- # Create the output
- snp.names = paste("snp",1:sum(!is.na(indices)), sep="")
- out.data = cbind(snp.names, known_SNPs[!is.na(indices),1:4], genotypes)
-
- write.table(out.data, file=output.file, row.names=F, col.names=F, quote=F)
- if(is.na(as.numeric(chrom_name))) {
- sample.g.file = paste(dirname(output.file), "/sample_g.txt", sep="")
- #not sure this is necessary, because only the PAR regions are used for males
- #if(is.male){
- # sample_g_data=data.frame(ID_1=c(0,"INDIVI1"),ID_2=c(0,"INDIVI1"),missing=c(0,0),sex=c("D",1))
- #}else{
- sample_g_data = data.frame(ID_1=c(0,"INDIVI1"), ID_2=c(0,"INDIVI1"), missing=c(0,0), sex=c("D",2))
- #}
- write.table(sample_g_data, file=sample.g.file, row.names=F, col.names=T, quote=F)
+
+ # Compute BAF
+ # Accessing columns by character strings to avoid NSE
+ ref_cols <- paste0("i.", found_data[["a0"]])
+ alt_cols <- paste0("i.", found_data[["a1"]])
+ rows <- seq_len(n)
+
+ # Column indexing via match ensures no variable binding issues
+ ref_counts <- as.numeric(found_data[cbind(rows, match(ref_cols, names(found_data)))])
+ alt_counts <- as.numeric(found_data[cbind(rows, match(alt_cols, names(found_data)))])
+
+ BAFs <- alt_counts / (alt_counts + ref_counts)
+ BAFs[is.nan(BAFs)] <- 0
+
+ # Generate genotypes
+ minBaf <- min(heterozygous_filter, 1 - heterozygous_filter)
+ maxBaf <- max(heterozygous_filter, 1 - heterozygous_filter)
+
+ genotypes <- matrix(0L, nrow = n, ncol = 3)
+ genotypes[BAFs <= minBaf, 1] <- 1
+ genotypes[BAFs > minBaf & BAFs < maxBaf, 2] <- 1
+ genotypes[BAFs >= maxBaf, 3] <- 1
+
+ genotype_dt <- data.table::as.data.table(genotypes)
+ data.table::setnames(genotype_dt, c("G1", "G2", "G3"))
+
+ # Assemble output
+ # Use set() to modify by reference using a character string for the column name
+ data.table::set(found_data, j = "snp.names", value = paste0("snp", seq_len(n)))
+ found_data <- data.table::as.data.table(cbind(found_data, genotype_dt))
+
+ output_cols <- c("snp.names", "id", "position", "a0", "a1", "G1", "G2", "G3")
+
+ # Use with = FALSE to select columns by character vector
+ data.table::fwrite(
+ found_data[, output_cols, with = FALSE],
+ file = output_file,
+ sep = " ",
+ col.names = FALSE,
+ quote = FALSE
+ )
+
+ # Write sample_g.txt for sex chromosomes
+ if (is.na(as.numeric(chrom_name))) {
+ sample_g_file <- file.path(dirname(output_file), "sample_g.txt")
+ sample_g_data <- data.table::data.table(
+ ID_1 = c("0", "INDIVI1"),
+ ID_2 = c("0", "INDIVI1"),
+ missing = c("0", "0"),
+ sex = c("D", "2")
+ )
+ data.table::fwrite(sample_g_data, sample_g_file, sep = " ")
}
+
+ invisible(NULL)
+}
+
+# Helper function to replicate stats::cor(matrix, vector) using collapse speed
+# This performs C++ based scaling and a cross-product
+fast_cor_vec <- function(X, y) {
+ # Handle missing values equivalent to use = "complete.obs"
+ keep <- stats::complete.cases(X, y)
+
+ # Standardize using collapse (extremely fast)
+ X_std <- collapse::fscale(as.matrix(X[keep, ]))
+ y_std <- collapse::fscale(y[keep])
+
+ # Correlation = (X'y) / (n - 1)
+ n_obs <- sum(keep)
+ res <- (crossprod(X_std, y_std) / (n_obs - 1))[, 1]
+ return(res)
}
#' Function to correct LogR for waivyness that correlates with GC content
#' @param germline_LogR_file String pointing to the germline LogR output
#' @param outfile String pointing to where the GC corrected LogR should be written
#' @param correlations_outfile File where correlations are to be saved
-#' @param gc_content_file_prefix String pointing to where GC windows for this reference genome can be
+#' @param gc_content_file_prefix String pointing to where GC windows for this reference genome can be
#' found. These files should be split per chromosome and this prefix must contain the full path until
#' chr in its name. The .txt extension is automatically added.
#' @param replic_timing_file_prefix Like the gc_content_file_prefix, containing replication timing info (supply NULL if no replication timing correction is to be applied)
@@ -802,206 +952,266 @@ generate.impute.input.wgs.germline = function(chrom, germline.allele.counts.file
#' @param recalc_corr_afterwards Set to TRUE to recalculate correlations after correction
#' @author jonas demeulemeester, sd11, Naser Ansari-Pour (BDI, Oxford)
#' @export
-gc.correct.wgs.germline = function(germline_LogR_file, outfile, correlations_outfile, gc_content_file_prefix, replic_timing_file_prefix, chrom_names, recalc_corr_afterwards=F) {
-
+gc_correct_wgs_germline <- function(germline_LogR_file, outfile, correlations_outfile,
+ gc_content_file_prefix, replic_timing_file_prefix,
+ chrom_names, recalc_corr_afterwards = FALSE) {
if (is.null(gc_content_file_prefix)) {
- stop("GC content reference files must be supplied to WGS GC content correction")
+ log_failure("GC content reference files must be supplied to WGS GC content correction")
}
-
- Germline_LogR = read_logr(germline_LogR_file)
-
- print("Processing GC content data")
- chrom_idx = 1:length(chrom_names)
- gc_files = paste0(gc_content_file_prefix, chrom_idx, ".txt.gz")
- GC_data = do.call(rbind, lapply(gc_files, read_gccontent))
- colnames(GC_data) = c("chr", "Position", paste0(c(25,50,100,200,500), "bp"),
- paste0(c(1,2,5,10,20,50,100), "kb"))#,200,500), "kb"),
- # paste0(c(1,2,5,10), "Mb"))
-
- if (!is.null(replic_timing_file_prefix)) {
- print("Processing replciation timing data")
- replic_files = paste0(replic_timing_file_prefix, chrom_idx, ".txt.gz")
- replic_data = do.call(rbind, lapply(replic_files, read_replication))
- }
-
- # omit non-matching loci, replication data generated at exactly same GC loci
- locimatches = match(x = paste0(Germline_LogR$Chromosome, "_", Germline_LogR$Position),
- table = paste0(GC_data$chr, "_", GC_data$Position))
- Germline_LogR = Germline_LogR[which(!is.na(locimatches)), ]
- GC_data = GC_data[na.omit(locimatches), ]
+
+ # Fast reading of LogR
+ Germline_LogR <- read_logr(germline_LogR_file)
+
+ log_info("Processing GC content data")
+ chrom_idx <- seq_along(chrom_names)
+
+ # Efficiently reading and binding GC data
+ gc_files <- paste0(gc_content_file_prefix, chrom_idx, ".txt.gz")
+ GC_data <- data.table::rbindlist(lapply(gc_files, read_gccontent))
+ colnames(GC_data) <- c(
+ "chr", "Position", paste0(c(25, 50, 100, 200, 500), "bp"),
+ paste0(c(1, 2, 5, 10, 20, 50, 100), "kb")
+ )
+
+ # Optional Replication timing data
if (!is.null(replic_timing_file_prefix)) {
- replic_data = replic_data[na.omit(locimatches), ]
+ log_info("Processing replication timing data")
+ replic_files <- paste0(replic_timing_file_prefix, chrom_idx, ".txt.gz")
+ replic_data <- data.table::rbindlist(lapply(replic_files, read_replication))
}
- rm(locimatches)
-
- corr = abs(cor(GC_data[, 3:ncol(GC_data)], Germline_LogR[,3], use="complete.obs")[,1])
+
+ # Fast Loci Synchronization - strip 'chr' from keys for maximum alignment
+ logr_chr <- gsub("chr", "", as.character(Germline_LogR$Chromosome))
+ gc_chr <- gsub("chr", "", as.character(GC_data$Chromosome))
+ key_logr <- paste0(logr_chr, "_", Germline_LogR$Position)
+ key_gc <- paste0(gc_chr, "_", GC_data$Position)
+
+ locimatches <- match(key_logr, key_gc)
+
+ valid_idx <- which(!is.na(locimatches))
+ matched_gc_idx <- locimatches[valid_idx]
+
+ Germline_LogR <- Germline_LogR[valid_idx, ]
+ GC_data <- GC_data[matched_gc_idx, ]
+
if (!is.null(replic_timing_file_prefix)) {
- corr_rep = abs(cor(replic_data[, 3:ncol(replic_data)], Germline_LogR[,3], use="complete.obs")[,1])
+ replic_data <- replic_data[matched_gc_idx, ]
}
-
- index_1kb = which(names(corr)=="1kb")
- maxGCcol_insert = names(which.max(corr[1:index_1kb]))
- index_100kb = which(names(corr)=="100kb")
- # start large window sizes at 5kb rather than 2kb to avoid overly correlated expl variables
- maxGCcol_amplic = names(which.max(corr[(index_1kb+2):index_100kb]))
+
+ rm(key_logr, key_gc, locimatches, valid_idx, matched_gc_idx)
+
+ # Fast Correlation calculation
+ # Replaced stats::cor and non-existent fcor with helper
+ corr <- abs(
+ fast_cor_vec(GC_data[, 3:ncol(GC_data)], Germline_LogR[[3]])
+ )
+
if (!is.null(replic_timing_file_prefix)) {
- maxreplic = names(which.max(corr_rep))
+ corr_rep <- abs(
+ fast_cor_vec(replic_data[, 3:ncol(replic_data)], Germline_LogR[[3]])
+ )
}
-
+
+ # Identify best window sizes
+ index_1kb <- which(names(corr) == "1kb")
+ maxGCcol_insert <- names(which.max(corr[1:index_1kb]))
+ index_100kb <- which(names(corr) == "100kb")
+ maxGCcol_amplic <- names(which.max(corr[(index_1kb + 2):index_100kb]))
+
if (!is.null(replic_timing_file_prefix)) {
- cat("Replication timing correlation: ",paste(names(corr_rep),format(corr_rep,digits=2), ";"),"\n")
- cat("Replication dataset: " ,maxreplic,"\n")
+ maxreplic <- names(which.max(corr_rep))
+ log_info("Replication timing correlation: {paste(names(corr_rep), format(corr_rep, digits = 2), collapse = '; ')}")
+ log_info("Replication dataset: {maxreplic}")
}
- cat("GC correlation: ",paste(names(corr),format(corr,digits=2), ";"),"\n")
- cat("Short window size: ",maxGCcol_insert,"\n")
- cat("Long window size: ",maxGCcol_amplic,"\n")
-
+
+ log_info("GC correlation: {paste(names(corr), format(corr, digits = 2), collapse = '; ')}")
+ log_info("Short window size: {maxGCcol_insert}")
+ log_info("Long window size: {maxGCcol_amplic}")
+
+ logr_vec <- Germline_LogR[[3]]
+
+ # Create spline design matrices
+ X_ins <- splines::ns(GC_data[[maxGCcol_insert]], df = 5, intercept = TRUE)
+ X_amp <- splines::ns(GC_data[[maxGCcol_amplic]], df = 5, intercept = TRUE)
+
if (!is.null(replic_timing_file_prefix)) {
- # Multiple regression - with replication timing
- corrdata = data.frame(logr = Germline_LogR[,3, drop = T],
- GC_insert = GC_data[,maxGCcol_insert, drop = T],
- GC_amplic = GC_data[,maxGCcol_amplic, drop = T],
- replic = replic_data[, maxreplic, drop = T])
- colnames(corrdata) = c("logr", "GC_insert", "GC_amplic", "replic")
- if (!recalc_corr_afterwards)
- rm(GC_data, replic_data)
-
- model = lm(logr ~ splines::ns(x = GC_insert, df = 5, intercept = T) + splines::ns(x = GC_amplic, df = 5, intercept = T) + splines::ns(x = replic, df = 5, intercept = T), y=F, model = F, data = corrdata, na.action="na.exclude")
-
- corr = data.frame(windowsize=c(names(corr), names(corr_rep)), correlation=c(corr, corr_rep))
- write.table(corr, file=gsub(".txt", "_beforeCorrection.txt", correlations_outfile), sep="\t", quote=F, row.names=F)
-
+ X_rep <- splines::ns(replic_data[[maxreplic]], df = 5, intercept = TRUE)
+ X_design <- cbind(X_ins, X_amp, X_rep)
+
+ before_corr_df <- data.frame(
+ windowsize = c(names(corr), names(corr_rep)),
+ correlation = c(as.numeric(corr), as.numeric(corr_rep))
+ )
} else {
- # Multiple regression - without replication timing
- corrdata = data.frame(logr = Germline_LogR[,3, drop = T],
- GC_insert = GC_data[,maxGCcol_insert, drop = T],
- GC_amplic = GC_data[,maxGCcol_amplic, drop = T])
- colnames(corrdata) = c("logr", "GC_insert", "GC_amplic")
- if (!recalc_corr_afterwards)
- rm(GC_data)
-
- model = lm(logr ~ splines::ns(x = GC_insert, df = 5, intercept = T) + splines::ns(x = GC_amplic, df = 5, intercept = T), y=F, model = F, data = corrdata, na.action="na.exclude")
-
- corr = data.frame(windowsize=names(corr), correlation=corr)
- write.table(corr, file=gsub(".txt", "_beforeCorrection.txt", correlations_outfile), sep="\t", quote=F, row.names=F)
+ X_design <- cbind(X_ins, X_amp)
+ before_corr_df <- data.frame(
+ windowsize = names(corr),
+ correlation = as.numeric(corr)
+ )
+ }
+
+ # Fast Linear Model via collapse
+ coeffs <- collapse::flm(logr_vec, X_design)
+
+ # Calculate residuals (Corrected LogR)
+ Germline_LogR[, 3] <- logr_vec - (X_design %*% coeffs)
+
+ rm(X_ins, X_amp, X_design, coeffs)
+ if (!is.null(replic_timing_file_prefix)) rm(X_rep)
+
+ data.table::fwrite(before_corr_df,
+ file = gsub(".txt", "_beforeCorrection.txt", correlations_outfile),
+ sep = "\t", quote = FALSE
+ )
+
+ if (!recalc_corr_afterwards) {
+ rm(GC_data)
+ if (exists("replic_data")) rm(replic_data)
}
-
- Germline_LogR[,3] = residuals(model)
- rm(model, corrdata)
-
- readr::write_tsv(x=Germline_LogR[which(!is.na(Germline_LogR[,3])), ], file=outfile)
-
+
+ data.table::fwrite(
+ Germline_LogR[!is.na(Germline_LogR[[3]]), ],
+ file = outfile, sep = "\t"
+ )
+
+ # Optional Post-correction Analysis
if (recalc_corr_afterwards) {
- # Recalculate the correlations to see how much there is left
- corr = abs(cor(GC_data[, 3:ncol(GC_data)], Germline_LogR[,3], use="complete.obs")[,1])
- if (!is.null(replic_timing_file_prefix)) {
- corr_rep = abs(cor(replic_data[, 3:ncol(replic_data)], Germline_LogR[,3], use="complete.obs")[,1])
- cat("Replication timing correlation post correction: ",paste(names(corr_rep),format(corr_rep,digits=2), ";"),"\n")
- }
- cat("GC correlation post correction: ",paste(names(corr),format(corr,digits=2), ";"),"\n")
-
+ # Re-using the helper for consistency and speed
+ post_corr <- abs(
+ fast_cor_vec(GC_data[, 3:ncol(GC_data)], Germline_LogR[[3]])
+ )
+
if (!is.null(replic_timing_file_prefix)) {
- corr = data.frame(windowsize=c(names(corr), names(corr_rep)), correlation=c(corr, corr_rep))
- write.table(corr, file=gsub(".txt", "_afterCorrection.txt", correlations_outfile), sep="\t", quote=F, row.names=F)
+ post_corr_rep <- abs(
+ fast_cor_vec(
+ replic_data[, 3:ncol(replic_data)], Germline_LogR[[3]]
+ )
+ )
+
+ log_info("Replication timing correlation post correction: {paste(names(post_corr_rep), format(post_corr_rep, digits = 2), collapse = '; ')}")
+
+ after_corr_df <- data.frame(
+ windowsize = c(
+ names(post_corr),
+ names(post_corr_rep)
+ ),
+ correlation = c(
+ as.numeric(post_corr),
+ as.numeric(post_corr_rep)
+ )
+ )
} else {
- corr = data.frame(windowsize=c(names(corr)), correlation=corr)
- write.table(corr, file=gsub(".txt", "_afterCorrection.txt", correlations_outfile), sep="\t", quote=F, row.names=F)
+ after_corr_df <- data.frame(
+ windowsize = names(post_corr),
+ correlation = as.numeric(post_corr)
+ )
}
- } else {
- corr$correlation = NA
- write.table(corr, file=gsub(".txt", "_afterCorrection.txt", correlations_outfile), sep="\t", quote=F, row.names=F)
+
+ log_info("GC correlation post correction: {paste(names(post_corr), format(post_corr, digits = 2), collapse = '; ')}")
+ data.table::fwrite(
+ after_corr_df,
+ file = gsub(
+ ".txt", "_afterCorrection.txt",
+ correlations_outfile
+ ),
+ sep = "\t",
+ quote = FALSE
+ )
}
}
#' Prepare WGS data of germline for haplotype construction
-#'
-#' This function performs part of the Battenberg WGS pipeline: Counting alleles, generating BAF and logR,
+#'
+#' This function performs part of the Battenberg WGS pipeline: Counting alleles, generating BAF and logR,
#' reconstructing normal-pair allele counts for the germline and performing GC content correction.
-#'
+#'
#' @param chrom_names A vector containing the names of chromosomes to be included
#' @param chrom_coord Full path to the file with chromosome coordinates including start, end and left/right centromere positions
-#' @param germlinebam Full path to the germline BAM file
+#' @param germlinebam Full path to the germline BAM file
#' @param germlinename Identifier to be used for germline output files (i.e. the germline BAM file name without the '.bam' extension).
#' @param g1000lociprefix Prefix path to the 1000 Genomes loci reference files
#' @param g1000allelesprefix Prefix path to the 1000 Genomes SNP allele reference files
#' @param gamma_ivd The PCF gamma value for segmentation of 1000G hetSNP IVD values (Default 1e5).
#' @param kmin_ivd The min number of SNPs to support a segment in PCF of 1000G hetSNP IVD values (Default 50)
+#' @param centromere_noise_seg_size The maximum size of PCF segment to be removed as noise when it overlaps with the centromere due to the noisy nature of data (Default 1e6)
#' @param centromere_dist The minimum distance from the centromere to ignore in analysis due to the noisy nature of data in the vicinity of centromeres (Default 5e5)
#' @param min_het_dist The minimum distance for detecting higher resolution inter-hetSNP regions with potential LOH while accounting for inherent homozygote stretches (Default 1e5)
-#' @param gamma_logr The PCF gamma value for confirming LOH within each inter-hetSNP candidate segment (Default 100)
-#' @param length_adjacent The length of adjacent regions either side of a candidate inter-hetSNP LOH region to be plotted (Default 5e4)
-#' @param gccorrectprefix Prefix path to GC content reference data
-#' @param repliccorrectprefix Prefix path to replication timing reference data (supply NULL if no replication timing correction is to be applied)
-#' @param min_base_qual Minimum base quality required for a read to be counted
-#' @param min_map_qual Minimum mapping quality required for a read to be counted
-#' @param allelecounter_exe Path to the allele counter executable (can be found in $PATH)
+
+#' @param allele_counts_dir Directory containing the allele counts files
#' @param min_normal_depth Minimum depth required in the normal for a SNP to be included
-#' @param skip_allele_counting Flag, set to TRUE if allele counting is already complete (files are expected in the working directory on disk)
+#' @param libs Path to the R libraries to be used by parallel workers
#' @author Naser Ansari-Pour (BDI, Oxford)
#' @export
-prepare_wgs_germline = function(chrom_names, chrom_coord, germlinebam, germlinename, g1000lociprefix, g1000allelesprefix, gamma_ivd=1e5, kmin_ivd=50, centromere_noise_seg_size=1e6,
- centromere_dist=5e5, min_het_dist=2e3, gamma_logr=100, length_adjacent=5e4, gccorrectprefix,repliccorrectprefix, min_base_qual, min_map_qual,
- allelecounter_exe, min_normal_depth, skip_allele_counting) {
-
- requireNamespace("foreach")
- requireNamespace("doParallel")
- requireNamespace("parallel")
-
- if (!skip_allele_counting) {
- # Obtain allele counts for 1000 Genomes locations for the germline
- foreach::foreach(i=1:length(chrom_names)) %dopar% {
- getAlleleCounts(bam.file=germlinebam,
- output.file=paste(germlinename,"_alleleFrequencies_chr", i, ".txt", sep=""),
- g1000.loci=paste(g1000lociprefix, i, ".txt", sep=""),
- min.base.qual=min_base_qual,
- min.map.qual=min_map_qual,
- allelecounter.exe=allelecounter_exe)
- }
+prepare_wgs_germline <- function(
+ chrom_names, chrom_coord, germlinebam,
+ germlinename, g1000lociprefix, g1000allelesprefix,
+ gamma_ivd = 1e5, kmin_ivd = 50,
+ centromere_noise_seg_size = 1e6,
+ centromere_dist = 5e5, min_het_dist = 2e3,
+ gamma_logr = 100, length_adjacent = 5e4,
+ gccorrectprefix, repliccorrectprefix,
+ min_base_qual, min_map_qual,
+ allele_counts_dir, min_normal_depth,
+ nthreads = 1,
+ libs
+) {
+ germline_prefix <- file.path(allele_counts_dir, germlinename)
+
+ # Check existence of at least one file
+ first_file <- paste0(germline_prefix, "_alleleFrequencies_chr", chrom_names[1], ".txt")
+ if (!file.exists(first_file)) {
+ log_failure("Expected allele counts file not found: {first_file}")
+ log_failure("Missing allele counts file: {first_file}")
}
-
- # Standardise Chr notation (removes 'chr' string if present; essential for cell_line_baf_logR)
- standardiseChrNotation_germline(GERMLINENAME=germlinename)
-
+ # Standardise Chr notation (removes 'chr' string if present)
+ # Skipping modification of external files. Assuming files are correct or handled in R reading.
+ # standardise_chr_notation_germline(GERMLINENAME = germlinename)
+
# Obtain BAF and LogR from the raw allele counts of the germline
- germline_baf_logR(GERMLINENAME=germlinename,
- g1000alleles.prefix=g1000allelesprefix,
- chrom_names=chrom_names
+ cl_data <- germline_baf_logR(
+ GERMLINENAME = germline_prefix,
+ g1000alleles_prefix = g1000allelesprefix,
+ chrom_names = chrom_names
)
-
- # Reconstruct normal-pair allele count files for the germline
-
- foreach::foreach(i=1:length(chrom_names),.export=c("germline_reconstruct_normal","GL_OHET","GL_AL","GL_AC","GL_LogR"),.packages=c("copynumber","ggplot2","grid")) %dopar% {
-
- germline_reconstruct_normal(GERMLINENAME=germlinename,
- NORMALNAME=paste0(germlinename,"_normal"),
- chrom_coord=chrom_coord,
- chrom=i,
- GL_OHET=GL_OHET,
- GL_AL=GL_AL,
- GL_AC=GL_AC,
- GL_LogR=GL_LogR,
- GAMMA_IVD=gamma_ivd,
- KMIN_IVD=kmin_ivd,
- CENTROMERE_NOISE_SEG_SIZE=centromere_noise_seg_size,
- CENTROMERE_DIST=centromere_dist,
- MIN_HET_DIST=min_het_dist,
- GAMMA_LOGR=gamma_logr,
- LENGTH_ADJACENT=length_adjacent)
- }
-
- if (length(list.files(pattern="normal_alleleFrequencies"))==length(chrom_names)){
- print("STEP 2 - Normal allelecounts reconstruction - completed")
- } else {
- stop("Missing 'normal' allelecount files - all chromosomes NOT reconstructed")
+
+ run_with_error_handling(
+ iterator = seq_along(chrom_names),
+ func = function(i) {
+ germline_reconstruct_normal(
+ GERMLINENAME = germlinename,
+ NORMALNAME = paste(germlinename, "_normal", sep = ""),
+ chrom_coord = chrom_coord,
+ chrom = i,
+ GL_OHET = cl_data$OHET,
+ GL_AL = cl_data$AL,
+ GL_AC = cl_data$AC,
+ GL_LogR = cl_data$LogR,
+ GAMMA_IVD = gamma_ivd,
+ KMIN_IVD = kmin_ivd,
+ CENTROMERE_NOISE_SEG_SIZE = centromere_noise_seg_size,
+ CENTROMERE_DIST = centromere_dist,
+ MIN_HET_DIST = min_het_dist,
+ GAMMA_LOGR = gamma_logr,
+ LENGTH_ADJACENT = length_adjacent
+ )
+ }, libs, nthreads = nthreads
+ )
+
+ if (length(list.files(pattern = "normal_alleleFrequencies")) == length(chrom_names)) {
+ log_info("STEP 2 - Normal allelecounts reconstruction - completed")
+ } else {
+ log_failure("Missing 'normal' allelecount files - all chromosomes NOT reconstructed")
}
-
+
# Perform GC correction
- gc.correct.wgs.germline(germline_LogR_file=paste(germlinename,"_mutantLogR.tab", sep=""),
- outfile=paste(germlinename,"_mutantLogR_gcCorrected.tab", sep=""),
- correlations_outfile=paste(germlinename, "_GCwindowCorrelations.txt", sep=""),
- gc_content_file_prefix=gccorrectprefix,
- replic_timing_file_prefix=repliccorrectprefix,
- chrom_names=chrom_names)
+ gc_correct_wgs_germline(
+ germline_LogR_file = paste(germlinename, "_mutantLogR.tab", sep = ""),
+ outfile = paste(germlinename, "_mutantLogR_gcCorrected.tab", sep = ""),
+ correlations_outfile = paste(germlinename, "_GCwindowCorrelations.txt", sep = ""),
+ gc_content_file_prefix = gccorrectprefix,
+ replic_timing_file_prefix = repliccorrectprefix,
+ chrom_names = chrom_names
+ )
}
diff --git a/R/reader.R b/R/reader.R
new file mode 100644
index 00000000..55dd87e8
--- /dev/null
+++ b/R/reader.R
@@ -0,0 +1,297 @@
+########################################################################################
+# Generic table reader
+########################################################################################
+#' Generic reading function using the readr R package, tailored for reading in genomic data
+#' @param file Filename of the file to read in
+#' @param header Whether the file contains a header (Default: TRUE)
+#' @param row.names Whether the file contains row names (Default: FALSE)
+#' @param stringsAsFactor Legacy parameter that is no longer used (Default: FALSE)
+#' @param sep Column separator (Default: \\t)
+#' @param chrom_col The column number that contains chromosome denominations. This column will automatically be cast as a character. Should be counted including the row.names (Default: 1)
+#' @param skip The number of rows to skip before reading (Default: 0)
+#' @return A data frame with contents of the file
+#' @export
+read_table_generic <- function(file, header = TRUE, stringsAsFactor = FALSE, sep = "\t", chrom_col = 1, skip = 0) {
+ # We use a named character vector to force the chromosome column(s) to character
+ # This prevents loss of leading zeros or scientific notation issues
+ col_classes <- list(character = chrom_col)
+
+ # fread is the fastest modern parser for large genomic tables
+ d <- data.table::fread(
+ file = file,
+ sep = sep,
+ header = header,
+ skip = skip,
+ colClasses = col_classes,
+ check.names = TRUE,
+ data.table = TRUE
+ )
+ log_info("Verified headers generic for {basename(file)}: {paste(colnames(d), collapse = ', ')}")
+ return(d)
+}
+
+
+#' Parser for logR data
+#' @param filename Filename of the file to read in
+#' @param header Whether the file contains a header (Default: TRUE)
+#' @return A data frame with logR content
+read_logr <- function(filename, header = TRUE) {
+ log_info("Reading LogR data from: {normalizePath(filename, mustWork = FALSE)}")
+ dt <- data.table::fread(
+ file = filename,
+ header = header,
+ colClasses = c("character", "integer", "numeric")
+ )
+ log_info("Verified headers read_logr for {basename(filename)}: {paste(colnames(dt), collapse = ', ')}")
+ return(dt)
+}
+
+#' Parser for BAF data
+#' @param filename Filename of the file to read in
+#' @param header Whether the file contains a header (Default: TRUE)
+#' @return A data frame with BAF content
+read_baf_as_data_frame <- function(filename, header = TRUE) {
+ log_info("Reading BAF data from: {normalizePath(filename, mustWork = FALSE)}")
+ output <- data.table::fread(
+ file = filename,
+ header = header,
+ colClasses = c("character", "integer", "numeric")
+ )
+ data.table::setDF(output)
+ log_info("Verified headers read_baf_as_data_frame for {basename(filename)}: {paste(colnames(output), collapse = ', ')}")
+ return(output)
+}
+
+#' Parser for GC content reference data
+#' @param filename Filename of the file to read in
+#' @return A data frame with GC content
+read_gccontent <- function(filename) {
+ log_info("Reading gccontent from: {normalizePath(filename, mustWork = FALSE)}")
+ dt <- data.table::fread(
+ file = filename,
+ header = TRUE,
+ sep = "auto",
+ skip = "chr",
+ check.names = FALSE,
+ fill = TRUE,
+ data.table = FALSE
+ )
+
+ # Standardize headers (support both 'chr'/'pos' and 'Chromosome'/'Position')
+ if ("chr" %in% colnames(dt)) names(dt)[names(dt) == "chr"] <- "Chromosome"
+ if ("pos" %in% colnames(dt)) names(dt)[names(dt) == "pos"] <- "Position"
+
+ # Ensure all window columns are numeric
+ win_cols <- setdiff(colnames(dt), c("Chromosome", "Position"))
+ for (col in win_cols) {
+ if (!is.numeric(dt[[col]])) {
+ dt[[col]] <- as.numeric(dt[[col]])
+ }
+ }
+
+ log_info("Verified headers gccontent for {basename(filename)}: {paste(colnames(dt), collapse = ', ')}")
+ return(dt)
+}
+
+#' Parser for replication timing reference data
+#' @param filename Filename of the file to read in
+#' @return A data frame with replication timing
+read_replication <- function(filename) {
+ log_info("Reading replication timing data from: {normalizePath(filename, mustWork = FALSE)}")
+ dt <- data.table::fread(
+ file = filename,
+ header = TRUE,
+ sep = "auto",
+ skip = "chr",
+ data.table = FALSE
+ )
+
+ # Standardize headers
+ if ("chr" %in% colnames(dt)) names(dt)[names(dt) == "chr"] <- "Chromosome"
+ if ("pos" %in% colnames(dt)) names(dt)[names(dt) == "pos"] <- "Position"
+
+ # Ensure replication columns are numeric
+ win_cols <- setdiff(colnames(dt), c("Chromosome", "Position"))
+ for (col in win_cols) {
+ if (!is.numeric(dt[[col]])) {
+ dt[[col]] <- as.numeric(dt[[col]])
+ }
+ }
+
+ log_info("Verified headers replication {paste(colnames(dt), collapse = ', ')}")
+ return(dt)
+}
+
+#' Parser for BAFsegmented data
+#' @param filename Filename of the file to read in
+#' @param header Whether the file contains a header (Default: TRUE)
+#' @return A data frame with BAFsegmented content
+read_bafsegmented <- function(filename, header = TRUE) {
+ log_info("Reading BAFsegmented data from: {normalizePath(filename, mustWork = FALSE)}")
+
+ dt <- data.table::fread(
+ file = filename,
+ header = header,
+ sep = "\t",
+ # Force column types to prevent the coercion warnings
+ colClasses = c(Chromosome = "character", Position = "integer")
+ )
+ # If the file uses 'chr', 'chrom', or 'CHR', we standardize it to 'Chromosome'
+ if ("CHR" %in% colnames(dt)) {
+ data.table::setnames(dt, "CHR", "Chromosome")
+ } else if ("chr" %in% colnames(dt)) {
+ data.table::setnames(dt, "chr", "Chromosome")
+ }
+
+ log_info("Verified headers bafsegmented: {paste(colnames(dt), collapse = ', ')}")
+ return(dt)
+}
+#' Parser for imputed genotype data
+#' @param filename Filename of the file to read in
+#' @return A data frame with the imputed genotype output
+read_imputed_output <- function(filename) {
+ log_info("Reading imputed genotype data from: {normalizePath(filename, mustWork = FALSE)}")
+ dt <- data.table::fread(
+ file = filename,
+ col.names = c("snpidx", "rsidx", "pos", "ref", "alt", "hap1", "hap2"),
+ colClasses = c("character", "character", "integer", "character", "character", "integer", "integer"),
+ header = FALSE
+ )
+ log_info("Verified headers read_imputed_output {paste(colnames(dt), collapse = ', ')}")
+ return(dt)
+}
+
+#' Parser for allele frequencies data
+#' @param filename Filename of the file to read in
+#' @return A data frame with the alleleCounter output
+read_alleleFrequencies <- function(filename) {
+ log_info("Reading allele frequencies data from: {normalizePath(filename, mustWork = FALSE)}")
+ # skip = "#" handles the comment lines typically found in alleleCounter output
+ dt <- data.table::fread(
+ file = filename,
+ col.names = c("CHR", "POS", "Count_A", "Count_C", "Count_G", "Count_T", "Good_depth"),
+ colClasses = c("character", "integer", "integer", "integer", "integer", "integer", "integer"),
+ skip = "#"
+ )
+ log_info("Verified headers read_alleleFrequencies {paste(colnames(dt), collapse = ', ')}")
+ return(dt)
+}
+
+#' Parser for impute input data
+#' @param filename Filename of the file to read in
+#' @return A data frame with the input for impute
+#' @export
+read_impute_input <- function(filename) {
+ # :: syntax used for log_info or other package calls
+ log_info("Reading impute input data from: {normalizePath(filename, mustWork = FALSE)}")
+
+ # Read with data.table for speed
+ dt <- data.table::fread(
+ file = filename,
+ header = FALSE,
+ sep = "auto"
+ )
+ # Convert to data.frame to ensure compatibility with legacy indexing
+ dt_df <- as.data.frame(dt)
+
+ # Force column names to start with 'X' instead of 'V'
+ # This fixes the 'inp$X6' NULL issue in the Beagle converter
+ colnames(dt_df) <- paste0("X", seq_len(ncol(dt_df)))
+ log_info("Verified headers read_impute_input: {paste(colnames(dt_df), collapse = ', ')}")
+ return(dt_df)
+}
+
+#' Parser for beagle5 output data
+#' @param filename Filename of the file to read in
+#' @return A data frame with the beagle5 output
+read_beagle_output <- function(filename) {
+ # :: syntax and pure comments
+ log_info("Reading beagle5 output data from: {normalizePath(filename, mustWork = FALSE)}")
+
+ # Check if file exists and has content before trying to read
+ if (!file.exists(filename) || file.info(filename)$size < 100) {
+ log_info("Beagle output file is missing or too small (likely no SNPs phased).")
+ # Return an empty data table with the expected structure to prevent dimnames errors
+ empty_dt <- data.table::data.table(
+ "#CHROM" = character(), POS = integer(), ID = character(),
+ REF = character(), ALT = character(), QUAL = character(),
+ FILTER = character(), INFO = character(), FORMAT = character(),
+ SAMP001 = character()
+ )
+ return(empty_dt)
+ }
+ dt <- data.table::fread(
+ file = filename,
+ skip = "#CHROM",
+ header = FALSE
+ )
+
+ colnames(dt) <- c("#CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO", "FORMAT", "SAMP001")
+ log_info("Successfully read {nrow(dt)} phased SNPs from Beagle output.")
+ return(dt)
+}
+
+#' Load the rho and psi estimates from a file.
+#' @noRd
+load_rho_psi_file <- function(rho_psi_file) {
+ log_info("Reading rho and psi estimates from: {normalizePath(rho_psi_file, mustWork = FALSE)}")
+ # Use read.table to correctly handle row headers if present (standard Battenberg output)
+ rho_psi_info <- read.table(rho_psi_file, header = TRUE, sep = "\t", stringsAsFactors = FALSE)
+
+ # Access by row name "FRAC_GENOME"
+ rho <- rho_psi_info["FRAC_GENOME", "rho"]
+ psit <- rho_psi_info["FRAC_GENOME", "psi"]
+ goodness <- rho_psi_info["FRAC_GENOME", "distance"]
+
+ # Fallback if row access fails (e.g. if row names weren't set correctly)
+ if (is.na(rho) || length(rho) == 0) {
+ log_info("Row-name lookup for FRAC_GENOME failed. Searching all columns.")
+
+ # Search for "FRAC_GENOME" in any column
+ label_found <- FALSE
+ for (col_idx in seq_len(ncol(rho_psi_info))) {
+ row_idx <- which(rho_psi_info[[col_idx]] == "FRAC_GENOME")
+ if (length(row_idx) > 0) {
+ idx <- row_idx[1]
+ rho <- rho_psi_info[idx, "rho"]
+ psit <- rho_psi_info[idx, "psi"]
+ goodness <- rho_psi_info[idx, "distance"]
+ label_found <- TRUE
+ log_info("Found FRAC_GENOME in column {col_idx}, row {idx}.")
+ break
+ }
+ }
+
+ if (!label_found) {
+ # Last row is traditionally FRAC_GENOME in Battenberg
+ idx <- nrow(rho_psi_info)
+ if (idx > 0) {
+ log_info("FRAC_GENOME label not found. defaulting to last row (row {idx}).")
+ rho <- rho_psi_info[idx, "rho"]
+ psit <- rho_psi_info[idx, "psi"]
+ goodness <- rho_psi_info[idx, "distance"]
+ }
+ }
+ }
+
+ if (is.na(rho)) log_failure("Failed to load rho (purity) from {rho_psi_file}")
+
+ return(list(rho = rho, psit = psit, goodness = goodness))
+}
+
+#' Parse the reference info file
+#' @param snp6_reference_info_file A SNP6 reference info master file
+#' @noRd
+parse_snp6_ref_file <- function(snp6_reference_info_file) {
+ log_info("Reading SNP6 reference info from: {normalizePath(snp6_reference_info_file, mustWork = FALSE)}")
+ return(data.table::fread(snp6_reference_info_file, header = TRUE))
+}
+
+#' Infer the gender using the birdseed report file
+#' @param birdseed_report_file The birdseed report file
+#' @export
+infer_gender_birdseed <- function(birdseed_report_file) {
+ log_info("Reading birdseed report from: {normalizePath(birdseed_report_file, mustWork = FALSE)}")
+ z <- data.table::fread(birdseed_report_file)
+ return(as.character(z$em.cluster.chrX.het.contrast_gender))
+}
diff --git a/R/refit.R b/R/refit.R
new file mode 100644
index 00000000..8c4578ba
--- /dev/null
+++ b/R/refit.R
@@ -0,0 +1,122 @@
+########################################################################################
+# Refitting functions
+########################################################################################
+#' Calculate rho and psi values from a refit suggestion
+#'
+#' Use this function to calculate the refit values from a refit suggestion.
+#' @param refBAF BAF of the segment
+#' @param refLogR logR of the segment
+#' @param refMajor Major allele copy number
+#' @param refMinor Minor allele copy number
+#' @param rho Sample rho parameter
+#' @param gamma_param Platform gamma parameter
+#' @return A list with a field for rho and psi_t
+#' @author sd11
+#' @export
+calc_rho_psi_refit <- function(refBAF, refLogR, refMajor, refMinor, rho, gamma_param) {
+ rho <- (2 * refBAF - 1) / (2 * refBAF - refBAF * (refMajor + refMinor) - 1 + refMajor)
+ psi <- (rho * (refMajor + refMinor) + 2 - 2 * rho) / (2^(refLogR / gamma_param))
+ psi_t <- psi2psit(rho, psi)
+ return(list(rho = rho, psi_t = psi_t))
+}
+
+#' Calculate refit values from a refit suggestion
+#'
+#' Use this function to calculate the refit values from a refit suggestion.
+#' @param subclones_file A Battenberg subclones.txt file
+#' @param segment_chrom Chromsome of the segment to use for refitting
+#' @param segment_pos Position within the start/end coordinates of the segment to use for refitting
+#' @param new_nMaj Major allele copy number
+#' @param new_nMin Minor allele copy number
+#' @param rho Sample rho parameter
+#' @param gamma_param Platform gamma parameter
+#' @return A list with a field for rho and psi_t
+#' @author sd11
+#' @export
+suggest_refit <- function(subclones_file, segment_chrom, segment_pos, new_nMaj, new_nMin, rho, gamma_param) {
+ subclones <- data.table::fread(subclones_file, header = TRUE, stringsAsFactors = FALSE)
+ segment <- subclones[!is.na(subclones$chr) & !is.na(subclones$startpos) & !is.na(subclones$endpos) &
+ subclones$chr == segment_chrom & subclones$startpos <= segment_pos & subclones$endpos >= segment_pos, ]
+ segment_BAF <- segment$BAF
+ segment_LogR <- segment$LogR
+ return(calc_rho_psi_refit(segment_BAF, segment_LogR, new_nMaj, new_nMin, rho, gamma_param))
+}
+
+#' Create refit suggestions for a fit copy number profile
+#'
+#' This function takes a fit copy number profile and generates refit suggestions for a future rerun.
+#' If there are clonal alterations above a specified size, then those written out as supplied as suggestions,
+#' otherwise a refit suggestion of an external purity value will be saved.
+#' @param samplename Samplename for the output file
+#' @param subclones_file File containing a fit copy number profile
+#' @param rho_psi_file File with rho and psi values
+#' @param gamma_param Platform gamma parameter
+#' @param min_segment_size_mb Minimum size of a segment in Mb to be considered for a refit suggestion (Default: 2)
+#' @author sd11
+#' @export
+cnfit_to_refit_suggestions <- function(samplename, subclones_file, rho_psi_file, gamma_param, min_segment_size_mb = 2) {
+ subclones <- read_table_generic(subclones_file)
+ subclones$len <- subclones$endpos / 1000000 - subclones$startpos / 1000000
+ subclones$is_cna <- subclones$nMaj1_A != subclones$nMin1_A
+
+ log_info("min_segment_size_mb: '{min_segment_size_mb}'")
+ log_info("subclones$is_cna: '{subclones$is_cna}'")
+
+ if (any(!is.na(subclones$len) & !is.na(subclones$is_cna) & subclones$len > min_segment_size_mb & subclones$is_cna)) {
+ # There are large scale alterations, save the top couple as suggestions
+ rho_psi <- utils::read.table(rho_psi_file, header = TRUE, stringsAsFactors = FALSE)
+ rho <- rho_psi["FRAC_GENOME", "rho"]
+ psi_t <- rho_psi["FRAC_GENOME", "psi"]
+
+ # Take only segments that are clonal and are an alteration
+ is_subclonal <- !is.na(subclones$frac1_A) & subclones$frac1_A < 1
+ subclones_clonal_cna <- subset(subclones, !is_subclonal & subclones$is_cna)
+ subclones_clonal_cna <- subclones_clonal_cna[order(subclones_clonal_cna$len, decreasing = TRUE), ]
+ if (nrow(subclones_clonal_cna) == 0) {
+ output <- data.table::data.table(
+ project = NA, samplename = samplename,
+ qc = NA, cellularity_refit = TRUE,
+ chrom = NA, pos = NA, maj = NA,
+ min = NA, baf = NA, logr = NA,
+ rho_estimate = NA, psi_t_estimate = NA,
+ rho_diff = NA, psi_t_diff = NA
+ )
+ data.table::setDF(output)
+ } else {
+ # Generate a couple of solutions, but not more than are possibly available
+ max_solutions <- ifelse(nrow(subclones_clonal_cna) >= 5, 5, nrow(subclones_clonal_cna))
+ subclones_clonal_cna <- subclones_clonal_cna[1:max_solutions, , drop = FALSE]
+
+ # Determine position in Mb within the segment
+ position <- subclones_clonal_cna$startpos + (subclones_clonal_cna$endpos - subclones_clonal_cna$startpos) / 2
+ position <- position / 1000000
+ position_round_up <- ceiling(position)
+ position_round_down <- floor(position)
+ position <- ifelse(position_round_up < subclones_clonal_cna$endpos, position_round_up, position_round_down)
+
+ output <- data.frame(
+ project = rep(NA, max_solutions),
+ samplename = rep(samplename, max_solutions),
+ qc = rep(NA, max_solutions),
+ cellularity_refit = rep(FALSE, max_solutions),
+ chrom = subclones_clonal_cna$chr[1:max_solutions],
+ pos = paste(position, "M", sep = ""),
+ maj = subclones_clonal_cna$nMaj1_A[1:max_solutions],
+ min = subclones_clonal_cna$nMin1_A[1:max_solutions],
+ baf = subclones_clonal_cna$BAF[1:max_solutions],
+ logr = subclones_clonal_cna$LogR[1:max_solutions]
+ )
+
+ # refBAF, refLogR, refMajor, refMinor, rho, gamma_param
+ res <- calc_rho_psi_refit(output$baf, output$logr, output$maj, output$min, rho, gamma_param)
+ output$rho_estimate <- res$rho
+ output$psi_t_estimate <- res$psi_t
+ output$rho_diff <- abs(rho - output$rho_estimate)
+ output$psi_t_diff <- abs(psi_t - output$psi_t_estimate)
+ }
+ } else {
+ # No large clonal alteration, save a suggestion that should use an external purity value
+ output <- data.frame(project = NA, samplename = samplename, qc = NA, cellularity_refit = TRUE, chrom = NA, pos = NA, maj = NA, min = NA, baf = NA, logr = NA, rho_estimate = NA, psi_t_estimate = NA, rho_diff = NA, psi_t_diff = NA)
+ }
+ data.table::fwrite(output, file = paste0(samplename, "_refit_suggestion.txt"), quote = FALSE, sep = "\t", row.names = FALSE)
+}
diff --git a/R/run_ascat.R b/R/run_ascat.R
new file mode 100644
index 00000000..adfa82b9
--- /dev/null
+++ b/R/run_ascat.R
@@ -0,0 +1,428 @@
+#' A modified ASCAT main function to fit Battenberg
+#'
+#' This function returns an initial rho and psi estimate for a clonal copy number fit. It uses an internal distance metric to create a distance matrix.
+#' Using that matrix it will search for a rho and psi combination that yields the least heavy penalty.
+#' @param lrr (unsegmented) log R, in genomic sequence (all probes), with probe IDs
+#' @param baf (unsegmented) B Allele Frequency, in genomic sequence (all probes), with probe IDs
+#' @param lrrsegmented log R, segmented, in genomic sequence (all probes), with probe IDs
+#' @param bafsegmented B Allele Frequency, segmented, in genomic sequence (only probes heterozygous in germline), with probe IDs
+#' @param chromosomes a list containing c vectors, where c is the number of chromosomes and every vector contains all probe numbers per chromosome
+#' @param dist_choice The distance metric to be used internally to penalise a copy number solution
+#' @param distancepng if NA: distance is plotted, if filename is given, the plot is written to a .png file (Default NA)
+#' @param copynumberprofilespng if NA: possible copy number profiles are plotted, if filename is given, the plot is written to a .png file (Default NA)
+#' @param nonroundedprofilepng if NA: copy number profile before rounding is plotted (total copy number as well as the copy number of the minor allele), if filename is given, the plot is written to a .png file (Default NA)
+#' @param cnaStatusFile File where the copy number profile status is written to. This contains either the message "No suitable copy number solution found" or "X copy number solutions found" (Default copynumber_solution_status.txt)
+#' @param gamma technology parameter, compaction of Log R profiles (expected decrease in case of deletion in diploid sample, 100 "\%" aberrant cells; 1 in ideal case, 0.55 of Illumina 109K arrays) (Default 0.55)
+#' @param allow100percent A boolean whether to allow a 100"\%" cellularity solution
+#' @param reliabilityFile String to where fit reliabilty information should be written. This file contains backtransformed BAF and LogR values for segments using the fitted copy number profile (Default NA)
+#' @param min_ploidy The minimum ploidy to consider (Default 1.6)
+#' @param max_ploidy The maximum ploidy to consider (Default 4.8)
+#' @param min_rho The minimum cellularity to consider (Default 0.1)
+#' @param max_rho The maximum cellularity to consider (Default 1.0)
+#' @param min_goodness The minimum goodness of fit for a solution to have to be considered (Default 63)
+#' @param uninformative_baf_threshold The threshold beyond which BAF becomes uninformative (Default 0.51)
+#' @param chr_names A vector with chromosome names used for plotting
+#' @param analysis A String representing the type of analysis to be run, this determines whether the distance figure is produced (Default paired)
+#' @param nthreads The number of paralel processes to run
+#' @param n_neighbors_search Number of top grid points to search (integer). Set to Inf for exhaustive search. If NULL, only local minima are searched.
+#' @param local_min_window_size Window size for local minimum detection (Default 7)
+#' @return A list with fields psi, rho and ploidy
+#' @export
+# the limit on rho is lenient and may lead to spurious solutions
+runASCAT <- function(
+ lrr, baf, lrrsegmented,
+ bafsegmented, chromosomes,
+ dist_choice, distancepng = NA,
+ copynumberprofilespng = NA,
+ nonroundedprofilepng = NA,
+ cnaStatusFile = "copynumber_solution_status.txt",
+ gamma = 0.55, allow100percent,
+ reliabilityFile = NA, min_ploidy = 1.6,
+ max_ploidy = 4.8, min_rho = 0.1,
+ max_rho = 1.0, min_goodness = 0.63,
+ uninformative_baf_threshold = 0.51,
+ chr_names, analysis = "paired",
+ local_min_window_size = 7,
+ n_neighbors_search = NULL,
+ nthreads = 1
+) {
+ # Validate parameters
+ if (!is.numeric(local_min_window_size) || local_min_window_size < 3 || local_min_window_size %% 2 == 0) {
+ log_failure("local_min_window_size must be an odd integer >= 3, got: {local_min_window_size}")
+ stop("Invalid local_min_window_size")
+ }
+
+ # Setup inputs and segments
+ ch <- chromosomes
+ b <- bafsegmented
+ r <- lrrsegmented[names(bafsegmented)]
+
+ # Adapt the rho/psi boundaries
+ dist_min_psi <- max(min_ploidy - 0.6, 0)
+ dist_max_psi <- max_ploidy + 0.6
+ dist_min_rho <- max(min_rho - 0.03, 0.05)
+ dist_max_rho <- max_rho + 0.03
+
+ s <- make_segments_internal(r, b)
+ dist_matrix_info <- create_distance_matrix(
+ s, dist_choice, gamma,
+ uninformative_baf_threshold = uninformative_baf_threshold,
+ min_psi = dist_min_psi,
+ max_psi = dist_max_psi,
+ min_rho = dist_min_rho,
+ max_rho = dist_max_rho,
+ nthreads = nthreads
+ )
+ d <- dist_matrix_info$distance_matrix
+ if (all(is.na(d)) || all(is.infinite(d))) {
+ log_failure("Distance matrix is entirely NA or Inf in runASCAT. No valid copy number solution possible.")
+ }
+ minimise <- dist_matrix_info$minimise
+
+ # Calculate theoretical max distance for goodness of fit
+ TheoretMaxdist <- sum(rep(0.25, dim(s)[1]) * s[, "length"], na.rm = TRUE)
+ total_len <- sum(s[, "length"])
+
+ # Ensure we are always searching for a minimum
+ if (!minimise) d <- -d
+
+ # VECTORIZED LOCAL MINIMA SEARCH
+ nr <- nrow(d)
+ nc <- ncol(d)
+ is_local_min <- matrix(TRUE, nrow = nr, ncol = nc)
+
+ # Check half window size
+ half_window <- (local_min_window_size - 1) / 2
+
+ exhaustive_mode <- !is.null(n_neighbors_search) && (is.infinite(n_neighbors_search) || n_neighbors_search > 0)
+
+ if (exhaustive_mode) {
+ # In exhaustive mode, all points within boundaries are candidates
+ row_range <- (half_window + 1):(nr - half_window)
+ col_range <- (half_window + 1):(nc - half_window)
+ } else {
+ # Constrain search to the interior to match window logic
+ row_range <- (half_window + 1):(nr - half_window)
+ col_range <- (half_window + 1):(nc - half_window)
+
+ # Check every neighbor in the window
+ for (dx in -half_window:half_window) {
+ for (dy in -half_window:half_window) {
+ if (dx == 0 && dy == 0) next
+ is_local_min[row_range, col_range] <- is_local_min[row_range, col_range] &
+ (d[row_range, col_range] < d[row_range + dx, col_range + dy])
+ }
+ }
+ }
+
+ # Zero out the margins
+ is_local_min[-row_range, ] <- FALSE
+ is_local_min[, -col_range] <- FALSE
+
+ # Extraction helper to process candidates
+ evaluate_candidates <- function(indices, current_d) {
+ if (nrow(indices) == 0) {
+ return(NULL)
+ }
+
+ # Pre-calculate segment masks for efficiency
+ is_not_balanced <- s[, "b"] != 0.5
+ weight_unbalanced <- sum(s[, "length"] * is_not_balanced)
+
+ results <- apply(indices, 1, function(idx) {
+ i <- idx[1]
+ j <- idx[2]
+ m <- current_d[i, j]
+ psi <- as.numeric(rownames(current_d)[i])
+ rho <- as.numeric(colnames(current_d)[j])
+
+ # Copy number algebra
+ common_term <- 2^(s[, "r"] / gamma) * ((1 - rho) * 2 + rho * psi)
+ nA <- (rho - 1 - (s[, "b"] - 1) * common_term) / rho
+ nB <- (rho - 1 + s[, "b"] * common_term) / rho
+
+ ploidy <- sum((nA + nB) * s[, "length"]) / total_len
+
+ # Biological viability checks
+ is_nA_zero <- round(nA) == 0
+ is_nB_zero <- round(nB) == 0
+ percentzero <- (sum(is_nA_zero * s[, "length"]) + sum(is_nB_zero * s[, "length"])) / total_len
+ perczeroAbb <- (sum(is_nA_zero * s[, "length"] * is_not_balanced) + sum(is_nB_zero * s[, "length"] * is_not_balanced)) / weight_unbalanced
+ if (is.na(perczeroAbb)) perczeroAbb <- 0
+
+ # Goodness of fit calculation (0-1 scale)
+ fit <- if (minimise) (1 - m / TheoretMaxdist) else -m / TheoretMaxdist
+
+ # Return data if it meets primary constraints (percentzero checks applied later if allow100percent is used)
+ return(list(m = m, i = i, j = j, ploidy = ploidy, fit = fit, pz = percentzero, pza = perczeroAbb, rho = rho, psi = psi))
+ })
+ return(results)
+ }
+
+ # First pass: find optima meeting the percentzero conditions
+ opt_indices <- which(is_local_min, arr.ind = TRUE)
+ candidates <- evaluate_candidates(opt_indices, d)
+
+ # Debug stats container
+ debug_stats <- list(
+ ploidy_bounds = 0,
+ rho_bounds = 0,
+ low_goodness = 0,
+ zero_constraint = 0
+ )
+
+ # Log all candidates before filtering for debugging
+ if (!is.null(candidates)) {
+ log_info("Found {length(candidates)} candidate solutions:")
+ for (i in seq_along(candidates)) {
+ cand <- candidates[[i]]
+ log_info(" Cand {i}: rho={round(cand$rho, 3)}, psi={round(cand$psi, 3)}, dist={round(cand$m, 4)}, goodness={round(cand$fit * 100, 2)}%, pz={round(cand$pz, 4)}, pza={round(cand$pza, 4)}")
+ }
+ }
+
+ # Filtering based on standard Battenberg criteria with logging
+ valid_optima <- list()
+ if (!is.null(candidates)) {
+ valid_optima <- Filter(function(x) {
+ if (x$ploidy < min_ploidy || x$ploidy > max_ploidy) {
+ log_info(" Rejected cand (rho={round(x$rho, 2)}) due to ploidy {round(x$ploidy, 2)} (bounds: {min_ploidy}-{max_ploidy})")
+ debug_stats$ploidy_bounds <<- debug_stats$ploidy_bounds + 1
+ return(FALSE)
+ }
+ if (x$rho < min_rho) {
+ log_info(" Rejected cand (rho={round(x$rho, 2)}) due to rho < {min_rho}")
+ debug_stats$rho_bounds <<- debug_stats$rho_bounds + 1
+ return(FALSE)
+ }
+ if (x$fit < min_goodness) {
+ log_info(" Rejected cand (rho={round(x$rho, 2)}) due to goodness {round(x$fit * 100, 2)}% < {round(min_goodness * 100, 2)}%")
+ debug_stats$low_goodness <<- debug_stats$low_goodness + 1
+ return(FALSE)
+ }
+ if (!(x$pz > 0.01 || x$pza > 0.1)) {
+ log_info(" Rejected cand (rho={round(x$rho, 2)}) due to zero constraint (pz={round(x$pz, 3)}, pza={round(x$pza, 3)})")
+ debug_stats$zero_constraint <<- debug_stats$zero_constraint + 1
+ return(FALSE)
+ }
+ log_info(" Accepted cand (rho={round(x$rho, 2)})")
+ return(TRUE)
+ }, candidates)
+ }
+
+ # Second pass: If allow100percent is TRUE and no solutions found, relax constraints
+ if (allow100percent && length(valid_optima) == 0) {
+ # Penalize cellularity > 1 as per original code
+ cold_idx <- which(as.numeric(colnames(d)) > 1)
+ d[, cold_idx] <- 1e20
+
+ # Reset debug stats for second pass (optional, or keep cumulative)
+ # Re-evaluate all local minima with relaxed biological constraints
+ valid_optima <- Filter(function(x) {
+ return(x$ploidy > min_ploidy && x$ploidy < max_ploidy &&
+ x$rho >= min_rho && x$fit >= min_goodness)
+ }, candidates)
+ }
+
+ # Process the winning solution
+ nropt <- length(valid_optima)
+ psi_opt1_plot <- vector(mode = "numeric")
+ rho_opt1_plot <- vector(mode = "numeric")
+
+ if (nropt > 0) {
+ data.table::fwrite(
+ list(paste(nropt, " copy number solutions found", sep = "")),
+ file = cnaStatusFile, quote = FALSE, col.names = FALSE, row.names = FALSE
+ )
+
+ # Find the global minimum among the local optima
+ all_m <- sapply(valid_optima, function(x) x$m)
+ optlim <- min(all_m)
+
+ # Extract ties for plotting and set the final result
+ for (opt in valid_optima) {
+ if (opt$m == optlim) {
+ psi_opt1 <- opt$psi
+ rho_opt1 <- min(opt$rho, 1)
+ ploidy_opt1 <- opt$ploidy
+ goodness_of_fit_opt1 <- opt$fit
+
+ psi_opt1_plot <- c(psi_opt1_plot, psi_opt1)
+ rho_opt1_plot <- c(rho_opt1_plot, rho_opt1)
+ }
+ }
+
+ log_info("DEBUG: After filtering, {nropt} valid solutions remain")
+ log_info("DEBUG: Selected solution: rho={round(rho_opt1, 3)}, psi={round(psi_opt1, 3)}, ploidy={round(ploidy_opt1, 3)}, goodness={round(goodness_of_fit_opt1 * 100, 2)}%")
+ } else {
+ writeLines("no copy number solutions found", con = cnaStatusFile)
+ log_info("No suitable copy number solution found.")
+ log_info("Debug Rejection Stats: PloidyBounds={debug_stats$ploidy_bounds}, RhoBounds={debug_stats$rho_bounds}, LowGoodness={debug_stats$low_goodness}, ZeroConstraint={debug_stats$zero_constraint}")
+ psi <- ploidy <- rho <- NA
+ psi_opt1_plot <- rho_opt1_plot <- -1
+ }
+
+ # Plotting Sunrise (if paired) - Delayed to run in parallel with other plots
+ # (Logic moved to plotting section below)
+
+ # Final calculations for the best solution
+ if (nropt > 0) {
+ rho <- rho_opt1
+ psi <- psi_opt1
+ ploidy <- ploidy_opt1
+
+ # Optimized Back-transformation with data.table chunking
+ # This matches the enhanced version's logic for speed and memory efficiency
+ log_info("Starting back-transformation (Chunked execution, threads={nthreads})...")
+
+ indices <- seq_along(r)
+ num_chunks <- max(1, nthreads)
+ chunks <- parallel::splitIndices(length(indices), num_chunks)
+
+ results <- bt_mclapply(chunks, function(idx) {
+ b_sub <- b[idx]
+ r_sub <- r[idx]
+
+ # Calculate mult locally
+ mult_sub <- 2^(r_sub / gamma) * ((1 - rho) * 2 + rho * psi)
+
+ nAfull_sub <- (rho - 1 - (b_sub - 1) * mult_sub) / rho
+ nBfull_sub <- (rho - 1 + b_sub * mult_sub) / rho
+ nA_sub <- pmax(round(nAfull_sub), 0)
+ nB_sub <- pmax(round(nBfull_sub), 0)
+
+ rBT_sub <- gamma * log(
+ (rho * (nA_sub + nB_sub) + (1 - rho) * 2) / ((1 - rho) * 2 + rho * psi),
+ 2
+ )
+ bBT_sub <- (1 - rho + rho * nB_sub) / (2 - 2 * rho + rho * (nA_sub + nB_sub))
+
+ return(data.table::data.table(
+ segmentedBAF = b_sub, backTransformedBAF = bBT_sub, segmentedR = r_sub,
+ backTransformedR = rBT_sub, nA = nA_sub, nB = nB_sub, nAfull = nAfull_sub,
+ nBfull = nBfull_sub
+ ))
+ }, mc.cores = nthreads)
+
+ log_info("Aggregating results...")
+ final_dt <- data.table::rbindlist(results)
+
+ # Extract variables for standard plotting/usage downstream
+ nA <- final_dt$nA
+ nB <- final_dt$nB
+ nAfull <- final_dt$nAfull
+ nBfull <- final_dt$nBfull
+ rBT <- final_dt$backTransformedR
+ bBT <- final_dt$backTransformedBAF
+
+ if (!is.na(reliabilityFile)) {
+ # Use threaded writing
+ data.table::fwrite(
+ list(
+ segmentedBAF = b, backTransformedBAF = bBT, segmentedR = r,
+ backTransformedR = rBT, nA = nA, nB = nB, nAfull = nAfull,
+ nBfull = nBfull
+ ),
+ reliabilityFile,
+ sep = ",", row.names = FALSE,
+ nThread = nthreads
+ )
+ }
+
+ # SMART DOWNSAMPLING for performance
+ log_info("Applying chromosome-aware smart downsampling to plotting data...")
+
+ target_total <- 500000
+ total_probes <- length(lrr)
+ lrr_list <- vector("list", length(ch))
+ baf_list <- vector("list", length(ch))
+ nA_list <- vector("list", length(ch))
+ nB_list <- vector("list", length(ch))
+ nAfull_list <- vector("list", length(ch))
+ nBfull_list <- vector("list", length(ch))
+ ch_ds <- vector("list", length(ch))
+ curr_pos <- 1
+
+ for (i in seq_along(ch)) {
+ idx <- ch[[i]]
+ if (length(idx) == 0) next
+ chr_target <- max(500, round(target_total * length(idx) / total_probes))
+ keep_rel <- bt_downsample_indices(lrr[idx], chr_target)
+ keep_abs <- idx[keep_rel]
+
+ lrr_list[[i]] <- lrr[keep_abs]
+ baf_list[[i]] <- bafsegmented[keep_abs]
+ nA_list[[i]] <- nA[keep_abs]
+ nB_list[[i]] <- nB[keep_abs]
+ nAfull_list[[i]] <- nAfull[keep_abs]
+ nBfull_list[[i]] <- nBfull[keep_abs]
+
+ new_len <- length(keep_abs)
+ ch_ds[[i]] <- seq(curr_pos, length.out = new_len)
+ curr_pos <- curr_pos + new_len
+ }
+
+ lrr_ds <- unlist(lrr_list)
+ bafsegmented_ds <- unlist(baf_list)
+ nA_ds <- unlist(nA_list)
+ nB_ds <- unlist(nB_list)
+ nAfull_ds <- unlist(nAfull_list)
+ nBfull_ds <- unlist(nBfull_list)
+ if (!is.null(names(ch))) names(ch_ds) <- names(ch)
+
+ # Generate Profile Plots in Parallel
+ plot_tasks <- list()
+
+ if (analysis == "paired" && !is.na(distancepng)) {
+ plot_tasks[["sunrise"]] <- function() {
+ # Recalculate res based on original logic (1000/7 approx 142.8)
+ grDevices::png(filename = distancepng, width = 1000, height = 1000, res = 1000 / 7, type = "cairo")
+ ASCAT::ascat.plotSunrise(-d, psi_opt1_plot, rho_opt1_plot, minimise)
+ grDevices::dev.off()
+ }
+ }
+
+ if (!is.na(copynumberprofilespng)) {
+ plot_tasks[["profile"]] <- function() {
+ grDevices::png(
+ filename = copynumberprofilespng,
+ width = 2000, height = 500,
+ res = 200, type = "cairo"
+ )
+ ASCAT::ascat.plotAscatProfile(
+ n1all = nA_ds, n2all = nB_ds, heteroprobes = TRUE,
+ ploidy = ploidy_opt1, rho = rho_opt1,
+ goodnessOfFit = goodness_of_fit_opt1 * 100,
+ nonaberrant = FALSE, ch = ch_ds,
+ lrr = lrr_ds, bafsegmented = bafsegmented_ds,
+ chrs = chr_names
+ )
+ grDevices::dev.off()
+ }
+ }
+
+ if (!is.na(nonroundedprofilepng)) {
+ plot_tasks[["nonrounded"]] <- function() {
+ grDevices::png(
+ filename = nonroundedprofilepng,
+ width = 2000, height = 500,
+ res = 200, type = "cairo"
+ )
+ ASCAT::ascat.plotNonRounded(
+ ploidy = ploidy_opt1, rho = rho_opt1,
+ goodnessOfFit = goodness_of_fit_opt1 * 100,
+ nonaberrant = FALSE, nAfull = nAfull_ds,
+ nBfull = nBfull_ds, bafsegmented = bafsegmented_ds,
+ ch = ch_ds, lrr = lrr_ds, chrs = chr_names
+ )
+ grDevices::dev.off()
+ }
+ }
+
+ if (length(plot_tasks) > 0) {
+ log_info("Generating {length(plot_tasks)} genome-wide plots sequentially...")
+ lapply(plot_tasks, function(f) f())
+ }
+ }
+
+ return(list(psi = psi, rho = rho, ploidy = ploidy))
+}
diff --git a/R/run_ascat_enhanced.R b/R/run_ascat_enhanced.R
new file mode 100644
index 00000000..39d3658e
--- /dev/null
+++ b/R/run_ascat_enhanced.R
@@ -0,0 +1,846 @@
+#' Key optimizations:
+#' 1. Early termination after first good solution (like original)
+#' 2. Vectorized distance calculations
+#' 3. Optimized constraint checking
+#' 4. Smart search ordering (best regions first)
+#' 5. Reduced memory allocations
+#' @export
+runASCAT_enhanced <- function(
+ lrr, baf, lrrsegmented, bafsegmented, chromosomes, dist_choice,
+ distancepng = NA, copynumberprofilespng = NA, nonroundedprofilepng = NA,
+ cnaStatusFile = "copynumber_solution_status.txt", gamma = 0.55,
+ allow100percent, reliabilityFile = NA, min_ploidy = 1.6, max_ploidy = 4.8,
+ min_rho = 0.1, max_rho = 1.0, min_goodness = 0.63,
+ uninformative_baf_threshold = 0.51, chr_names, analysis = "paired",
+ smart_ordering = TRUE, early_termination = FALSE, verbose = TRUE,
+ n_neighbors_search = NULL, psi_step = 0.05, rho_step = 0.01,
+ local_min_window_size = 7, nthreads = 1
+) {
+ start_time <- Sys.time()
+
+ # 0. Input Validation
+ if (missing(lrr) || missing(baf) || missing(lrrsegmented) || missing(bafsegmented)) {
+ log_failure("Missing required input arguments for runASCAT_enhanced")
+ stop("Missing input arguments")
+ }
+
+ # Validate new parameters
+ if (!is.numeric(local_min_window_size) || local_min_window_size < 3 || local_min_window_size %% 2 == 0) {
+ log_failure("local_min_window_size must be an odd integer >= 3, got: {local_min_window_size}")
+ stop("Invalid local_min_window_size")
+ }
+
+ if (!is.null(n_neighbors_search)) {
+ if (!is.numeric(n_neighbors_search) || (!is.infinite(n_neighbors_search) && n_neighbors_search < 1)) {
+ log_failure("n_neighbors_search must be NULL, a positive integer, or Inf, got: {n_neighbors_search}")
+ stop("Invalid n_neighbors_search")
+ }
+ }
+
+ # 1. Setup Data Processing
+ ch <- chromosomes
+ b <- bafsegmented
+ # CRITICAL FIX: Match original logic - subset LRR to match BAF (heterozygous probes)
+ # The refactor used the full LRR vector which caused segment misalignment and garbage results
+ if (!is.null(names(bafsegmented))) {
+ logR_segmented <- lrrsegmented[names(bafsegmented)]
+ } else {
+ # Fallback if names are missing (should not happen in standard pipeline)
+ log_info("names(bafsegmented) is NULL. Assuming lrrsegmented and bafsegmented are already aligned or this will fail.")
+ logR_segmented <- lrrsegmented
+ }
+
+ if (length(logR_segmented) != length(b)) {
+ log_failure("Length mismatch in runASCAT_enhanced: LRR {length(logR_segmented)} vs BAF {length(b)}")
+ }
+
+ dist_min_psi <- max(min_ploidy - 0.6, 0)
+ dist_max_psi <- max_ploidy + 0.6
+ dist_min_rho <- max(min_rho - 0.03, 0.05)
+ dist_max_rho <- max_rho + 0.03
+
+ # 2. Create Segments & Distance Matrix
+ # Use internal tolerance-based make_segments to handle floating point jitter
+ s <- make_segments_internal(logR_segmented, b)
+ log_info("Number of segments created: {nrow(s)}")
+
+ if (nrow(s) == 0) {
+ log_failure("No valid segments created in runASCAT_enhanced. Cannot proceed with grid search.")
+ }
+
+ dist_matrix_info <- create_distance_matrix(s, dist_choice, gamma,
+ uninformative_baf_threshold = uninformative_baf_threshold,
+ min_psi = dist_min_psi, max_psi = dist_max_psi,
+ min_rho = dist_min_rho, max_rho = dist_max_rho,
+ nthreads = nthreads
+ )
+ d <- dist_matrix_info$distance_matrix
+
+ # Theoretical maximum distance (weighted by length)
+ TheoretMaxdist <- collapse::fsum(rep(0.25, nrow(s)) * s[, "length"],
+ na.rm = TRUE
+ )
+
+ minimise <- dist_matrix_info$minimise
+
+ log_debug("Distance matrix dimensions: {nrow(d)} x: {ncol(d)}")
+ log_debug("Theoretical Max Distance: {round(TheoretMaxdist, 4)}")
+
+ log_info("Distance matrix stats: min={min(d, na.rm=TRUE)}, max={max(d, na.rm=TRUE)}, mean={mean(d, na.rm=TRUE)}")
+ # We handle minimization/maximization explicitly in the search functions.
+
+ # 3. Pre-compute Search Parameters
+ rho_values <- as.numeric(colnames(d))
+ psi_values <- as.numeric(rownames(d))
+ s_length <- s[, "length"]
+ s_b <- s[, "b"]
+ s_r <- s[, "r"]
+ total_length <- collapse::fsum(s_length)
+
+ # Pre-compute masks for calculate_solution_fast
+ baf_mask <- s_b != 0.5
+ denom_abb <- collapse::fsum(s_length[baf_mask])
+
+ # 3.1 Vectorized Local Minima Detection
+ nr <- nrow(d)
+ nc <- ncol(d)
+ is_local_min <- matrix(TRUE, nrow = nr, ncol = nc)
+ half_window <- (local_min_window_size - 1) / 2
+ row_range <- (half_window + 1):(nr - half_window)
+ col_range <- (half_window + 1):(nc - half_window)
+ is_local_min[, ] <- FALSE
+
+ if (!is.null(n_neighbors_search)) {
+ if (verbose) {
+ if (is.infinite(n_neighbors_search)) {
+ log_info("Search Mode: Exhaustive search (all grid points)")
+ } else {
+ log_info("Search Mode: Top {n_neighbors_search} neighbors by distance")
+ }
+ }
+ is_local_min[row_range, col_range] <- TRUE
+ } else {
+ if (verbose) log_info("Search Mode: Local minima only (window size: {local_min_window_size})")
+ is_local_min[row_range, col_range] <- TRUE
+ for (dx in -half_window:half_window) {
+ for (dy in -half_window:half_window) {
+ if (dx == 0 && dy == 0) next
+ neighbor_vals <- d[row_range + dx, col_range + dy]
+
+ if (minimise) {
+ neighbor_vals[is.na(neighbor_vals)] <- Inf
+ comparison <- (d[row_range, col_range] < neighbor_vals)
+ } else {
+ neighbor_vals[is.na(neighbor_vals)] <- -Inf
+ comparison <- (d[row_range, col_range] > neighbor_vals)
+ }
+
+ comparison[is.na(comparison)] <- FALSE
+ is_local_min[row_range, col_range] <- is_local_min[row_range, col_range] & comparison
+ }
+ }
+ }
+
+ search_order <- create_smart_search_order(
+ d, smart_ordering, verbose, minimise,
+ local_min_window_size = local_min_window_size,
+ skip_local_min = !is.null(n_neighbors_search)
+ )
+ total_points_in_grid <- nrow(search_order)
+
+ # Apply top N filtering if specified
+ if (!is.null(n_neighbors_search) && !is.infinite(n_neighbors_search)) {
+ if (total_points_in_grid > n_neighbors_search) {
+ # Search order is already sorted by distance (best first)
+ # Just take the top N
+ search_order <- search_order[1:n_neighbors_search, , drop = FALSE]
+ total_points_in_grid <- n_neighbors_search
+ if (verbose) log_info("Limited search to top {n_neighbors_search} neighbors")
+ }
+ }
+
+ # Log how many local minima detected by each method
+ num_vectorized_minima <- sum(is_local_min, na.rm = TRUE)
+ log_info("Vectorized detection found {num_vectorized_minima} local minima")
+ log_info("Smart search order returns {total_points_in_grid} points")
+
+ # Check specific grid point (psi=4.45, rho=0.74) if it exists
+ target_psi <- 4.45
+ target_rho <- 0.74
+ psi_idx <- which.min(abs(psi_values - target_psi))
+ rho_idx <- which.min(abs(rho_values - target_rho))
+ if (length(psi_idx) > 0 && length(rho_idx) > 0) {
+ actual_psi <- psi_values[psi_idx]
+ actual_rho <- rho_values[rho_idx]
+
+
+ # Show window to see why it's not a local min
+ if (psi_idx >= (half_window + 1) && psi_idx <= (nr - half_window) &&
+ rho_idx >= (half_window + 1) && rho_idx <= (nc - half_window)) {
+ window_vals <- d[
+ (psi_idx - half_window):(psi_idx + half_window),
+ (rho_idx - half_window):(rho_idx + half_window)
+ ]
+ center_val <- d[psi_idx, rho_idx]
+ min_neighbor <- min(window_vals[window_vals != center_val], na.rm = TRUE)
+ }
+ }
+
+ # Failsafe: If no strict local minima found, we MUST check the full grid
+ # as per the fallback logic in the original runASCAT.
+ if (sum(is_local_min, na.rm = TRUE) == 0 && total_points_in_grid > 0 && is.null(n_neighbors_search)) {
+ if (verbose) log_info("No strict local minima found. Activating FULL GRID search...")
+ is_local_min[row_range, col_range] <- TRUE
+ }
+
+
+ # 4. Main Search Loop
+ nropt <- 0
+ optima <- list()
+ localmin_vals <- numeric()
+ points_checked <- 0
+
+ # Debug stats
+ debug_stats <- list(
+ pre_check_bounds = 0,
+ ploidy_bounds = 0,
+ low_goodness = 0,
+ zero_constraint = 0,
+ max_goodness = -1
+ )
+
+ if (total_points_in_grid > 0) {
+ # Pre-calculate max possible goodness
+ max_poss_goodness <- if (minimise) {
+ min_dist <- min(d, na.rm = TRUE)
+ (1 - min_dist / TheoretMaxdist)
+ } else {
+ max_sim <- max(d, na.rm = TRUE)
+ max_sim / TheoretMaxdist
+ }
+ log_info("Start Search: Optimal Grid Value={if(minimise) min(d, na.rm=TRUE) else max(d, na.rm=TRUE)}, Max Possible Goodness={round(max_poss_goodness * 100, 2)}% (Threshold: {round(min_goodness * 100, 2)}%)")
+
+ if (verbose) log_info("Starting grid search over {total_points_in_grid} points...")
+ for (idx in seq_len(total_points_in_grid)) {
+ i <- search_order[idx, 1]
+ j <- search_order[idx, 2]
+
+ # Use the pre-computed mask
+ if (!is_local_min[i, j]) next
+
+ m <- d[i, j]
+ points_checked <- points_checked + 1
+
+ solution <- calculate_solution_fast(
+ psi_values[i], rho_values[j], s_b, s_r, s_length, total_length,
+ gamma, min_ploidy, max_ploidy, min_rho, max_rho,
+ min_goodness, m, TheoretMaxdist, minimise,
+ allow100percent = FALSE, # FIRST PASS ALWAYS REQUIRES LOH/DELETIONS
+ baf_mask = baf_mask, denom_abb = denom_abb
+ )
+
+ if (solution$valid) {
+ nropt <- nropt + 1
+ # Store as vector for consistency with original optima extraction
+ optima[[nropt]] <- c(m, i, j, solution$ploidy, solution$goodness)
+ localmin_vals[nropt] <- m
+
+ if (verbose) {
+ log_info("Found solution {nropt} at point {points_checked}: rho={round(rho_values[j], 3)}, psi={round(psi_values[i], 3)}")
+ }
+
+ if (early_termination && solution$goodness >= (min_goodness + 0.05)) {
+ if (verbose) log_info("Early termination triggered: Good solution found.")
+ break
+ }
+ } else {
+ # Track rejection reason
+ reject_reason <- solution$reason
+ if (!is.null(reject_reason)) {
+ debug_stats[[reject_reason]] <- debug_stats[[reject_reason]] + 1
+ }
+ if (!is.null(solution$goodness) && solution$goodness > debug_stats$max_goodness) {
+ debug_stats$max_goodness <- solution$goodness
+ }
+ }
+
+ # Correctly report progress inside the loop
+ if (verbose && (points_checked %% 1000 == 0 || points_checked == total_points_in_grid)) {
+ pct_val <- round(points_checked / total_points_in_grid * 100, 1)
+ log_info("Progress: {points_checked}/{total_points_in_grid} ({pct_val}%) points checked")
+ }
+ }
+
+ # 5. Handle 100% Aberrant Fallback
+ if (allow100percent && nropt == 0) {
+ log_info("DEBUG FIRST PASS FAILED: Rejected: pre_check={debug_stats$pre_check_bounds}, ploidy_bounds={debug_stats$ploidy_bounds}, low_goodness={debug_stats$low_goodness}, zero_constraint={debug_stats$zero_constraint}")
+ log_info("DEBUG FIRST PASS FAILED: Max Goodness found: {round(debug_stats$max_goodness, 2)}")
+
+ if (verbose) log_info("Trying 100% aberrant solutions...")
+ d_mod <- d
+ if (minimise) {
+ d_mod[, rho_values > 1] <- 1e20 # Bad for distance
+ } else {
+ d_mod[, rho_values > 1] <- -1e20 # Bad for similarity
+ }
+
+ # CONSISTENCY FIX: Use the same search strategy as first pass
+ # If n_neighbors_search was specified, use it for fallback too
+ if (!is.null(n_neighbors_search)) {
+ # Use top-N search (same as first pass)
+ search_order_100 <- create_smart_search_order(d_mod, smart_ordering, FALSE, minimise,
+ local_min_window_size = local_min_window_size,
+ skip_local_min = TRUE
+ )
+
+ # Apply top-N filtering if needed
+ fallback_search_limit <- if (!is.infinite(n_neighbors_search)) {
+ min(n_neighbors_search, nrow(search_order_100))
+ } else {
+ nrow(search_order_100)
+ }
+
+ if (nrow(search_order_100) > fallback_search_limit) {
+ search_order_100 <- search_order_100[1:fallback_search_limit, , drop = FALSE]
+ }
+
+ if (verbose) log_info("100% fallback: Searching top {nrow(search_order_100)} points (same as first pass)")
+
+ # Search all points in the order (no local min filtering)
+ if (nrow(search_order_100) > 0) {
+ for (idx in seq_len(nrow(search_order_100))) {
+ i <- search_order_100[idx, 1]
+ j <- search_order_100[idx, 2]
+
+ m <- d_mod[i, j]
+ solution <- calculate_solution_fast(
+ psi_values[i], rho_values[j], s_b, s_r, s_length, total_length, gamma,
+ min_ploidy, max_ploidy, min_rho, max_rho,
+ min_goodness, m, TheoretMaxdist, minimise, allow100percent,
+ baf_mask = baf_mask, denom_abb = denom_abb,
+ skip_zero_check = TRUE # RELAX CONSTRAINTS FOR FALLBACK
+ )
+ if (solution$valid) {
+ nropt <- nropt + 1
+ optima[[nropt]] <- c(m, i, j, solution$ploidy, solution$goodness)
+ localmin_vals[nropt] <- m
+ }
+ }
+ }
+ } else {
+ # Original local minima search (when n_neighbors_search is NULL)
+ search_order_100 <- create_smart_search_order(d_mod, smart_ordering, FALSE, minimise,
+ local_min_window_size = local_min_window_size
+ )
+
+ if (nrow(search_order_100) > 0) {
+ for (idx in seq_len(nrow(search_order_100))) {
+ i <- search_order_100[idx, 1]
+ j <- search_order_100[idx, 2]
+
+ # We don't need a redundant local min check here as create_smart_search_order
+ # already handles it correctly based on the 'minimise' flag.
+
+ m <- d_mod[i, j]
+ solution <- calculate_solution_fast(
+ psi_values[i], rho_values[j], s_b, s_r, s_length, total_length, gamma,
+ min_ploidy, max_ploidy, min_rho, max_rho,
+ min_goodness, m, TheoretMaxdist, minimise, allow100percent,
+ baf_mask = baf_mask, denom_abb = denom_abb,
+ skip_zero_check = TRUE # RELAX CONSTRAINTS FOR FALLBACK
+ )
+ if (solution$valid) {
+ nropt <- nropt + 1
+ optima[[nropt]] <- c(m, i, j, solution$ploidy, solution$goodness)
+ localmin_vals[nropt] <- m
+ }
+ }
+ }
+ }
+ }
+
+
+ optimization_time <- as.numeric(difftime(Sys.time(), start_time, units = "secs"))
+
+ # Select Best Solution & Collect Sunrise Plot Data
+ if (nropt > 0) {
+ data.table::fwrite(list(paste0(nropt, " copy number solutions found")), cnaStatusFile)
+
+ # IMPLMENTATION OF ORIGINAL CENTROID LOGIC
+ # Original Battenberg does NOT just take the best goodness.
+ # It calculates the "geometric center" of all valid solutions and picks the one closest to it.
+
+ # 1. Extract Grid Coordinates
+ grid_x_vect <- sapply(optima, function(z) psi_values[z[2]]) # Psi
+ grid_y_vect <- sapply(optima, function(z) rho_values[z[3]]) # Rho
+
+ # 2. Calculate Centroid (Median of means? Original code says: mean(median(grid_x_vect)))
+ # This seems redundant (mean of a scalar median is just the median), but we follow it exactly.
+ centre_x <- mean(stats::median(grid_x_vect))
+ centre_y <- mean(stats::median(grid_y_vect))
+ centre <- c(centre_x, centre_y)
+
+ # 3. Find optimum closest to centroid
+ best_idx <- 1
+ min_sq_dist <- Inf
+
+ # Function to calculate Euclidean distance squared
+ calc_sq_dist <- function(p1, p2) {
+ sum((p1 - p2)^2)
+ }
+
+ for (i in seq_along(optima)) {
+ grid_point <- c(psi_values[optima[[i]][2]], rho_values[optima[[i]][3]])
+ sq_dist <- calc_sq_dist(grid_point, centre)
+
+ if (sq_dist <= min_sq_dist) {
+ min_sq_dist <- sq_dist
+ best_idx <- i
+ }
+ }
+
+ # 4. Extract Winner
+ psi_opt1 <- psi_values[optima[[best_idx]][2]]
+ rho_opt1 <- min(rho_values[optima[[best_idx]][3]], 1.0)
+ ploidy_opt1 <- optima[[best_idx]][4]
+ goodness_of_fit_opt1 <- optima[[best_idx]][5] # This is now the clonal genomic proportion (0-1)
+
+ # 5. Collect Plotting Data (All points passing filters)
+ psi_opt1_plot <- grid_x_vect
+ rho_opt1_plot <- grid_y_vect
+ } else {
+ data.table::fwrite(list("no copy number solutions found"), cnaStatusFile)
+
+ log_info("ASCAT Optimization failed. Rejected: pre_check={debug_stats$pre_check_bounds}, ploidy_bounds={debug_stats$ploidy_bounds}, low_goodness={debug_stats$low_goodness}, zero_constraint={debug_stats$zero_constraint}")
+ if (debug_stats$max_goodness > -1) {
+ log_info("Best rejected candidate had goodness: {round(debug_stats$max_goodness * 100, 2)}% (threshold: {round(min_goodness * 100, 2)}%). If this is high, check ploidy/zero constraints.")
+ }
+
+ return(list(
+ psi = NA, rho = NA, ploidy = NA,
+ convergence_info = list(
+ converged = FALSE, n_solutions_found = 0,
+ optimization_time = optimization_time, points_checked = points_checked,
+ search_efficiency = points_checked / total_points_in_grid
+ )
+ ))
+ }
+
+ # Use the extracted "best" values for the final vectors
+ rho <- rho_opt1
+ psi <- psi_opt1
+ ploidy <- ploidy_opt1
+
+ # 7. Final Back-transformation
+ log_info("Backtransform: rho={rho}, psi={psi}, length(logR_segmented)={length(logR_segmented)}, class={class(logR_segmented)}, gamma={gamma}")
+ if (!is.numeric(logR_segmented)) {
+ log_failure("CRITICAL: logR_segmented corrupted. Value: {paste(head(logR_segmented), collapse=', ')}")
+ }
+
+ # Always use chunked execution to manage memory and provide consistent logging
+ # Even with nthreads=1, this prevents massive single-step allocations
+ log_info("Starting back-transformation (Chunked execution, threads={nthreads})...")
+
+ indices <- seq_along(logR_segmented)
+ # Ensure at least 1 chunk
+ num_chunks <- max(1, nthreads)
+ chunks <- parallel::splitIndices(length(indices), num_chunks)
+
+ results <- bt_mclapply(chunks, function(idx) {
+ # Extract subset
+ r_sub <- logR_segmented[idx]
+ b_sub <- b[idx]
+
+ # Calculate mult locally to save memory
+ mult_sub <- 2^(r_sub / gamma) * ((1 - rho) * 2 + rho * psi)
+
+ nAfull_sub <- (rho - 1 - (b_sub - 1) * mult_sub) / rho
+ nBfull_sub <- (rho - 1 + b_sub * mult_sub) / rho
+ nA_sub <- pmax(round(nAfull_sub), 0)
+ nB_sub <- pmax(round(nBfull_sub), 0)
+
+ rBT_sub <- gamma * log(
+ (rho * (nA_sub + nB_sub) + (1 - rho) * 2) / ((1 - rho) * 2 + rho * psi),
+ 2
+ )
+ bBT_sub <- (1 - rho + rho * nB_sub) / (2 - 2 * rho + rho * (nA_sub + nB_sub))
+
+ # Reliability
+ # Handle potentially empty r_sub
+ if (length(r_sub) > 0) {
+ rDiff <- 1 - abs(rBT_sub - r_sub) / abs(r_sub)
+ rConf_sub <- ifelse(abs(rBT_sub) > 0.15, pmin(100, pmax(0, 100 * rDiff)), NA)
+
+ bDiff <- 1 - abs(bBT_sub - b_sub) / abs(b_sub - 0.5)
+ bConf_sub <- ifelse(bBT_sub != 0.5,
+ pmin(100, pmax(0, ifelse(b_sub == 0.5, 100, 100 * bDiff))), NA
+ )
+ } else {
+ rConf_sub <- numeric(0)
+ bConf_sub <- numeric(0)
+ }
+
+ # Return as a data.table chunk for fast rbindlist
+ return(data.table::data.table(
+ segmentedBAF = b_sub,
+ backTransformedBAF = bBT_sub,
+ confidenceBAF = bConf_sub,
+ segmentedR = r_sub,
+ backTransformedR = rBT_sub,
+ confidenceR = rConf_sub,
+ nA = nA_sub,
+ nB = nB_sub,
+ nAfull = nAfull_sub,
+ nBfull = nBfull_sub
+ ))
+ }, mc.cores = nthreads)
+
+ # Fast aggregation
+ log_info("Aggregating results...")
+ start_agg <- Sys.time()
+ final_dt <- data.table::rbindlist(results)
+ log_info(paste("Aggregation complete in", round(difftime(Sys.time(), start_agg, units = "secs"), 2), "seconds"))
+
+ if (!is.na(reliabilityFile)) {
+ # Optimization: Write the prepared data.table directly
+ log_info(paste("Writing reliability file to", reliabilityFile, "..."))
+ start_write <- Sys.time()
+ # Use threaded writing if available
+ data.table::fwrite(
+ final_dt,
+ reliabilityFile,
+ sep = ",", row.names = FALSE,
+ nThread = nthreads
+ )
+ log_info(paste("Writing complete in", round(difftime(Sys.time(), start_write, units = "secs"), 2), "seconds"))
+ }
+
+ # Extract vectors for plotting (plotting functions expect these variable names)
+ nA <- final_dt$nA
+ nB <- final_dt$nB
+ nAfull <- final_dt$nAfull
+ nBfull <- final_dt$nBfull
+ # Ensure these are numeric vectors
+ if (is.null(nA)) log_failure("Critical: nA missing from results")
+
+ # 8. Plotting
+ # Define plotting tasks as closures
+ plot_tasks <- list()
+
+ # SMART DOWNSAMPLING for performance
+ log_info("Applying chromosome-aware smart downsampling to plotting data...")
+
+ target_total <- 500000
+ total_probes <- length(lrr)
+
+ # Accumulate in lists to avoid O(N^2) overhead
+ lrr_list <- vector("list", length(ch))
+ baf_list <- vector("list", length(ch))
+ nA_list <- vector("list", length(ch))
+ nB_list <- vector("list", length(ch))
+ nAfull_list <- vector("list", length(ch))
+ nBfull_list <- vector("list", length(ch))
+ ch_ds <- vector("list", length(ch))
+
+ curr_pos <- 1
+ start_ds <- Sys.time()
+
+ for (i in seq_along(ch)) {
+ idx <- ch[[i]]
+ if (length(idx) == 0) next
+
+ # Proportionate target for this chromosome
+ chr_target <- max(500, round(target_total * length(idx) / total_probes))
+
+ # Relies on data.table for speed
+ keep_rel <- bt_downsample_indices(lrr[idx], chr_target)
+ keep_abs <- idx[keep_rel]
+
+ lrr_list[[i]] <- lrr[keep_abs]
+ baf_list[[i]] <- bafsegmented[keep_abs]
+ nA_list[[i]] <- nA[keep_abs]
+ nB_list[[i]] <- nB[keep_abs]
+ nAfull_list[[i]] <- nAfull[keep_abs]
+ nBfull_list[[i]] <- nBfull[keep_abs]
+
+ new_len <- length(keep_abs)
+ ch_ds[[i]] <- seq(curr_pos, length.out = new_len)
+ curr_pos <- curr_pos + new_len
+ }
+
+ # Flatten lists once
+ lrr_ds <- unlist(lrr_list)
+ bafsegmented_ds <- unlist(baf_list)
+ nA_ds <- unlist(nA_list)
+ nB_ds <- unlist(nB_list)
+ nAfull_ds <- unlist(nAfull_list)
+ nBfull_ds <- unlist(nBfull_list)
+
+ log_info("Downsampling complete in {round(difftime(Sys.time(), start_ds, units='secs'), 2)} seconds. Reduced to {length(lrr_ds)} points.")
+ # Preserve names for plotter consistency if they exist
+ if (!is.null(names(ch))) names(ch_ds) <- names(ch)
+
+ if (analysis == "paired" && !is.na(distancepng)) {
+ plot_tasks[["sunrise"]] <- function() {
+ log_info("Sunrise Plot: Starting calculation for {distancepng}...")
+ log_info("Sunrise Plot: d matrix stats - min={min(d, na.rm=TRUE)}, max={max(d, na.rm=TRUE)}, NA_count={sum(is.na(d))}")
+ log_info("Sunrise Plot: psi_opt1_plot length={length(psi_opt1_plot)}, rho_opt1_plot length={length(rho_opt1_plot)}")
+ if (length(psi_opt1_plot) > 0) {
+ log_info("Sunrise Plot: first sol: rho={rho_opt1_plot[1]}, psi={psi_opt1_plot[1]}")
+ }
+
+ # Construct bounds for the plot
+ psi_values <- as.numeric(rownames(d))
+ rho_values <- as.numeric(colnames(d))
+ new_bounds <- list(
+ psi_min = min(psi_values),
+ psi_max = max(psi_values),
+ rho_min = min(rho_values),
+ rho_max = max(rho_values)
+ )
+
+ t1 <- Sys.time()
+ tryCatch(
+ {
+ grDevices::png(filename = distancepng, width = 1000, height = 1000, res = 150, type = "cairo")
+ # Use internal plotting function instead of ASCAT::ascat.plotSunrise which is unstable
+ clonal_findcentroid_plot(minimise, dist_choice, d, psi_opt1_plot, rho_opt1_plot, new_bounds)
+ grDevices::dev.off()
+ },
+ error = function(e) {
+ log_failure("CRITICAL ERROR: Failed to create Sunrise plot at {distancepng}. Error: {e$message}")
+ stop(paste("Serious Plotting Error:", e$message))
+ }
+ )
+ t2 <- Sys.time()
+ log_info("Sunrise: Finished in {round(difftime(t2, t1, units='secs'), 2)}s")
+ }
+ }
+
+ if (!is.na(copynumberprofilespng)) {
+ plot_tasks[["profile"]] <- function() {
+ log_info("Profile Plot: Starting genome-wide plot (probes={length(lrr_ds)})...")
+ t1 <- Sys.time()
+ grDevices::png(filename = copynumberprofilespng, width = 2000, height = 500, res = 200, type = "cairo")
+ ASCAT::ascat.plotAscatProfile(
+ n1all = nA_ds, n2all = nB_ds, heteroprobes = TRUE, ploidy = ploidy,
+ rho = rho, goodnessOfFit = goodness_of_fit_opt1 * 100, nonaberrant = FALSE,
+ ch = ch_ds, lrr = lrr_ds, bafsegmented = bafsegmented_ds, chrs = chr_names
+ )
+ grDevices::dev.off()
+ t2 <- Sys.time()
+ log_info("Profile Plot: Finished in {round(difftime(t2, t1, units='secs'), 2)}s")
+ }
+ }
+
+ if (!is.na(nonroundedprofilepng)) {
+ plot_tasks[["nonrounded"]] <- function() {
+ log_info("Nonrounded Plot: Starting genome-wide plot (probes={length(lrr_ds)})...")
+ t1 <- Sys.time()
+ grDevices::png(filename = nonroundedprofilepng, width = 2000, height = 500, res = 200, type = "cairo")
+ ASCAT::ascat.plotNonRounded(
+ ploidy = ploidy, rho = rho, goodnessOfFit = goodness_of_fit_opt1 * 100,
+ nonaberrant = FALSE, nAfull = nAfull_ds, nBfull = nBfull_ds,
+ bafsegmented = bafsegmented_ds, ch = ch_ds, lrr = lrr_ds, chrs = chr_names
+ )
+ grDevices::dev.off()
+ t2 <- Sys.time()
+ log_info("Nonrounded Plot: Finished in {round(difftime(t2, t1, units='secs'), 2)}s")
+ }
+ }
+
+ if (length(plot_tasks) > 0) {
+ log_info("Generating {length(plot_tasks)} genome-wide plots sequentially to ensure container stability...")
+ lapply(plot_tasks, function(f) f())
+ log_info("All plotting tasks completed.")
+ }
+
+ return(list(
+ psi = psi, rho = rho, ploidy = ploidy,
+ convergence_info = list(
+ converged = TRUE,
+ n_solutions_found = nropt,
+ optimization_time = optimization_time,
+ points_checked = points_checked,
+ search_efficiency = points_checked / total_points_in_grid
+ )
+ ))
+ }
+}
+
+
+#' Create search order for grid search
+create_smart_search_order <- function(d, smart_ordering, verbose, minimise, local_min_window_size = 7, skip_local_min = FALSE) {
+ nr <- nrow(d)
+ nc <- ncol(d)
+ search_points <- list()
+ half_window <- (local_min_window_size - 1) / 2
+
+ if (skip_local_min) {
+ for (i in (half_window + 1):(nr - half_window)) {
+ for (j in (half_window + 1):(nc - half_window)) {
+ m <- d[i, j]
+ if (is.finite(m)) {
+ search_points[[length(search_points) + 1]] <- list(i = i, j = j, distance = m)
+ }
+ }
+ }
+ } else if (nr >= local_min_window_size && nc >= local_min_window_size) {
+ for (i in (half_window + 1):(nr - half_window)) {
+ for (j in (half_window + 1):(nc - half_window)) {
+ m <- d[i, j]
+ if (is.finite(m)) {
+ seld <- d[(i - half_window):(i + half_window), (j - half_window):(j + half_window)]
+ center_idx <- half_window + 1
+
+ if (minimise) {
+ # Find local minima
+ seld[center_idx, center_idx] <- max(seld, na.rm = TRUE) + 1
+ if (min(seld, na.rm = TRUE) > m) search_points[[length(search_points) + 1]] <- list(i = i, j = j, distance = m)
+ } else {
+ # Find local maxima
+ seld[center_idx, center_idx] <- min(seld, na.rm = TRUE) - 1
+ if (max(seld, na.rm = TRUE) < m) search_points[[length(search_points) + 1]] <- list(i = i, j = j, distance = m)
+ }
+ }
+ }
+ }
+ } else {
+ idx_mat <- which(is.finite(d), arr.ind = TRUE)
+ for (k in seq_len(nrow(idx_mat))) {
+ search_points[[length(search_points) + 1]] <- list(i = idx_mat[k, 1], j = idx_mat[k, 2], distance = d[idx_mat[k, 1], idx_mat[k, 2]])
+ }
+ }
+
+ if (length(search_points) == 0) {
+ return(matrix(0, 0, 2))
+ }
+ if (smart_ordering) {
+ distances <- sapply(search_points, function(p) p$distance)
+ if (minimise) {
+ search_points <- search_points[order(distances)]
+ } else {
+ search_points <- search_points[order(distances, decreasing = TRUE)]
+ }
+ }
+ result <- matrix(0, nrow = length(search_points), ncol = 2)
+ for (k in seq_along(search_points)) {
+ result[k, 1] <- search_points[[k]]$i
+ result[k, 2] <- search_points[[k]]$j
+ }
+ return(result)
+}
+
+#' Fast solution calculation (vectorized and optimized)
+calculate_solution_fast <- function(
+ psi, rho, s_b, s_r, s_length, total_length, gamma,
+ min_ploidy, max_ploidy, min_rho, max_rho,
+ min_goodness, distance_value, TheoretMaxdist, minimise,
+ allow100percent, baf_mask, denom_abb, skip_zero_check = FALSE
+) {
+ # Guard against rho = 0 to prevent Inf
+ safe_rho <- pmax(rho, 1e-6)
+
+ # Constraint pre-check
+ if (psi < min_ploidy || psi > max_ploidy || rho < min_rho || rho > max_rho) {
+ return(list(valid = FALSE, reason = "pre_check_bounds"))
+ }
+
+ # Vectorized calculation
+ multiplier <- 2^(s_r / gamma) * ((1 - safe_rho) * 2 + safe_rho * psi)
+ nA <- (safe_rho - 1 - (s_b - 1) * multiplier) / safe_rho
+ nB <- (safe_rho - 1 + s_b * multiplier) / safe_rho
+
+ # Ploidy calculation
+ ploidy <- collapse::fsum((nA + nB) * s_length) / total_length
+
+ # Goodness check (cap at 1.0 to prevent overflow)
+ goodness_of_fit <- pmin(1.0, if (minimise) {
+ (1 - distance_value / TheoretMaxdist)
+ } else {
+ distance_value / TheoretMaxdist
+ })
+
+ if (is.na(goodness_of_fit) || goodness_of_fit < min_goodness) {
+ return(list(valid = FALSE, reason = "low_goodness", goodness = goodness_of_fit, ploidy = ploidy))
+ }
+
+ if (is.na(ploidy) || ploidy < min_ploidy || ploidy > max_ploidy) {
+ return(list(valid = FALSE, reason = "ploidy_bounds", ploidy = ploidy, goodness = goodness_of_fit))
+ }
+
+ if (!skip_zero_check && !allow100percent) {
+ # Battenberg heuristic: valid solutions usually have at least some segments with CN=0
+ # (Loss of Heterozygosity or deletion). Solutions with NO losses are often mathematical
+ # artifacts of high-ploidy fits.
+ # However, if allow100percent is TRUE, we relax this as the sample might actually have no losses.
+ nA_r <- round(nA)
+ nB_r <- round(nB)
+ # Edge case: sum(s_length[logical]) can be 0 if no indices match
+ percentzero <- (collapse::fsum(s_length[which(nA_r == 0)]) +
+ collapse::fsum(s_length[which(nB_r == 0)])) / total_length
+
+ perczeroAbb <- 0
+ if (denom_abb > 0) {
+ # Use which() to avoid NA issues in logical indexing
+ perczeroAbb <- (collapse::fsum(s_length[which(baf_mask & nA_r == 0)]) +
+ collapse::fsum(s_length[which(baf_mask & nB_r == 0)])) /
+ denom_abb
+ }
+ # Ensure we don't have NAs or empty results in our proportions
+ if (length(percentzero) == 0 || is.na(percentzero)) percentzero <- 0
+ if (length(perczeroAbb) == 0 || is.na(perczeroAbb)) perczeroAbb <- 0
+
+ if (!isTRUE(percentzero > 0.01 || perczeroAbb > 0.1)) {
+ if (goodness_of_fit > 0.40) { # Only log high-goodness rejections (decimal scale)
+ log_debug("Rejecting high-goodness candidate (no losses): rho={round(rho,3)}, psi={round(psi,3)}, goodness={round(goodness_of_fit,2)}, pz={round(percentzero,4)}, pza={round(perczeroAbb,4)}")
+ }
+ return(list(valid = FALSE, reason = "zero_constraint", goodness = goodness_of_fit))
+ } else {
+ if (goodness_of_fit > 0.40) {
+ log_debug("Accepting candidate: rho={round(rho,3)}, psi={round(psi,3)}, goodness={round(goodness_of_fit,2)}, pz={round(percentzero,4)}, pza={round(perczeroAbb,4)}")
+ }
+ }
+ }
+
+ return(list(valid = TRUE, psi = psi, rho = min(rho, 1.0), ploidy = ploidy, goodness = goodness_of_fit))
+}
+
+#' robust make_segments with tolerance
+#' @noRd
+make_segments_internal <- function(r, b) {
+ m <- matrix(ncol = 2, nrow = length(b))
+ m[, 1] <- r
+ m[, 2] <- b
+ m <- as.matrix(na.omit(m))
+
+ if (nrow(m) == 0) {
+ return(matrix(nrow = 0, ncol = 3, dimnames = list(NULL, c("r", "b", "length"))))
+ }
+
+ pcf_segments <- matrix(ncol = 3, nrow = dim(m)[1])
+ colnames(pcf_segments) <- c("r", "b", "length")
+
+ index <- 0
+ previousb <- -1
+ previousr <- 1E10
+
+ for (i in seq_len(dim(m)[1])) {
+ # Use a small tolerance for floating point comparisons to ensure segmented values collapse correctly
+ if (abs(m[i, 2] - previousb) > 1e-10 || abs(m[i, 1] - previousr) > 1e-10) {
+ index <- index + 1
+ count <- 1
+ pcf_segments[index, "r"] <- m[i, 1]
+ pcf_segments[index, "b"] <- m[i, 2]
+ } else {
+ count <- count + 1
+ }
+ pcf_segments[index, "length"] <- count
+ previousb <- m[i, 2]
+ previousr <- m[i, 1]
+ }
+
+ # Clean up the matrix to remove unused pre-allocated rows
+ pcf_segments <- pcf_segments[seq_len(index), , drop = FALSE]
+ return(pcf_segments)
+}
diff --git a/R/run_clonal_ascat.R b/R/run_clonal_ascat.R
new file mode 100755
index 00000000..ed2663a2
--- /dev/null
+++ b/R/run_clonal_ascat.R
@@ -0,0 +1,429 @@
+####################################################################################################
+#' ASCAT like function to obtain a clonal copy number profile
+#'
+#' This function takes an initial optimum rho/psi pair and uses
+#' an internal distance metric to calculate a score for each rho/psi pair allowed.
+#' The solution with the best score is then taken to obtain a global copy number
+#' profile. This function performs both a grid search and tries to find a reference
+#' segment, but the grid search result is always used for now.
+#' @param lrr (unsegmented) log R, in genomic sequence (all probes), with probe IDs
+#' @param baf (unsegmented) B Allele Frequency, in genomic sequence (all probes),
+#' with probe IDs
+#' @param lrrsegmented log R, segmented, in genomic sequence (all probes), with
+#' probe IDs
+#' @param bafsegmented B Allele Frequency, segmented, in genomic sequence (only
+#' probes heterozygous in germline), with probe IDs
+#' @param chromosomes a list containing c vectors, where c is the number of
+#' chromosomes and every vector contains all probe numbers per chromosome
+#' @param segBAF_table Segmented BAF data.frame from \code{get_segment_info}
+#' @param input_optimum_pair A list containing fields for rho, psi and ploidy,
+#' as is output from \code{runASCAT}
+#' @param dist_choice The distance metric to be used internally to penalise a copy
+#' number solution
+#' @param distancepng if NA: distance is plotted, if filename is given, the plot
+#' is written to a .png file (Default NA)
+#' @param copynumberprofilespng if NA: possible copy number profiles are plotted,
+#' if filename is given, the plot is written to a .png file (Default NA)
+#' @param nonroundedprofilepng if NA: copy number profile before rounding is
+#' plotted (total copy number as well as the copy number of the minor allele), if
+#' filename is given, the plot is written to a .png file (Default NA)
+#' @param gamma_param technology parameter, compaction of Log R profiles (expected
+#' decrease in case of deletion in diploid sample, 100 "\%" aberrant cells; 1 in
+#' ideal case, 0.55 of Illumina 109K arrays) (Default 0.55)
+#' @param read_depth TODO: unused parameter that should be removed
+#' @param uninformative_baf_threshold The threshold beyond which BAF becomes
+#' uninformative
+#' @param allow100percent A boolean whether to allow a 100"\%" cellularity
+#' solution
+#' @param reliabilityFile String to where fit reliabilty information should be
+#' written. This file contains backtransformed BAF and LogR values for segments
+#' using the fitted copy number profile (Default NA)
+#' @param psi_min_initial Minimum psi value to be considered (Default: 1.0)
+#' @param psi_max_initial Maximum psi value to be considered (Default: 5.4)
+#' @param rho_min_initial Minimum rho value to be considered (Default: 0.1)
+#' @param rho_max_initial Maximum rho value to be considered (Default: 1.05)
+#' @param chr_names A vector with chromosome names used for plotting
+#' @param nthreads The number of paralel processes to run
+#' @return A list with fields output_optimum_pair, output_optimum_pair_without_ref,
+#' distance, distance_without_ref, minimise and is_ref_better
+#' @export
+run_clonal_ASCAT <- function(
+ lrr, baf, lrrsegmented,
+ bafsegmented, chromosomes,
+ segBAF_table, input_optimum_pair,
+ dist_choice, distancepng = NA,
+ copynumberprofilespng = NA,
+ nonroundedprofilepng = NA,
+ gamma_param, read_depth,
+ uninformative_baf_threshold,
+ allow100percent,
+ reliabilityFile = NA,
+ psi_min_initial = 1.0,
+ psi_max_initial = 5.4,
+ rho_min_initial = 0.1,
+ rho_max_initial = 1.05,
+ chr_names,
+ nthreads = 1
+) {
+ siglevel_BAF <- 0.05
+ maxdist_BAF <- 0.01
+
+ # DCW 160314 - much more lenient logR thresholds (allow anything!)
+ # # TODO: This parameter is pushed down to is_segment_clonal but not used there (maybe not used at all?)
+ siglevel_LogR <- -0.01
+ maxdist_LogR <- 1
+
+ initial_bounds <- list(psi_min = psi_min_initial, psi_max = psi_max_initial, rho_min = rho_min_initial, rho_max = rho_max_initial)
+
+ new_bounds <- get_new_bounds(input_optimum_pair, initial_bounds)
+
+
+ ch <- chromosomes
+ b <- bafsegmented
+ r <- lrrsegmented[names(bafsegmented)]
+
+ # CRITICAL FIX: Subset LRR using PROBE NAMES (names of bafsegmented)
+ # segBAF_table rownames are numeric indices (1..N) which causes mismatch with named lrrsegmented vector
+ s <- get_segment_info(lrrsegmented[names(bafsegmented)], segBAF_table)
+ log_debug("get_segment_info returned: {nrow(s)} rows, {ncol(s)} columns")
+ if (nrow(s) > 0) {
+ log_debug("get_segment_info head: {paste(head(s, 1), collapse=', ')}")
+ } else {
+ log_debug("get_segment_info returned empty matrix")
+ }
+
+ if (is.null(s) || nrow(s) == 0) {
+ log_failure("No valid segments found in run_clonal_ASCAT. Cannot proceed with clonal copy number fitting.")
+ }
+
+ # Make sure no segment of length 1 remains
+ s <- s[s[, "length"] > 1, , drop = FALSE]
+ log_debug("After filtering length > 1: {nrow(s)} rows")
+ if (nrow(s) == 0) {
+ log_failure("No segments with length > 1 found in run_clonal_ASCAT.")
+ }
+
+ dist_matrix_info <- create_distance_matrix_clonal(
+ s, dist_choice, gamma_param, read_depth, siglevel_BAF, maxdist_BAF,
+ siglevel_LogR, maxdist_LogR, uninformative_baf_threshold, new_bounds,
+ nthreads = nthreads
+ ) # kjd 10-2-2013
+
+ d <- dist_matrix_info$distance_matrix
+ if (all(is.na(d)) || all(is.infinite(d))) {
+ log_failure("Distance matrix is entirely NA or Inf in run_clonal_ASCAT. No valid copy number solution possible.")
+ }
+ minimise <- dist_matrix_info$minimise
+
+ # DCW 210314
+ if (minimise) {
+ best.distance <- min(d)
+ } else {
+ best.distance <- max(d)
+ }
+
+ ref_seg_matrix <- dist_matrix_info$ref_seg_matrix
+
+ ref_major <- dist_matrix_info$ref_major
+ ref_minor <- dist_matrix_info$ref_minor
+
+ #########################################################
+
+ ret <- find_centroid_of_global_minima(
+ d, ref_seg_matrix, ref_major,
+ ref_minor, s, dist_choice, minimise,
+ new_bounds, distancepng, gamma_param,
+ siglevel_BAF, maxdist_BAF, siglevel_LogR,
+ maxdist_LogR, allow100percent,
+ uninformative_baf_threshold, read_depth
+ )
+ optima_info_without_ref <- ret$optima_info_without_ref
+ optima_info <- ret$optima_info
+
+ nropt <- optima_info$nropt
+ psi_opt1 <- optima_info$psi_opt1
+ rho_opt1 <- optima_info$rho_opt1
+ ploidy_opt1 <- optima_info$ploidy_opt1
+ goodness_of_fit_opt1 <- optima_info$goodness_of_fit_opt1
+
+ distance.from.ref.seg <- goodness_of_fit_opt1
+
+ is_ref_better <- FALSE
+ if (is.na(rho_opt1)) {
+ log_info("reference segment did not provide a possible solution")
+ } else if (psi_opt1 >= psi_min_initial && psi_opt1 <= psi_max_initial &&
+ rho_opt1 >= rho_min_initial && rho_opt1 <= rho_max_initial &&
+ ((minimise && distance.from.ref.seg < best.distance) ||
+ (!minimise && distance.from.ref.seg > best.distance))) {
+ is_ref_better <- T
+ log_info("reference segment gives better results than grid search")
+ } else {
+ log_info("reference segment gives no better results than grid search. \\
+ Reverting to grid search solution")
+ }
+
+ psi_without_ref <- optima_info_without_ref$psi_opt1
+ rho_without_ref <- optima_info_without_ref$rho_opt1
+ ploidy_without_ref <- optima_info_without_ref$ploidy_opt1
+ goodness_of_fit_without_ref <- optima_info_without_ref$goodness_of_fit_opt1
+
+ #########################################################
+
+ if (nropt > 0) {
+ if (is_ref_better) {
+ rho <- rho_opt1
+ psi <- psi_opt1
+ ploidy <- ploidy_opt1
+ goodness_of_fit <- goodness_of_fit_opt1
+ } else {
+ rho <- rho_without_ref
+ psi <- psi_without_ref
+ ploidy <- ploidy_without_ref
+ goodness_of_fit <- goodness_of_fit_without_ref
+ }
+ nAfull <- (rho - 1 - (b - 1) * 2^(r / gamma_param) *
+ ((1 - rho) * 2 + rho * psi)) / rho
+ nBfull <- (rho - 1 + b * 2^(r / gamma_param) *
+ ((1 - rho) * 2 + rho * psi)) / rho
+ nA <- pmax(round(nAfull), 0)
+ nB <- pmax(round(nBfull), 0)
+
+ rBacktransform <- gamma_param *
+ log((rho * (nA + nB) + (1 - rho) * 2) / ((1 - rho) * 2 + rho * psi), 2)
+ bBacktransform <- (1 - rho + rho * nB) / (2 - 2 * rho + rho * (nA + nB))
+ rDiff <- 1 - abs(rBacktransform - r) / abs(r)
+ rConf <- ifelse(abs(rBacktransform) > 0.15,
+ pmin(100, pmax(0, 100 * rDiff)), NA
+ )
+ bDiff <- 1 - abs(bBacktransform - b) / abs(b - 0.5)
+ bConf <- ifelse(bBacktransform != 0.5,
+ pmin(100, pmax(0, ifelse(b == 0.5, 100, 100 * bDiff))), NA
+ )
+ # DCW 150711 - get deviations from expected values
+ if (!is.na(reliabilityFile)) {
+ data.table::fwrite(
+ data.frame(
+ segmentedBAF = b, backTransformedBAF = bBacktransform,
+ confidenceBAF = bConf, segmentedR = r,
+ backTransformedR = rBacktransform, confidenceR = rConf,
+ nA = nA, nB = nB, nAfull = nAfull, nBfull = nBfull
+ ),
+ reliabilityFile,
+ sep = ",", row.names = FALSE
+ )
+ }
+
+ # SMART DOWNSAMPLING for performance
+ log_info("Applying chromosome-aware smart downsampling to plotting data...")
+ target_total <- 500000
+ total_probes <- length(lrr)
+ lrr_list <- vector("list", length(ch))
+ baf_list <- vector("list", length(ch))
+ nA_list <- vector("list", length(ch))
+ nB_list <- vector("list", length(ch))
+ nAfull_list <- vector("list", length(ch))
+ nBfull_list <- vector("list", length(ch))
+ ch_ds <- vector("list", length(ch))
+ curr_pos <- 1
+
+ for (i in seq_along(ch)) {
+ idx <- ch[[i]]
+ if (length(idx) == 0) next
+ chr_target <- max(500, round(target_total * length(idx) / total_probes))
+ keep_rel <- bt_downsample_indices(lrr[idx], chr_target)
+ keep_abs <- idx[keep_rel]
+
+ lrr_list[[i]] <- lrr[keep_abs]
+ baf_list[[i]] <- bafsegmented[keep_abs]
+ nA_list[[i]] <- nA[keep_abs]
+ nB_list[[i]] <- nB[keep_abs]
+ nAfull_list[[i]] <- nAfull[keep_abs]
+ nBfull_list[[i]] <- nBfull[keep_abs]
+
+ new_len <- length(keep_abs)
+ ch_ds[[i]] <- seq(curr_pos, length.out = new_len)
+ curr_pos <- curr_pos + new_len
+ }
+
+ lrr_ds <- unlist(lrr_list)
+ bafsegmented_ds <- unlist(baf_list)
+ nA_ds <- unlist(nA_list)
+ nB_ds <- unlist(nB_list)
+ nAfull_ds <- unlist(nAfull_list)
+ nBfull_ds <- unlist(nBfull_list)
+ if (!is.null(names(ch))) names(ch_ds) <- names(ch)
+
+ # Make plots in parallel if requested
+ plot_tasks <- list()
+
+ if (!is.na(copynumberprofilespng)) {
+ plot_tasks[["profile"]] <- function() {
+ grDevices::png(
+ filename = copynumberprofilespng,
+ width = 2000, height = 500,
+ res = 200, type = "cairo"
+ )
+ ASCAT::ascat.plotAscatProfile(
+ n1all = nA_ds, n2all = nB_ds,
+ heteroprobes = TRUE,
+ ploidy = ploidy, rho = rho,
+ goodnessOfFit = goodness_of_fit * 100,
+ nonaberrant = FALSE,
+ ch = ch_ds, lrr = lrr_ds,
+ bafsegmented = bafsegmented_ds,
+ chrs = chr_names
+ )
+ grDevices::dev.off()
+ }
+ }
+
+ if (!is.na(nonroundedprofilepng)) {
+ plot_tasks[["nonrounded"]] <- function() {
+ grDevices::png(
+ filename = nonroundedprofilepng,
+ width = 2000, height = 500,
+ res = 200, type = "cairo"
+ )
+ ASCAT::ascat.plotNonRounded(
+ ploidy = ploidy, rho = rho,
+ goodnessOfFit = goodness_of_fit * 100,
+ nonaberrant = FALSE, nAfull = nAfull_ds,
+ nBfull = nBfull_ds, bafsegmented = bafsegmented_ds,
+ ch = ch_ds, lrr = lrr_ds, chrs = chr_names
+ )
+ grDevices::dev.off()
+ }
+ }
+
+ if (length(plot_tasks) > 0) {
+ log_info("Generating {length(plot_tasks)} genome-wide plots sequentially to ensure container stability...")
+ lapply(plot_tasks, function(f) f())
+ }
+ }
+
+ # Recalculate the psi_t for this rho using only clonal segments
+ psi_t <- recalc_psi_t(
+ psi_without_ref, rho_without_ref, gamma_param, r, segBAF_table,
+ siglevel_BAF, maxdist_BAF,
+ include_subcl_segments = FALSE
+ )
+
+ # If there aren't any clonally fit segments, the above yields NA. In this case, revert to the original grid search psi_t
+ if (is.na(psi_t)) {
+ log_info("Recalculated psi_t was NA, reverting to grid search solution. This occurs when no segment could be fit with a clonal state, check sample for contamination")
+ psi_t <- input_optimum_pair$psi
+ rho_without_ref <- input_optimum_pair$rho
+ ploidy_without_ref <- input_optimum_pair$ploidy
+ }
+
+ output_optimum_pair <- list(psi = psi_opt1, rho = rho_opt1, ploidy = ploidy_opt1)
+ # output_optimum_pair_without_ref = list(psi = psi_without_ref, rho = rho_without_ref, ploidy = ploidy_without_ref)
+ # Use the recalculated psi_t from the clonal segments as our final estimate
+ # of psi_t which is data driven with rho fixed
+ output_optimum_pair_without_ref <- list(
+ psi = psi_t, rho = rho_without_ref, ploidy = ploidy_without_ref
+ )
+ return(
+ list(
+ output_optimum_pair = output_optimum_pair,
+ output_optimum_pair_without_ref = output_optimum_pair_without_ref,
+ distance = goodness_of_fit_opt1,
+ distance_without_ref = goodness_of_fit_without_ref,
+ minimise = minimise,
+ is_ref_better = is_ref_better,
+ dist_matrix_info = dist_matrix_info
+ )
+ )
+}
+
+#' Function extends the ASCAT \code{make_segments} function to make segments
+#' of constant BAF and LogR. This function returns a matrix with for each
+#' segment the LogR, BAF, the length of the segment (twice), and the mean and
+#' standard deviation of the BAF values
+#' @noRd
+get_segment_info <- function(segLogR, segBAF_table) {
+ # Column names for robust access
+ col_names <- names(segBAF_table)
+ baf_col <- if ("BAFseg" %in% col_names) "BAFseg" else 5
+ phased_col <- if ("BAFphased" %in% col_names) "BAFphased" else 4
+
+ b_raw_full <- segBAF_table[[baf_col]]
+ b_phased_full <- segBAF_table[[phased_col]]
+
+ # 1. Consensus filtering: ensures r, b, and phased BAF are aligned and non-NA
+ # This matches the internal logic of make_segments for consistency
+ valid_mask <- !is.na(segLogR) & !is.na(b_raw_full)
+ r <- segLogR[valid_mask]
+ b <- b_raw_full[valid_mask]
+ bp <- b_phased_full[valid_mask]
+
+ if (length(r) == 0) {
+ return(matrix(0, 0, 7))
+ }
+
+ # 2. Identify contiguous segments using Run-Length Encoding ID
+ # This is the defining logic of a segment: contiguous regions with same values
+ seg_id <- data.table::rleid(r, b)
+
+ # 3. Aggregate stats per segment
+ # This avoids any indexing mismatch errors and handles the matrix creation in one pass
+ dt <- data.table::data.table(r = r, b = b, bp = bp, seg_id = seg_id)
+
+ # We need 7 columns: r, b, length, length.1, size, mean, sd
+ # Battenberg legacy format repeats length/size columns
+ stats_dt <- dt[, .(
+ r = .subset2(r, 1),
+ b = .subset2(b, 1),
+ len1 = .N,
+ len2 = .N,
+ size = .N,
+ mean_bp = mean(bp, na.rm = TRUE),
+ sd_bp = sd(bp, na.rm = TRUE)
+ ), by = seg_id]
+
+ # 4. Return as matrix with exact column names expected by Battenberg
+ res <- as.matrix(stats_dt[, .(r, b, len1, len2, size, mean_bp, sd_bp)])
+ colnames(res) <- c("r", "b", "length", "length.1", "size", "mean", "sd")
+
+ return(res)
+}
+
+
+#' Optimized Segment Maker - Returns 3 columns like ASCAT original
+#' @noRd
+make_segments <- function(r, b) {
+ m <- matrix(ncol = 2, nrow = length(b))
+ m[, 1] <- r
+ m[, 2] <- b
+ m <- as.matrix(na.omit(m))
+
+ if (nrow(m) == 0) {
+ return(matrix(nrow = 0, ncol = 3, dimnames = list(NULL, c("r", "b", "length"))))
+ }
+
+ pcf_segments <- matrix(ncol = 3, nrow = dim(m)[1])
+ colnames(pcf_segments) <- c("r", "b", "length")
+
+ index <- 0
+ previousb <- -1
+ previousr <- 1E10
+
+ for (i in seq_len(dim(m)[1])) {
+ # Use a small tolerance for floating point comparisons to ensure segmented values collapse correctly
+ if (abs(m[i, 2] - previousb) > 1e-10 || abs(m[i, 1] - previousr) > 1e-10) {
+ index <- index + 1
+ count <- 1
+ pcf_segments[index, "r"] <- m[i, 1]
+ pcf_segments[index, "b"] <- m[i, 2]
+ } else {
+ count <- count + 1
+ }
+ pcf_segments[index, "length"] <- count
+ previousb <- m[i, 2]
+ previousr <- m[i, 1]
+ }
+
+ # Clean up the matrix to remove unused pre-allocated rows
+ pcf_segments <- pcf_segments[seq_len(index), , drop = FALSE]
+ return(pcf_segments)
+}
diff --git a/R/run_part.R b/R/run_part.R
new file mode 100644
index 00000000..89cdc0dc
--- /dev/null
+++ b/R/run_part.R
@@ -0,0 +1,107 @@
+#' Run code in parallel or serial based on debug status
+#'
+#' A helper function to abstract the pattern of switching between parallel
+#' execution via foreach and serial execution via lapply.
+#'
+#' @param iterator A vector or list to iterate over (e.g., seq_along(x)).
+#' @param func A function to apply to each element of the iterator.
+#' @param libs Path to library paths for workers.
+#'
+#' @return A list of results from the applied function.
+#' @keywords internal
+run_with_error_handling <- function(iterator, func, libs, nthreads = 1) {
+ if (length(iterator) == 0) {
+ return(list())
+ }
+
+ # Set up foreach to use the registered backend
+ # Use %dopar% if a backend is registered and nthreads > 1, else %do%
+ `%op%` <- if (foreach::getDoParWorkers() > 1) foreach::`%dopar%` else foreach::`%do%`
+
+ results <- foreach::foreach(i = iterator) %op% {
+ # Set thread budget for this worker
+ data.table::setDTthreads(nthreads)
+ Sys.setenv(OMP_NUM_THREADS = nthreads, MKL_NUM_THREADS = nthreads, OPENBLAS_NUM_THREADS = nthreads)
+
+ .libPaths(libs)
+
+ # Execute the function and capture its result
+ worker_result <- withCallingHandlers(
+ {
+ func(i)
+ },
+ error = function(e) {
+ msg <- sprintf("!!! BATTENBERG ERROR IN PARALLEL WORKER NODE %s !!!\nMessage: %s\nStack Trace:", i, conditionMessage(e))
+ calls <- sys.calls()
+ for (j in rev(seq_along(calls))) {
+ msg <- paste(msg, sprintf("%d: %s", j, deparse(calls[[j]])), sep = "\n")
+ }
+ msg <- paste(msg, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", sep = "\n")
+ stop(msg, call. = FALSE)
+ }
+ )
+
+ # Trigger garbage collection after each worker finishes its task to free up RAM
+ gc()
+
+ # The last expression in the loop body is what gets returned to the results list
+ worker_result
+ }
+ return(results)
+}
+
+#' Safe wrapper for mclapply that prevents deadlocks
+#'
+#' This function disables data.table multi-threading before forking and restores it after.
+#' This is critical to prevent hangs in Singularity/Linux environments.
+#'
+#' @param X A vector or list to iterate over.
+#' @param FUN The function to be applied.
+#' @param mc.cores The number of cores to use.
+#' @param ... Additional arguments passed to mclapply.
+#' @return A list of results.
+bt_mclapply <- function(X, FUN, mc.cores = 1, ...) {
+ # If we are already in a parallel worker (e.g., from sample-level parallelism),
+ # or if 1 core is requested, we MUST run sequentially.
+ is_nested <- FALSE
+ if (requireNamespace("foreach", quietly = TRUE)) {
+ is_nested <- foreach::getDoParWorkers() > 1
+ }
+
+ if (mc.cores <= 1 || is_nested) {
+ return(lapply(X, FUN, ...))
+ }
+
+ # Ensure data.table multi-threading is off before forking to prevent deadlocks
+ old_threads <- data.table::getDTthreads()
+ data.table::setDTthreads(1)
+
+ on.exit({
+ data.table::setDTthreads(old_threads)
+ })
+
+ # mc.preschedule=FALSE is more stable in container environments with varying task sizes
+ parallel::mclapply(X, FUN, mc.cores = mc.cores, mc.preschedule = FALSE, ...)
+}
+
+#' Helper for chromosome-aware smart downsampling for plot performance
+#'
+#' @param v The vector to downsample.
+#' @param target Target number of points.
+#' @return A vector of indices to keep.
+bt_downsample_indices <- function(v, target) {
+ n <- length(v)
+ if (n <= target) {
+ return(seq_along(v))
+ }
+ # We use a combined approach: uniform sampling + local extremes (min/max)
+ # to preserve visual dips/peaks in LogR/BAF
+ bin_size <- ceiling(n / (target / 2))
+
+ # Use data.table for speed and concise grouping
+ dt_ds <- data.table::data.table(val = as.numeric(v), id = seq_along(v))
+ dt_ds[, bin := ceiling(id / bin_size)]
+ keep <- dt_ds[, .(id_min = id[which.min(val)], id_max = id[which.max(val)]), by = bin]
+
+ sort(unique(c(keep$id_min, keep$id_max)))
+}
diff --git a/R/segmentation.R b/R/segmentation.R
index 95f7cf42..dc6fe19b 100644
--- a/R/segmentation.R
+++ b/R/segmentation.R
@@ -1,4 +1,3 @@
-
#' Helper function to adjust the BAF segmented values. By default the segmentation
#' takes the mean BAFphased for each segment, but that doesn't work very well with
#' outliers (i.e. badly phased regions). This function is then called to adjust
@@ -7,166 +6,26 @@
#' @return A data frame with columns BAFphased and BAFseg.
#' @author sd11
#' @noRd
-adjustSegmValues = function(baf_chrom) {
- segs = rle(baf_chrom$BAFseg)
- for (i in 1:length(segs$lengths)) {
- end = cumsum(segs$lengths[1:i])
- end = end[length(end)]
- start = (end-segs$lengths[i]) + 1 # segs$lengths contains end points
- # baf_chrom$bafmean[start:end] = mean(baf_chrom$BAFphased[start:end])
- baf_chrom$BAFseg[start:end] = median(baf_chrom$BAFphased[start:end])
- # This needs the ASCAT version of PCF
- # datwins = madWins(baf_chrom$BAFphased[start:end], 2.5, 25)$ywin
- # baf_chrom$madwins_mean[start:end] = mean(datwins)
- # baf_chrom$madwins_median[start:end] = median(datwins)
+adjustSegmValues <- function(baf_chrom) {
+ # Use original rle-based algorithm for exact equivalence with original Battenberg
+ segs <- rle(baf_chrom$BAFseg)
+ for (i in seq_along(segs$lengths)) {
+ end <- cumsum(segs$lengths[1:i])
+ end <- end[length(end)]
+ start <- (end - segs$lengths[i]) + 1
+ baf_chrom$BAFseg[start:end] <- median(baf_chrom$BAFphased[start:end])
}
return(baf_chrom)
}
-#' Segment the haplotyped and phased data using fastPCF. This is the legacy segmentation function as it was used in the original Battenberg versions
-#'
-#' This function performs segmentation. This is done in two steps. First a segmentation step
-#' that aims to find short segments. These are used to find haplotype blocks that have been
-#' switched. These blocks are switched into the correct order first after which the second
-#' segmentation step is performed. This second step aims to segment the data that will go into
-#' fit.copy.number. This function produces a BAF segmented file with 5 columns: chromosome, position,
-#' original BAF, switched BAF and BAF segment. The BAF segment column should be used subsequently
-#' @param samplename Name of the sample, which is used to name output figures
-#' @param inputfile String that points to the output from the \code{combine.baf.files} function. This contains the phased SNPs with their BAF values
-#' @param outputfile String where the segmentation output will be written
-#' @param gamma The gamma parameter controls the size of the penalty of starting a new segment during segmentation. It is therefore the key parameter for controlling the number of segments (Default: 10)
-#' @param kmin Kmin represents the minimum number of probes/SNPs that a segment should consist of (Default: 3)
-#' @param phasegamma Gamma parameter used when correcting phasing mistakes (Default: 3)
-#' @param phasekmin Kmin parameter used when correcting phasing mistakes (Default: 3)
-#' @param calc_seg_baf_option Various options to recalculate the BAF of a segment. Options are: 1 - median, 2 - mean. (Default: 1)
-#' @author dw9
-#' @export
-segment.baf.phased.legacy = function(samplename, inputfile, outputfile, gamma=10, phasegamma=3, kmin=3, phasekmin=3, calc_seg_baf_option=1) {
-
-
-}
-
-#' Segment the haplotyped and phased data using fastPCF. This is the legacy segmentation function as it was used in the original Battenberg versions
-#'
-#' This function performs segmentation. This is done in two steps. First a segmentation step
-#' that aims to find short segments. These are used to find haplotype blocks that have been
-#' switched. These blocks are switched into the correct order first after which the second
-#' segmentation step is performed. This second step aims to segment the data that will go into
-#' fit.copy.number. This function produces a BAF segmented file with 5 columns: chromosome, position,
-#' original BAF, switched BAF and BAF segment. The BAF segment column should be used subsequently
-#' @param samplename Name of the sample, which is used to name output figures
-#' @param inputfile String that points to the output from the \code{combine.baf.files} function. This contains the phased SNPs with their BAF values
-#' @param outputfile String where the segmentation output will be written
-#' @param gamma The gamma parameter controls the size of the penalty of starting a new segment during segmentation. It is therefore the key parameter for controlling the number of segments (Default: 10)
-#' @param kmin Kmin represents the minimum number of probes/SNPs that a segment should consist of (Default: 3)
-#' @param phasegamma Gamma parameter used when correcting phasing mistakes (Default: 3)
-#' @param phasekmin Kmin parameter used when correcting phasing mistakes (Default: 3)
-#' @author dw9
-#' @export
-segment.baf.phased.legacy = function(samplename, inputfile, outputfile, gamma=10, phasegamma=3, kmin=3, phasekmin=3) {
- BAFraw = as.data.frame(read_baf(inputfile))
-
- BAFoutput = NULL
- for (chr in unique(BAFraw[,1])) {
- BAFrawchr = BAFraw[BAFraw[,1]==chr,c(2,3)]
- BAFrawchr = BAFrawchr[!is.na(BAFrawchr[,2]),]
-
- BAF = BAFrawchr[,2]
- pos = BAFrawchr[,1]
- names(BAF) = rownames(BAFrawchr)
- names(pos) = rownames(BAFrawchr)
-
- sdev <- getMad(ifelse(BAF<0.5,BAF,1-BAF),k=25)
- # Standard deviation is not defined for a single value
- if (is.na(sdev)) {
- sdev = 0
- }
- #DCW 250314
- #for cell lines, sdev goes to zero in regions of LOH, which causes problems.
- #0.09 is around the value expected for a binomial distribution around 0.5 with depth 30
- if(sdev<0.09){
- sdev = 0.09
- }
-
- print(paste("BAFlen=",length(BAF),sep=""))
- if(length(BAF)<50){
- BAFsegm = rep(mean(BAF),length(BAF))
- }else{
- res= selectFastPcf(BAF,phasekmin,phasegamma*sdev,T)
- BAFsegm = res$yhat
- }
-
- png(filename = paste(samplename,"_RAFseg_chr",chr,".png",sep=""), width = 2000, height = 1000, res = 200, type = "cairo")
- create.segmented.plot(chrom.position=pos/1000000,
- points.red=BAF,
- points.green=BAFsegm,
- x.min=min(pos)/1000000,
- x.max=max(pos)/1000000,
- title=paste(samplename,", chromosome ", chr, sep=""),
- xlab="Position (Mb)",
- ylab="BAF (phased)")
- dev.off()
-
- BAFphased = ifelse(BAFsegm>0.5,BAF,1-BAF)
-
- if(length(BAFphased)<50){
- BAFphseg = rep(mean(BAFphased),length(BAFphased))
- }else{
- res = selectFastPcf(BAFphased,kmin,gamma*sdev,T)
- BAFphseg = res$yhat
- }
-
- png(filename = paste(samplename,"_segment_chr",chr,".png",sep=""), width = 2000, height = 1000, res = 200, type = "cairo")
- create.baf.plot(chrom.position=pos/1000000,
- points.red.blue=BAF,
- plot.red=BAFsegm>0.5,
- points.darkred=BAFphseg,
- points.darkblue=1-BAFphseg,
- x.min=min(pos)/1000000,
- x.max=max(pos)/1000000,
- title=paste(samplename,", chromosome ", chr, sep=""),
- xlab="Position (Mb)",
- ylab="BAF (phased)")
- dev.off()
-
- BAFphased = ifelse(BAFsegm>0.5, BAF, 1-BAF)
- BAFoutputchr = data.frame(Chromosome=rep(chr, length(BAFphseg)), Position=pos, BAF=BAF, BAFphased=BAFphased, BAFseg=BAFphseg)
- BAFoutput = rbind(BAFoutput, BAFoutputchr)
- }
- colnames(BAFoutput) = c("Chromosome","Position","BAF","BAFphased","BAFseg")
- write.table(BAFoutput, outputfile, sep="\t", row.names=F, col.names=T, quote=F)
-}
-
-#' Segment BAF with the inclusion of structural variant breakpoints - This function is now deprecated, call segment.baf.phased instead
-#'
-#' This function takes the SV breakpoints as initial segments and runs PCF on each
-#' of those independently. The SVs must be supplied as a simple data.frame with columns
-#' chromosome and position
-#' @param samplename Name of the sample, which is used to name output figures
-#' @param inputfile String that points to the output from the \code{combine.baf.files} function. This contains the phased SNPs with their BAF values
-#' @param outputfile String where the segmentation output will be written
-#' @param svs Data.frame with chromosome and position columns (Default: NULL)
-#' @param gamma The gamma parameter controls the size of the penalty of starting a new segment during segmentation. It is therefore the key parameter for controlling the number of segments (Default 10)
-#' @param kmin Kmin represents the minimum number of probes/SNPs that a segment should consist of (Default 3)
-#' @param phasegamma Gamma parameter used when correcting phasing mistakes (Default 3)
-#' @param phasekmin Kmin parameter used when correcting phasing mistakes (Default 3)
-#' @param no_segmentation Do not perform segmentation. This step will switch the haplotype blocks, but then just takes the mean BAFphased as BAFsegm
-#' @param calc_seg_baf_option Various options to recalculate the BAF of a segment. Options are: 1 - median, 2 - mean. (Default: 1)
-#' @author sd11
-#' @export
-segment.baf.phased.sv = function(samplename, inputfile, outputfile, svs=NULL, gamma=10, phasegamma=3, kmin=3, phasekmin=3, no_segmentation=F, calc_seg_baf_option=1) {
- .Deprecated("segment.baf.phased")
- print("Stopping now")
-}
-
#' Segment BAF, with the possible inclusion of structural variant breakpoints
-#'
+#'
#' This function breaks the genome up into chromosomes, possibly further when SV breakpoints
-#' are provided, and runs PCF on each to segment the chromosomes independently.
+#' are provided, and runs PCF on each to segment the chromosomes independently.
#' @param samplename Name of the sample, which is used to name output figures
-#' @param inputfile String that points to the output from the \code{combine.baf.files} function. This contains the phased SNPs with their BAF values
+#' @param inputfile String that points to the output from the \code{concatenate_baf_files} function. This contains the phased SNPs with their BAF values
#' @param outputfile String where the segmentation output will be written
-#' @param prior_breakpoints_file String that points to a file with prior breakpoints (from SVs for example) with chromosome and position columns (Default: NULL)
+#' @param prior_breakpoints_file String that points to a file with prior breakpoints (from SVs for example) with chromosome and position columns (header case-insensitive) (Default: NULL)
#' @param gamma The gamma parameter controls the size of the penalty of starting a new segment during segmentation. It is therefore the key parameter for controlling the number of segments (Default 10)
#' @param kmin Kmin represents the minimum number of probes/SNPs that a segment should consist of (Default 3)
#' @param phasegamma Gamma parameter used when correcting phasing mistakes (Default 3)
@@ -175,89 +34,24 @@ segment.baf.phased.sv = function(samplename, inputfile, outputfile, svs=NULL, ga
#' @param calc_seg_baf_option Various options to recalculate the BAF of a segment. Options are: 1 - median, 2 - mean, 3 - ifelse median==0 or 1, median, mean. (Default: 3)
#' @author sd11
#' @export
-segment.baf.phased = function(samplename, inputfile, outputfile, prior_breakpoints_file=NULL, gamma=10, phasegamma=3, kmin=3, phasekmin=3, no_segmentation=F, calc_seg_baf_option=3) {
- # Function that takes SNPs that belong to a single segment and looks for big holes between
- # each pair of SNPs. If there is a big hole it will add another breakpoint to the breakpoints data.frame
- addin_bigholes = function(breakpoints, positions, chrom, startpos, maxsnpdist) {
- # If there is a big hole (i.e. centromere), add it in as a separate set of breakpoints
-
- # Get the chromosome coordinate right before a big hole
- bigholes = which(diff(positions)>=maxsnpdist)
- if (length(bigholes) > 0) {
- for (endindex in bigholes) {
- breakpoints = rbind(breakpoints,
- data.frame(chrom=chrom, start=startpos, end=positions[endindex]))
- startpos = positions[endindex+1]
- }
- }
- return(list(breakpoints=breakpoints, startpos=startpos))
+segment_baf_phased <- function(samplename, inputfile, outputfile, prior_breakpoints_file = NULL,
+ gamma = 10, phasegamma = 3, kmin = 3,
+ phasekmin = 3, no_segmentation = FALSE,
+ calc_seg_baf_option = 3) {
+ # guard rail - check if input file exists and is not empty
+ if (!file.exists(inputfile) || file.size(inputfile) == 0) {
+ log_failure("Segment BAF input file '{inputfile}' is missing or empty.")
}
-
- # Helper function that creates segment breakpoints from SV calls
- # @param bkps_chrom Breakpoints for a single chromosome
- # @param BAFrawchr Raw BAF values of germline heterozygous SNPs on a single chromosome
- # @param addin_bigholes Flag whether bog holes in data are to be added as breakpoints
- # @return A data.frame with chrom, start and end columns
- # @author sd11
- bkps_to_presegment_breakpoints = function(chrom, bkps_chrom, BAFrawchr, addin_bigholes) {
- maxsnpdist = 3000000
-
- bkps_breakpoints = bkps_chrom$position
-
- # If there are no prior breakpoints, we cannot insert any
- if (length(bkps_breakpoints) > 0) {
- breakpoints = data.frame()
-
- # check which comes first, the breakpoint or the first SNP
- if (BAFrawchr$Position[1] < bkps_breakpoints[1]) {
- startpos = BAFrawchr$Position[1]
- startfromsv = 1 # We're starting from SNP data, so the first SV should be added first
- } else {
- startpos = bkps_breakpoints[1]
- startfromsv = 2 # We've just added the first SV, don't use it again
- }
-
- for (svposition in bkps_breakpoints[startfromsv:length(bkps_breakpoints)]) {
- selectedsnps = BAFrawchr$Position >= startpos & BAFrawchr$Position <= svposition
- if (sum(selectedsnps, na.rm=T) > 0) {
-
- if (addin_bigholes) {
- # If there is a big hole (i.e. centromere), add it in as a separate set of breakpoints
- res = addin_bigholes(breakpoints, BAFrawchr$Position[selectedsnps], chrom, startpos, maxsnpdist)
- breakpoints = res$breakpoints
- startpos = res$startpos
- }
-
- endindex = max(which(selectedsnps))
- breakpoints = rbind(breakpoints, data.frame(chrom=chrom, start=startpos, end=BAFrawchr$Position[endindex]))
- # Previous SV is the new starting point for the next segment
- startpos = BAFrawchr$Position[endindex + 1]
- }
- }
-
- # Add the remainder of the chromosome, if available
- if (BAFrawchr$Position[nrow(BAFrawchr)] > bkps_breakpoints[length(bkps_breakpoints)]) {
- endindex = nrow(BAFrawchr)
- breakpoints = rbind(breakpoints, data.frame(chrom=chrom, start=startpos, end=BAFrawchr$Position[endindex]))
- }
- } else {
- # There are no SVs, so create one big segment
- print("No prior breakpoints found")
- startpos = BAFrawchr$Position[1]
- breakpoints = data.frame()
-
- if (addin_bigholes) {
- # If there is a big hole (i.e. centromere), add it in as a separate set of breakpoints
- res = addin_bigholes(breakpoints, BAFrawchr$Position, chrom, startpos, maxsnpdist=maxsnpdist)
- breakpoints = res$breakpoints
- startpos = res$startpos
- }
-
- breakpoints = rbind(breakpoints, data.frame(chrom=chrom, start=startpos, end=BAFrawchr$Position[nrow(BAFrawchr)]))
- }
- return(breakpoints)
+
+ data <- data.table::fread(inputfile, header = TRUE, stringsAsFactors = FALSE)
+ if (nrow(data) == 0) {
+ log_failure("Phased BAF data in '{inputfile}' is empty.")
}
-
+
+ # Helpers are now top-level functions below
+
+ BAFoutput_list <- list()
+
# Run PCF on presegmented data
# @param BAFrawchr Raw BAF for this chromosome
# @param presegment_chrom_start
@@ -268,393 +62,526 @@ segment.baf.phased = function(samplename, inputfile, outputfile, prior_breakpoin
# @param gamma
# @param no_segmentation Do not perform segmentation. This step will switch the haplotype blocks, but then just takes the mean BAFphased as BAFsegm
# @return A data.frame with columns Chromosome,Position,BAF,BAFphased,BAFseg
- run_pcf = function(BAFrawchr, presegment_chrom_start, presegment_chrom_end, phasekmin, phasegamma, kmin, gamma, no_segmentation=F) {
- row.indices = which(BAFrawchr$Position >= presegment_chrom_start &
- BAFrawchr$Position <= presegment_chrom_end)
-
- BAF = BAFrawchr[row.indices,2]
- pos = BAFrawchr[row.indices,1]
- # names(BAF) = rownames(BAFrawchr[row.indices])
- # names(pos) = rownames(BAFrawchr[row.indices])
-
- sdev <- getMad(ifelse(BAF<0.5,BAF,1-BAF),k=25)
- # Standard deviation is not defined for a single value
- if (is.na(sdev)) {
- sdev = 0
- }
- #DCW 250314
- #for cell lines, sdev goes to zero in regions of LOH, which causes problems.
- #0.09 is around the value expected for a binomial distribution around 0.5 with depth 30
- if(sdev<0.09){
- sdev = 0.09
- }
-
- print(paste("BAFlen=",length(BAF),sep=""))
- if(length(BAF)<50){
- BAFsegm = rep(mean(BAF),length(BAF))
- }else{
- res = selectFastPcf(BAF,phasekmin,phasegamma*sdev,T)
- BAFsegm = res$yhat
+ BAFoutputchr_list <- list()
+
+ BAFoutput_list <- list()
+ BAFraw <- read_baf_as_data_frame(inputfile)
+ if (!is.null(prior_breakpoints_file)) {
+ bkps <- utils::read.table(prior_breakpoints_file, header = TRUE, stringsAsFactors = FALSE)
+ colnames(bkps) <- tolower(colnames(bkps))
+ colnames(bkps)[colnames(bkps) %in% c("chr")] <- "chromosome"
+ colnames(bkps)[colnames(bkps) %in% c("pos")] <- "position"
+ if (!all(c("chromosome", "position") %in% colnames(bkps))) {
+ log_failure("Prior breakpoints file must contain 'chromosome' and 'position' columns. Found: {paste(colnames(bkps), collapse=', ')}")
}
-
- BAFphased = ifelse(BAFsegm>0.5,BAF,1-BAF)
-
- if(length(BAFphased)<50 | no_segmentation){
- BAFphseg = rep(mean(BAFphased),length(BAFphased))
- }else{
- res = selectFastPcf(BAFphased,kmin,gamma*sdev,T)
- BAFphseg = res$yhat
+ } else {
+ bkps <- NULL
+ }
+
+ for (chr in unique(BAFraw[, 1])) {
+ log_info("Segmenting: '{chr}'")
+ BAFrawchr <- BAFraw[BAFraw[, 1] == chr, c(2, 3)]
+ BAFrawchr <- BAFrawchr[!is.na(BAFrawchr[, 2]), ]
+ if (!is.null(bkps)) {
+ bkps_chrom <- bkps[bkps$chromosome == chr, ]
+ } else {
+ bkps_chrom <- data.frame(chromosome = character(), position = numeric())
}
-
- if (length(BAF) > 0) {
-
- #
- # Note: When adding options, also add to merge_segments
- #
-
- # Recalculate the BAF of each segment, if required
- if (calc_seg_baf_option==1) {
- # Adjust the segment BAF to not take the mean as that is sensitive to improperly phased segments
- BAFphseg = adjustSegmValues(data.frame(BAFphased=BAFphased, BAFseg=BAFphseg))$BAFseg
- } else if (calc_seg_baf_option==2) {
- # Don't do anything, the BAF is already the mean
- } else if (calc_seg_baf_option==3) {
- # Take the median, unless the median is exactly 0 or 1. At the extreme
- # there is no difference between lets say 40 and 41 copies and BB cannot
- # fit a copy number state. The mean is less prone to become exactly 0 or 1
- # but the median is generally a better estimate that is less sensitive to
- # how well the haplotypes have been reconstructed
- BAFphseg_median = adjustSegmValues(data.frame(BAFphased=BAFphased, BAFseg=BAFphseg))$BAFseg
- BAFphseg = ifelse(BAFphseg_median %in% c(0,1), BAFphseg, BAFphseg_median)
- # if (BAFphseg_median!=0 & BAFphseg_median!=1) {
- # BAFphseg = BAFphseg_median
- # }
- } else {
- warning("Supplied calc_seg_baf_option to segment.baf.phased not valid, using mean BAF by default")
+
+ breakpoints_chrom <- bkps_to_presegment_breakpoints(chr, bkps_chrom, BAFrawchr, use_bigholes = TRUE)
+ BAFoutputchr_list <- list()
+
+ for (r in seq_len(nrow(breakpoints_chrom))) {
+ current_snps <- which(BAFrawchr$Position >= breakpoints_chrom$start[r] &
+ BAFrawchr$Position <= breakpoints_chrom$end[r])
+
+ if (length(current_snps) < 2) {
+ log_info("Skipping empty/tiny segment {r} on chr {chr} (SNPs: {length(current_snps)})")
+ next
+ }
+ BAFoutput_preseg <- run_pcf(
+ BAFrawchr = BAFrawchr,
+ presegment_chrom_start = breakpoints_chrom$start[r],
+ presegment_chrom_end = breakpoints_chrom$end[r],
+ phasekmin = phasekmin,
+ phasegamma = phasegamma,
+ kmin = kmin,
+ gamma = gamma,
+ chr = chr,
+ calc_seg_baf_option = calc_seg_baf_option,
+ no_segmentation = no_segmentation
+ )
+ if (!is.null(BAFoutput_preseg)) {
+ BAFoutputchr_list[[length(BAFoutputchr_list) + 1]] <- BAFoutput_preseg
}
}
-
- return(data.frame(Chromosome=rep(chr, length(row.indices)),
- Position=BAFrawchr[row.indices,1],
- BAF=BAF,
- BAFphased=BAFphased,
- BAFseg=BAFphseg,
- tempBAFsegm=BAFsegm)) # Keep track of BAFsegm for the plot below
+
+ # Efficiently combine segments for this chromosome
+ if (length(BAFoutputchr_list) == 0) {
+ next
+ }
+ BAFoutputchr <- as.data.frame(collapse::rowbind(BAFoutputchr_list))
+
+
+ grDevices::png(
+ filename = paste(samplename, "_RAFseg_chr", chr, ".png", sep = ""),
+ width = 2000, height = 1000, res = 200, type = "cairo"
+ )
+ create_segmented_plot(
+ chrom_position = BAFoutputchr$Position / 1000000,
+ points.red = BAFoutputchr$BAF,
+ points.green = BAFoutputchr$tempBAFsegm,
+ x_min = min(BAFoutputchr$Position) / 1000000,
+ x_max = max(BAFoutputchr$Position) / 1000000,
+ title = paste(samplename, ", chromosome ", chr, sep = ""),
+ xlab = "Position (Mb)",
+ ylab = "BAF (phased)",
+ prior_bkps_pos = bkps_chrom$position / 1000000
+ )
+ grDevices::dev.off()
+
+ grDevices::png(
+ filename = paste(samplename, "_segment_chr", chr, ".png", sep = ""),
+ width = 2000, height = 1000, res = 200, type = "cairo"
+ )
+ create_baf_plot(
+ chrom_position = BAFoutputchr$Position / 1000000,
+ points_red_blue = BAFoutputchr$BAF,
+ plot_red = BAFoutputchr$tempBAFsegm > 0.5,
+ points_darkred = BAFoutputchr$BAFseg,
+ points_darkblue = 1 - BAFoutputchr$BAFseg,
+ x_min = min(BAFoutputchr$Position) / 1000000,
+ x_max = max(BAFoutputchr$Position) / 1000000,
+ title = paste(samplename, ", chromosome ", chr, sep = ""),
+ xlab = "Position (Mb)",
+ ylab = "BAF (phased)",
+ prior_bkps_pos = bkps_chrom$position / 1000000
+ )
+ grDevices::dev.off()
+
+ BAFoutputchr$BAFphased <- ifelse(BAFoutputchr$tempBAFsegm > 0.5, BAFoutputchr$BAF, 1 - BAFoutputchr$BAF)
+ # Remove the temp BAFsegm values as they are only needed for plotting
+ BAFoutput_list[[length(BAFoutput_list) + 1]] <- BAFoutputchr[, c(1:5)]
}
-
- BAFraw = as.data.frame(read_baf(inputfile))
- if (!is.null(prior_breakpoints_file)) { bkps = read.table(prior_breakpoints_file, header=T, stringsAsFactors=F) } else { bkps = NULL }
-
- BAFoutput = NULL
- for (chr in unique(BAFraw[,1])) {
- print(paste0("Segmenting ", chr))
- BAFrawchr = BAFraw[BAFraw[,1]==chr,c(2,3)]
- # BAFrawchr = bafsegments[bafsegments$Chromosome==chr, c(2,3)]
- BAFrawchr = BAFrawchr[!is.na(BAFrawchr[,2]),]
- if (!is.null(bkps)) {
- bkps_chrom = bkps[bkps$chromosome==chr,]
+
+ # Efficiently combine all chromosome outputs
+ if (length(BAFoutput_list) == 0) {
+ # Return empty frame with correct columns
+ return(data.frame(Chromosome = character(), Position = numeric(), BAF = numeric(), BAFphased = numeric(), BAFseg = numeric()))
+ }
+ BAFoutput <- as.data.frame(collapse::rowbind(BAFoutput_list))
+
+ colnames(BAFoutput) <- c("Chromosome", "Position", "BAF", "BAFphased", "BAFseg")
+ data.table::fwrite(BAFoutput, outputfile, sep = "\t", row.names = FALSE, col.names = TRUE, quote = FALSE)
+}
+
+# --- Top-level Helper Functions ---
+
+#' @noRd
+addin_bigholes <- function(breakpoints, positions, chrom, startpos, maxsnpdist) {
+ # Calculate gaps between consecutive SNPs
+ gaps <- diff(positions)
+ gap_indices <- which(gaps >= maxsnpdist)
+
+ # If no holes, we don't return a new table, just the original
+ if (length(gap_indices) == 0) {
+ return(list(breakpoints = breakpoints, startpos = startpos))
+ }
+
+ # Define segment boundaries
+ # Segment ends at the SNP before the gap
+ ends <- c(positions[gap_indices], positions[length(positions)])
+
+ # Segment starts at the original startpos, then the SNP AFTER each gap
+ starts <- c(startpos, positions[gap_indices + 1])
+
+ # Safety: Remove segments where start == end (the BAFlen=1 case)
+ # Also ensures we don't have overlapping boundaries
+ valid_mask <- (starts < ends)
+
+ new_segments <- data.table::data.table(
+ chrom = chrom,
+ start = starts[valid_mask],
+ end = ends[valid_mask]
+ )
+
+ updated_breakpoints <- data.table::rbindlist(
+ list(breakpoints, new_segments),
+ use.names = TRUE
+ )
+
+ # The startpos for the NEXT segment in the outer loop
+ # should be the position AFTER the last SNP of this batch
+ return(list(
+ breakpoints = updated_breakpoints,
+ startpos = positions[length(positions)] + 1
+ ))
+}
+
+#' @noRd
+bkps_to_presegment_breakpoints <- function(chrom, bkps_chrom, BAFrawchr, use_bigholes) {
+ maxsnpdist <- 3000000
+ bkps_breakpoints <- bkps_chrom$position
+
+ # Use a list to accumulate segments instead of O(N^2) rbind
+ breakpoints_list <- list()
+
+ # If there are no prior breakpoints, we cannot insert any
+ if (length(bkps_breakpoints) > 0) {
+ # check which comes first, the breakpoint or the first SNP
+ if (BAFrawchr$Position[1] < bkps_breakpoints[1]) {
+ startpos <- BAFrawchr$Position[1]
+ # We're starting from SNP data, so the first SV should be added first
+ startfromsv <- 1
} else {
- bkps_chrom = data.frame(chromosome=character(), position=numeric())
+ startpos <- bkps_breakpoints[1]
+ startfromsv <- 2 # We've just added the first SV, don't use it again
+ }
+
+ for (svposition in bkps_breakpoints[startfromsv:length(bkps_breakpoints)]) {
+ selectedsnps <- BAFrawchr$Position >= startpos & BAFrawchr$Position <= svposition
+ if (sum(selectedsnps, na.rm = TRUE) > 0) {
+ if (use_bigholes) {
+ # If there is a big hole (i.e. centromere), add it in as a separate set of breakpoints
+ res <- addin_bigholes(data.table::data.table(), BAFrawchr$Position[selectedsnps], chrom, startpos, maxsnpdist)
+ if (nrow(res$breakpoints) > 0) {
+ breakpoints_list[[length(breakpoints_list) + 1]] <- res$breakpoints
+ }
+ startpos <- res$startpos
+ }
+
+ endindex <- max(which(selectedsnps))
+ breakpoints_list[[length(breakpoints_list) + 1]] <- data.table::data.table(chrom = chrom, start = startpos, end = BAFrawchr$Position[endindex])
+ # Previous SV is the new starting point for the next segment
+ startpos <- BAFrawchr$Position[endindex + 1]
+ }
}
-
- breakpoints_chrom = bkps_to_presegment_breakpoints(chr, bkps_chrom, BAFrawchr, addin_bigholes=T)
- BAFoutputchr = NULL
-
- for (r in 1:nrow(breakpoints_chrom)) {
- BAFoutput_preseg = run_pcf(BAFrawchr, breakpoints_chrom$start[r], breakpoints_chrom$end[r], phasekmin, phasegamma, kmin, gamma, no_segmentation)
- BAFoutputchr = rbind(BAFoutputchr, BAFoutput_preseg)
+
+ # Add the remainder of the chromosome, if available
+ if (BAFrawchr$Position[nrow(BAFrawchr)] > bkps_breakpoints[length(bkps_breakpoints)]) {
+ endindex <- nrow(BAFrawchr)
+ breakpoints_list[[length(breakpoints_list) + 1]] <- data.table::data.table(chrom = chrom, start = startpos, end = BAFrawchr$Position[endindex])
}
-
- png(filename = paste(samplename,"_RAFseg_chr",chr,".png",sep=""), width = 2000, height = 1000, res = 200, type = "cairo")
- create.segmented.plot(chrom.position=BAFoutputchr$Position/1000000,
- points.red=BAFoutputchr$BAF,
- points.green=BAFoutputchr$tempBAFsegm,
- x.min=min(BAFoutputchr$Position)/1000000,
- x.max=max(BAFoutputchr$Position)/1000000,
- title=paste(samplename,", chromosome ", chr, sep=""),
- xlab="Position (Mb)",
- ylab="BAF (phased)",
- prior_bkps_pos=bkps_chrom$position/1000000)
- dev.off()
-
- png(filename = paste(samplename,"_segment_chr",chr,".png",sep=""), width = 2000, height = 1000, res = 200, type = "cairo")
- create.baf.plot(chrom.position=BAFoutputchr$Position/1000000,
- points.red.blue=BAFoutputchr$BAF,
- plot.red=BAFoutputchr$tempBAFsegm>0.5,
- points.darkred=BAFoutputchr$BAFseg,
- points.darkblue=1-BAFoutputchr$BAFseg,
- x.min=min(BAFoutputchr$Position)/1000000,
- x.max=max(BAFoutputchr$Position)/1000000,
- title=paste(samplename,", chromosome ", chr, sep=""),
- xlab="Position (Mb)",
- ylab="BAF (phased)",
- prior_bkps_pos=bkps_chrom$position/1000000)
- dev.off()
-
- BAFoutputchr$BAFphased = ifelse(BAFoutputchr$tempBAFsegm>0.5, BAFoutputchr$BAF, 1-BAFoutputchr$BAF)
- # Remove the temp BAFsegm values as they are only needed for plotting
- BAFoutput = rbind(BAFoutput, BAFoutputchr[,c(1:5)])
+ } else {
+ # There are no SVs, so create one big segment
+ log_info("No prior breakpoints found")
+ startpos <- BAFrawchr$Position[1]
+
+ if (use_bigholes) {
+ # If there is a big hole (i.e. centromere), add it in as a separate set of breakpoints
+ res <- addin_bigholes(data.table::data.table(), BAFrawchr$Position, chrom, startpos, maxsnpdist = maxsnpdist)
+ if (nrow(res$breakpoints) > 0) {
+ breakpoints_list[[length(breakpoints_list) + 1]] <- res$breakpoints
+ }
+ startpos <- res$startpos
+ }
+
+ breakpoints_list[[length(breakpoints_list) + 1]] <- data.table::data.table(chrom = chrom, start = startpos, end = BAFrawchr$Position[nrow(BAFrawchr)])
+ }
+
+ # Efficiently combine all collected segments
+ if (length(breakpoints_list) == 0) {
+ return(data.frame(chrom = character(), start = numeric(), end = numeric()))
}
- colnames(BAFoutput) = c("Chromosome","Position","BAF","BAFphased","BAFseg")
- write.table(BAFoutput, outputfile, sep="\t", row.names=F, col.names=T, quote=F)
+ return(as.data.frame(collapse::rowbind(breakpoints_list)))
}
+#' @noRd
+run_pcf <- function(
+ BAFrawchr,
+ presegment_chrom_start,
+ presegment_chrom_end,
+ phasekmin,
+ phasegamma,
+ kmin,
+ gamma,
+ chr,
+ calc_seg_baf_option = 3,
+ no_segmentation = FALSE
+) {
+ row.indices <- which(BAFrawchr$Position >= presegment_chrom_start &
+ BAFrawchr$Position <= presegment_chrom_end)
+
+ BAF <- BAFrawchr[row.indices, 2]
+ sdev <- get_mad(ifelse(BAF < 0.5, BAF, 1 - BAF), k = 25)
+ # Standard deviation is not defined for a single value
+ if (is.na(sdev)) {
+ sdev <- 0
+ }
+ # for cell lines, sdev goes to zero in regions of LOH, which causes problems.
+ # 0.09 is around the value expected for a binomial distribution around 0.5 with depth 30
+ if (sdev < 0.09) {
+ sdev <- 0.09
+ }
+
+ log_info("BAFlen={length(BAF)}")
+ if (length(BAF) < 50) {
+ BAFsegm <- rep(mean(BAF), length(BAF))
+ } else {
+ res <- selectFastPcf(BAF, phasekmin, phasegamma * sdev, TRUE)
+ BAFsegm <- res$yhat
+ # Guard rail - segment explosion check on phasing
+ if (res$nIntervals > 1000) {
+ log_warning("High number of segments detected during phasing on chr {chr} (n={res$nIntervals}).")
+ }
+ if (res$nIntervals > 5000) {
+ log_failure("Segment explosion during phasing on chr {chr} (n={res$nIntervals}). Data is too noisy.")
+ }
+ }
+
+ BAFphased <- ifelse(BAFsegm > 0.5, BAF, 1 - BAF)
+
+ if (length(BAFphased) < 50 || no_segmentation) {
+ BAFphseg <- rep(mean(BAFphased), length(BAFphased))
+ } else {
+ res <- selectFastPcf(BAFphased, kmin, gamma * sdev, TRUE)
+ BAFphseg <- res$yhat
+ # Guard rail - segment explosion check on segmentation
+ if (res$nIntervals > 1000) {
+ log_warning("High number of segments detected during segmentation on chr {chr} (n={res$nIntervals}).")
+ }
+ if (res$nIntervals > 5000) {
+ log_failure("Segment explosion during segmentation on chr {chr} (n={res$nIntervals}). Data is too noisy.")
+ }
+ }
+ if (length(BAF) > 0) {
+ # Recalculate the BAF of each segment, if required
+ if (calc_seg_baf_option == 1) {
+ # Adjust the segment BAF to not take the mean as that is sensitive to improperly phased segments
+ BAFphseg <- adjustSegmValues(data.frame(BAFphased = BAFphased, BAFseg = BAFphseg))$BAFseg
+ } else if (calc_seg_baf_option == 2) {
+ # Don't do anything, the BAF is already the mean
+ } else if (calc_seg_baf_option == 3) {
+ # Take the median, unless the median is exactly 0 or 1. At the extreme
+ # there is no difference between lets say 40 and 41 copies and BB cannot
+ # fit a copy number state. The mean is less prone to become exactly 0 or 1
+ # but the median is generally a better estimate that is less sensitive to
+ # how well the haplotypes have been reconstructed
+ BAFphseg_median <- adjustSegmValues(data.frame(BAFphased = BAFphased, BAFseg = BAFphseg))$BAFseg
+ BAFphseg <- ifelse(BAFphseg_median %in% c(0, 1), BAFphseg, BAFphseg_median)
+ } else {
+ log_warning("Supplied calc_seg_baf_option to segment_baf_phased not valid, using mean BAF by default")
+ }
+ }
+ return(data.frame(
+ Chromosome = rep(chr, length(row.indices)),
+ Position = BAFrawchr[row.indices, 1],
+ BAF = BAF,
+ BAFphased = BAFphased,
+ BAFseg = BAFphseg,
+ tempBAFsegm = BAFsegm
+ )) # Keep track of BAFsegm for the plot below
+}
#' Segment BAF, with the possible inclusion of structural variant breakpoints
-#'
+#'
#' This function breaks the genome up into chromosomes, possibly further when SV breakpoints
-#' are provided, and runs PCF on each to segment the chromosomes independently.
+#' are provided, and runs PCF on each to segment the chromosomes independently.
#' @param samplename Name of the sample, which is used to name output figures
-#' @param inputfile String that points to the output from the \code{combine.baf.files} function. This contains the phased SNPs with their BAF values
+#' @param inputfile String that points to the output from the \code{concatenate_baf_files} function. This contains the phased SNPs with their BAF values
#' @param outputfile String where the segmentation output will be written
-#' @param prior_breakpoints_file String that points to a file with prior breakpoints (from SVs for example) with chromosome and position columns (Default: NULL)
+#' @param prior_breakpoints_file String that points to a file with prior breakpoints (from SVs for example) with chromosome and position columns (header case-insensitive) (Default: NULL)
#' @param gamma The gamma parameter controls the size of the penalty of starting a new segment during segmentation. It is therefore the key parameter for controlling the number of segments (Default 10)
#' @param calc_seg_baf_option Various options to recalculate the BAF of a segment. Options are: 1 - median, 2 - mean, 3 - ifelse median==0 or 1, median, mean. (Default: 3)
#' @param GENOMEBUILD Genome build upon which the 1000G SNP coordinates were obtained
#' @author jdemeul, sd11
#' @export
-segment.baf.phased.multisample = function(samplename, inputfile, outputfile, prior_breakpoints_file=NULL, gamma=10, calc_seg_baf_option=3,GENOMEBUILD) {
- ##### internal function definitions
- # Function that takes SNPs that belong to a single segment and looks for big holes between
- # each pair of SNPs. If there is a big hole it will add another breakpoint to the breakpoints data.frame
- addin_bigholes = function(breakpoints, positions, chrom, startpos, maxsnpdist) {
- # If there is a big hole (i.e. centromere), add it in as a separate set of breakpoints
-
- # Get the chromosome coordinate right before a big hole
- bigholes = which(diff(positions)>=maxsnpdist)
- if (length(bigholes) > 0) {
- for (endindex in bigholes) {
- breakpoints = rbind(breakpoints,
- data.frame(chrom=chrom, start=startpos, end=positions[endindex]))
- startpos = positions[endindex+1]
- }
+segment_baf_phased_multisample <- function(
+ samplename, inputfile,
+ outputfile, prior_breakpoints_file = NULL,
+ gamma = 10, calc_seg_baf_option = 3,
+ GENOMEBUILD
+) {
+ # guard rail - check if input files exist
+ for (f in inputfile) {
+ if (!file.exists(f) || file.size(f) == 0) {
+ log_failure("Multisample input file '{f}' is missing or empty.")
}
- return(list(breakpoints=breakpoints, startpos=startpos))
}
-
-
- # Helper function that creates segment breakpoints from SV calls
- # @param bkps_chrom Breakpoints for a single chromosome
- # @param BAFrawchr Raw BAF values of germline heterozygous SNPs on a single chromosome
- # @param addin_bigholes Flag whether bog holes in data are to be added as breakpoints
- # @return A data.frame with chrom, start and end columns
- # @author sd11
- bkps_to_presegment_breakpoints = function(chrom, bkps_chrom, BAFrawchr, addin_bigholes) {
- maxsnpdist = 3000000
-
- bkps_breakpoints = bkps_chrom$position
-
- # If there are no prior breakpoints, we cannot insert any
- if (length(bkps_breakpoints) > 0) {
- breakpoints = data.frame()
-
- # check which comes first, the breakpoint or the first SNP
- if (BAFrawchr$Position[1] < bkps_breakpoints[1]) {
- startpos = BAFrawchr$Position[1]
- startfromsv = 1 # We're starting from SNP data, so the first SV should be added first
- } else {
- startpos = bkps_breakpoints[1]
- startfromsv = 2 # We've just added the first SV, don't use it again
- }
-
- for (svposition in bkps_breakpoints[startfromsv:length(bkps_breakpoints)]) {
- selectedsnps = BAFrawchr$Position >= startpos & BAFrawchr$Position <= svposition
- if (sum(selectedsnps, na.rm=T) > 0) {
-
- if (addin_bigholes) {
- # If there is a big hole (i.e. centromere), add it in as a separate set of breakpoints
- res = addin_bigholes(breakpoints, BAFrawchr$Position[selectedsnps], chrom, startpos, maxsnpdist)
- breakpoints = res$breakpoints
- startpos = res$startpos
- }
-
- endindex = max(which(selectedsnps))
- breakpoints = rbind(breakpoints, data.frame(chrom=chrom, start=startpos, end=BAFrawchr$Position[endindex]))
- # Previous SV is the new starting point for the next segment
- startpos = BAFrawchr$Position[endindex + 1]
- }
- }
-
- # Add the remainder of the chromosome, if available
- if (BAFrawchr$Position[nrow(BAFrawchr)] > bkps_breakpoints[length(bkps_breakpoints)]) {
- endindex = nrow(BAFrawchr)
- breakpoints = rbind(breakpoints, data.frame(chrom=chrom, start=startpos, end=BAFrawchr$Position[endindex]))
- }
- } else {
- # There are no SVs, so create one big segment
- print("No prior breakpoints found")
- startpos = BAFrawchr$Position[1]
- breakpoints = data.frame()
-
- if (addin_bigholes) {
- # If there is a big hole (i.e. centromere), add it in as a separate set of breakpoints
- res = addin_bigholes(breakpoints, BAFrawchr$Position, chrom, startpos, maxsnpdist=maxsnpdist)
- breakpoints = res$breakpoints
- startpos = res$startpos
- }
-
- breakpoints = rbind(breakpoints, data.frame(chrom=chrom, start=startpos, end=BAFrawchr$Position[nrow(BAFrawchr)]))
- }
- return(breakpoints)
+ get_segments <- function(chrom, bkps_chrom, BAFrawchr, maxsnpdist = 3000000) {
+ snps <- BAFrawchr$Position
+
+ # Identify gaps using base R vectorization
+ gaps <- which(diff(snps) >= maxsnpdist)
+ gap_bkps <- snps[gaps]
+
+ # Merge SV and Gap breakpoints
+ all_cuts <- sort(unique(c(bkps_chrom$position, gap_bkps)))
+
+ # Define start/end pairs
+ cut_indices <- findInterval(all_cuts, snps)
+
+ seg_starts <- c(snps[1], snps[cut_indices + 1])
+ seg_ends <- c(snps[cut_indices], snps[length(snps)])
+
+ # Explicitly use data.table namespace for construction
+ segments <- data.table::data.table(
+ chrom = chrom, start = seg_starts, end = seg_ends
+ )
+ return(segments[segments$start <= segments$end])
}
-
-
- # Run PCF on presegmented data
- # @param BAFrawchr Raw BAF for this chromosome
- # @param presegment_chrom_start
- # @param presegment_chrom_end
- # @param kmin
- # @param gamma
- # @param no_segmentation Do not perform segmentation. This step will switch the haplotype blocks, but then just takes the mean BAFphased as BAFsegm
- # @return A data.frame with columns Chromosome,Position,BAF,BAFphased,BAFseg
- run_pcf = function(BAFrawchr, presegment_chrom_start, presegment_chrom_end, gamma) {
-
- row.indices = which(BAFrawchr$Position >= presegment_chrom_start &
- BAFrawchr$Position <= presegment_chrom_end)
-
- BAFrawchrseg <- BAFrawchr[row.indices,]
- # BAF = BAFrawchr[row.indices,2:ncol(BAFrawchr)]
- # pos = BAFrawchr[row.indices,1]
-
- sdevs <- unlist(apply(X = BAFrawchrseg[,-c(1:2)], MARGIN = 2, FUN = function(x) getMad(ifelse(x<0.5,x,1-x), k=25)))
- # sdev <- getMad(ifelse(BAF<0.5,BAF,1-BAF),k=25)
- # Standard deviation is not defined for a single value
- sdevs[is.na(sdevs)] <- 0
- #DCW 250314
- #for cell lines, sdev goes to zero in regions of LOH, which causes problems.
- #0.09 is around the value expected for a binomial distribution around 0.5 with depth 30
- sdevs[sdevs < 0.09] <- 0.09
+
+ run_pcf_helper <- function(BAFrawchr, start, end, gamma) {
+ Position <- NULL
+ BAF_subset <- BAFrawchr[Position >= start & Position <= end]
+
+ if (nrow(BAF_subset) == 0) {
+ return(NULL)
+ }
+
+ vals <- as.matrix(BAF_subset[, -c(1:2)])
+
+ # Calculate sdev using Mean Absolute Deviation
+ sdevs <- apply(vals, 2, function(x) {
+ get_mad(ifelse(x < 0.5, x, 1 - x), k = 25)
+ })
+ sdevs[is.na(sdevs) | sdevs < 0.09] <- 0.09
sdev <- mean(sdevs)
-
- print(paste0("BAFlen=",nrow(BAFrawchrseg)))
- if (nrow(BAFrawchrseg) < 50) {
- BAFsegm = matrix(data = colMeans(BAFrawchrseg[,-c(1:2)]), nrow = nrow(BAFrawchrseg), ncol = ncol(BAFrawchrseg)-2, byrow = T)
+
+ if (nrow(BAF_subset) < 50) {
+ BAFsegm <- matrix(colMeans(vals), nrow = nrow(BAF_subset), ncol = ncol(vals), byrow = TRUE)
} else {
- res = copynumber::multipcf(data = copynumber::winsorize(data = BAFrawchrseg, assembly = GENOMEBUILD),
- Y = BAFrawchrseg, fast = T, gamma = gamma*sdev, return.est = T, normalize = F, assembly = GENOMEBUILD)
- BAFsegm = res$estimates[,-c(1:2)]
+ winsor_data <- copynumber::winsorize(BAF_subset, assembly = GENOMEBUILD)
+ res <- copynumber::multipcf(
+ data = winsor_data,
+ Y = BAF_subset,
+ fast = TRUE,
+ gamma = gamma * sdev,
+ return.est = TRUE,
+ normalize = FALSE,
+ assembly = GENOMEBUILD
+ )
+ BAFsegm <- as.matrix(res$estimates[, -c(1:2)])
}
-
- BAFphased <- do.call(cbind, sapply(X = 1:ncol(BAFsegm), FUN = function(x, bafsegm, baf) ifelse(bafsegm[,x] > 0.5, baf[,x], 1-baf[,x]), bafsegm = BAFsegm, baf = BAFrawchrseg[,-c(1:2)], simplify = F))
-
- if (nrow(BAFphased) < 50){
- BAFphseg = matrix(data = colMeans(BAFphased), nrow = nrow(BAFphased), ncol = ncol(BAFphased), byrow = T)
+
+ BAFphased <- ifelse(BAFsegm > 0.5, vals, 1 - vals)
+
+ # Logic for segment BAF calculation
+ if (calc_seg_baf_option %in% c(1, 3)) {
+ BAFphseg <- apply(BAFphased, 2, stats::median)
+ if (calc_seg_baf_option == 3) {
+ means <- apply(BAFsegm, 2, function(x) ifelse(x[1] > 0.5, x[1], 1 - x[1]))
+ BAFphseg <- ifelse(BAFphseg %in% c(0, 1), means, BAFphseg)
+ }
} else {
- BAFphseg = sapply(X = 1:ncol(BAFsegm), FUN = function(x, bafsegm) ifelse(bafsegm[,x] > 0.5, bafsegm[,x], 1-bafsegm[,x]), bafsegm = BAFsegm)
+ BAFphseg <- apply(BAFsegm, 2, function(x) ifelse(x[1] > 0.5, x[1], 1 - x[1]))
}
-
- if (nrow(BAFrawchrseg) > 0) {
-
- #
- # Note: When adding options, also add to merge_segments
- #
-
- # Recalculate the BAF of each segment, if required
- if (calc_seg_baf_option==1) {
- # Adjust the segment BAF to not take the mean as that is sensitive to improperly phased segments
- BAFphseg = do.call(cbind, sapply(X = 1:ncol(BAFphseg), FUN = function(idx, BAFphased, BAFseg) adjustSegmValues(data.frame(BAFphased=BAFphased[,idx], BAFseg=BAFphseg[,idx]))$BAFseg,
- BAFphased = BAFphased, BAFseg = BAFphseg, simplify = F))
- # BAFphseg = adjustSegmValues(data.frame(BAFphased=BAFphased, BAFseg=BAFphseg))$BAFseg
- } else if (calc_seg_baf_option==2) {
- # Don't do anything, the BAF is already the mean
- } else if (calc_seg_baf_option==3) {
- # Take the median, unless the median is exactly 0 or 1. At the extreme
- # there is no difference between lets say 40 and 41 copies and BB cannot
- # fit a copy number state. The mean is less prone to become exactly 0 or 1
- # but the median is generally a better estimate that is less sensitive to
- # how well the haplotypes have been reconstructed
- BAFphseg_median = do.call(cbind, sapply(X = 1:ncol(BAFphseg), FUN = function(idx, BAFphased, BAFseg) adjustSegmValues(data.frame(BAFphased=BAFphased[,idx], BAFseg=BAFphseg[,idx]))$BAFseg,
- BAFphased = BAFphased, BAFseg = BAFphseg, simplify = F))
- BAFphseg <- do.call(cbind, sapply(X = 1:ncol(BAFphseg), FUN = function(idx, BAFphseg_median, BAFphseg) ifelse(BAFphseg_median[,idx] %in% c(0,1), BAFphseg[,idx], BAFphseg_median[,idx]),
- BAFphseg_median = BAFphseg_median, BAFphseg = BAFphseg, simplify = F))
- } else {
- warning("Supplied calc_seg_baf_option to segment.baf.phased not valid, using mean BAF by default")
- }
+
+ out <- lapply(seq_along(samplename), function(i) {
+ data.table::data.table(
+ Chromosome = BAF_subset$Chromosome,
+ Position = BAF_subset$Position,
+ BAF = vals[, i],
+ BAFphased = BAFphased[, i],
+ BAFseg = rep(BAFphseg[i], nrow(BAF_subset)),
+ tempBAFsegm = BAFsegm[, i]
+ )
+ })
+ stats::setNames(out, samplename)
+ }
+
+ BAFraw <- data.table::as.data.table(
+ Reduce(function(...) merge(..., sort = FALSE), lapply(inputfile, read_baf_as_data_frame))
+ )
+
+ bkps <- if (!is.null(prior_breakpoints_file)) {
+ dt <- data.table::fread(prior_breakpoints_file, header = TRUE)
+ data.table::setnames(dt, tolower(colnames(dt)))
+ if ("chr" %in% colnames(dt)) data.table::setnames(dt, "chr", "chromosome")
+ if ("pos" %in% colnames(dt)) data.table::setnames(dt, "pos", "position")
+
+ if (!all(c("chromosome", "position") %in% colnames(dt))) {
+ log_failure("Prior breakpoints file must contain 'chromosome' and 'position' columns. Found: {paste(colnames(dt), collapse=', ')}")
}
-
- outlist <- lapply(X = 1:(ncol(BAFrawchr)-2),
- FUN = function(x, BAF, BAFphased, BAFseg, tempBAFsegm) {
- data.frame(BAF[, 1:2],
- BAF = BAF[, x+2],
- BAFphased = BAFphased[, x],
- BAFseg = BAFseg[, x],
- tempBAFsegm = tempBAFsegm[, x], stringsAsFactors = F)
- }, BAF = BAFrawchrseg, BAFphased = BAFphased, BAFseg = BAFphseg, tempBAFsegm = BAFsegm)
- names(outlist) <- colnames(BAFrawchr)[-c(1,2)]
-
- return(outlist) # Keep track of BAFsegm for the plot below
+ dt
+ } else {
+ NULL
}
- ######## End internal function definitions
-
-
- BAFraw <- Reduce(f = function(...) merge(..., sort = F, all = F), x = lapply(X = inputfile, FUN = Battenberg:::read_baf))
- # BAFraw = as.data.frame(read_tsv(inputfile, col_types = paste0("ci", paste0(rep("n", length(samplename)), collapse = ""), collapse = "")))
- if (!is.null(prior_breakpoints_file)) { bkps = read.table(prior_breakpoints_file, header=T, stringsAsFactors=F) } else { bkps = NULL }
-
- BAFoutput = list()
- for (chr in unique(BAFraw[,1])) {
- print(paste0("Segmenting ", chr))
- BAFrawchr = BAFraw[BAFraw[,1]==chr,]
- # BAFrawchr = bafsegments[bafsegments$Chromosome==chr, c(2,3)]
- BAFrawchr = BAFrawchr[complete.cases(BAFrawchr[,c(3:ncol(BAFrawchr))]),]
- if (!is.null(bkps)) {
- bkps_chrom = bkps[bkps$chromosome==chr,]
+
+ all_results <- list()
+
+ Chromosome <- chromosome <- NULL
+
+ # Using string indexing to avoid warnings in the loop header
+ for (chr in unique(BAFraw[["Chromosome"]])) {
+ log_info("Processing {chr}...")
+
+ chr_data <- BAFraw[Chromosome == chr]
+ chr_data <- chr_data[stats::complete.cases(chr_data[, -c(1:2)])]
+
+ chr_bkps <- if (!is.null(bkps)) {
+ bkps[chromosome == chr]
} else {
- bkps_chrom = data.frame(chromosome=character(), position=numeric())
+ data.table::data.table(position = numeric())
}
-
- breakpoints_chrom = bkps_to_presegment_breakpoints(chr, bkps_chrom, BAFrawchr, addin_bigholes=T)
- BAFoutputchr = list()
-
- for (r in 1:nrow(breakpoints_chrom)) {
- BAFoutputchr[[r]] = run_pcf(BAFrawchr = BAFrawchr, presegment_chrom_start = breakpoints_chrom$start[r], presegment_chrom_end = breakpoints_chrom$end[r], gamma = gamma)
- # BAFoutputchr = rbind(BAFoutputchr, BAFoutput_preseg)
- }
-
- BAFoutputchr <- lapply(X = samplename, FUN = function(x, seglist) do.call(what = rbind, args = lapply(X = seglist, FUN = '[[', x)), seglist = BAFoutputchr)
- names(BAFoutputchr) <- samplename
-
+
+ segments <- get_segments(chr, chr_bkps, chr_data)
+
+ seg_results <- lapply(seq_len(nrow(segments)), function(i) {
+ run_pcf_helper(chr_data, segments$start[i], segments$end[i], gamma)
+ })
+
+ # We combine the segments for this specific chromosome once
+ # This creates a named list of DataTables, one per sample
+ chr_sample_results <- lapply(samplename, function(id) {
+ data.table::rbindlist(lapply(seg_results, `[[`, id))
+ })
+ names(chr_sample_results) <- samplename
+
for (id in samplename) {
- png(filename = paste(id,"_RAFseg_chr",chr,".png",sep=""), width = 2000, height = 1000, res = 200, type = "cairo")
- create.segmented.plot(chrom.position=BAFoutputchr[[id]]$Position/1000000,
- points.red=BAFoutputchr[[id]]$BAF,
- points.green=BAFoutputchr[[id]]$tempBAFsegm,
- x.min=min(BAFoutputchr[[id]]$Position)/1000000,
- x.max=max(BAFoutputchr[[id]]$Position)/1000000,
- title=paste(id,", chromosome ", chr, sep=""),
- xlab="Position (Mb)",
- ylab="BAF (phased)",
- prior_bkps_pos=bkps_chrom$position/1000000)
- dev.off()
-
- png(filename = paste(id,"_segment_chr",chr,".png",sep=""), width = 2000, height = 1000, res = 200, type = "cairo")
- create.baf.plot(chrom.position=BAFoutputchr[[id]]$Position/1000000,
- points.red.blue=BAFoutputchr[[id]]$BAF,
- plot.red=BAFoutputchr[[id]]$tempBAFsegm>0.5,
- points.darkred=BAFoutputchr[[id]]$BAFseg,
- points.darkblue=1-BAFoutputchr[[id]]$BAFseg,
- x.min=min(BAFoutputchr[[id]]$Position)/1000000,
- x.max=max(BAFoutputchr[[id]]$Position)/1000000,
- title=paste(id,", chromosome ", chr, sep=""),
- xlab="Position (Mb)",
- ylab="BAF (phased)",
- prior_bkps_pos=bkps_chrom$position/1000000)
- dev.off()
-
+ # Reference the combined data for this sample/chromosome
+ sample_dt <- chr_sample_results[[id]]
+
+ # Plot 1: RAFseg
+ grDevices::png(
+ filename = paste0(id, "_RAFseg_chr", chr, ".png"),
+ width = 2000, height = 1000, res = 200, type = "cairo"
+ )
+ create_segmented_plot(
+ chrom_position = sample_dt$Position / 1e6,
+ points.red = sample_dt$BAF,
+ points.green = sample_dt$tempBAFsegm,
+ x_min = min(sample_dt$Position) / 1e6,
+ x_max = max(sample_dt$Position) / 1e6,
+ title = paste0(id, ", chromosome ", chr),
+ xlab = "Position (Mb)",
+ ylab = "BAF (phased)",
+ prior_bkps_pos = chr_bkps$position / 1e6
+ )
+ grDevices::dev.off()
+
+ # Plot 2: BAF segments
+ grDevices::png(
+ filename = paste0(id, "_segment_chr", chr, ".png"),
+ width = 2000, height = 1000, res = 200, type = "cairo"
+ )
+ create_baf_plot(
+ chrom_position = sample_dt$Position / 1e6,
+ points_red_blue = sample_dt$BAF,
+ plot_red = sample_dt$tempBAFsegm > 0.5,
+ points_darkred = sample_dt$BAFseg,
+ points_darkblue = 1 - sample_dt$BAFseg,
+ x_min = min(sample_dt$Position) / 1e6,
+ x_max = max(sample_dt$Position) / 1e6,
+ title = paste0(id, ", chromosome ", chr),
+ xlab = "Position (Mb)",
+ ylab = "BAF (phased)",
+ prior_bkps_pos = chr_bkps$position / 1e6
+ )
+ grDevices::dev.off()
+
+ # Store for final export, removing the temp column used for plotting
+ if (is.null(all_results[[id]])) all_results[[id]] <- list()
+ all_results[[id]][[chr]] <- sample_dt[, !"tempBAFsegm"]
}
-
- # Remove the temp BAFsegm values as they are only needed for plotting
- BAFoutput[[chr]] <- lapply(X = BAFoutputchr, FUN = function(x) x[,-6])
}
-
- BAFoutput <- lapply(X = samplename, FUN = function(x, chrlist) do.call(what = rbind, args = lapply(X = chrlist, FUN = '[[', x)), chrlist = BAFoutput)
- lapply(X = 1:length(samplename), FUN = function(sidx, outfile, output) write.table(x = output[[sidx]], file = outfile[sidx], sep="\t", row.names=F,
- col.names=c("Chromosome","Position","BAF","BAFphased","BAFseg"), quote=F),
- outfile = outputfile, output = BAFoutput)
-
- return(NULL)
-}
+ # Final Export
+ for (i in seq_along(samplename)) {
+ final_dt <- data.table::rbindlist(all_results[[samplename[i]]])
+ data.table::fwrite(final_dt, file = outputfile[i], sep = "\t")
+ }
+}
diff --git a/R/util.R b/R/util.R
index 9d88ad3f..610464cb 100644
--- a/R/util.R
+++ b/R/util.R
@@ -1,358 +1,11 @@
-########################################################################################
-# Generic table reader
-########################################################################################
-#' Generic reading function using the readr R package, tailored for reading in genomic data
-#' @param file Filename of the file to read in
-#' @param header Whether the file contains a header (Default: TRUE)
-#' @param row.names Whether the file contains row names (Default: FALSE)
-#' @param stringsAsFactor Legacy parameter that is no longer used (Default: FALSE)
-#' @param sep Column separator (Default: \\t)
-#' @param chrom_col The column number that contains chromosome denominations. This column will automatically be cast as a character. Should be counted including the row.names (Default: 1)
-#' @param skip The number of rows to skip before reading (Default: 0)
-#' @return A data frame with contents of the file
-#' @export
-read_table_generic = function(file, header=T, row.names=F, stringsAsFactor=F, sep="\t", chrom_col=1, skip=0) {
- # stringsAsFactor is not needed here, but kept for legacy purposes
-
- # Read in first line to obtain the header
- d = readr::read_delim(file=file, delim=sep, col_names=header, n_max=1, skip=skip, col_types = readr::cols())
-
- # fetch the name of the first column to set its col_type for reading in the whole file
- # this is needed as readr does not understand the chromosome column properly
- col_types = list()
- for (i in chrom_col) {
- first_colname = colnames(d)[i]
- col_types[[first_colname]] = readr::col_character()
- }
- d = readr::read_delim(file=file, delim=sep, col_names=header, col_types=col_types, skip=skip)
-
- # readr never reads row.names, so this needs to be manually corrected
- if (row.names) {
- row.names(d) = d[,1]
- d = d[,-1]
- }
- # Replace spaces with dots as is the standard with the regular read.table
- colnames(d) = gsub(" ", ".", colnames(d))
- return(d)
-}
-
-#' Parser for logR data
-#' @param filename Filename of the file to read in
-#' @param header Whether the file contains a header (Default: TRUE)
-#' @return A data frame with logR content
-read_logr = function(filename, header=T) {
- #return(readr::read_tsv(file = filename, col_names = header, col_types = "cin"))
- return(readr::read_delim(file = filename, delim = NULL, col_names = header, col_types = "cin"))
-}
-
-#' Parser for BAF data
-#' @param filename Filename of the file to read in
-#' @param header Whether the file contains a header (Default: TRUE)
-#' @return A data frame with BAF content
-read_baf = function(filename, header=T) {
- #return(readr::read_tsv(file = filename, col_names = header, col_types = "cin"))
- return(readr::read_delim(file = filename, delim = NULL, col_names = header, col_types = "cin"))
-}
-
-#' Parser for GC content reference data
-#' @param filename Filename of the file to read in
-#' @return A data frame with GC content
-read_gccontent = function(filename) {
- #return(readr::read_tsv(file=filename, skip = 1, col_names = F, col_types = "-cinnnnnnnnnnnn------"))
- return(readr::read_delim(file=filename, skip = 1, delim = NULL, col_names = F, col_types = "-cinnnnnnnnnnnn------"))
-}
-
-#' Parser for replication timing reference data
-#' @param filename Filename of the file to read in
-#' @return A data frame with replication timing
-read_replication = function(filename) {
- #return(readr::read_tsv(file=filename, col_types = paste0("ci", paste0(rep("n", 15), collapse = ""))))
- return(readr::read_delim(file=filename, delim = NULL, col_types = paste0("ci", paste0(rep("n", 15), collapse = ""))))
-}
-
-#' Parser for BAFsegmented data
-#' @param filename Filename of the file to read in
-#' @param header Whether the file contains a header (Default: TRUE)
-#' @return A data frame with BAFsegmented content
-read_bafsegmented = function(filename, header=T) {
- #return(readr::read_tsv(file = filename, col_names = header, col_types = "cinnn"))
- return(readr::read_delim(file = filename, delim = NULL, col_names = header, col_types = "cinnn"))
-}
-
-#' Parser for imputed genotype data
-#' @param filename Filename of the file to read in
-#' @return A data frame with the imputed genotype output
-read_imputed_output = function(filename) {
- #return(readr::read_tsv(file = filename, col_names = c("snpidx", "rsidx", "pos", "ref", "alt", "hap1", "hap2"), col_types = "cciccii"))
- return(readr::read_delim(file = filename, delim = NULL, col_names = c("snpidx", "rsidx", "pos", "ref", "alt", "hap1", "hap2"), col_types = "cciccii"))
-}
-
-#' Parser for allele frequencies data
-#' @param filename Filename of the file to read in
-#' @return A data frame with the alleleCounter output
-read_alleleFrequencies = function(filename) {
- #return(readr::read_tsv(file = filename, col_names = c("CHR", "POS", "Count_A", "Count_C", "Count_G", "Count_T", "Good_depth"), col_types = "ciiiiii", comment = "#"))
- return(readr::read_delim(file = filename, delim = NULL, col_names = c("CHR", "POS", "Count_A", "Count_C", "Count_G", "Count_T", "Good_depth"), col_types = "ciiiiii", comment = "#"))
-}
-
-#' Parser for impute input data
-#' @param filename Filename of the file to read in
-#' @return A data frame with the input for impute
-read_impute_input = function(filename) {
- #return(readr::read_delim(file = filename, col_names = F, col_types = "ccicciii", delim = " "))
- return(readr::read_delim(file = filename, col_names = F, col_types = "ccicciii", delim = NULL))
-}
-
-#' Parser for beagle5 output data
-#' @param filename Filename of the file to read in
-#' @return A data frame with the beagle5 output
-read_beagle_output = function(filename) {
- #return(readr::read_tsv(file = filename, col_names = c("#CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO", "FORMAT", "SAMP001"), col_types = "cicccccccc", comment = "#"))
- return(readr::read_delim(file = filename, delim = NULL, col_names = c("#CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO", "FORMAT", "SAMP001"), col_types = "cicccccccc", comment = "#"))
-}
-
-
-########################################################################################
-# Concatenate files
-########################################################################################
-#' Function to concatenate Impute output
-#' @noRd
-concatenateImputeFiles<-function(inputStart, boundaries) { #outputFile,
- infiles = c()
- for(i in 1:nrow(boundaries)) {
- filename = paste(inputStart,"_",boundaries[i,1]/1000,"K_",boundaries[i,2]/1000,"K.txt_haps",sep="")
- # Only add files that exist and have data
- if(file.exists(filename) && file.info(filename)$size>0) {
- infiles = c(infiles, filename)
- }
- }
- return(do.call(rbind, lapply(infiles, FUN=function(x) { read.table(x, sep=" ") })))
-}
-
-#' Function to concatenate haplotyped BAF output
-#' @noRd
-concatenateBAFfiles<-function(inputStart, inputEnd, outputFile, chr_names) {
- all_data<-NULL
- colNames<-NULL
- for(i in chr_names)
- {
- filename = paste(inputStart,i,inputEnd,sep="")
- if(file.exists(filename) && file.info(filename)$size>0)
- {
- data<-as.data.frame(read_table_generic(filename))
- all_data<-rbind(all_data,data)
- colNames<-names(data)
- }
- }
- #rnames=paste("snp",1:nrow(all_data),sep="")
- write.table(all_data,outputFile, row.names=F, col.names=colNames, quote=F, sep="\t")
-}
-
-#' Function to concatenate allele counter output
-#' @noRd
-concatenateAlleleCountFiles = function(inputStart, inputEnd, chr_names) {
- infiles = c()
- for(chrom in chr_names) {
- filename = paste(inputStart, chrom, inputEnd, sep="")
- # Only add files that exist and have data
- if(file.exists(filename) && file.info(filename)$size>0) {
- infiles = c(infiles, filename)
- }
- }
- return(as.data.frame(do.call(rbind, lapply(infiles, FUN=function(x) { read_table_generic(x) }))))
-}
-
-#' Function to concatenate 1000 Genomes SNP reference files
-#' @noRd
-concatenateG1000SnpFiles = function(inputStart, inputEnd, chr_names) {
- data = list()
- for(chrom in chr_names) {
- filename = paste(inputStart, chrom, inputEnd, sep="")
- # Only add files that exist and have data
- if(file.exists(filename) && file.info(filename)$size>0) {
- # infiles = c(infiles, filename)
- data[[chrom]] = cbind(chromosome=chrom, read_table_generic(filename))
- }
- }
- return(as.data.frame(do.call(rbind, data)))
-}
-
-
-
-########################################################################################
-# Various functions for calculating from data
-########################################################################################
-#' Calc copy number of major allele per segment from a subclones data.frame
-#' @noRd
-calc_total_cn_major = function(bb) {
- return(bb$nMaj1_A*bb$frac1_A + ifelse(bb$frac1_A < 1, bb$nMaj2_A*bb$frac2_A, 0))
-}
-
-#' Calc copy number of minor allele per segment from a subclones data.frame
-#' @noRd
-calc_total_cn_minor = function(bb) {
- return(bb$nMin1_A*bb$frac1_A + ifelse(bb$frac1_A < 1, bb$nMin2_A*bb$frac2_A, 0))
-}
-
-#' Calc total copy number per segment from a subclones data.frame
-#' @noRd
-calculate_bb_total_cn = function(bb) {
- return((bb$nMaj1_A+bb$nMin1_A)*bb$frac1_A + ifelse(!is.na(bb$frac2_A), (bb$nMaj2_A+bb$nMin2_A)*bb$frac2_A, 0))
-}
-
-#' Calc ploidy from a subclones data.frame
-#' @noRd
-calc_ploidy = function(bb) {
- bb$len = bb$endpos/1000-bb$startpos/1000
- bb$total_cn = calculate_bb_total_cn(bb)
- ploidy = sum(bb$total_cn*bb$len) / sum(bb$len)
- return(ploidy)
-}
-
-#' Transform logR into an estimate of total copy number given purity and total ploidy (tumour+normal)
-#' @noRd
-logr2tumcn = function(cellularity, total_ploidy, logR) {
- return(((total_ploidy*(2^logR)) - 2*(1-cellularity)) / cellularity)
-}
-
-#' Calc psi from psi_t and rho
-#' @noRd
-psit2psi = function(rho, psi_t) {
- return(rho*psi_t + 2*(1-rho))
-}
-
-#' Calc psi_t from psi and rho
-#' @noRd
-psi2psit = function(rho, psi) {
- return((psi-2*(1-rho))/rho)
-}
-
-########################################################################################
-# Refitting functions
-########################################################################################
-#' Calculate rho and psi values from a refit suggestion
-#'
-#' Use this function to calculate the refit values from a refit suggestion.
-#' @param refBAF BAF of the segment
-#' @param refLogR logR of the segment
-#' @param refMajor Major allele copy number
-#' @param refMinor Minor allele copy number
-#' @param rho Sample rho parameter
-#' @param gamma_param Platform gamma parameter
-#' @return A list with a field for rho and psi_t
-#' @author sd11
-#' @export
-calc_rho_psi_refit = function(refBAF, refLogR, refMajor, refMinor, rho, gamma_param) {
- rho = (2*refBAF-1)/(2*refBAF-refBAF*(refMajor+refMinor)-1+refMajor)
- psi = (rho*(refMajor+refMinor)+2-2*rho)/(2^(refLogR/gamma_param))
- psi_t = psi2psit(rho, psi)
- return(list(rho=rho, psi_t=psi_t))
-}
-
-#' Calculate refit values from a refit suggestion
-#'
-#' Use this function to calculate the refit values from a refit suggestion.
-#' @param subclones_file A Battenberg subclones.txt file
-#' @param segment_chrom Chromsome of the segment to use for refitting
-#' @param segment_pos Position within the start/end coordinates of the segment to use for refitting
-#' @param new_nMaj Major allele copy number
-#' @param new_nMin Minor allele copy number
-#' @param rho Sample rho parameter
-#' @param gamma_param Platform gamma parameter
-#' @return A list with a field for rho and psi_t
-#' @author sd11
-#' @export
-suggest_refit = function(subclones_file, segment_chrom, segment_pos, new_nMaj, new_nMin, rho, gamma_param) {
- # segment_pos = as.numeric(gsub("M", "000000", segment_pos))
- subclones = read.table(subclones_file, header=T, stringsAsFactors=F)
- segment = subclones[subclones$chr==segment_chrom & subclones$startpos<=segment_pos & subclones$endpos>=segment_pos,]
- segment_BAF = segment$BAF
- segment_LogR = segment$LogR
- return(calc_rho_psi_refit(segment_BAF, segment_LogR, new_nMaj, new_nMin, rho, gamma_param))
-}
-
-#' Create refit suggestions for a fit copy number profile
-#'
-#' This function takes a fit copy number profile and generates refit suggestions for a future rerun.
-#' If there are clonal alterations above a specified size, then those written out as supplied as suggestions,
-#' otherwise a refit suggestion of an external purity value will be saved.
-#' @param samplename Samplename for the output file
-#' @param subclones_file File containing a fit copy number profile
-#' @param rho_psi_file File with rho and psi values
-#' @param gamma_param Platform gamma parameter
-#' @param min_segment_size_mb Minimum size of a segment in Mb to be considered for a refit suggestion (Default: 2)
-#' @author sd11
-#' @export
-cnfit_to_refit_suggestions = function(samplename, subclones_file, rho_psi_file, gamma_param, min_segment_size_mb=2) {
- # samplename = "NASCR-0016"
- # subclones_file = "NASCR-0016_subclones.txt"
- subclones = Battenberg::read_table_generic(subclones_file)
- subclones$len = subclones$endpos/1000000-subclones$startpos/1000000
- subclones$is_cna = subclones$nMaj1_A!=subclones$nMin1_A
-
- #df[c("is_cna")][is.na(df[c("is_cna")])] <- FALSE
- #print(subclones$len)
- print(min_segment_size_mb)
- print(subclones$is_cna)
- if (any(subclones$len > min_segment_size_mb & subclones$is_cna)) {
- # There are large scale alterations, save the top couple as suggestions
- rho_psi = read.table(rho_psi_file, header=T, stringsAsFactors=F)
- rho = rho_psi["FRAC_GENOME", "rho"]
- psi_t = rho_psi["FRAC_GENOME", "psi"]
-
- # Take only segments that are clonal and are an alteration
- is_subclonal = subclones$frac1_A < 1
- subclones_clonal_cna = subset(subclones, !is_subclonal & subclones$is_cna)
- subclones_clonal_cna = subclones_clonal_cna[with(subclones_clonal_cna, order(len, decreasing=T)),]
-
- if (nrow(subclones_clonal_cna)==0) {
- output = data.frame(project=NA, samplename=samplename, qc=NA, cellularity_refit=T, chrom=NA, pos=NA, maj=NA, min=NA, baf=NA, logr=NA, rho_estimate=NA, psi_t_estimate=NA, rho_diff=NA, psi_t_diff=NA)
- } else {
-
- # Generate a couple of solutions, but not more than are possibly available
- max_solutions = ifelse(nrow(subclones_clonal_cna) >= 5, 5, nrow(subclones_clonal_cna))
- subclones_clonal_cna = subclones_clonal_cna[1:max_solutions, , drop=F]
-
- # Determine position in Mb within the segment
- position = subclones_clonal_cna$startpos + (subclones_clonal_cna$endpos - subclones_clonal_cna$startpos) / 2
- position = position / 1000000
- position_round_up = ceiling(position)
- position_round_down = floor(position)
- position = ifelse(position_round_up < subclones_clonal_cna$endpos, position_round_up, position_round_down)
-
- output = data.frame(project=rep(NA, max_solutions),
- samplename=rep(samplename, max_solutions),
- qc=rep(NA, max_solutions),
- cellularity_refit=rep(F, max_solutions),
- chrom=subclones_clonal_cna$chr[1:max_solutions],
- pos=paste(position, "M", sep=""),
- maj=subclones_clonal_cna$nMaj1_A[1:max_solutions],
- min=subclones_clonal_cna$nMin1_A[1:max_solutions],
- baf=subclones_clonal_cna$BAF[1:max_solutions],
- logr=subclones_clonal_cna$LogR[1:max_solutions])
-
- #refBAF, refLogR, refMajor, refMinor, rho, gamma_param
- res = calc_rho_psi_refit(output$baf, output$logr, output$maj, output$min, rho, gamma_param)
- output$rho_estimate = res$rho
- output$psi_t_estimate = res$psi_t
- output$rho_diff = abs(rho-output$rho_estimate)
- output$psi_t_diff = abs(psi_t-output$psi_t_estimate)
- }
- } else {
- # No large clonal alteration, save a suggestion that should use an external purity value
- output = data.frame(project=NA, samplename=samplename, qc=NA, cellularity_refit=T, chrom=NA, pos=NA, maj=NA, min=NA, baf=NA, logr=NA, rho_estimate=NA, psi_t_estimate=NA, rho_diff=NA, psi_t_diff=NA)
- }
- write.table(output, file=paste0(samplename, "_refit_suggestion.txt"), quote=F, sep="\t", row.names=F)
-}
-
########################################################################################
# Other
########################################################################################
#' Check if a file exists, if it doesn't, exit non-clean
#' @noRd
-assert.file.exists = function(filename) {
+assert_file_exists <- function(filename) {
if (!file.exists(filename)) {
- warning(paste("Supplied file does not exist: ", filename, sep=""))
- quit(save="no", status=1)
+ log_failure("Supplied file does not exist: {filename}")
+ quit(save = "no", status = 1)
}
}
diff --git a/R/writer.R b/R/writer.R
new file mode 100644
index 00000000..db6cb9cb
--- /dev/null
+++ b/R/writer.R
@@ -0,0 +1,14 @@
+write_chr_pos_metric <- function(
+ chr, pos, value, file, value_name
+) {
+ data.table::fwrite(
+ data.table::data.table(
+ Chromosome = chr,
+ Position = pos,
+ value = value
+ ),
+ file = file,
+ sep = "\t",
+ col.names = c("Chromosome", "Position", value_name)
+ )
+}
diff --git a/R/zzz.R b/R/zzz.R
index e9322131..04b5628e 100644
--- a/R/zzz.R
+++ b/R/zzz.R
@@ -1,3 +1,12 @@
+#' Battenberg: Subclonal Copy Number Caller
+#'
+#' @useDynLib Battenberg, .registration = TRUE
+#' @importFrom Rcpp sourceCpp
+NULL
+
.onLoad <- function(libname, pkgname) {
- options(scipen = 999)
+ # Keep your scipen setting
+ options(scipen = 999)
}
+
+.datatable.aware <- TRUE
diff --git a/README.md b/README.md
index 07a5126c..5c2fa229 100755
--- a/README.md
+++ b/README.md
@@ -65,6 +65,104 @@ The bundle contains the following files:
Go into ```inst/example``` for example WGS and SNP6 R-only pipelines.
+## Pre-processing and External Tools
+
+Battenberg now requires pre-calculated allele counts and haplotype information to be provided via directories. This approach offers better flexibility for integration into workflow managers (like Nextflow or Snakemake).
+
+Below are the exact command-line requirements for the tools previously managed internally by Battenberg.
+
+### 1. Allele Counting (`alleleCounter`)
+
+You must count alleles for both the **tumor** and **normal** samples across all autosomes and the X chromosome.
+
+**Command Template:**
+```bash
+alleleCounter \
+ -b \
+ -l \
+ -o \
+ -m \
+ -q \
+ --dense-snps
+```
+
+**Naming Convention:**
+- Tumor: `[tumourname]_alleleFrequencies_chr[chrom].txt`
+- Normal: `[normalname]_alleleFrequencies_chr[chrom].txt`
+
+**Required Arguments:**
+- `-b`: Input BAM file.
+- `-l`: 1000 Genomes SNP loci file (e.g., `1kg.phase3.v5a_GRCh38nounref_loci_chr1.txt`).
+- `-m`: Minimum **base quality** (Default: 20).
+- `-q`: Minimum **mapping quality** (Default: 35).
+- `--dense-snps`: Required for performance when using 1000G loci (supported in alleleCounter >= v4.0.0).
+
+---
+
+### 2. Haplotype Phasing
+
+Battenberg supports two phasing backends: **IMPUTE2** and **Beagle5**. Results should be placed in the directory specified by `--impute_results_dir`.
+
+#### Option A: IMPUTE2
+If using IMPUTE2, you must provide combined haplotype info files.
+
+**Command Template:**
+```bash
+impute2 \
+ -m \
+ -h \
+ -l \
+ -g \
+ -int \
+ -Ne 20000 \
+ -o \
+ -phase \
+ -os 2
+```
+
+**Naming Convention:**
+- `[tumourname]_impute_output_chr[chrom]_allHaplotypeInfo.txt`
+
+**Format:** A space-separated file with 7 columns (ID, rsID, position, allele1, allele2, hap1, hap2). Note that Battenberg expects the *total* phased information for the chromosome in one file.
+
+#### Option B: Beagle5
+If using Beagle5, you can provide phased VCF files. Battenberg will automatically convert these to its internal format if `--usebeagle` is set.
+
+**Command Template:**
+```bash
+java -Xmxg -jar beagle.jar \
+ gt= \
+ ref= \
+ map= \
+ out= \
+ nthreads= \
+ window=40 \
+ overlap=4 \
+ impute=false
+```
+
+**Naming Convention:**
+- `[tumourname]_beagle_output_chr[chrom].vcf.gz` (or `.vcf`)
+
+---
+
+### 3. Dir Structure and Execution
+
+When running Battenberg, point it to the directories containing these files:
+
+```bash
+R/cli.R \
+ --samplename SLX-1234.T \
+ --normalname SLX-1234.N \
+ --allele_counts_dir ./counts \
+ --impute_results_dir ./phasing \
+ --usebeagle TRUE \
+ ...
+```
+
+Battenberg will look for files matching the sample names inside those directories.
+
+
## Description of the output
### Key output files
@@ -148,7 +246,7 @@ S3method(plot,haplotype.data)
to:
```
-export(plot.haplotype.data)
+export(plot_haplotype_data)
```
@@ -361,7 +459,7 @@ mclapply(ffs[length(ffs):1],function(x)
ndf <- data.frame(position=df[,1],
a0=ref,
a1=alt)
- write.table(ndf,file=out,
+ data.table::fwrite(ndf,file=out,
row.names=F,col.names=T,sep="\t",quote=F)
},mc.cores=5)
##########################################################################
@@ -433,7 +531,7 @@ getRefGenome <- function (fasta = FASTA, CHRS = paste0("", c(1:22, "X", "Y",
"MT")))
{
dna <- Biostrings::readDNAStringSet(fasta, format = "fasta")
- dna <- lapply(1:length(CHRS), function(x) dna[[x]])
+ dna <- lapply(seq_along(CHRS), function(x) dna[[x]])
names(dna) <- CHRS
return(dna)
}
@@ -476,7 +574,7 @@ names(windows) <- sapply(names(windows),function(x) if(grepl("[0-9]$",x)) paste0
writeGC <- function(gccontent,chr,outdir)
{
- write.table(gccontent,
+ data.table::fwrite(gccontent,
file=gzfile(paste0(outdir,"/1000_genomes_GC_corr_chr_",chr,".txt.gz")),
col.names=T,
row.names=T,quote=F,sep="\t")
@@ -509,58 +607,23 @@ The map plink files for Beagle can be downloaded from:
http://bochet.gcc.biostat.washington.edu/beagle/genetic_maps/
+```R
+battenberg(
+ samplename = "TUMOURNAME",
+ normalname = "NORMALNAME",
+ sample_data_file = "TUMOURBAM",
+ normal_data_file = "NORMALBAM",
+ imputeinfofile = "IMPUTEINFOFILE",
+ g1000prefix = "G1000PREFIX",
+ problemloci = "PROBLEMLOCI",
+ allele_counts_dir = "PATH/TO/ALLELE_COUNTS",
+ impute_results_dir = "PATH/TO/IMPUTE_RESULTS",
+ gccorrectprefix = "GCCORRECTPREFIX",
+ repliccorrectprefix = "REPLICCORRECTPREFIX",
+ g1000allelesprefix = "G1000PREFIX_AC",
+ ismale = TRUE,
+ data_type = "wgs",
+ nthreads = 8,
+ usebeagle = TRUE # Set to TRUE if using Beagle VCFs in impute_results_dir
+)
```
-BEAGLEJAR <- "$PATHTOBEAGLEFILES/beagle.24Aug19.3e8.jar"
-BEAGLEREF.template <- "$PATHTOBEAGLEFILES/chrCHROMNAME.1kg.phase3.v5a.b37.bref3"
-BEAGLEPLINK.template <- "$PATHTOBEAGLEFILES/plink.chrCHROMNAME.GRCh37.map"
-
-timed <- system.time(battenberg(tumourname=TUMOURNAME,
- normalname=NORMALNAME,
- tumour_data_file=TUMOURBAM,
- normal_data_file=NORMALBAM,
- imputeinfofile=IMPUTEINFOFILE,
- g1000prefix=G1000PREFIX,
- problemloci=PROBLEMLOCI,
- gccorrectprefix=GCCORRECTPREFIX,
- repliccorrectprefix=REPLICCORRECTPREFIX,
- g1000allelesprefix=G1000PREFIX_AC,
- ismale=IS_MALE,
- data_type="wgs",
- impute_exe="impute2",
- allelecounter_exe="alleleCounter",
- nthreads=NTHREADS,
- platform_gamma=1,
- phasing_gamma=1,
- segmentation_gamma=10,
- segmentation_kmin=3,
- phasing_kmin=1,
- clonality_dist_metric=0,
- ascat_dist_metric=1,
- min_ploidy=1.6,
- max_ploidy=4.8, min_rho=0.1,
- min_goodness=0.63,
- uninformative_BAF_threshold=0.51,
- min_normal_depth=10,
- min_base_qual=20,
- min_map_qual=35,
- calc_seg_baf_option=1,
- skip_allele_counting=F,
- skip_preprocessing=F,
- skip_phasing=F,
- usebeagle=USEBEAGLE, ##set to TRUE to use beagle
- beaglejar=BEAGLEJAR, ##path
- beagleref=BEAGLEREF.template, ##pathtemplate
- beagleplink=BEAGLEPLINK.template, ##pathtemplate
- beaglemaxmem=15,
- beaglenthreads=1,
- beaglewindow=40,
- beagleoverlap=4,
- snp6_reference_info_file=NA,
- apt.probeset.genotype.exe="apt-probeset-genotype",
- apt.probeset.summarize.exe="apt-probeset-summarize",
- norm.geno.clust.exe="normalize_affy_geno_cluster.pl",
- birdseed_report_file="birdseed.report.txt",
- heterozygousFilter="none",
- prior_breakpoints_file=NULL))
-```
-
diff --git a/docs/articles/advanced-usage.html b/docs/articles/advanced-usage.html
index f3cd0619..ba4c221f 100644
--- a/docs/articles/advanced-usage.html
+++ b/docs/articles/advanced-usage.html
@@ -1,107 +1,235 @@
-
-
-
-
-
-
-
-Advanced Usage and Parameter Optimization • Battenberg
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Advanced Usage and Parameter Optimization • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+ Articles
-
-
+
+
+
+
+
+
2025-07-04
+
+
Source:
+ vignettes/advanced-usage.Rmd
+
+ advanced-usage.Rmd
+
+
-
-
-
-
-
Advanced Parameter Tuning
-
-
-
Segmentation Parameters
-
-
The segmentation behavior can be controlled by several parameters:
-
+
+
+
+ Advanced Parameter Tuning
+
+
+
+ Segmentation Parameters
+
+
+ The segmentation behavior can be controlled by
+ several parameters:
+
+
+
# More sensitive segmentation (more segments)
battenberg (
# ... other parameters ...
@@ -116,13 +244,23 @@
-
-
-
Purity and Ploidy Constraints
-
-
Adjust expected ranges based on sample characteristics:
-
+
+
+
+ Purity and Ploidy Constraints
+
+
+ Adjust expected ranges based on sample
+ characteristics:
+
+
+
# High purity sample
battenberg (
# ... other parameters ...
@@ -137,34 +275,51 @@
-
-
-
Quality Control Parameters
-
-
+
+
+
+ Quality Control Parameters
+
+
+
# Strict quality control
battenberg (
# ... other parameters ...
min_normal_depth = 15 , # Higher coverage requirement
min_base_qual = 25 , # Higher base quality
min_map_qual = 40 , # Higher mapping quality
- uninformative_BAF_threshold = 0.49 # Stricter BAF threshold
-)
-
-
-
-
Using Prior Structural Variant Breakpoints
-
-
Battenberg can incorporate prior breakpoints from structural variant calls:
-
+ uninformative_baf_threshold = 0.49 # Stricter BAF threshold
+)
+
+
+
+
+
+ Using Prior Structural Variant Breakpoints
+
+
+ Battenberg can incorporate prior breakpoints from
+ structural variant calls:
+
+
+
# Create prior breakpoints file (2 columns: chr, pos)
prior_breakpoints <- data.frame (
chr = c ( "1" , "1" , "2" , "3" ) ,
pos = c ( 1500000 , 2500000 , 5000000 , 1000000 )
)
-write.table ( prior_breakpoints , "prior_breakpoints.txt" ,
+data.table::fwrite ( prior_breakpoints , "prior_breakpoints.txt" ,
row.names = FALSE , col.names = FALSE ,
quote = FALSE , sep = "\t" )
@@ -172,13 +327,23 @@ Using Prior Structural Varia
battenberg (
# ... other parameters ...
prior_breakpoints_file = "prior_breakpoints.txt"
-)
-
-
-
Using Beagle5 for Imputation
-
-
For improved phasing, especially with newer reference panels:
-
+
+
+
+ Using Beagle5 for Imputation
+
+
+ For improved phasing, especially with newer
+ reference panels:
+
+
+
# Setup Beagle5 parameters
BEAGLEJAR <- "path/to/beagle.24Aug19.3e8.jar"
BEAGLEREF_TEMPLATE <- "path/to/beagle_ref_chrCHROMNAME.1kg.phase3.v5a.b37.bref3"
@@ -194,13 +359,20 @@
-
-
-
Multisample Analysis
-
-
For analyzing multiple samples together:
-
+
+
+
+ Multisample Analysis
+
+
For analyzing multiple samples together:
+
+
# Define multiple samples
tumournames <- c ( "sample1_tumor" , "sample2_tumor" , "sample3_tumor" )
normalnames <- c ( "sample1_normal" , "sample2_normal" , "sample3_normal" )
@@ -219,13 +391,20 @@
-
-
-
Cell Line Analysis
-
-
For cell line data (tumor-only analysis):
-
+
+
+
+ Cell Line Analysis
+
+
For cell line data (tumor-only analysis):
+
+
battenberg (
analysis = "cell_line" , # Changed from default "paired"
tumourname = "cell_line_sample" ,
@@ -235,54 +414,87 @@
-
-
-
SNP Array Analysis
-
-
For SNP6 array data:
-
+
+
+
+ SNP Array Analysis
+
+
For SNP6 array data:
+
+
battenberg (
# ... other parameters ...
data_type = "snp6" ,
platform_gamma = 1 ,
snp6_reference_info_file = "path/to/snp6_reference_info.txt" ,
- apt.probeset.genotype.exe = "apt-probeset-genotype" ,
- apt.probeset.summarize.exe = "apt-probeset-summarize" ,
- norm.geno.clust.exe = "normalize_affy_geno_cluster.pl" ,
+ apt_probeset_genotype_exe = "apt-probeset-genotype" ,
+ apt_probeset_summarize_exe = "apt-probeset-summarize" ,
+ norm_geno_clust_exe = "normalize_affy_geno_cluster.pl" ,
birdseed_report_file = "birdseed.report.txt"
-)
-
-
-
-
-
Parallel Processing
-
-
+
+
+
+
+
+ Parallel Processing
+
+
+
# Use more threads for faster processing
battenberg (
# ... other parameters ...
nthreads = 16 , # Use 16 CPU cores
beaglenthreads = 8 # Use 8 cores for Beagle (if using)
-)
-
-
+
+
+ Memory Management
+
+
+
# For large datasets, adjust memory settings
battenberg (
# ... other parameters ...
beaglemaxmem = 32 , # 32GB for Beagle
# Consider running chromosomes separately for very large files
-)
-
-
-
Skipping Steps
-
-
For rerunning parts of the analysis:
-
+
+
+
+ Skipping Steps
+
+
For rerunning parts of the analysis:
+
+
# Skip allele counting if already done
battenberg (
# ... other parameters ...
@@ -297,87 +509,156 @@
-
-
-
-
Custom Genome Builds
-
-
For different reference genomes:
-
+
+
+
+
+ Custom Genome Builds
+
+
For different reference genomes:
+
+
# Specify genome build
battenberg (
# ... other parameters ...
GENOMEBUILD = "hg38" , # or "hg19"
# Ensure reference files match the specified build
-)
-
-
-
External Haplotype Files
-
-
Using external phasing information:
-
+
+
+
+ External Haplotype Files
+
+
Using external phasing information:
+
+
battenberg (
# ... other parameters ...
externalhaplotypefile = "path/to/external_haplotypes.vcf" ,
write_battenberg_phasing = TRUE
-)
-
-
-
Troubleshooting Common Issues
-
-
-
Low Quality Samples
-
-
-Increase min_normal_depth and quality thresholds
-Adjust min_goodness to be more lenient
-Check coverage uniformity
-
-
-
-
Highly Aneuploid Samples
-
-
-Increase max_ploidy range
-Adjust segmentation_gamma for appropriate segment resolution
-
-
-
-
Contaminated Samples
-
-
-Lower min_rho threshold
-Consider pre-processing to estimate contamination
-
-
-
-
Memory Issues
-
-
-Reduce beaglemaxmem if running out of memory
-Process chromosomes separately
-Use fewer threads if memory-limited
-
-
-
-
-
Quality Assessment
-
-
After running Battenberg, assess quality using:
-
-
-Distance plot : Check purity/ploidy solution space
-
-Profile plots : Examine copy number profiles for artifacts
-
-Coverage plots : Verify uniform coverage
-
-BAF plots : Check for proper phase separation
-
-
+
+
+
+ Troubleshooting Common Issues
+
+
+
+ Low Quality Samples
+
+
+
+ Increase min_normal_depth and
+ quality thresholds
+
+
+ Adjust min_goodness to be more
+ lenient
+
+ Check coverage uniformity
+
+
+
+
+ Highly Aneuploid Samples
+
+
+ Increase max_ploidy range
+
+ Adjust segmentation_gamma for
+ appropriate segment resolution
+
+
+
+
+
+ Contaminated Samples
+
+
+ Lower min_rho threshold
+
+ Consider pre-processing to estimate
+ contamination
+
+
+
+
+
+ Memory Issues
+
+
+
+ Reduce beaglemaxmem if running
+ out of memory
+
+ Process chromosomes separately
+ Use fewer threads if memory-limited
+
+
+
+
+
+ Quality Assessment
+
+
After running Battenberg, assess quality using:
+
+
+ Distance plot : Check
+ purity/ploidy solution space
+
+
+ Profile plots : Examine copy
+ number profiles for artifacts
+
+
+ Coverage plots : Verify uniform
+ coverage
+
+
+ BAF plots : Check for proper
+ phase separation
+
+
+
+
# Example quality check
-cn_data <- read.delim ( "sample_tumor_copynumber.txt" )
+cn_data <- read.delim ( "sample_tumor_copynumber.txt" )
# Check for very short segments (potential artifacts)
short_segments <- cn_data [ cn_data $ endpos - cn_data $ startpos < 1000000 , ]
@@ -386,39 +667,44 @@
-
-
-
-
-
-
-
-
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
Site built with pkgdown 2.1.2.
-
-
-
-
-
-
-
+
}
+
+
+
+
+
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
-
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
diff --git a/docs/articles/data-interpretation.html b/docs/articles/data-interpretation.html
index 88102259..8ba3c17e 100644
--- a/docs/articles/data-interpretation.html
+++ b/docs/articles/data-interpretation.html
@@ -113,7 +113,7 @@
diff --git a/docs/articles/getting-started.html b/docs/articles/getting-started.html
index b604fee2..4a68de67 100644
--- a/docs/articles/getting-started.html
+++ b/docs/articles/getting-started.html
@@ -1,112 +1,247 @@
-
-
-
-
-
-
-
-Getting Started with Battenberg • Battenberg
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Getting Started with Battenberg • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+ Articles
-
-
+
+
+
+
+
+
2025-07-04
+
+
Source:
+ vignettes/getting-started.Rmd
+
+ getting-started.Rmd
+
+
-
-
-
-
-
Introduction
-
-
Battenberg is a whole genome sequencing subclonal copy number caller that estimates subclonal copy number alterations from matched tumor-normal whole genome sequencing data. It can detect both clonal and subclonal copy number changes and provides estimates of tumor purity and ploidy.
-
-
-
Installation
-
-
-
Prerequisites
-
-
Battenberg requires several dependencies. Install them first:
-
+
+
+
+ Introduction
+
+
+ Battenberg is a whole genome sequencing subclonal
+ copy number caller that estimates subclonal copy
+ number alterations from matched tumor-normal whole
+ genome sequencing data. It can detect both clonal
+ and subclonal copy number changes and provides
+ estimates of tumor purity and ploidy.
+
+
+
+
+ Installation
+
+
+
+ Prerequisites
+
+
+ Battenberg requires several dependencies.
+ Install them first:
+
+
-
-
-
Install Battenberg
-
-
+
+
+
+ Install Battenberg
+
+
+
# Install from GitHub (pre_3.0 branch)
-devtools :: install_github ( "Wedge-lab/battenberg" , ref= "pre_3.0" )
-
-
-
-
Reference Data Requirements
-
-
Before running Battenberg, you need to download reference data:
-
-
-
-
-
Basic Usage
-
-
-
Running the Full Pipeline
-
-
The main function battenberg() runs the complete analysis pipeline:
-
+
+
+
+
+ Reference Data Requirements
+
+
+ Before running Battenberg, you need to download
+ reference data:
+
+
+
+ For GRCh37/hg19:
+
+
+
+
+
+ For GRCh38/hg38:
+
+
+
+
+
+
+ Basic Usage
+
+
+
+ Running the Full Pipeline
+
+
+ The main function
+ battenberg()
+ runs the complete analysis pipeline:
+
+
-
-
-
Key Parameters
-
-
-
-tumourname/normalname : Sample identifiers used as prefixes for output files
-
-tumour_data_file/normal_data_file : Paths to BAM files
-
-data_type : “wgs” for whole genome sequencing, “snp6” for SNP array data
-
-ismale : TRUE for male samples, FALSE for female samples
-
-platform_gamma : Platform-specific gamma parameter (1 for WGS, 1 for SNP6)
-
-segmentation_gamma : Controls segmentation sensitivity (higher = more segments)
-
-min_ploidy/max_ploidy : Expected range of tumor ploidy
-
-min_rho : Minimum tumor purity to consider
-
-
-
-
-
Output Files
-
-
Battenberg produces several key output files:
-
-
Output Files
-
-
Battenberg produces several key output files:
-
-
-
Primary Results
-
-
-
-[samplename]_copynumber.txt: Copy number segments with clonal/subclonal states
-
-[samplename]_rho_and_psi.txt: Tumor purity and ploidy estimates
-
-
-
-
Visualization
-
-
-
-[samplename]_BattenbergProfile.png: Genome-wide copy number profile
-
-[samplename]_BattenbergProfile_subclones.png: Alternative subclonal view
-
-[samplename]_subclones_chr*.png: Per-chromosome detailed plots
-
-[samplename]_distance.png: Purity/ploidy solution space
-
-
-
-
Quality Control
-
-
-
-[samplename].tumour.png: Raw tumor BAF and LogR
-
-[samplename].germline.png: Raw normal BAF and LogR
-
-[samplename]_coverage.png: Coverage profiles
-
-
-
-
-
Reading Results
-
-
-
Load Copy Number Data
-
-
+
+
+
+ Key Parameters
+
+
+
+ tumourname/normalname :
+ Sample identifiers used as prefixes for
+ output files
+
+
+ tumour_data_file/normal_data_file : Paths to BAM files
+
+
+ data_type : “wgs” for whole
+ genome sequencing, “snp6” for SNP array data
+
+
+ ismale : TRUE for male
+ samples, FALSE for female samples
+
+
+ platform_gamma :
+ Platform-specific gamma parameter (1 for
+ WGS, 1 for SNP6)
+
+
+ segmentation_gamma :
+ Controls segmentation sensitivity (higher =
+ more segments)
+
+
+ min_ploidy/max_ploidy :
+ Expected range of tumor ploidy
+
+
+ min_rho : Minimum tumor
+ purity to consider
+
+
+
+
+
+
+ Output Files
+
+
Battenberg produces several key output files:
+
+
+ Output Files
+
+
Battenberg produces several key output files:
+
+
+
+ Primary Results
+
+
+
+ [samplename]_copynumber.txt:
+ Copy number segments with clonal/subclonal
+ states
+
+
+ [samplename]_rho_and_psi.txt:
+ Tumor purity and ploidy estimates
+
+
+
+
+
+ Visualization
+
+
+
+ [samplename]_BattenbergProfile.png: Genome-wide copy number profile
+
+
+ [samplename]_BattenbergProfile_subclones.png: Alternative subclonal view
+
+
+ [samplename]_subclones_chr*.png: Per-chromosome detailed plots
+
+
+ [samplename]_distance.png:
+ Purity/ploidy solution space
+
+
+
+
+
+ Quality Control
+
+
+
+ [samplename].tumour.png: Raw
+ tumor BAF and LogR
+
+
+ [samplename].germline.png: Raw
+ normal BAF and LogR
+
+
+ [samplename]_coverage.png:
+ Coverage profiles
+
+
+
+
+
+
+ Reading Results
+
+
+
+ Load Copy Number Data
+
+
+
# Read the main results file
-cn_data <- read.delim ( "sample_tumor_copynumber.txt" )
+cn_data <- read.delim ( "sample_tumor_copynumber.txt" )
# Examine the structure
head ( cn_data )
-str ( cn_data )
-
-
-
Load Purity/Ploidy Estimates
-
-
+
+
+
+ Load Purity/Ploidy Estimates
+
+
+
# Read purity and ploidy estimates
-rho_psi <- read.delim ( "sample_tumor_rho_and_psi.txt" )
+rho_psi <- read.delim ( "sample_tumor_rho_and_psi.txt" )
# Extract purity (rho) - use FRAC_genome value from second row
tumor_purity <- rho_psi $ rho [ 2 ]
tumor_ploidy <- rho_psi $ psi [ 2 ]
cat ( "Estimated tumor purity:" , tumor_purity , "\n" )
-cat ( "Estimated tumor ploidy:" , tumor_ploidy , "\n" )
-
-
-
-
Understanding the Output
-
-
-
Copy Number States
-
-
Each segment can have: - Clonal : Single copy number state (frac1_A = 1, frac2_A = NA) - Subclonal : Two copy number states (frac1_A + frac2_A = 1)
-
-
-
Key Columns in copynumber.txt
-
-
-
-nMaj1_A, nMin1_A: Major/minor allele copy numbers for state 1
-
-nMaj2_A, nMin2_A: Major/minor allele copy numbers for state 2 (if subclonal)
-
-frac1_A, frac2_A: Fraction of tumor cells with each state
-
-pval: P-value for subclonal vs clonal model
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
Site built with pkgdown 2.1.2.
-
-
-
-
-
-
-
+
cat ( "Estimated tumor ploidy:" , tumor_ploidy , "\n" )
+
+
+
+
+
+ Understanding the Output
+
+
+
+ Copy Number States
+
+
+ Each segment can have: -
+ Clonal : Single copy number
+ state (frac1_A = 1, frac2_A = NA) -
+ Subclonal : Two copy number
+ states (frac1_A + frac2_A = 1)
+
+
+
+
+ Key Columns in copynumber.txt
+
+
+
+ nMaj1_A, nMin1_A:
+ Major/minor allele copy numbers for state 1
+
+
+ nMaj2_A, nMin2_A:
+ Major/minor allele copy numbers for state 2
+ (if subclonal)
+
+
+ frac1_A, frac2_A:
+ Fraction of tumor cells with each state
+
+
+ pval: P-value for subclonal vs
+ clonal model
+
+
+
+
+
+
+
+
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
-
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
diff --git a/docs/index.html b/docs/index.html
index 77887bf4..8400be79 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1,271 +1,704 @@
-
-
-
-
-
-
-
-Battenberg subclonal copy number caller • Battenberg
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Battenberg subclonal copy number caller • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+ Articles
-
-
-
-
-
-
This repository contains code for the whole genome sequencing subclonal copy number caller Battenberg, as described in Nik-Zainal, Van Loo, Wedge, et al. (2012), Cell .
-
-
Installation instructions
-
-
The instructions below will install the latest stable Battenberg version.
-
-
Prerequisites
-
-
Installing from Github requires devtools and Battenberg requires the modified copynumber package from “igordot/copynumber” and readr, gtools, splines, ggplot2, gridExtra, RColorBrewer, VariantAnnotation, GenomicRanges and ASCAT. The pipeline requires parallel and doParallel. From the command line run:
-
R -q -e 'BiocManager::install(c("devtools", "splines", "readr", "doParallel", "ggplot2", "RColorBrewer", "gridExtra", "gtools", "parallel", "igordot/copynumber", "VariantAnnotation", "GenomicRanges"))'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This repository contains code for the whole genome
+ sequencing subclonal copy number caller Battenberg,
+ as described in
+ Nik-Zainal, Van Loo, Wedge, et al. (2012),
+ Cell .
+
+
+
+ Installation instructions
+
+
+ The instructions below will install the latest
+ stable Battenberg version.
+
+
+
+ Prerequisites
+
+
+ Installing from Github requires devtools and
+ Battenberg requires the modified copynumber
+ package from “igordot/copynumber” and readr,
+ gtools, splines, ggplot2, gridExtra,
+ RColorBrewer, VariantAnnotation,
+ GenomicRanges and ASCAT. The pipeline
+ requires parallel and doParallel. From the
+ command line run:
+
+
R -q -e 'BiocManager::install(c("devtools", "splines", "readr", "doParallel", "ggplot2", "RColorBrewer", "gridExtra", "gtools", "parallel", "igordot/copynumber", "VariantAnnotation", "GenomicRanges"))'
R -q -e 'devtools::install_github("VanLoo-lab/ascat/ASCAT")'
-
-
-
Installation from Github
-
-
To install Battenberg, run the following from the command line:
-
R -q -e 'devtools::install_github("Wedge-Oxford/battenberg")'
-
-
-
Required reference files
-
-
GRCh37 reference files may downloaded from here: https://ora.ox.ac.uk/objects/uuid:2c1fec09-a504-49ab-9ce9-3f17bac531bc
-
The bundle contains the following files: * battenberg_1000genomesloci2012_v3.tar.gz * battenberg_impute_1000G_v3.tar.gz * probloci_270415.txt.gz * battenberg_wgs_gc_correction_1000g_v3.tar.gz * battenberg_wgs_replic_correction_1000g_v3.tar.gz * battenberg_snp6_exe.tgz (SNP6 only) * battenberg_snp6_ref.tgz (SNP6 only)
-
GRCh38 reference files may be downloaded from here: https://ora.ox.ac.uk/objects/uuid:08e24957-7e76-438a-bd38-66c48008cf52
-
The bundle contains the following files: * 1000G_loci_hg38.zip * imputation.zip * beagle5.zip * probloci.zip * GC_correction_hg38.zip * RT_correction_hg38.zip * README.txt
-
-
-
Pipeline
-
-
Go into inst/example for example WGS and SNP6 R-only pipelines.
-
-
-
-
Description of the output
-
-
-
Key output files
-
-
-
-[samplename]_copynumber.txt contains the copy number data (see table below)
-
-[samplename]_rho_and_psi.txt contains the purity estimate (make sure to use the FRAC_genome, rho field in the second row, first column)
-
-[samplename]_BattenbergProfile*png shows the profile (the two variants show subclonal copy number in a different way)
-
-[samplename]_subclones_chr*.png show detailed figures of the copy number calls per chromosome
-
-[samplename]_distance.png This shows the purity and ploidy solution space and can be used to pick alternative solutions
-
-
The copy number profile saved in the [samplename]_copynumber.txt is a tab delimited file in text format. Within this file there is a line for each segment in the tumour genome. Each segment will have either one or two copy number states:
-
-If there is one state that line represents the clonal copy number (i.e. all tumour cells have this state)
-If there are two states that line represents subclonal copy number (i.e. there are two populations of cells, each with a different state)
-
-
A copy number state consists of a major and a minor allele and their frequencies, which together add give the total copy number for that segment and an estimate fraction of tumour cells that carry each allele.
-
The following columns are available in the Battenberg output:
-
-
-
-
-
-
-
-
-chr
-The chromosome of the segment
-
-
-startpos
-Start position on the chromosome
-
-
-endpos
-End position on the chromosome
-
-
-BAF
-The B-allele frequency of the segment
-
-
-pval
-P-value that is obtained when testing whether this segment should be represented by one or two states. A low p-value will result in the fitting of a second copy number state
-
-
-LogR
-The log ratio of normalised tumour coverage versus its matched normal sequencing sample
-
-
-ntot
-An internal total copy number value used to determine the priority of solutions. NOTE: This is not the total copy number of this segment!
-
-
-nMaj1_A
-The major allele copy number of state 1 from solution A
-
-
-nMin1_A
-The minor allele copy number of state 1 from solution A
-
-
-frac1_A
-Fraction of tumour cells carrying state 1 in solution A
-
-
-nMaj2_A
-The major allele copy number of state 2 from solution A. This value can be NA
-
-
-nMin2_A
-The minor allele copy number of state 2 from solution A. This value can be NA
-
-
-frac2_A
-Fraction of tumour cells carrying state 2 in solution A. This value can be NA
-
-
-SDfrac_A
-Standard deviation on the BAF of SNPs in this segment, can be used as a measure of uncertainty
-
-
-SDfrac_A_BS
-Bootstrapped standard deviation
-
-
-frac1_A_0.025
-Associated 95% confidence interval of the bootstrap measure of uncertainty
-
-
-
-
Followed by possible equivalent solutions B to F with the same columns as defined above for solution A (due to the way a profile is fit Battenberg can generate a series of equivalent solutions that are reported separately in the output).
-
-
-
Plots for QC
-
-
It also produces a number plots that show the raw data and are useful for QC (and their raw data files denoted by *.tab)
-
-
-[samplename].tumour.png and [samplename].germline.png show the raw BAF and logR
-
-[samplename]_coverage.png contains coverage divided by the mean coverage of both tumour and normal
-
-[samplename]_alleleratio.png shows BAF*logR, a rough approximation of what the data looks like shortly before copy number calling
-
-
-
-
Intermediate figures
-
-
Finally, a range of plots show intermediate steps and can occasionally be useful
-
-
-[samplename]_chr*_heterozygousData.png shows reconstructed haplotype blocks in the characteristic Battenberg cake pattern
-
-[samplename]_RAFseg_chr*.png and [samplename]_segment_chr*.png contains segmentation data for step 1 and step 2 respectively
-
-[samplename]_nonroundedprofile.png shows the copy number profile without rounding to integers
-
-[samplename]_copynumberprofile.png shows the copy number profile with (including subclonal copy number) rounding to integers
-
-
-
-
-
Advice for including structural variant breakpoints
-
-
Battenberg can take prior breakpoints, from structural variants (SVs) for example, as input. SV breakpoints are typically much more precise and a pair of SVs can be closer together then what typically can be obtained from a BAF or coverage track. It is therefore adventageous to include prior breakpoints in a Battenberg run. However, including too many (as in 100s) incorrect breakpoints can have adverse effects by allowing many small segments to be affected by noise where there isn’t any signal and increasing the runtime of the pipeline. It is therefore advised to filter prior breakpoints from SVs such that the genome is slightly oversegmented. Finally, some SV types, such as inversions, do not constitute a change in copy number and therefore also add breakpoints that should not be considered. It is therefore also advised to filter breakpoints from SVs that do not cause a change in copynumber, such as inversions. Please note that the chromosome names in the SV file do not include the “chr” prefix.
-
-
-
Building a release
-
-
In RStudio: In the Build tab, click Check Package
-
Then open the NAMESPACE file and edit:
-
S3method ( plot ,haplotype.data )
-
to:
-
export ( plot.haplotype.data )
-
-
-
hg38 for Beagle5
-
-
Modified original code to derive the input vcf for Beagle5 and hg38:
-
#!/bin/bash
+
+
+
+ Installation from Github
+
+
+ To install Battenberg, run the following
+ from the command line:
+
+
R -q -e 'devtools::install_github("Wedge-Oxford/battenberg")'
+
+
+
+ Required reference files
+
+
+ GRCh37 reference files may
+ downloaded from here:
+ https://ora.ox.ac.uk/objects/uuid:2c1fec09-a504-49ab-9ce9-3f17bac531bc
+
+
+ The bundle contains the following files: *
+ battenberg_1000genomesloci2012_v3.tar.gz *
+ battenberg_impute_1000G_v3.tar.gz *
+ probloci_270415.txt.gz *
+ battenberg_wgs_gc_correction_1000g_v3.tar.gz
+ *
+ battenberg_wgs_replic_correction_1000g_v3.tar.gz
+ * battenberg_snp6_exe.tgz (SNP6 only) *
+ battenberg_snp6_ref.tgz (SNP6 only)
+
+
+ GRCh38 reference files may be
+ downloaded from here:
+ https://ora.ox.ac.uk/objects/uuid:08e24957-7e76-438a-bd38-66c48008cf52
+
+
+ The bundle contains the following files: *
+ 1000G_loci_hg38.zip * imputation.zip *
+ beagle5.zip * probloci.zip *
+ GC_correction_hg38.zip *
+ RT_correction_hg38.zip * README.txt
+
+
+
+
+ Pipeline
+
+
+ Go into inst/example for
+ example WGS and SNP6 R-only pipelines.
+
+
+
+
+
+ Description of the output
+
+
+
+ Key output files
+
+
+
+ [samplename]_copynumber.txt
+ contains the copy number data (see table
+ below)
+
+
+ [samplename]_rho_and_psi.txt
+ contains the purity estimate (make sure
+ to use the FRAC_genome, rho field in the
+ second row, first column)
+
+
+ [samplename]_BattenbergProfile*png
+ shows the profile (the two variants show
+ subclonal copy number in a different
+ way)
+
+
+ [samplename]_subclones_chr*.png
+ show detailed figures of the copy number
+ calls per chromosome
+
+
+ [samplename]_distance.png
+ This shows the purity and ploidy
+ solution space and can be used to pick
+ alternative solutions
+
+
+
+ The copy number profile saved in the
+ [samplename]_copynumber.txt is
+ a tab delimited file in text format. Within
+ this file there is a line for each segment
+ in the tumour genome. Each segment will have
+ either one or two copy number states:
+
+
+
+ If there is one state that line
+ represents the clonal copy number
+ (i.e. all tumour cells have this state)
+
+
+ If there are two states that line
+ represents subclonal copy number
+ (i.e. there are two populations of
+ cells, each with a different state)
+
+
+
+ A copy number state consists of a major and
+ a minor allele and their frequencies, which
+ together add give the total copy number for
+ that segment and an estimate fraction of
+ tumour cells that carry each allele.
+
+
+ The following columns are available in the
+ Battenberg output:
+
+
+
+
+
+
+
+
+
+
+
+ chr
+
+ The chromosome of the segment
+
+
+
+ startpos
+
+ Start position on the chromosome
+
+
+
+ endpos
+
+ End position on the chromosome
+
+
+
+ BAF
+
+ The B-allele frequency of the
+ segment
+
+
+
+ pval
+
+ P-value that is obtained when
+ testing whether this segment
+ should be represented by one or
+ two states. A low p-value will
+ result in the fitting of a
+ second copy number state
+
+
+
+ LogR
+
+ The log ratio of normalised
+ tumour coverage versus its
+ matched normal sequencing sample
+
+
+
+ ntot
+
+ An internal total copy number
+ value used to determine the
+ priority of solutions. NOTE:
+ This is not the total copy
+ number of this segment!
+
+
+
+ nMaj1_A
+
+ The major allele copy number of
+ state 1 from solution A
+
+
+
+ nMin1_A
+
+ The minor allele copy number of
+ state 1 from solution A
+
+
+
+ frac1_A
+
+ Fraction of tumour cells
+ carrying state 1 in solution A
+
+
+
+ nMaj2_A
+
+ The major allele copy number of
+ state 2 from solution A. This
+ value can be NA
+
+
+
+ nMin2_A
+
+ The minor allele copy number of
+ state 2 from solution A. This
+ value can be NA
+
+
+
+ frac2_A
+
+ Fraction of tumour cells
+ carrying state 2 in solution A.
+ This value can be NA
+
+
+
+ SDfrac_A
+
+ Standard deviation on the BAF of
+ SNPs in this segment, can be
+ used as a measure of uncertainty
+
+
+
+ SDfrac_A_BS
+
+ Bootstrapped standard deviation
+
+
+
+ frac1_A_0.025
+
+ Associated 95% confidence
+ interval of the bootstrap
+ measure of uncertainty
+
+
+
+
+
+ Followed by possible equivalent solutions B
+ to F with the same columns as defined above
+ for solution A (due to the way a profile is
+ fit Battenberg can generate a series of
+ equivalent solutions that are reported
+ separately in the output).
+
+
+
+
+ Plots for QC
+
+
+ It also produces a number plots that show
+ the raw data and are useful for QC (and
+ their raw data files denoted by *.tab)
+
+
+
+ [samplename].tumour.png and
+ [samplename].germline.png
+ show the raw BAF and logR
+
+
+ [samplename]_coverage.png
+ contains coverage divided by the mean
+ coverage of both tumour and normal
+
+
+ [samplename]_alleleratio.png
+ shows BAF*logR, a rough approximation of
+ what the data looks like shortly before
+ copy number calling
+
+
+
+
+
+ Intermediate figures
+
+
+ Finally, a range of plots show intermediate
+ steps and can occasionally be useful
+
+
+
+ [samplename]_chr*_heterozygousData.png
+ shows reconstructed haplotype blocks in
+ the characteristic Battenberg cake
+ pattern
+
+
+ [samplename]_RAFseg_chr*.png
+ and
+ [samplename]_segment_chr*.png
+ contains segmentation data for step 1
+ and step 2 respectively
+
+
+ [samplename]_nonroundedprofile.png
+ shows the copy number profile without
+ rounding to integers
+
+
+ [samplename]_copynumberprofile.png
+ shows the copy number profile with
+ (including subclonal copy number)
+ rounding to integers
+
+
+
+
+
+
+ Advice for including structural variant
+ breakpoints
+
+
+ Battenberg can take prior breakpoints, from
+ structural variants (SVs) for example, as input.
+ SV breakpoints are typically much more precise
+ and a pair of SVs can be closer together then
+ what typically can be obtained from a BAF or
+ coverage track. It is therefore adventageous to
+ include prior breakpoints in a Battenberg run.
+ However, including too many (as in 100s)
+ incorrect breakpoints can have adverse effects
+ by allowing many small segments to be affected
+ by noise where there isn’t any signal and
+ increasing the runtime of the pipeline. It is
+ therefore advised to
+ filter prior breakpoints from SVs such that
+ the genome is slightly oversegmented.
+ Finally, some SV types, such as inversions, do
+ not constitute a change in copy number and
+ therefore also add breakpoints that should not
+ be considered. It is therefore also advised to
+ filter breakpoints from SVs that do not
+ cause a change in copynumber, such as
+ inversions. Please note that the chromosome names in the
+ SV file do not include the
+ “chr” prefix.
+
+
+
+
+ Building a release
+
+
+ In RStudio: In the Build tab, click Check
+ Package
+
+
+ Then open the NAMESPACE file and
+ edit:
+
+
S3method ( plot ,haplotype.data )
+
to:
+
export ( plot_haplotype_data )
+
+
+
+ hg38 for Beagle5
+
+
+ Modified original code to derive the input vcf
+ for Beagle5 and hg38:
+
+
#!/bin/bash
#
# READ_ME file (08 Dec 2015)
#
@@ -376,8 +809,11 @@ hg38 for Beagle5
-
Run R code to generate loci, allele and gc_content files:
-
##########################################################################
+
+ Run R code to generate loci, allele and
+ gc_content files:
+
+ ##########################################################################
## set working directory to where the vcf files are located
setwd("./")
##########################################################################
@@ -464,7 +900,7 @@ hg38 for Beagle5hg38 for Beagle5 hg38 for Beagle5 hg38 for Beagle5
-
-
Example run
-
-
To run using Beagle5, simply parametrise the same way you would run under impute2. It should be back compatible, so you can run impute2 by setting usebeagle=FALSE. And it uses the same input files needed for the pipeline, i.e. 1000G loci/alleles + ref panel + prob loci + imputeinfo file etc.
-
The map plink files for Beagle can be downloaded from: http://bochet.gcc.biostat.washington.edu/beagle/genetic_maps/
-
BEAGLEJAR <- "$PATHTOBEAGLEFILES/beagle.24Aug19.3e8.jar"
-BEAGLEREF.template <- "$PATHTOBEAGLEFILES/chrCHROMNAME.1kg.phase3.v5a.b37.bref3"
-BEAGLEPLINK.template <- "$PATHTOBEAGLEFILES/plink.chrCHROMNAME.GRCh37.map"
+
-
-
-
-
-
-
-
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
Site built with pkgdown 2.1.2.
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
-
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
diff --git a/docs/pkgdown.css b/docs/pkgdown.css
index 80ea5b83..bf151880 100644
--- a/docs/pkgdown.css
+++ b/docs/pkgdown.css
@@ -13,82 +13,83 @@
*
*/
-html, body {
- height: 100%;
+html,
+body {
+ height: 100%;
}
body {
- position: relative;
+ position: relative;
}
body > .container {
- display: flex;
- height: 100%;
- flex-direction: column;
+ display: flex;
+ height: 100%;
+ flex-direction: column;
}
body > .container .row {
- flex: 1 0 auto;
+ flex: 1 0 auto;
}
footer {
- margin-top: 45px;
- padding: 35px 0 36px;
- border-top: 1px solid #e5e5e5;
- color: #666;
- display: flex;
- flex-shrink: 0;
+ margin-top: 45px;
+ padding: 35px 0 36px;
+ border-top: 1px solid #e5e5e5;
+ color: #666;
+ display: flex;
+ flex-shrink: 0;
}
footer p {
- margin-bottom: 0;
+ margin-bottom: 0;
}
footer div {
- flex: 1;
+ flex: 1;
}
footer .pkgdown {
- text-align: right;
+ text-align: right;
}
footer p {
- margin-bottom: 0;
+ margin-bottom: 0;
}
img.icon {
- float: right;
+ float: right;
}
/* Ensure in-page images don't run outside their container */
.contents img {
- max-width: 100%;
- height: auto;
+ max-width: 100%;
+ height: auto;
}
/* Fix bug in bootstrap (only seen in firefox) */
summary {
- display: list-item;
+ display: list-item;
}
/* Typographic tweaking ---------------------------------*/
.contents .page-header {
- margin-top: calc(-60px + 1em);
+ margin-top: calc(-60px + 1em);
}
dd {
- margin-left: 3em;
+ margin-left: 3em;
}
/* Section anchors ---------------------------------*/
a.anchor {
- display: none;
- margin-left: 5px;
- width: 20px;
- height: 20px;
+ display: none;
+ margin-left: 5px;
+ width: 20px;
+ height: 20px;
- background-image: url(./link.svg);
- background-repeat: no-repeat;
- background-size: 20px 20px;
- background-position: center center;
+ background-image: url(./link.svg);
+ background-repeat: no-repeat;
+ background-size: 20px 20px;
+ background-position: center center;
}
h1:hover .anchor,
@@ -97,288 +98,336 @@ h3:hover .anchor,
h4:hover .anchor,
h5:hover .anchor,
h6:hover .anchor {
- display: inline-block;
+ display: inline-block;
}
/* Fixes for fixed navbar --------------------------*/
-.contents h1, .contents h2, .contents h3, .contents h4 {
- padding-top: 60px;
- margin-top: -40px;
+.contents h1,
+.contents h2,
+.contents h3,
+.contents h4 {
+ padding-top: 60px;
+ margin-top: -40px;
}
/* Navbar submenu --------------------------*/
.dropdown-submenu {
- position: relative;
+ position: relative;
}
-.dropdown-submenu>.dropdown-menu {
- top: 0;
- left: 100%;
- margin-top: -6px;
- margin-left: -1px;
- border-radius: 0 6px 6px 6px;
+.dropdown-submenu > .dropdown-menu {
+ top: 0;
+ left: 100%;
+ margin-top: -6px;
+ margin-left: -1px;
+ border-radius: 0 6px 6px 6px;
}
-.dropdown-submenu:hover>.dropdown-menu {
- display: block;
+.dropdown-submenu:hover > .dropdown-menu {
+ display: block;
}
-.dropdown-submenu>a:after {
- display: block;
- content: " ";
- float: right;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- border-width: 5px 0 5px 5px;
- border-left-color: #cccccc;
- margin-top: 5px;
- margin-right: -10px;
+.dropdown-submenu > a:after {
+ display: block;
+ content: " ";
+ float: right;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ border-width: 5px 0 5px 5px;
+ border-left-color: #cccccc;
+ margin-top: 5px;
+ margin-right: -10px;
}
-.dropdown-submenu:hover>a:after {
- border-left-color: #ffffff;
+.dropdown-submenu:hover > a:after {
+ border-left-color: #ffffff;
}
.dropdown-submenu.pull-left {
- float: none;
+ float: none;
}
-.dropdown-submenu.pull-left>.dropdown-menu {
- left: -100%;
- margin-left: 10px;
- border-radius: 6px 0 6px 6px;
+.dropdown-submenu.pull-left > .dropdown-menu {
+ left: -100%;
+ margin-left: 10px;
+ border-radius: 6px 0 6px 6px;
}
/* Sidebar --------------------------*/
#pkgdown-sidebar {
- margin-top: 30px;
- position: -webkit-sticky;
- position: sticky;
- top: 70px;
+ margin-top: 30px;
+ position: -webkit-sticky;
+ position: sticky;
+ top: 70px;
}
#pkgdown-sidebar h2 {
- font-size: 1.5em;
- margin-top: 1em;
+ font-size: 1.5em;
+ margin-top: 1em;
}
#pkgdown-sidebar h2:first-child {
- margin-top: 0;
+ margin-top: 0;
}
#pkgdown-sidebar .list-unstyled li {
- margin-bottom: 0.5em;
+ margin-bottom: 0.5em;
}
/* bootstrap-toc tweaks ------------------------------------------------------*/
/* All levels of nav */
-nav[data-toggle='toc'] .nav > li > a {
- padding: 4px 20px 4px 6px;
- font-size: 1.5rem;
- font-weight: 400;
- color: inherit;
+nav[data-toggle="toc"] .nav > li > a {
+ padding: 4px 20px 4px 6px;
+ font-size: 1.5rem;
+ font-weight: 400;
+ color: inherit;
}
-nav[data-toggle='toc'] .nav > li > a:hover,
-nav[data-toggle='toc'] .nav > li > a:focus {
- padding-left: 5px;
- color: inherit;
- border-left: 1px solid #878787;
+nav[data-toggle="toc"] .nav > li > a:hover,
+nav[data-toggle="toc"] .nav > li > a:focus {
+ padding-left: 5px;
+ color: inherit;
+ border-left: 1px solid #878787;
}
-nav[data-toggle='toc'] .nav > .active > a,
-nav[data-toggle='toc'] .nav > .active:hover > a,
-nav[data-toggle='toc'] .nav > .active:focus > a {
- padding-left: 5px;
- font-size: 1.5rem;
- font-weight: 400;
- color: inherit;
- border-left: 2px solid #878787;
+nav[data-toggle="toc"] .nav > .active > a,
+nav[data-toggle="toc"] .nav > .active:hover > a,
+nav[data-toggle="toc"] .nav > .active:focus > a {
+ padding-left: 5px;
+ font-size: 1.5rem;
+ font-weight: 400;
+ color: inherit;
+ border-left: 2px solid #878787;
}
/* Nav: second level (shown on .active) */
-nav[data-toggle='toc'] .nav .nav {
- display: none; /* Hide by default, but at >768px, show it */
- padding-bottom: 10px;
+nav[data-toggle="toc"] .nav .nav {
+ display: none; /* Hide by default, but at >768px, show it */
+ padding-bottom: 10px;
}
-nav[data-toggle='toc'] .nav .nav > li > a {
- padding-left: 16px;
- font-size: 1.35rem;
+nav[data-toggle="toc"] .nav .nav > li > a {
+ padding-left: 16px;
+ font-size: 1.35rem;
}
-nav[data-toggle='toc'] .nav .nav > li > a:hover,
-nav[data-toggle='toc'] .nav .nav > li > a:focus {
- padding-left: 15px;
+nav[data-toggle="toc"] .nav .nav > li > a:hover,
+nav[data-toggle="toc"] .nav .nav > li > a:focus {
+ padding-left: 15px;
}
-nav[data-toggle='toc'] .nav .nav > .active > a,
-nav[data-toggle='toc'] .nav .nav > .active:hover > a,
-nav[data-toggle='toc'] .nav .nav > .active:focus > a {
- padding-left: 15px;
- font-weight: 500;
- font-size: 1.35rem;
+nav[data-toggle="toc"] .nav .nav > .active > a,
+nav[data-toggle="toc"] .nav .nav > .active:hover > a,
+nav[data-toggle="toc"] .nav .nav > .active:focus > a {
+ padding-left: 15px;
+ font-weight: 500;
+ font-size: 1.35rem;
}
/* orcid ------------------------------------------------------------------- */
.orcid {
- font-size: 16px;
- color: #A6CE39;
- /* margins are required by official ORCID trademark and display guidelines */
- margin-left:4px;
- margin-right:4px;
- vertical-align: middle;
+ font-size: 16px;
+ color: #a6ce39;
+ /* margins are required by official ORCID trademark and display guidelines */
+ margin-left: 4px;
+ margin-right: 4px;
+ vertical-align: middle;
}
/* Reference index & topics ----------------------------------------------- */
-.ref-index th {font-weight: normal;}
+.ref-index th {
+ font-weight: normal;
+}
-.ref-index td {vertical-align: top; min-width: 100px}
-.ref-index .icon {width: 40px;}
-.ref-index .alias {width: 40%;}
-.ref-index-icons .alias {width: calc(40% - 40px);}
-.ref-index .title {width: 60%;}
+.ref-index td {
+ vertical-align: top;
+ min-width: 100px;
+}
+.ref-index .icon {
+ width: 40px;
+}
+.ref-index .alias {
+ width: 40%;
+}
+.ref-index-icons .alias {
+ width: calc(40% - 40px);
+}
+.ref-index .title {
+ width: 60%;
+}
-.ref-arguments th {text-align: right; padding-right: 10px;}
-.ref-arguments th, .ref-arguments td {vertical-align: top; min-width: 100px}
-.ref-arguments .name {width: 20%;}
-.ref-arguments .desc {width: 80%;}
+.ref-arguments th {
+ text-align: right;
+ padding-right: 10px;
+}
+.ref-arguments th,
+.ref-arguments td {
+ vertical-align: top;
+ min-width: 100px;
+}
+.ref-arguments .name {
+ width: 20%;
+}
+.ref-arguments .desc {
+ width: 80%;
+}
/* Nice scrolling for wide elements --------------------------------------- */
table {
- display: block;
- overflow: auto;
+ display: block;
+ overflow: auto;
}
/* Syntax highlighting ---------------------------------------------------- */
-pre, code, pre code {
- background-color: #f8f8f8;
- color: #333;
+pre,
+code,
+pre code {
+ background-color: #f8f8f8;
+ color: #333;
}
-pre, pre code {
- white-space: pre-wrap;
- word-break: break-all;
- overflow-wrap: break-word;
+pre,
+pre code {
+ white-space: pre-wrap;
+ word-break: break-all;
+ overflow-wrap: break-word;
}
pre {
- border: 1px solid #eee;
+ border: 1px solid #eee;
}
-pre .img, pre .r-plt {
- margin: 5px 0;
+pre .img,
+pre .r-plt {
+ margin: 5px 0;
}
-pre .img img, pre .r-plt img {
- background-color: #fff;
+pre .img img,
+pre .r-plt img {
+ background-color: #fff;
}
-code a, pre a {
- color: #375f84;
+code a,
+pre a {
+ color: #375f84;
}
a.sourceLine:hover {
- text-decoration: none;
+ text-decoration: none;
}
-.fl {color: #1514b5;}
-.fu {color: #000000;} /* function */
-.ch,.st {color: #036a07;} /* string */
-.kw {color: #264D66;} /* keyword */
-.co {color: #888888;} /* comment */
+.fl {
+ color: #1514b5;
+}
+.fu {
+ color: #000000;
+} /* function */
+.ch,
+.st {
+ color: #036a07;
+} /* string */
+.kw {
+ color: #264d66;
+} /* keyword */
+.co {
+ color: #888888;
+} /* comment */
-.error {font-weight: bolder;}
-.warning {font-weight: bolder;}
+.error {
+ font-weight: bolder;
+}
+.warning {
+ font-weight: bolder;
+}
/* Clipboard --------------------------*/
.hasCopyButton {
- position: relative;
+ position: relative;
}
.btn-copy-ex {
- position: absolute;
- right: 0;
- top: 0;
- visibility: hidden;
+ position: absolute;
+ right: 0;
+ top: 0;
+ visibility: hidden;
}
.hasCopyButton:hover button.btn-copy-ex {
- visibility: visible;
+ visibility: visible;
}
/* headroom.js ------------------------ */
.headroom {
- will-change: transform;
- transition: transform 200ms linear;
+ will-change: transform;
+ transition: transform 200ms linear;
}
.headroom--pinned {
- transform: translateY(0%);
+ transform: translateY(0%);
}
.headroom--unpinned {
- transform: translateY(-100%);
+ transform: translateY(-100%);
}
/* mark.js ----------------------------*/
mark {
- background-color: rgba(255, 255, 51, 0.5);
- border-bottom: 2px solid rgba(255, 153, 51, 0.3);
- padding: 1px;
+ background-color: rgba(255, 255, 51, 0.5);
+ border-bottom: 2px solid rgba(255, 153, 51, 0.3);
+ padding: 1px;
}
/* vertical spacing after htmlwidgets */
.html-widget {
- margin-bottom: 10px;
+ margin-bottom: 10px;
}
/* fontawesome ------------------------ */
.fab {
- font-family: "Font Awesome 5 Brands" !important;
+ font-family: "Font Awesome 5 Brands", sans-serif !important;
}
/* don't display links in code chunks when printing */
/* source: https://stackoverflow.com/a/10781533 */
@media print {
- code a:link:after, code a:visited:after {
- content: "";
- }
+ code a:link:after,
+ code a:visited:after {
+ content: "";
+ }
}
/* Section anchors ---------------------------------
Added in pandoc 2.11: https://github.com/jgm/pandoc-templates/commit/9904bf71
*/
-div.csl-bib-body { }
div.csl-entry {
- clear: both;
+ clear: both;
}
.hanging-indent div.csl-entry {
- margin-left:2em;
- text-indent:-2em;
+ margin-left: 2em;
+ text-indent: -2em;
}
div.csl-left-margin {
- min-width:2em;
- float:left;
+ min-width: 2em;
+ float: left;
}
div.csl-right-inline {
- margin-left:2em;
- padding-left:1em;
+ margin-left: 2em;
+ padding-left: 1em;
}
div.csl-indent {
- margin-left: 2em;
+ margin-left: 2em;
}
diff --git a/docs/pkgdown.js b/docs/pkgdown.js
index 6f0eee40..6bca31af 100644
--- a/docs/pkgdown.js
+++ b/docs/pkgdown.js
@@ -1,108 +1,92 @@
/* http://gregfranko.com/blog/jquery-best-practices/ */
-(function($) {
- $(function() {
+(($) => {
+ $(() => {
+ $(".navbar-fixed-top").headroom();
- $('.navbar-fixed-top').headroom();
+ const updateBodyPadding = () => {
+ $("body").css("padding-top", $(".navbar").height() + 10);
+ };
- $('body').css('padding-top', $('.navbar').height() + 10);
- $(window).resize(function(){
- $('body').css('padding-top', $('.navbar').height() + 10);
- });
+ updateBodyPadding();
+ $(window).resize(updateBodyPadding);
$('[data-toggle="tooltip"]').tooltip();
- var cur_path = paths(location.pathname);
- var links = $("#navbar ul li a");
- var max_length = -1;
- var pos = -1;
- for (var i = 0; i < links.length; i++) {
- if (links[i].getAttribute("href") === "#")
- continue;
- // Ignore external links
- if (links[i].host !== location.host)
- continue;
+ const cur_path = paths(location.pathname);
+ const links = $("#navbar ul li a");
+ let max_length = -1;
+ let pos = -1;
+
+ links.each((i, link) => {
+ if (link.getAttribute("href") === "#") return;
+ if (link.host !== location.host) return;
- var nav_path = paths(links[i].pathname);
+ const nav_path = paths(link.pathname);
+ const length = prefix_length(nav_path, cur_path);
- var length = prefix_length(nav_path, cur_path);
if (length > max_length) {
max_length = length;
pos = i;
}
- }
+ });
- // Add class to parent , and enclosing if in dropdown
if (pos >= 0) {
- var menu_anchor = $(links[pos]);
+ const menu_anchor = $(links[pos]);
menu_anchor.parent().addClass("active");
menu_anchor.closest("li.dropdown").addClass("active");
}
});
- function paths(pathname) {
- var pieces = pathname.split("/");
+ const paths = (pathname) => {
+ const pieces = pathname.split("/");
pieces.shift(); // always starts with /
- var end = pieces[pieces.length - 1];
- if (end === "index.html" || end === "")
- pieces.pop();
- return(pieces);
- }
+ const end = pieces[pieces.length - 1];
+ if (end === "index.html" || end === "") pieces.pop();
+ return pieces;
+ };
- // Returns -1 if not found
- function prefix_length(needle, haystack) {
- if (needle.length > haystack.length)
- return(-1);
+ const prefix_length = (needle, haystack) => {
+ if (needle.length > haystack.length) return -1;
+ if (haystack.length === 0) return needle.length === 0 ? 0 : -1;
- // Special case for length-0 haystack, since for loop won't run
- if (haystack.length === 0) {
- return(needle.length === 0 ? 0 : -1);
+ for (let i = 0; i < haystack.length; i++) {
+ if (needle[i] !== haystack[i]) return i;
}
-
- for (var i = 0; i < haystack.length; i++) {
- if (needle[i] != haystack[i])
- return(i);
- }
-
- return(haystack.length);
- }
+ return haystack.length;
+ };
/* Clipboard --------------------------*/
- function changeTooltipMessage(element, msg) {
- var tooltipOriginalTitle=element.getAttribute('data-original-title');
- element.setAttribute('data-original-title', msg);
- $(element).tooltip('show');
- element.setAttribute('data-original-title', tooltipOriginalTitle);
- }
-
- if(ClipboardJS.isSupported()) {
- $(document).ready(function() {
- var copyButton = " ";
+ const changeTooltipMessage = (element, msg) => {
+ const tooltipOriginalTitle = element.getAttribute("data-original-title");
+ element.setAttribute("data-original-title", msg);
+ $(element).tooltip("show");
+ element.setAttribute("data-original-title", tooltipOriginalTitle);
+ };
- $("div.sourceCode").addClass("hasCopyButton");
+ if (window.ClipboardJS && ClipboardJS.isSupported()) {
+ $(document).ready(() => {
+ const copyButton =
+ " ";
- // Insert copy buttons:
- $(copyButton).prependTo(".hasCopyButton");
+ $("div.sourceCode").addClass("hasCopyButton").prepend(copyButton);
- // Initialize tooltips:
- $('.btn-copy-ex').tooltip({container: 'body'});
+ $(".btn-copy-ex").tooltip({ container: "body" });
- // Initialize clipboard:
- var clipboardBtnCopies = new ClipboardJS('[data-clipboard-copy]', {
- text: function(trigger) {
- return trigger.parentNode.textContent.replace(/\n#>[^\n]*/g, "");
- }
+ const clipboardBtnCopies = new ClipboardJS("[data-clipboard-copy]", {
+ text: (trigger) =>
+ trigger.parentNode.textContent.replace(/\n#>[^\n]*/g, ""),
});
- clipboardBtnCopies.on('success', function(e) {
- changeTooltipMessage(e.trigger, 'Copied!');
+ clipboardBtnCopies.on("success", (e) => {
+ changeTooltipMessage(e.trigger, "Copied!");
e.clearSelection();
});
- clipboardBtnCopies.on('error', function() {
- changeTooltipMessage(e.trigger,'Press Ctrl+C or Command+C to copy');
+ clipboardBtnCopies.on("error", (e) => {
+ changeTooltipMessage(e.trigger, "Press Ctrl+C or Command+C to copy");
});
});
}
-})(window.jQuery || window.$)
+})(window.jQuery || window.$);
diff --git a/docs/reference/battenberg.html b/docs/reference/battenberg.html
index f29b189d..05940127 100644
--- a/docs/reference/battenberg.html
+++ b/docs/reference/battenberg.html
@@ -1,72 +1,213 @@
-
-Run the Battenberg pipeline — battenberg • Battenberg
-
-
-
-
-
-
-
-
-
Run the Battenberg pipeline
-
-
-
-
battenberg (
+
+
+
+
+
+
+
+
+ Run the Battenberg pipeline — battenberg • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Run the Battenberg pipeline
+
+
+
+
+
battenberg (
analysis = "paired" ,
samplename ,
normalname ,
@@ -95,7 +236,7 @@ Run the Battenberg pipeline
min_rho = 0.1 ,
max_rho = 1 ,
min_goodness = 0.63 ,
- uninformative_BAF_threshold = 0.51 ,
+ uninformative_baf_threshold = 0.51 ,
min_normal_depth = 10 ,
min_base_qual = 20 ,
min_map_qual = 35 ,
@@ -108,8 +249,8 @@ Run the Battenberg pipeline
externalhaplotypefile = NA ,
usebeagle = FALSE ,
beaglejar = NA ,
- beagleref.template = NA ,
- beagleplink.template = NA ,
+ beagleref_template = NA ,
+ beagleplink_template = NA ,
beaglemaxmem = 10 ,
beaglenthreads = 1 ,
beaglewindow = 40 ,
@@ -120,292 +261,954 @@ Run the Battenberg pipeline
multisample_maxlag = 90 ,
segmentation_gamma_multisample = 5 ,
snp6_reference_info_file = NA ,
- apt.probeset.genotype.exe = "apt-probeset-genotype" ,
- apt.probeset.summarize.exe = "apt-probeset-summarize" ,
- norm.geno.clust.exe = "normalize_affy_geno_cluster.pl" ,
+ apt_probeset_genotype_exe = "apt-probeset-genotype" ,
+ apt_probeset_summarize_exe = "apt-probeset-summarize" ,
+ norm_geno_clust_exe = "normalize_affy_geno_cluster.pl" ,
birdseed_report_file = "birdseed.report.txt" ,
- heterozygousFilter = "none" ,
+ heterozygous_filter = "none" ,
prior_breakpoints_file = NULL ,
genomebuild = "hg19" ,
chrom_coord_file = NULL ,
enhanced_grid_search = F
-)
-
-
-
-
Arguments
-
-
-
analysis
-The mode of Battenberg copy number analysis to be undertaken: 'paired' for tumour-normal pair, 'cell_line' for Cell line tumour-only and 'germline' for germline CNV of normal sample (Default: 'paired')
-
-
-samplename
-Sample identifier (tumour or germline), this is used as a prefix for the output files. If allele counts are supplied separately, they are expected to have this identifier as prefix.
-
-
-normalname
-Matched normal identifier, this is used as a prefix for the output files. If allele counts are supplied separately, they are expected to have this identifier as prefix.
-
-
-sample_data_file
-A BAM or CEL file for the sample
-
-
-normal_data_file
-A BAM or CEL file for the normal-pair (paired analysis)
-
-
-imputeinfofile
-Full path to a Battenberg impute info file with pointers to Impute2 reference data
-
-
-g1000prefix
-Full prefix path to 1000 Genomes SNP loci data, as part of the Battenberg reference data
-
-
-problemloci
-Full path to a problem loci file that contains SNP loci that should be filtered out
-
-
-gccorrectprefix
-Full prefix path to GC content files, as part of the Battenberg reference data, not required for SNP6 data (Default: NULL)
-
-
-repliccorrectprefix
-Full prefix path to replication timing files, as part of the Battenberg reference data, not required for SNP6 data (Default: NULL)
-
-
-g1000allelesprefix
-Full prefix path to 1000 Genomes SNP alleles data, as part of the Battenberg reference data, not required for SNP6 data (Default: NA)
-
-
-ismale
-A boolean set to TRUE if the donor is male, set to FALSE if female, not required for SNP6 data (Default: NA)
-
-
-data_type
-String that contains either wgs or snp6 depending on the supplied input data (Default: wgs)
-
-
-impute_exe
-Pointer to the Impute2 executable (Default: impute2, i.e. expected in $PATH)
-
-
-allelecounter_exe
-Pointer to the alleleCounter executable (Default: alleleCounter, i.e. expected in $PATH)
-
-
-nthreads
-The number of concurrent processes to use while running the Battenberg pipeline (Default: 8)
-
-
-platform_gamma
-Platform scaling factor, suggestions are set to 1 for wgs and to 0.55 for snp6 (Default: 1)
-
-
-phasing_gamma
-Gamma parameter used when correcting phasing mistakes (Default: 1)
-
-
-segmentation_gamma
-The gamma parameter controls the size of the penalty of starting a new segment during segmentation. It is therefore the key parameter for controlling the number of segments (Default: 10)
-
-
-segmentation_kmin
-Kmin represents the minimum number of probes/SNPs that a segment should consist of (Default: 3)
-
-
-phasing_kmin
-Kmin used when correcting for phasing mistakes (Default: 3)
-
-
-clonality_dist_metric
-Distance metric to use when choosing purity/ploidy combinations (Default: 0)
-
-
-ascat_dist_metric
-Distance metric to use when choosing purity/ploidy combinations (Default: 1)
-
-
-min_ploidy
-Minimum ploidy to be considered (Default: 1.6)
-
-
-max_ploidy
-Maximum ploidy to be considered (Default: 4.8)
-
-
-min_rho
-Minimum purity to be considered (Default: 0.1)
-
-
-max_rho
-Maximum purity to be considered (Default: 1.0)
-
-
-min_goodness
-Minimum goodness of fit required for a purity/ploidy combination to be accepted as a solution (Default: 0.63)
-
-
-uninformative_BAF_threshold
-The threshold beyond which BAF becomes uninformative (Default: 0.51)
-
-
-min_normal_depth
-Minimum depth required in the matched normal for a SNP to be considered as part of the wgs analysis (Default: 10)
-
-
-min_base_qual
-Minimum base quality required for a read to be counted when allele counting (Default: 20)
-
-
-min_map_qual
-Minimum mapping quality required for a read to be counted when allele counting (Default: 35)
-
-
-max_allowed_state
-The maximum CN state allowed (Default 250)
-
-
-cn_upper_limit
-Maximum number of copy number that can be called (Default 1000)
-
-
-calc_seg_baf_option
-Sets way to calculate BAF per segment: 1=mean, 2=median, 3=ifelse median==0 | 1, mean, median (Default (paired): 3, cell_line & germline: 1)
-
-
-skip_allele_counting
-Provide TRUE when allele counting can be skipped (i.e. its already done) (Default: FALSE)
-
-
-skip_preprocessing
-Provide TRUE when preprocessing is already complete (Default: FALSE)
-
-
-skip_phasing
-Provide TRUE when phasing is already complete (Default: FALSE)
-
-
-externalhaplotypefile
-Vcf containing externally obtained haplotype blocks (Default: NA)
-
-
-usebeagle
-Should use beagle5 instead of impute2 Default: FALSE
-
-
-beaglejar
-Full path to Beagle java jar file Default: NA
-
-
-beagleref.template
-Full path template to Beagle reference files where the chromosome is replaced by 'CHROMNAME' Default: NA
-
-
-beagleplink.template
-Full path template to Beagle plink files where the chromosome is replaced by 'CHROMNAME' Default: NA
-
-
-beaglemaxmem
-Integer Beagle max heap size in Gb Default: 10
-
-
-beaglenthreads
-Integer number of threads used by beagle5 Default:1
-
-
-beaglewindow
-Integer size of the genomic window for beagle5 (cM) Default:40
-
-
-beagleoverlap
-Integer size of the overlap between windows beagle5 Default:4
-
-
-javajre
-Path to the Java JRE executable, only required for haplotype reconstruction with Beagle (default java, i.e. in $PATH)
-
-
-write_battenberg_phasing
-Write the Battenberg phasing results as vcf to disk, e.g. for multisample cases (Default: TRUE)
-
-
-multisample_relative_weight_balanced
-Relative weight to give to haplotype info from a sample without allelic imbalance in the region (Default: 0.25)
-
-
-multisample_maxlag
-Maximal number of upstream SNPs used in the multisample haplotyping to inform the haplotype at another SNP (Default: 100)
-
-
-segmentation_gamma_multisample
-The gamma parameter controls the size of the penalty of starting a new segment during mutlisample segmentation. It is the key parameter for controlling the number of segments (Default: 10)
-
-
-snp6_reference_info_file
-Reference files for the SNP6 pipeline only (Default: NA)
-
-
-apt.probeset.genotype.exe
-Helper tool for extracting data from CEL files, SNP6 pipeline only (Default: apt-probeset-genotype)
-
-
-apt.probeset.summarize.exe
-Helper tool for extracting data from CEL files, SNP6 pipeline only (Default: apt-probeset-summarize)
-
-
-norm.geno.clust.exe
-Helper tool for extracting data from CEL files, SNP6 pipeline only (Default: normalize_affy_geno_cluster.pl)
-
-
-birdseed_report_file
-Sex inference output file, SNP6 pipeline only (Default: birdseed.report.txt)
-
-
-heterozygousFilter
-Legacy option to set a heterozygous SNP filter, SNP6 pipeline only (Default: "none")
-
-
-prior_breakpoints_file
-A two column file with prior breakpoints to be used during segmentation (Default: NULL)
-
-
-genomebuild
-Genome build upon which the 1000G SNP coordinates were obtained (Default: hg19; options: "hg19" or "hg38")
-
-
-enhanced_grid_search
-Should use multi-start, parallelized and multi-approach grid search (Default: FALSE)
-
-
-
-
Author
-
sd11, jdemeul, Naser Ansari-Pour, Julio Cesar Cortes Rios
-
-
-
-
-
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
-
-
-
-
-
-
-
-
+)
+
+
+
+
+
Arguments
+
+
+
+ analysis
+
+
+
+ The mode of Battenberg copy number analysis
+ to be undertaken: 'paired' for tumour-normal
+ pair, 'cell_line' for Cell line tumour-only
+ and 'germline' for germline CNV of normal
+ sample (Default: 'paired')
+
+
+
+
+ samplename
+
+
+
+ Sample identifier (tumour or germline), this
+ is used as a prefix for the output files. If
+ allele counts are supplied separately, they
+ are expected to have this identifier as
+ prefix.
+
+
+
+
+ normalname
+
+
+
+ Matched normal identifier, this is used as a
+ prefix for the output files. If allele
+ counts are supplied separately, they are
+ expected to have this identifier as prefix.
+
+
+
+
+ sample_data_file
+
+ A BAM or CEL file for the sample
+
+
+ normal_data_file
+
+
+
+ A BAM or CEL file for the normal-pair
+ (paired analysis)
+
+
+
+
+ imputeinfofile
+
+
+
+ Full path to a Battenberg impute info file
+ with pointers to Impute2 reference data
+
+
+
+
+ g1000prefix
+
+
+
+ Full prefix path to 1000 Genomes SNP loci
+ data, as part of the Battenberg reference
+ data
+
+
+
+
+ problemloci
+
+
+
+ Full path to a problem loci file that
+ contains SNP loci that should be filtered
+ out
+
+
+
+
+ gccorrectprefix
+
+
+
+ Full prefix path to GC content files, as
+ part of the Battenberg reference data, not
+ required for SNP6 data (Default: NULL)
+
+
+
+
+ repliccorrectprefix
+
+
+
+ Full prefix path to replication timing
+ files, as part of the Battenberg reference
+ data, not required for SNP6 data (Default:
+ NULL)
+
+
+
+
+ g1000allelesprefix
+
+
+
+ Full prefix path to 1000 Genomes SNP alleles
+ data, as part of the Battenberg reference
+ data, not required for SNP6 data (Default:
+ NA)
+
+
+
+
+ ismale
+
+
+
+ A boolean set to TRUE if the donor is male,
+ set to FALSE if female, not required for
+ SNP6 data (Default: NA)
+
+
+
+
+ data_type
+
+
+
+ String that contains either wgs or snp6
+ depending on the supplied input data
+ (Default: wgs)
+
+
+
+
+ impute_exe
+
+
+
+ Pointer to the Impute2 executable (Default:
+ impute2, i.e. expected in $PATH)
+
+
+
+
+ allelecounter_exe
+
+
+
+ Pointer to the alleleCounter executable
+ (Default: alleleCounter, i.e. expected in
+ $PATH)
+
+
+
+
+ nthreads
+
+
+
+ The number of concurrent processes to use
+ while running the Battenberg pipeline
+ (Default: 8)
+
+
+
+
+ platform_gamma
+
+
+
+ Platform scaling factor, suggestions are set
+ to 1 for wgs and to 0.55 for snp6 (Default:
+ 1)
+
+
+
+
+ phasing_gamma
+
+
+
+ Gamma parameter used when correcting phasing
+ mistakes (Default: 1)
+
+
+
+
+ segmentation_gamma
+
+
+
+ The gamma parameter controls the size of the
+ penalty of starting a new segment during
+ segmentation. It is therefore the key
+ parameter for controlling the number of
+ segments (Default: 10)
+
+
+
+
+ segmentation_kmin
+
+
+
+ Kmin represents the minimum number of
+ probes/SNPs that a segment should consist of
+ (Default: 3)
+
+
+
+
+ phasing_kmin
+
+
+
+ Kmin used when correcting for phasing
+ mistakes (Default: 3)
+
+
+
+
+ clonality_dist_metric
+
+
+
+ Distance metric to use when choosing
+ purity/ploidy combinations (Default: 0)
+
+
+
+
+ ascat_dist_metric
+
+
+
+ Distance metric to use when choosing
+ purity/ploidy combinations (Default: 1)
+
+
+
+
+ min_ploidy
+
+
+
+ Minimum ploidy to be considered (Default:
+ 1.6)
+
+
+
+
+ max_ploidy
+
+
+
+ Maximum ploidy to be considered (Default:
+ 4.8)
+
+
+
+
+ min_rho
+
+
+
+ Minimum purity to be considered (Default:
+ 0.1)
+
+
+
+
+ max_rho
+
+
+
+ Maximum purity to be considered (Default:
+ 1.0)
+
+
+
+
+ min_goodness
+
+
+
+ Minimum goodness of fit required for a
+ purity/ploidy combination to be accepted as
+ a solution (Default: 0.63)
+
+
+
+
+ uninformative_baf_threshold
+
+
+
+ The threshold beyond which BAF becomes
+ uninformative (Default: 0.51)
+
+
+
+
+ min_normal_depth
+
+
+
+ Minimum depth required in the matched normal
+ for a SNP to be considered as part of the
+ wgs analysis (Default: 10)
+
+
+
+
+ min_base_qual
+
+
+
+ Minimum base quality required for a read to
+ be counted when allele counting (Default:
+ 20)
+
+
+
+
+ min_map_qual
+
+
+
+ Minimum mapping quality required for a read
+ to be counted when allele counting (Default:
+ 35)
+
+
+
+
+ max_allowed_state
+
+
+
+ The maximum CN state allowed (Default 250)
+
+
+
+
+ cn_upper_limit
+
+
+
+ Maximum number of copy number that can be
+ called (Default 1000)
+
+
+
+
+ calc_seg_baf_option
+
+
+
+ Sets way to calculate BAF per segment:
+ 1=mean, 2=median, 3=ifelse median==0 | 1,
+ mean, median (Default (paired): 3, cell_line
+ & germline: 1)
+
+
+
+
+ skip_allele_counting
+
+
+
+ Provide TRUE when allele counting can be
+ skipped (i.e. its already done) (Default:
+ FALSE)
+
+
+
+
+ skip_preprocessing
+
+
+
+ Provide TRUE when preprocessing is already
+ complete (Default: FALSE)
+
+
+
+
+ skip_phasing
+
+
+
+ Provide TRUE when phasing is already
+ complete (Default: FALSE)
+
+
+
+
+ externalhaplotypefile
+
+
+
+ Vcf containing externally obtained haplotype
+ blocks (Default: NA)
+
+
+
+
+ usebeagle
+
+
+
+ Should use beagle5 instead of impute2
+ Default: FALSE
+
+
+
+
+ beaglejar
+
+
+
+ Full path to Beagle java jar file Default:
+ NA
+
+
+
+
+ beagleref_template
+
+
+
+ Full path template to Beagle reference files
+ where the chromosome is replaced by
+ 'CHROMNAME' Default: NA
+
+
+
+
+ beagleplink_template
+
+
+
+ Full path template to Beagle plink files
+ where the chromosome is replaced by
+ 'CHROMNAME' Default: NA
+
+
+
+
+ beaglemaxmem
+
+
+
+ Integer Beagle max heap size in Gb Default:
+ 10
+
+
+
+
+ beaglenthreads
+
+
+
+ Integer number of threads used by beagle5
+ Default:1
+
+
+
+
+ beaglewindow
+
+
+
+ Integer size of the genomic window for
+ beagle5 (cM) Default:40
+
+
+
+
+ beagleoverlap
+
+
+
+ Integer size of the overlap between windows
+ beagle5 Default:4
+
+
+
+
+ javajre
+
+
+
+ Path to the Java JRE executable, only
+ required for haplotype reconstruction with
+ Beagle (default java, i.e. in $PATH)
+
+
+
+
+ write_battenberg_phasing
+
+
+
+ Write the Battenberg phasing results as vcf
+ to disk, e.g. for multisample cases
+ (Default: TRUE)
+
+
+
+
+ multisample_relative_weight_balanced
+
+
+
+ Relative weight to give to haplotype info
+ from a sample without allelic imbalance in
+ the region (Default: 0.25)
+
+
+
+
+ multisample_maxlag
+
+
+
+ Maximal number of upstream SNPs used in the
+ multisample haplotyping to inform the
+ haplotype at another SNP (Default: 100)
+
+
+
+
+ segmentation_gamma_multisample
+
+
+
+ The gamma parameter controls the size of the
+ penalty of starting a new segment during
+ mutlisample segmentation. It is the key
+ parameter for controlling the number of
+ segments (Default: 10)
+
+
+
+
+ snp6_reference_info_file
+
+
+
+ Reference files for the SNP6 pipeline only
+ (Default: NA)
+
+
+
+
+ apt_probeset_genotype_exe
+
+
+
+ Helper tool for extracting data from CEL
+ files, SNP6 pipeline only (Default:
+ apt-probeset-genotype)
+
+
+
+
+ apt_probeset_summarize_exe
+
+
+
+ Helper tool for extracting data from CEL
+ files, SNP6 pipeline only (Default:
+ apt-probeset-summarize)
+
+
+
+
+ norm_geno_clust_exe
+
+
+
+ Helper tool for extracting data from CEL
+ files, SNP6 pipeline only (Default:
+ normalize_affy_geno_cluster.pl)
+
+
+
+
+ birdseed_report_file
+
+
+
+ Sex inference output file, SNP6 pipeline
+ only (Default: birdseed.report.txt)
+
+
+
+
+ heterozygous_filter
+
+
+
+ Legacy option to set a heterozygous SNP
+ filter, SNP6 pipeline only (Default: "none")
+
+
+
+
+ prior_breakpoints_file
+
+
+
+ A two column file with prior breakpoints to
+ be used during segmentation (Default: NULL)
+
+
+
+
+ genomebuild
+
+
+
+ Genome build upon which the 1000G SNP
+ coordinates were obtained (Default: hg19;
+ options: "hg19" or "hg38")
+
+
+
+
+ enhanced_grid_search
+
+
+
+ Should use multi-start, parallelized and
+ multi-approach grid search (Default: FALSE)
+
+
+
+
+
+
Author
+
+ sd11, jdemeul, Naser Ansari-Pour, Julio Cesar Cortes
+ Rios
+
+
+
+
+
+
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
+
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
+
diff --git a/docs/reference/calculate_solution_fast.html b/docs/reference/calculate_solution_fast.html
index 56dfe697..8e61110a 100644
--- a/docs/reference/calculate_solution_fast.html
+++ b/docs/reference/calculate_solution_fast.html
@@ -1,72 +1,223 @@
-
-Fast solution calculation (vectorized and optimized) — calculate_solution_fast • Battenberg
+
+
+
+
+
+
+
+
+
+ Fast solution calculation (vectorized and optimized) —
+ calculate_solution_fast • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+ Articles
-
+
+
+
-
-
-
-
-
-
-
-
-
Fast solution calculation (vectorized and optimized)
-
-
-
-
calculate_solution_fast (
+
+
+
calculate_solution_fast (
psi ,
rho ,
s_b ,
@@ -74,41 +225,50 @@ Fast solution calculation (vectorized and optimized)
s_length ,
total_length ,
gamma ,
- min.ploidy ,
- max.ploidy ,
- min.rho ,
- max.rho ,
- min.goodness ,
+ min_ploidy ,
+ max_ploidy ,
+ min_rho ,
+ max_rho ,
+ min_goodness ,
distance_value ,
TheoretMaxdist ,
minimise ,
allow100percent ,
skip_zero_check = FALSE
-)
-
-
-
-
-
-
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
-
-
-
-
-
+
)
+
+
+
+
+
-
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
+
diff --git a/docs/reference/callSubclones.html b/docs/reference/callSubclones.html
index 40924df6..3a8bf3b3 100644
--- a/docs/reference/callSubclones.html
+++ b/docs/reference/callSubclones.html
@@ -1,208 +1,561 @@
-
-Fit subclonal copy number — callSubclones • Battenberg
+
+
+
+
+
+ Fit subclonal copy number — callSubclones • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
This function fits a subclonal copy number profile where a clonal profile is unlikely.
-It goes over each segment of a clonal copy number profile and does a simple t-test. If the
-test is significant it is unlikely that the data can be explained by a single copy number
-state. We therefore fit a second state, i.e. there are two cellular populations with each
-a different state: Subclonal copy number.
-
-
-
-
callSubclones (
- sample.name ,
- baf.segmented.file ,
- logr.file ,
- rho.psi.file ,
- output.file ,
- output.figures.prefix ,
- output.gw.figures.prefix ,
+a different state: Subclonal copy number."
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This function fits a subclonal copy number profile
+ where a clonal profile is unlikely. It goes over
+ each segment of a clonal copy number profile and
+ does a simple t-test. If the test is significant it
+ is unlikely that the data can be explained by a
+ single copy number state. We therefore fit a second
+ state, i.e. there are two cellular populations with
+ each a different state: Subclonal copy number.
+
+
+
+
+
+
callSubclones (
+ sample_name ,
+ baf_segmented_file ,
+ logr_file ,
+ rho_psi_file ,
+ output_file ,
+ output_figures_prefix ,
+ output_gw_figures_prefix ,
chr_names ,
masking_output_file ,
max_allowed_state = 250 ,
cn_upper_limit = 1000 ,
prior_breakpoints_file = NULL ,
gamma = 1 ,
- segmentation.gamma = NA ,
+ segmentation_gamma = NA ,
siglevel = 0.05 ,
maxdist = 0.01 ,
noperms = 1000 ,
seed = as.integer ( Sys.time ( ) ) ,
calc_seg_baf_option = 3
-)
-
-
-
-
Arguments
-
-
-
sample.name
-Name of the sample, used in figures
-
-
-baf.segmented.file
-String that points to a file with segmented BAF output
-
-
-logr.file
-String that points to the raw LogR file to be used in the subclonal copy number figures
-
-
-rho.psi.file
-String pointing to the rho_and_psi file generated by fit.copy.number
-
-
-output.file
-Filename of the file where the final copy number fit will be written to
-
-
-output.figures.prefix
-Prefix of the filenames for the chromosome specific copy number figures
-
-
-output.gw.figures.prefix
-Prefix of the filenames for the genome wide copy number figures
-
-
-chr_names
-Vector of allowed chromosome names
-
-
-masking_output_file
-Filename of where the masking details need to be written. Masking is performed to remove very high copy number state segments
-
-
-max_allowed_state
-The maximum CN state allowed (Default 250)
-
-
-cn_upper_limit
-The maximum CN that can be called (Default 1000)
-
-
-prior_breakpoints_file
-A two column file with prior breakpoints, possibly from structural variants. This file must contain two columns: chromosome and position. These are used when making the figures
-
-
-gamma
-Technology specific scaling parameter for LogR (Default 1)
-
-
-segmentation.gamma
-Legacy parameter that is no longer used (Default NA)
-
-
-siglevel
-Threshold under which a p-value becomes significant. When it is significant a second copy number state will be fitted (Default 0.05)
-
-
-maxdist
-Slack in BAF space to allow a segment to be off it's optimum before becoming significant. A segment becomes significant very quickly when a breakpoint is missed, this parameter alleviates the effect (Default 0.01)
-
-
-noperms
-The number of permutations to be run when bootstrapping the confidence intervals on the copy number state of each segment (Default 1000)
-
-
-seed
-Seed to set when performing bootstrapping (Default: Current time)
-
-
-calc_seg_baf_option
-Various options to recalculate the BAF of a segment. Options are: 1 - median, 2 - mean, 3 - ifelse median==0|1, mean, median. (Default: 3)
-
-
-
-
-
-
-
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
-
-
-
-
-
-
-
-
+)
+
+
+
+
+
Arguments
+
+
+
+ sample_name
+
+ Name of the sample, used in figures
+
+
+ baf_segmented_file
+
+
+
+ String that points to a file with segmented
+ BAF output
+
+
+
+
+ logr_file
+
+
+
+ String that points to the raw LogR file to
+ be used in the subclonal copy number figures
+
+
+
+
+ rho_psi_file
+
+
+
+ String pointing to the rho_and_psi file
+ generated by fit_copy_number
+
+
+
+
+ output_file
+
+
+
+ Filename of the file where the final copy
+ number fit will be written to
+
+
+
+
+ output_figures_prefix
+
+
+
+ Prefix of the filenames for the chromosome
+ specific copy number figures
+
+
+
+
+ output_gw_figures_prefix
+
+
+
+ Prefix of the filenames for the genome wide
+ copy number figures
+
+
+
+
+ chr_names
+
+ Vector of allowed chromosome names
+
+
+ masking_output_file
+
+
+
+ Filename of where the masking details need
+ to be written. Masking is performed to
+ remove very high copy number state segments
+
+
+
+
+ max_allowed_state
+
+
+
+ The maximum CN state allowed (Default 250)
+
+
+
+
+ cn_upper_limit
+
+
+
+ The maximum CN that can be called (Default
+ 1000)
+
+
+
+
+ prior_breakpoints_file
+
+
+
+ A two column file with prior breakpoints,
+ possibly from structural variants. This file
+ must contain two columns: chromosome and
+ position. These are used when making the
+ figures
+
+
+
+
+ gamma
+
+
+
+ Technology specific scaling parameter for
+ LogR (Default 1)
+
+
+
+
+ segmentation_gamma
+
+
+
+ Legacy parameter that is no longer used
+ (Default NA)
+
+
+
+
+ siglevel
+
+
+
+ Threshold under which a p-value becomes
+ significant. When it is significant a second
+ copy number state will be fitted (Default
+ 0.05)
+
+
+
+
+ maxdist
+
+
+
+ Slack in BAF space to allow a segment to be
+ off it's optimum before becoming
+ significant. A segment becomes significant
+ very quickly when a breakpoint is missed,
+ this parameter alleviates the effect
+ (Default 0.01)
+
+
+
+
+ noperms
+
+
+
+ The number of permutations to be run when
+ bootstrapping the confidence intervals on
+ the copy number state of each segment
+ (Default 1000)
+
+
+
+
+ seed
+
+
+
+ Seed to set when performing bootstrapping
+ (Default: Current time)
+
+
+
+
+ calc_seg_baf_option
+
+
+
+ Various options to recalculate the BAF of a
+ segment. Options are: 1 - median, 2 - mean,
+ 3 - ifelse median==0|1, mean, median.
+ (Default: 3)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
+
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
+
diff --git a/docs/reference/cel2baf.logr.html b/docs/reference/cel2baf.logr.html
index 2a678e81..1f22b307 100644
--- a/docs/reference/cel2baf.logr.html
+++ b/docs/reference/cel2baf.logr.html
@@ -1,146 +1,377 @@
-
-Transform cel files into BAF and LogR — cel2baf.logr • Battenberg
+
+
+
+
+
+
+ Transform cel files into BAF and LogR — cel2baf_logr • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
This function takes a cel file from a tumour and a matched normal and
-extracts the BAF and LogR, which is saved into a single file. The gc.correct
-function can read that file and transforms it into separate BAF and LogR files that
-both Battenberg and ASCAT can use.
-
-
-
-
cel2baf.logr (
+both Battenberg and ASCAT can use."
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This function takes a cel file from a tumour and a
+ matched normal and extracts the BAF and LogR, which
+ is saved into a single file. The
+ gc_correct function can read that file
+ and transforms it into separate BAF and LogR files
+ that both Battenberg and ASCAT can use.
+
+
+
+
+
+
cel2baf_logr (
normal_cel_file ,
tumour_cel_file ,
output_file ,
snp6_reference_info_file ,
- apt.probeset.genotype.exe = "apt-probeset-genotype" ,
- apt.probeset.summarize.exe = "apt-probeset-summarize" ,
- norm.geno.clust.exe = "normalize_affy_geno_cluster.pl"
-)
-
-
-
-
Arguments
-
-
-
normal_cel_file
-String that points to the cel file containing the matched normal data
-
-
-tumour_cel_file
-String that points to the cel file containing the tumour data
-
-
-output_file
-String where the BAF and LogR should be written
-
-
-snp6_reference_info_file
-String to the SNP6 reference info file that comes with Battenberg SNP6
-
-
-apt.probeset.genotype.exe
-Path to the apt.probeset.genotype executable (Default $PATH)
-
-
-apt.probeset.summarize.exe
-Path to the apt.probeset.summarize executable (Default $PATH)
-
-
-norm.geno.clust.exe
-Path to the normalize_affy_geno_cluster.pl script (Default $PATH)
-
-
-
-
-
-
-
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
-
-
-
-
-
-
-
-
+ apt_probeset_genotype_exe = "apt-probeset-genotype" ,
+ apt_probeset_summarize_exe = "apt-probeset-summarize" ,
+ norm_geno_clust_exe = "normalize_affy_geno_cluster.pl"
+)
+
+
+
+
+
Arguments
+
+
+
+ normal_cel_file
+
+
+
+ String that points to the cel file
+ containing the matched normal data
+
+
+
+
+ tumour_cel_file
+
+
+
+ String that points to the cel file
+ containing the tumour data
+
+
+
+
+ output_file
+
+
+
+ String where the BAF and LogR should be
+ written
+
+
+
+
+ snp6_reference_info_file
+
+
+
+ String to the SNP6 reference info file that
+ comes with Battenberg SNP6
+
+
+
+
+ apt_probeset_genotype_exe
+
+
+
+ Path to the apt.probeset.genotype executable
+ (Default $PATH)
+
+
+
+
+ apt_probeset_summarize_exe
+
+
+
+ Path to the apt.probeset.summarize
+ executable (Default $PATH)
+
+
+
+
+ norm_geno_clust_exe
+
+
+
+ Path to the normalize_affy_geno_cluster.pl
+ script (Default $PATH)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
+
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
+
diff --git a/docs/reference/cell_line_baf_logR.html b/docs/reference/cell_line_baf_logR.html
index 04571b20..fbb30333 100644
--- a/docs/reference/cell_line_baf_logR.html
+++ b/docs/reference/cell_line_baf_logR.html
@@ -1,118 +1,311 @@
-
-Obtain BAF and LogR from the Cell line (tumour only) allele counts — cell_line_baf_logR • Battenberg
+
+
+
+
+
+
+
+
+
+ Obtain BAF and LogR from the Cell line (tumour only) allele counts —
+ cell_line_baf_logR • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
+
diff --git a/docs/reference/check.imputeinfofile.html b/docs/reference/check.imputeinfofile.html
index 4628a684..6625cca3 100644
--- a/docs/reference/check.imputeinfofile.html
+++ b/docs/reference/check.imputeinfofile.html
@@ -1,108 +1,271 @@
-
-Check impute info file consistency — check.imputeinfofile • Battenberg
+
+
+
+
+
+
+
+
+
+ Check impute info file consistency — check_imputeinfofile •
+ Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/reference/combine.baf.files.html b/docs/reference/combine.baf.files.html
index a85f1358..a6b0257a 100644
--- a/docs/reference/combine.baf.files.html
+++ b/docs/reference/combine.baf.files.html
@@ -1,120 +1,320 @@
-
-Combines all separate BAF files per chromosome into a single file — combine.baf.files • Battenberg
-
-
-
-
-
-
-
-
-
Combines all separate BAF files per chromosome into a single file
-
-
-
-
combine.baf.files ( inputfile.prefix , inputfile.postfix , outputfile , chr_names )
-
-
-
-
Arguments
-
-
-
inputfile.prefix
-Prefix of the input files until the chromosome number. The chromosome number will be added internally
-
-
-inputfile.postfix
-Postfix of the input files from the chromosome number
-
-
-outputfile
-Full path to where the output will be written
-
-
-chr_names
-A list of allowed chromosome names.
-
-
-
-
-
-
-
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Combines all separate BAF files per chromosome into a single file —
+ concatenate_baf_files • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Combines all separate BAF files per chromosome into
+ a single file
+
+
+
+
+
+
concatenate_baf_files ( inputfile.prefix , inputfile.postfix , outputfile , chr_names )
+
+
+
+
+
Arguments
+
+
+
+ inputfile.prefix
+
+
+
+ Prefix of the input files until the
+ chromosome number. The chromosome number
+ will be added internally
+
+
+
+
+ inputfile.postfix
+
+
+
+ Postfix of the input files from the
+ chromosome number
+
+
+
+
+ outputfile
+
+
+
+ Full path to where the output will be
+ written
+
+
+
+
+ chr_names
+
+ A list of allowed chromosome names.
+
+
+
+
+
+
+
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
+
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
+
diff --git a/docs/reference/combine.impute.output.html b/docs/reference/combine.impute.output.html
index 114f019c..daa013c8 100644
--- a/docs/reference/combine.impute.output.html
+++ b/docs/reference/combine.impute.output.html
@@ -1,135 +1,350 @@
-
-Concatenate the impute output generated for each of the regions. — combine.impute.output • Battenberg
-
-
-
-
-
-
-
-
-
This function assembles the impute output generated.
-
-
-
-
combine.impute.output (
+
+
+
+
+
+
+
+
+
+ Concatenate the impute output generated for each of the regions. —
+ combine_impute_output • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This function assembles the impute output generated.
+
+
+
+
+
+
combine_impute_output (
inputfile.prefix ,
outputfile ,
- is.male ,
+ is_male ,
imputeinfofile ,
region.size = 5000000 ,
chrom = NA
-)
-
-
-
-
Arguments
-
-
-
inputfile.prefix
-Prefix of the input files (this is typically the outputfile.prefix option supplied when calling run.impute).
-
-
-outputfile
-Where to store the output.
-
-
-is.male
-Boolean describing whether the sample is male (TRUE) or female (FALSE).
-
-
-imputeinfofile
-Path to the imputeinfofile on disk.
-
-
-region.size
-An integer describing the region size to be used by impute (optional).
-
-
-chrom
-The name of a chromosome on which this function should run (names are used, supply X as 'X').
-
-
-
-
-
-
-
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
-
-
-
-
-
-
-
-
+)
+
+
+
+
+
Arguments
+
+
+
+ inputfile.prefix
+
+
+
+ Prefix of the input files (this is typically
+ the outputfile_prefix option supplied when
+ calling run_impute).
+
+
+
+
+ outputfile
+
+ Where to store the output.
+
+
+ is_male
+
+
+
+ Boolean describing whether the sample is
+ male (TRUE) or female (FALSE).
+
+
+
+
+ imputeinfofile
+
+ Path to the imputeinfofile on disk.
+
+
+ region.size
+
+
+
+ An integer describing the region size to be
+ used by impute (optional).
+
+
+
+
+ chrom
+
+
+
+ The name of a chromosome on which this
+ function should run (names are used, supply
+ X as 'X').
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
+
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
+
diff --git a/docs/reference/convert.impute.input.to.beagle.input.html b/docs/reference/convert.impute.input.to.beagle.input.html
index 0d8455df..7c05708e 100644
--- a/docs/reference/convert.impute.input.to.beagle.input.html
+++ b/docs/reference/convert.impute.input.to.beagle.input.html
@@ -1,112 +1,283 @@
-
-Converts impute input to a beagle input — convert.impute.input.to.beagle.input • Battenberg
+
+
+
+
+
+
+
+
+
+ Converts impute input to a beagle input —
+ convert_impute_input_to_beagle_input • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
This function takes the impute input file and converts it to a beagle input
-
-
-
-
convert.impute.input.to.beagle.input ( imputeinput , chrom )
-
-
-
-
Arguments
-
-
-
imputeinput
-path to the impute input file
-
-
-chrom
-chromosome
-
-
-
-
Author
-
maxime.tarabichi
-
-
-
-
-
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
+
diff --git a/docs/reference/find_centroid_of_global_minima.html b/docs/reference/find_centroid_of_global_minima.html
index 5cbf63dd..9af410f6 100644
--- a/docs/reference/find_centroid_of_global_minima.html
+++ b/docs/reference/find_centroid_of_global_minima.html
@@ -90,7 +90,7 @@ This function is an alternative procedure for finding the optimum (psi, rho)
siglevel_LogR ,
maxdist_LogR ,
allow100percent ,
- uninformative_BAF_threshold ,
+ uninformative_baf_threshold ,
read_depth
)
@@ -159,7 +159,7 @@ Arguments
Boolean whether to allow for a 100"%" cellularity solution
-uninformative_BAF_threshold
+uninformative_baf_threshold
The threshold above which BAF becomes uninformative
diff --git a/docs/reference/fit.copy.number.html b/docs/reference/fit.copy.number.html
index b1fcf397..f959b265 100644
--- a/docs/reference/fit.copy.number.html
+++ b/docs/reference/fit.copy.number.html
@@ -1,91 +1,236 @@
-
-Fit copy number — fit.copy.number • Battenberg
+
+
+
+
+
+ Fit copy number — fit_copy_number • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
Function that will fit a clonal copy number profile to segmented data. It first
-matches the raw LogR with the segmented BAF to create segmented LogR. Then ASCAT
-is run to obtain a clonal copy number profile. Beyond logRsegmented it produces
-the rho_and_psi file and the cellularity_ploidy file.
-
-
-
-
fit.copy.number (
+the rho_and_psi file and the cellularity_ploidy file."
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Function that will fit a clonal copy number profile
+ to segmented data. It first matches the raw LogR
+ with the segmented BAF to create segmented LogR.
+ Then ASCAT is run to obtain a clonal copy number
+ profile. Beyond logRsegmented it produces the
+ rho_and_psi file and the cellularity_ploidy file.
+
+
+
+
+
+
fit_copy_number (
samplename ,
- outputfile.prefix ,
- inputfile.baf.segmented ,
- inputfile.baf ,
- inputfile.logr ,
+ outputfile_prefix ,
+ inputfile_baf_segmented ,
+ inputfile_baf ,
+ inputfile_logr ,
dist_choice ,
ascat_dist_choice ,
- min.ploidy = 1.6 ,
- max.ploidy = 4.8 ,
- min.rho = 0.1 ,
- max.rho = 1 ,
- min.goodness = 63 ,
- uninformative_BAF_threshold = 0.51 ,
+ min_ploidy = 1.6 ,
+ max_ploidy = 4.8 ,
+ min_rho = 0.1 ,
+ max_rho = 1 ,
+ min_goodness = 63 ,
+ uninformative_baf_threshold = 0.51 ,
gamma_param = 1 ,
use_preset_rho_psi = F ,
preset_rho = NA ,
@@ -94,115 +239,312 @@ Fit copy number
analysis = "paired" ,
nthreads ,
enhanced_grid_search = F
-)
-
-
-
-
Arguments
-
-
-
samplename
-Samplename used to name the segmented logr output file
-
-
-outputfile.prefix
-Prefix used for all output file names, except logRsegmented
-
-
-inputfile.baf.segmented
-Filename that points to the BAF segmented data
-
-
-inputfile.baf
-Filename that points to the raw BAF data
-
-
-inputfile.logr
-Filename that points to the raw LogR data
-
-
-dist_choice
-The distance metric that is used internally to rank clonal copy number solutions
-
-
-ascat_dist_choice
-The distance metric used to obtain an initial cellularity and ploidy estimate
-
-
-min.ploidy
-The minimum ploidy to consider (Default 1.6)
-
-
-max.ploidy
-The maximum ploidy to consider (Default 4.8)
-
-
-min.rho
-The minimum cellularity to consider (Default 0.1)
-
-
-max.rho
-The maximum cellularity to consider (Default 1.0)
-
-
-min.goodness
-The minimum goodness of fit for a solution to have to be considered (Default 63)
-
-
-uninformative_BAF_threshold
-The threshold beyond which BAF becomes uninformative (Default 0.51)
-
-
-gamma_param
-Technology parameter, compaction of Log R profiles. Expected decrease in case of deletion in diploid sample, 100 "%" aberrant cells; 1 in ideal case, 0.55 of Illumina 109K arrays (Default 1)
-
-
-use_preset_rho_psi
-Boolean whether to use user specified rho and psi values (Default F)
-
-
-preset_rho
-A user specified rho to fit a copy number profile to (Default NA)
-
-
-preset_psi
-A user specified psi to fit a copy number profile to (Default NA)
-
-
-read_depth
-Legacy parameter that is no longer used (Default 30)
-
-
-analysis
-A String representing the type of analysis to be run, this determines whether the distance figure is produced (Default paired)
-
-
-
-
-
-
-
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
-
-
-
-
-
-
-
-
+)
+
+
+
+
+
Arguments
+
+
+
+ samplename
+
+
+
+ Samplename used to name the segmented logr
+ output file
+
+
+
+
+ outputfile_prefix
+
+
+
+ Prefix used for all output file names,
+ except logRsegmented
+
+
+
+
+ inputfile_baf_segmented
+
+
+
+ Filename that points to the BAF segmented
+ data
+
+
+
+
+ inputfile_baf
+
+
+ Filename that points to the raw BAF data
+
+
+
+ inputfile_logr
+
+
+ Filename that points to the raw LogR data
+
+
+
+ dist_choice
+
+
+
+ The distance metric that is used internally
+ to rank clonal copy number solutions
+
+
+
+
+ ascat_dist_choice
+
+
+
+ The distance metric used to obtain an
+ initial cellularity and ploidy estimate
+
+
+
+
+ min_ploidy
+
+
+
+ The minimum ploidy to consider (Default 1.6)
+
+
+
+
+ max_ploidy
+
+
+
+ The maximum ploidy to consider (Default 4.8)
+
+
+
+
+ min_rho
+
+
+
+ The minimum cellularity to consider (Default
+ 0.1)
+
+
+
+
+ max_rho
+
+
+
+ The maximum cellularity to consider (Default
+ 1.0)
+
+
+
+
+ min_goodness
+
+
+
+ The minimum goodness of fit for a solution
+ to have to be considered (Default 63)
+
+
+
+
+ uninformative_baf_threshold
+
+
+
+ The threshold beyond which BAF becomes
+ uninformative (Default 0.51)
+
+
+
+
+ gamma_param
+
+
+
+ Technology parameter, compaction of Log R
+ profiles. Expected decrease in case of
+ deletion in diploid sample, 100 "%" aberrant
+ cells; 1 in ideal case, 0.55 of Illumina
+ 109K arrays (Default 1)
+
+
+
+
+ use_preset_rho_psi
+
+
+
+ Boolean whether to use user specified rho
+ and psi values (Default F)
+
+
+
+
+ preset_rho
+
+
+
+ A user specified rho to fit a copy number
+ profile to (Default NA)
+
+
+
+
+ preset_psi
+
+
+
+ A user specified psi to fit a copy number
+ profile to (Default NA)
+
+
+
+
+ read_depth
+
+
+
+ Legacy parameter that is no longer used
+ (Default 30)
+
+
+
+
+ analysis
+
+
+
+ A String representing the type of analysis
+ to be run, this determines whether the
+ distance figure is produced (Default paired)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
+
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
+
diff --git a/docs/reference/gc.correct.html b/docs/reference/gc.correct.html
index 9a501c4f..cd564c03 100644
--- a/docs/reference/gc.correct.html
+++ b/docs/reference/gc.correct.html
@@ -1,78 +1,226 @@
-
-Correct the LogR estimates for GC content — gc.correct • Battenberg
+
+
+
+
+
+
+ Correct the LogR estimates for GC content — gc_correct • Battenberg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
This function performs GC correction of the LogR
-data. Sometimes a wave pattern is observed there
-that correlates with GC content. Internally it uses
-the ASCAT gc correction function.
-
-
-
-
gc.correct (
+the ASCAT gc correction function."
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This function performs GC correction of the LogR
+ data. Sometimes a wave pattern is observed there
+ that correlates with GC content. Internally it uses
+ the ASCAT gc correction function.
+
+
+
+
+
+
gc_correct (
samplename ,
infile.logr.baf ,
outfile.tumor.LogR ,
@@ -84,79 +232,194 @@ Correct the LogR estimates for GC content
chr_names ,
birdseed_report_file = "birdseed.report.txt" ,
genomebuild = "hg19"
-)
-
-
-
-
Arguments
-
-
-
samplename
-Name of the sample to be used to name columns
-
-
-infile.logr.baf
-String that points to the raw combined BAF and LogR file that is the result of cel2baf.logr
-
-
-outfile.tumor.LogR
-The filename of the file where the tumour LogR will be written
-
-
-outfile.tumor.BAF
-The filename of the file where the tumour BAF will be written
-
-
-outfile.normal.LogR
-The filename of the file where the normal LogR will be written
-
-
-outfile.normal.BAF
-The filename of the file where the normal BAF will be written
-
-
-outfile.probeBAF
-The filename of the file where the probe ids and their BAF will be saved
-
-
-snp6_reference_info_file
-String to the SNP6 reference info file that comes with Battenberg SNP6
-
-
-chr_names
-A vector of chromosome names that are to be used
-
-
-birdseed_report_file
-Name of the birdseed output file. This is a temp output file of one of the internally called functions of which the name cannot be defined. Don't change this parameter. (Default birdseed.report.txt)
-
-
-
-
-
-
-
-
-
-
-
Developed by David Wedge, Peter Van Loo, Naser Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas Demeulemeester.
-
-
-
-
-
-
-
-
-
-
-
-
-
+)
+
+
+
+
+
Arguments
+
+
+
+ samplename
+
+
+
+ Name of the sample to be used to name
+ columns
+
+
+
+
+ infile.logr.baf
+
+
+
+ String that points to the raw combined BAF
+ and LogR file that is the result of
+ cel2baf_logr
+
+
+
+
+ outfile.tumor.LogR
+
+
+
+ The filename of the file where the tumour
+ LogR will be written
+
+
+
+
+ outfile.tumor.BAF
+
+
+
+ The filename of the file where the tumour
+ BAF will be written
+
+
+
+
+ outfile.normal.LogR
+
+
+
+ The filename of the file where the normal
+ LogR will be written
+
+
+
+
+ outfile.normal.BAF
+
+
+
+ The filename of the file where the normal
+ BAF will be written
+
+
+
+
+ outfile.probeBAF
+
+
+
+ The filename of the file where the probe ids
+ and their BAF will be saved
+
+
+
+
+ snp6_reference_info_file
+
+
+
+ String to the SNP6 reference info file that
+ comes with Battenberg SNP6
+
+
+
+
+ chr_names
+
+
+
+ A vector of chromosome names that are to be
+ used
+
+
+
+
+ birdseed_report_file
+
+
+
+ Name of the birdseed output file. This is a
+ temp output file of one of the internally
+ called functions of which the name cannot be
+ defined. Don't change this parameter.
+ (Default birdseed.report.txt)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Developed by David Wedge, Peter Van Loo, Naser
+ Ansari-Pour, Stefan Dentro, Maxime Tarabichi, Jonas
+ Demeulemeester.
+
+
+
+
+
+
+ Site built with
+ pkgdown
+ 2.1.2.
+
+
+
+
+
+