view configureStrelkaWorkflow.pl @ 9:0e8e6011082b

Strelka Workflow 0.0.2
author mini
date Fri, 26 Sep 2014 15:51:35 +0200
parents 87568e5a7d4f
children
line wrap: on
line source

#!/usr/bin/env perl

=head1 LICENSE

Strelka Workflow Software
Copyright (c) 2009-2013 Illumina, Inc.

This software is provided under the terms and conditions of the
Illumina Open Source Software License 1.

You should have received a copy of the Illumina Open Source
Software License 1 along with this program. If not, see
<https://github.com/downloads/sequencing/licenses/>.

=head1 SYNOPSIS

configureStrelkaWorkflow.pl --tumor FILE --normal FILE --ref FILE --config FILE [options]

This script configures the strelka workflow for somatic variant
calling on matched tumor-normal BAM files. The configuration process
will produce an analysis makefile and directory structure. The
makefile can be used to run the analysis on a workstation or compute
cluster via make/qmake or other makefile compatible process.

=head1 ARGUMENTS

=over 4

=item --tumor FILE

Path to tumor sample BAM file (required)

=item --normal FILE

Path to normal sample BAM file (required)

=item --ref FILE

Path to reference genome fasta (required)

=item --config FILE

Strelka configuration file. Default config files can be found in
${STRELKA_INSTALL_ROOT}/etc/ for both ELAND and BWA alignments. (required)

=back

=head1 OPTIONS

=over 4

=item --output-dir DIRECTORY

Root of the analysis directory. This script will place all
configuration files in the analysis directory, and after configuration
all results and intermediate files will be written within the analysis
directory during a run. This directory must not already
exist. (default: ./strelkaAnalysis)

=back

=cut

use warnings FATAL => 'all';
use strict;

use Carp;
$SIG{__DIE__} = \&Carp::confess;

use Cwd qw(getcwd);
use File::Spec;
use Getopt::Long;
use Pod::Usage;

my $baseDir;
my $libDir;
BEGIN {
    my $thisDir=(File::Spec->splitpath($0))[1];
    $baseDir=$thisDir;#$baseDir=File::Spec->catdir($thisDir,File::Spec->updir());
    $libDir=File::Spec->catdir($baseDir,'lib');
}
use lib $libDir;
use Utils;

if(getAbsPath($baseDir)) {
    errorX("Can't resolve path for strelka_workflow install directory: '$baseDir'");
}
my $libexecDir=File::Spec->catdir($baseDir,'libexec');
#my $optDir=File::Spec->catdir($baseDir,'opt');


my $scriptName=(File::Spec->splitpath($0))[2];
my $argCount=scalar(@ARGV);
my $cmdline = join(' ',$0,@ARGV);


sub usage() { pod2usage(-verbose => 1,
                        -exitval => 2); }

#
# user configuration:
#

my ($tumorBam, $normalBam, $refFile, $configFile, $outDir);
my $help;

GetOptions( "tumor=s" => \$tumorBam,
            "normal=s" => \$normalBam,
            "ref=s" => \$refFile,
            "config=s" => \$configFile,
            "output-dir=s" => \$outDir,
            "help|h" => \$help) || usage();

usage() if($help);
usage() unless($argCount);


#
# Validate input conditions:
#

sub checkFileArg($$) {
   my ($file,$label) = @_;

   errorX("Must specify $label file") unless(defined($file)); #raise an error if file not defined
   checkFile($file,$label);
}

checkFileArg($tumorBam,"tumor BAM");
checkFileArg($normalBam,"normal BAM");
checkFileArg($refFile,"reference fasta");
checkFileArg($configFile,"configuration ini");

sub makeAbsoluteFilePaths(\$) {
    my ($filePathRef) = @_;

    my ($v,$fileDir,$fileName) = File::Spec->splitpath($$filePathRef);
    if(getAbsPath($fileDir)) {
        errorX("Can't resolve directory path for '$fileDir' from input file argument: '$$filePathRef'");
    }
    $$filePathRef = File::Spec->catfile($fileDir,$fileName);
}

makeAbsoluteFilePaths($tumorBam);
makeAbsoluteFilePaths($normalBam);
makeAbsoluteFilePaths($refFile);
makeAbsoluteFilePaths($configFile);

# also check for BAM index files:
sub checkBamIndex($) {
    my ($file) = @_;
    my $ifile = $file . ".bai";
    if(! -f $ifile) {
        errorX("Can't find index for BAM file '$file'");
    }
}

checkBamIndex($tumorBam);
checkBamIndex($normalBam);


sub checkFaIndex($) {
    my ($file) = @_;
    my $ifile = $file . ".fai";
    if(! -f $ifile) {
        errorX("Can't find index for fasta file '$file'");
    }
    # check that fai file isn't improperly formatted (a la the GATK bundle NCBI 37 fai files)
    open(my $FH,"< $ifile") || errorX("Can't open fai file '$ifile'");
    my $lineno=1;
    while(<$FH>) {
          chomp;
          my @F=split();
          if(scalar(@F) != 5) {
              errorX("Unexpected format for line number '$lineno' of fasta index file: '$ifile'\n\tRe-running fasta indexing may fix the issue. To do so, run: \"samtools faidx $file\"");
          }
          $lineno++;
    }
    close($FH);
}

checkFaIndex($refFile);


if(defined($outDir)) {
    if(getAbsPath($outDir)) {
        errorX("Can't resolve path for ouput directory: '$outDir'");
    }
} else {
    $outDir=File::Spec->catdir(Cwd::getcwd(),'strelkaAnalysis');
}

if(-e $outDir) {
    errorX("Output path already exists: '$outDir'");
}

if(getAbsPath($baseDir)) {
    errorX("Can't resolve path for strelka install directory: '$baseDir'");
}

		#my $samtoolsDir = File::Spec->catdir($optDir,'samtools');
checkDir($libexecDir,"strelka libexec");
		#checkDir($samtoolsDir,"samtools");

my $callScriptName = "callSomaticVariants.pl";
my $filterScriptName = "filterSomaticVariants.pl";
my $finishScriptName = "consolidateResults.pl";
my $callScript = File::Spec->catfile($libexecDir,$callScriptName);
my $filterScript = File::Spec->catfile($libexecDir,$filterScriptName);
my $finishScript = File::Spec->catfile($libexecDir,$finishScriptName);
my $countFasta = File::Spec->catfile($libexecDir,"countFastaBases");
		#my $samtoolsBin = File::Spec->catfile($samtoolsDir,"samtools");
checkFile($callScript,"somatic variant call script");
checkFile($filterScript,"somatic variant filter script");
checkFile($finishScript,"result consolidation script");
checkFile($countFasta,"fasta scanner");
#checkFile($samtoolsBin,"samtools");

#
# Configure bin runs:
#
checkMakeDir($outDir);

#
# Configure bin runs: open and validate config ini
#
my $config = parseConfigIni($configFile);

sub checkConfigKeys($) {
    my ($keyref) = @_;
    for (@$keyref) {
        errorX("Undefined configuration option: '$_'") unless(defined($config->{user}{$_}));
    }
}

# these are the keys we need at configuration time:
my @config_keys = qw(binSize);

# these are additional keys we will need to run the workflow:
# (note we don't check for (maxInputDepth,minTier2Mapq) for back compatibility with older config files)
my @workflow_keys = qw(
minTier1Mapq isWriteRealignedBam ssnvPrior sindelPrior ssnvNoise sindelNoise ssnvNoiseStrandBiasFrac
ssnvQuality_LowerBound sindelQuality_LowerBound isSkipDepthFilters depthFilterMultiple
snvMaxFilteredBasecallFrac snvMaxSpanningDeletionFrac indelMaxRefRepeat
indelMaxWindowFilteredBasecallFrac indelMaxIntHpolLength);

checkConfigKeys(\@config_keys);
checkConfigKeys(\@workflow_keys);


my $binSize = int($config->{user}{binSize});

$config->{derived}{configurationCmdline} = $cmdline;
$config->{derived}{normalBam} = $normalBam;
$config->{derived}{tumorBam} = $tumorBam;
$config->{derived}{refFile} = $refFile;
$config->{derived}{outDir} = $outDir;

#
# Configure bin runs: check for consistent chrom info between BAMs and reference
#
sub getBamChromInfo($) {
    my $file = shift;
    my $cmd = "samtools view -H $file |";
    open(my $FH,$cmd) || errorX("Can't open process $cmd");

    my %info;
    my $n=0;
    while(<$FH>) {
        next unless(/^\@SQ/);
        chomp;
        my @F = split(/\t/);
        scalar(@F) >= 3 || errorX("Unexpected bam header for file '$file'");

        my %h = ();
        foreach (@F) {
            my @vals = split(':');
            $h{$vals[0]} = $vals[1];
        }
        $F[1] = $h{'SN'};
        $F[2] = $h{'LN'};

        my $size = int($F[2]);
        ($size > 0) || errorX("Unexpected chromosome size '$size' in bam header for file '$file'");
        $info{$F[1]}{size} = $size;
        $info{$F[1]}{order} = $n;
        $n++;
    }
    close($FH) || errorX("Can't close process $cmd");
    return %info;
}


my %chromInfo = getBamChromInfo($normalBam);
my @chroms = sort { $chromInfo{$a}{order} <=> $chromInfo{$b}{order} } (keys(%chromInfo));

{
    #consistency check:
    my %tumorChromInfo = getBamChromInfo($tumorBam);
    for my $chrom (@chroms) {
        my $ln = $chromInfo{$chrom}{size};
        my $tln = $tumorChromInfo{$chrom}{size};
        my $order = $chromInfo{$chrom}{order};
        my $torder = $tumorChromInfo{$chrom}{order};
        unless(defined($tln) && ($tln==$ln) && ($torder==$order)) {
            errorX("Tumor and normal BAM file headers disagree on chromosome: '$chrom'");
        }
        delete $tumorChromInfo{$chrom};
    }
    for my $chrom (keys(%tumorChromInfo)) {
        errorX("Tumor and normal BAM file headers disagree on chromosome: '$chrom'");
    }
}


my %refChromInfo;
logX("Scanning reference genome");
{
    my $knownGenomeSize=0;
    my $cmd="$countFasta $refFile |";
    open(my $FFH,$cmd) || errorX("Failed to open process '$cmd'");

    while(<$FFH>) {
        chomp;
        my @F = split(/\t/);
        scalar(@F) == 4 || errorX("Unexpected value from '$cmd'");
        $knownGenomeSize += int($F[2]);
        $refChromInfo{$F[1]}{knownSize} = int($F[2]);
        $refChromInfo{$F[1]}{size} = int($F[3]);
    }
    close($FFH) || errorX("Failed to close process '$cmd'");

    #consistency check:
    for my $chrom (@chroms) {
        my $ln = $chromInfo{$chrom}{size};
        my $rln = $refChromInfo{$chrom}{size};
        unless(defined($rln) && ($rln==$ln)) {
            errorX("BAM headers and reference fasta disagree on chromosome: '$chrom'");
        }
        $config->{derived}{"chrom_${chrom}_size"} = $rln;
        $config->{derived}{"chrom_${chrom}_knownSize"} = $refChromInfo{$chrom}{knownSize};
    }
    $config->{derived}{chromOrder} = join("\t",@chroms);

    $config->{derived}{knownGenomeSize} = $knownGenomeSize;
}
logX("Scanning reference genome complete");



#
# Configure bin runs: create directory structure
#
my $resultsDir = File::Spec->catdir($outDir,'results');
checkMakeDir($resultsDir);
if($config->{user}{isWriteRealignedBam}) {
    my $bamDir = File::Spec->catdir($outDir,'realigned');
    checkMakeDir($bamDir);
}
my $chromRootDir = File::Spec->catdir($outDir,'chromosomes');
checkMakeDir($chromRootDir);
for my $chrom (@chroms) {
    my $chromDir = File::Spec->catdir($chromRootDir,$chrom);
    checkMakeDir($chromDir);

    my $chromRef = $chromInfo{$chrom};
    $chromRef->{dir} = $chromDir;
    $chromRef->{binList} = getBinList($chromRef->{size},$binSize);

    my $binRootDir = File::Spec->catdir($chromDir,'bins');
    checkMakeDir($binRootDir);

    for my $binId ( @{$chromRef->{binList}} ) {
        my $binDir = File::Spec->catdir($binRootDir,$binId);
        checkMakeDir($binDir);
    }
}



#
# write run config file:
#
my $runConfigFile;
{
    my $cstr = <<END;
;
; Strelka workflow configuration file
;
; This is an automatically generated file, you probably don't want to edit it. If starting a new run,
; input configuration templates (with comments) can be found in the Strelka etc/ directory.
;
END

    $cstr .= writeConfigIni($config);

    my $configDir = File::Spec->catdir($outDir,'config');
    checkMakeDir($configDir);
    $runConfigFile = File::Spec->catdir($configDir,'run.config.ini');
    open(my $FH,"> $runConfigFile") || errorX("Can't open file '$runConfigFile'");
    print $FH $cstr;
    close($FH);
}



#
# create makefile
#
my $makeFile = File::Spec->catfile($outDir,"Makefile");
open(my $MAKEFH, "> $makeFile") || errorX("Can't open file: '$makeFile'");

my $completeFile = "task.complete";

print $MAKEFH <<ENDE;
# This makefile was automatically generated by $scriptName
#
# Please do not edit.

script_dir := $libexecDir
call_script := \$(script_dir)/$callScriptName
filter_script := \$(script_dir)/$filterScriptName
finish_script := \$(script_dir)/$finishScriptName

config_file := $runConfigFile

analysis_dir := $outDir
results_dir := \$(analysis_dir)/results

ENDE

print $MAKEFH <<'ENDE';

complete_tag := task.complete

finish_task := $(analysis_dir)/$(complete_tag)

get_chrom_dir = $(analysis_dir)/chromosomes/$1
get_chrom_task = $(call get_chrom_dir,$1)/$(complete_tag)
get_bin_task = $(call get_chrom_dir,$1)/bins/$2/$(complete_tag)



all: $(finish_task)
	@$(print_success)


define print_success
echo;\
echo Analysis complete. Final somatic calls can be found in $(results_dir);\
echo
endef


# top level results target:
#
$(finish_task):
	$(finish_script) --config=$(config_file) && touch $@


# chromosome targets:
#
ENDE

for my $chrom (@chroms) {

    print $MAKEFH <<ENDE;
chrom_${chrom}_task := \$(call get_chrom_task,$chrom)
\$(finish_task): \$(chrom_${chrom}_task)
\$(chrom_${chrom}_task):
	\$(filter_script) --config=\$(config_file) --chrom=$chrom && touch \$@

ENDE

}

print $MAKEFH <<ENDE;

# chromosome bin targets:
#
ENDE

for my $chrom (@chroms) {
    for my $bin (@{$chromInfo{$chrom}{binList}}) {

print $MAKEFH <<ENDE;
chrom_${chrom}_bin_${bin}_task := \$(call get_bin_task,$chrom,$bin)
\$(chrom_${chrom}_task): \$(chrom_${chrom}_bin_${bin}_task)
\$(chrom_${chrom}_bin_${bin}_task):
	\$(call_script) --config=\$(config_file) --chrom=$chrom --bin=$bin && touch \$@

ENDE

    }
}


# If the eval function is available, this is the way we could finish
# the makefile without being so verbose but it doesn't look like qmake
# understands this function.

=cut

print $MAKEFH <<ENDE;

chroms := @chroms

ENDE

for my $chrom (@chroms) {
    print $MAKEFH "${chrom}_bins := " . join(" ",@{$chromInfo{$chrom}{binList}}) . "\n";
}

print $MAKEFH <<'ENDE';

define chrom_task_template
chrom_$1_task := $(call get_chrom_task,$1)
$(finish_task): $$(chrom_$1_task)
$$(chrom_$1_task):
	$$(filter_script) --config=$$(config_file) --chrom=$1 && touch $$@
endef

$(foreach c,$(chroms),$(eval $(call chrom_task_template,$c)))


# chromosome bin targets:
#
define chrom_bin_task_template
chrom_$1_bin_$2_task := $(call get_bin_task,$1,$2)
$$(chrom_$1_task): $$(chrom_$1_bin_$2_task)
$$(chrom_$1_bin_$2_task):
	$$(call_script) --config=$$(config_file) --chrom=$1 --bin=$2 && touch $$@
endef

$(foreach c,$(chroms), \
    $(foreach b,$($c_bins),$(eval $(call chrom_bin_task_template,$c,$b))) \
 )

ENDE

=cut



print <<END;


Successfully configured analysis and created makefile '$makeFile'.

To run the analysis locally using make, run:

make -C $outDir

...or:

cd $outDir
make

END

1;

__END__