From 30bfb4b43eee7c8b4e791fec994f23342b4a657a Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Fri, 29 Oct 2021 16:55:31 -0400 Subject: [PATCH 001/189] Install basespace CLI --- MIPTools.def | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MIPTools.def b/MIPTools.def index d3810f3..f594d1f 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -186,6 +186,10 @@ From: amd64/ubuntu:20.04 # install parasight scp /opt/programs/parasight_v7.6/parasight.pl /opt/bin/parasight76.pl + # install basespace cli + BS_PATH="https://launch.basespace.illumina.com/CLI/latest/amd64-linux/bs" + wget $BS_PATH -O /opt/bin/bs + # add executable flag to executables chmod -R +xr /usr/bin chmod -R +xr /opt/bin From 8256cac840b7efd1b55403cdd3be0ef670031a66 Mon Sep 17 00:00:00 2001 From: ashlinharris <90787010+ashlinharris@users.noreply.github.com> Date: Mon, 1 Nov 2021 09:50:54 -0400 Subject: [PATCH 002/189] Added default options and settings --- .../processing-and-filtering-variant-calls.ipynb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/base_resources/processing-and-filtering-variant-calls.ipynb b/base_resources/processing-and-filtering-variant-calls.ipynb index a2d7d45..3ca08ae 100644 --- a/base_resources/processing-and-filtering-variant-calls.ipynb +++ b/base_resources/processing-and-filtering-variant-calls.ipynb @@ -137,17 +137,17 @@ "# provide a file that maps gene names to gene IDs\n", "# this is necessary when targeted variant annotations use\n", "# gene names instead of gene IDs. Otherwise provide None\n", - "geneid_to_genename = \n", + "geneid_to_genename = None\n", "# annotate targeted amino acid changes in the tables\n", "# using the file, or otherwise provide None\n", - "target_aa_annotation = \n", + "target_aa_annotation = None\n", "# decompose multi amino acid changes and combine counts of\n", "# resulting single amino acid changes\n", - "aggregate_aminoacids = \n", + "aggregate_aminoacids = None\n", "# decompose MNVs and combine counts for resulting SNVs\n", - "aggregate_nucleotides = \n", + "aggregate_nucleotides = None\n", "# annotate targeted nucleotide changes in the tables.\n", - "target_nt_annotation = " + "target_nt_annotation = None" ] }, { @@ -159,7 +159,7 @@ "# OPTIONAL USER INPUT\n", "\n", "# analysis settings dictionary\n", - "settings = settings\n", + "#settings = settings\n", "# provide the path to the settings file\n", "# if settings dictionary has not been loaded\n", "settings_file = None\n", From 968358062c64f4e9984bfd87a1c5957739fe013d Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 1 Nov 2021 18:33:51 -0400 Subject: [PATCH 003/189] Remove old python download script --- bin/BaseSpaceRunDownloader_v2.py | 91 -------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 bin/BaseSpaceRunDownloader_v2.py diff --git a/bin/BaseSpaceRunDownloader_v2.py b/bin/BaseSpaceRunDownloader_v2.py deleted file mode 100644 index 9709a53..0000000 --- a/bin/BaseSpaceRunDownloader_v2.py +++ /dev/null @@ -1,91 +0,0 @@ -from urllib.request import Request, urlopen, URLError -import json -import math -import sys -import os -import socket -import optparse - - -def arg_parser(): - parser = optparse.OptionParser() - parser.add_option('-r', dest='runid', help='Run ID: required') - parser.add_option('-a', dest='accesstoken', help='Access Token: required') - (options, args) = parser.parse_args() - try: - if options.runid is None: - raise Exception - if options.accesstoken is None: - raise Exception - except Exception: - print("Usage: BaseSpaceRunDownloader_vN.py -r " - " -a ") - sys.exit() - return options - - -def restrequest(rawrequest): - request = Request(rawrequest) - try: - response = urlopen(request) - json_string = response.read() - json_obj = json.loads(json_string) - except URLError as e: - print('Got an error code:', e) - sys.exit() - return json_obj - - -def downloadrestrequest(rawrequest, path): - dirname = RunID + os.sep + os.path.dirname(path) - if dirname != '': - if not os.path.isdir(dirname): - os.makedirs(dirname) - request = (rawrequest) - outfile = open(RunID + os.sep + path, 'wb') - try: - response = urlopen(request, timeout=1) - outfile.write(response.read()) - outfile.close() - except URLError as e: - print('Got an error code:', e) - outfile.close() - downloadrestrequest(rawrequest, path) - except socket.error: - print('Got a socket error: retrying') - outfile.close() - downloadrestrequest(rawrequest, path) - - -options = arg_parser() -RunID = options.runid -AccessToken = options.accesstoken -request = ('http://api.basespace.illumina.com/v1pre3/runs/{}/files?' - 'access_token={}').format(RunID, AccessToken) -json_obj = restrequest(request) -totalCount = json_obj['Response']['TotalCount'] -noffsets = int(math.ceil(float(totalCount)/1000.0)) -hreflist = [] -pathlist = [] -filenamelist = [] - -for index in range(noffsets): - offset = 1000*index - request = ( - 'http://api.basespace.illumina.com/v1pre3/runs/{}/files' - '?access_token={}&limit=1000&Offset={}' - ).format(RunID, AccessToken, offset) - json_obj = restrequest(request) - nfiles = len(json_obj['Response']['Items']) - for fileindex in range(nfiles): - href = json_obj['Response']['Items'][fileindex]['Href'] - hreflist.append(href) - path = json_obj['Response']['Items'][fileindex]['Path'] - pathlist.append(path) - -for index in range(len(hreflist)): - request = ( - 'http://api.basespace.illumina.com/{}/content?access_token={}' - ).format(hreflist[index], AccessToken) - print('downloading {}'.format(pathlist[index])) - downloadrestrequest(request, pathlist[index]) From 429139371ff67dc60d357c8a79bb3ba9783546b2 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 1 Nov 2021 18:36:19 -0400 Subject: [PATCH 004/189] Define basespace download app --- MIPTools.def | 53 +++++++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/MIPTools.def b/MIPTools.def index f594d1f..e5134e3 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -338,16 +338,15 @@ From: amd64/ubuntu:20.04 . /opt/analysis/wrangle.sh ################################################################## -## Download App ## +## Basespace Download App ## ################################################################## -%apprun download - # Parse options +%apprun bs_download set -e - set -u - while getopts r: opt; do - case $opt in - r) run_id=$OPTARG;; - ?) echo "Usage: singularity run --app download \\" + + # Set default values for paths + output_path="/opt/analysis" + config_path="/opt/resources/basespace.cfg" + echo " -B /path_to_output_dir:/opt/analysis \\" echo " -B /path_to_base_resources:/opt/resources \\" echo " mycontainer.sif -r my_Illumina_run_ID" @@ -357,23 +356,31 @@ From: amd64/ubuntu:20.04 echo "must be mounted to /opt/data." exit 1;; esac + + # Argument handling using getopts + # Could alternatively use manual processing. This would allow for longer + # formed inputs. + while getopts 'i:o:c:' opt; do + case "${opt}" in + i) run_id=${OPTARG} ;; + o) ouput_path=${OPTARG} ;; + c) config_path="${OPTARG}" ;; + esac done - - # Print to CLI - echo "Downloading NextSeq run $run_id from BaseSpace." - echo "Depending on the data size, this can take very long (up to 10 h)" - echo "It is recommended to run this app in a screen (GNU screen)." - echo "A message indicating the end of download will be printed when done." - echo "Check nohup.out file in your output directory for the download log." - # cd and run app - # Use nohup to make command keep running even if get hangup signal - cd /opt/analysis - nohup python /opt/bin/BaseSpaceRunDownloader_v2.py \ - -r $run_id -a "$(cat /opt/resources/access_token.txt)" - - # Print to CLI - echo "Download finished." + # Ensure run_id is specified + if [ ! "$run_id" ]; then + echo "Argument -i must be provided" + echo "$usage" >&2; exit 1 + fi + + # Read data from config file + # Remove whitespace from each line and export each line as a variable + export BASESPACE_API_SERVER=$(sed '1q;d' ${config_path} | sed 's/.*=.//g') + export BASESPACE_ACCESS_TOKEN=$(sed '2q;d' ${config_path} | sed 's/.*=.//g') + + # Download data + bs download run -i ${run_id} -o ${output_path} ################################################################# ## Demux App ## From cf7e317603b4a879d3289b1f35bdefe105cb2a5b Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 1 Nov 2021 18:37:37 -0400 Subject: [PATCH 005/189] Add help page --- MIPTools.def | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/MIPTools.def b/MIPTools.def index e5134e3..999ec84 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -347,15 +347,26 @@ From: amd64/ubuntu:20.04 output_path="/opt/analysis" config_path="/opt/resources/basespace.cfg" - echo " -B /path_to_output_dir:/opt/analysis \\" - echo " -B /path_to_base_resources:/opt/resources \\" - echo " mycontainer.sif -r my_Illumina_run_ID" - echo "An 'access_token.txt' file with a valid access token is " - echo "required. It must be present in base_resources directory." - echo "A data directory where the data will be downloaded to" - echo "must be mounted to /opt/data." - exit 1;; - esac + help() { + echo "Download data from the Illumina BaseSpace Sequence Hub" + echo "" + echo "Usage:" + echo " singularity run [options] --app bs_download "\ + "[app_options]" + echo "" + echo "Options:" + echo " See 'singularity run'" + echo "" + echo "App Options:" + echo " -i Required. The run ID of the data to download." + echo " -o The path to the output directory." + echo " Default: '/opt/analysis'." + echo " -c The path to the authentication credentials file." + echo " This file is created by 'bs auth'. For additional" + echo " information see the help page for that command." + echo " Default: '/opt/resources/basespace.cfg'." + echo " -h Print the help page." + } # Argument handling using getopts # Could alternatively use manual processing. This would allow for longer @@ -365,6 +376,10 @@ From: amd64/ubuntu:20.04 i) run_id=${OPTARG} ;; o) ouput_path=${OPTARG} ;; c) config_path="${OPTARG}" ;; + h) help + exit 1 ;; + *) help + exit 1 ;; esac done From b67ffafa93afe1b7d3be55fe8b683815b41b9e23 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 1 Nov 2021 18:39:30 -0400 Subject: [PATCH 006/189] Add template basespace authentication file --- base_resources/basespace.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 base_resources/basespace.cfg diff --git a/base_resources/basespace.cfg b/base_resources/basespace.cfg new file mode 100644 index 0000000..34fa528 --- /dev/null +++ b/base_resources/basespace.cfg @@ -0,0 +1,2 @@ +apiServer = https://api.basespace.illumina.com +accessToken = From d87cd34bdfe7002e837ca3dc89c89ca92bf6867e Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 1 Nov 2021 18:41:31 -0400 Subject: [PATCH 007/189] Add -h as a flag --- MIPTools.def | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIPTools.def b/MIPTools.def index 999ec84..9eb2e7f 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -371,7 +371,7 @@ From: amd64/ubuntu:20.04 # Argument handling using getopts # Could alternatively use manual processing. This would allow for longer # formed inputs. - while getopts 'i:o:c:' opt; do + while getopts 'i:o:c:h' opt; do case "${opt}" in i) run_id=${OPTARG} ;; o) ouput_path=${OPTARG} ;; From 2fa35edabf7d4bbb4b01847a1be52c6211529504 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 1 Nov 2021 19:07:50 -0400 Subject: [PATCH 008/189] Add an example to help page --- MIPTools.def | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/MIPTools.def b/MIPTools.def index 9eb2e7f..9b32948 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -348,14 +348,14 @@ From: amd64/ubuntu:20.04 config_path="/opt/resources/basespace.cfg" help() { - echo "Download data from the Illumina BaseSpace Sequence Hub" + echo "Download data from the Illumina BaseSpace Sequence Hub." echo "" echo "Usage:" echo " singularity run [options] --app bs_download "\ "[app_options]" echo "" echo "Options:" - echo " See 'singularity run'" + echo " See 'singularity run'." echo "" echo "App Options:" echo " -i Required. The run ID of the data to download." @@ -366,11 +366,22 @@ From: amd64/ubuntu:20.04 echo " information see the help page for that command." echo " Default: '/opt/resources/basespace.cfg'." echo " -h Print the help page." + echo "" + echo "Examples:" + echo " # Set paths" + echo " $ resource_dir=/bin/MIPTools/base_resources" + echo " $ run_dir=/work/usr/example" + echo "" + echo " # Run app" + echo " $ singularity run \\" + echo " -B \$resource_dir:/opt/resources"\ + "-B \$run_dir:/opt/analysis \\" + echo " --app bs_download -i " } # Argument handling using getopts - # Could alternatively use manual processing. This would allow for longer - # formed inputs. + # Could alternatively use manual processing. This would allow for long + # form inputs. while getopts 'i:o:c:h' opt; do case "${opt}" in i) run_id=${OPTARG} ;; From f95b6a31221665f9a8c312ae2a0bf2be44e4df5f Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Tue, 2 Nov 2021 15:45:04 -0400 Subject: [PATCH 009/189] Format code --- MIPTools.def | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/MIPTools.def b/MIPTools.def index 9b32948..3ba2276 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -341,6 +341,7 @@ From: amd64/ubuntu:20.04 ## Basespace Download App ## ################################################################## %apprun bs_download + # Exit if something fails set -e # Set default values for paths @@ -382,11 +383,11 @@ From: amd64/ubuntu:20.04 # Argument handling using getopts # Could alternatively use manual processing. This would allow for long # form inputs. - while getopts 'i:o:c:h' opt; do + while getopts "i:o:c:h" opt; do case "${opt}" in i) run_id=${OPTARG} ;; o) ouput_path=${OPTARG} ;; - c) config_path="${OPTARG}" ;; + c) config_path=${OPTARG} ;; h) help exit 1 ;; *) help @@ -395,15 +396,16 @@ From: amd64/ubuntu:20.04 done # Ensure run_id is specified - if [ ! "$run_id" ]; then + if [ ! $run_id ]; then echo "Argument -i must be provided" - echo "$usage" >&2; exit 1 + echo "$usage" >&2 + exit 1 fi # Read data from config file # Remove whitespace from each line and export each line as a variable - export BASESPACE_API_SERVER=$(sed '1q;d' ${config_path} | sed 's/.*=.//g') - export BASESPACE_ACCESS_TOKEN=$(sed '2q;d' ${config_path} | sed 's/.*=.//g') + export BASESPACE_API_SERVER=$(sed "1q;d" ${config_path} | sed "s/.*=.//g") + export BASESPACE_ACCESS_TOKEN=$(sed "2q;d" ${config_path} | sed "s/.*=.//g") # Download data bs download run -i ${run_id} -o ${output_path} From 83b58dd684d53385a722e197a02800653d32c0d7 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Tue, 2 Nov 2021 16:15:52 -0400 Subject: [PATCH 010/189] Don't require both `/opt/data` and `/opt/analysis` --- MIPTools.def | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MIPTools.def b/MIPTools.def index 3ba2276..2a4db95 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -422,7 +422,6 @@ From: amd64/ubuntu:20.04 s) sample_list=$OPTARG;; p) platform=$OPTARG;; ?) echo "Usage: singularity run --app demux \\" - echo " -B /path_to_run_dir:/opt/data \\" echo " -B /path_to_output_dir:/opt/analysis \\" echo " -B /path_to_base_resources:/opt/resources \\" echo " mycontainer.sif -s sample_list_file \\" @@ -448,7 +447,7 @@ From: amd64/ubuntu:20.04 "'"$output_dir"'")' # cd to where bcl files are. - cd /opt/data + cd /opt/analysis # Create a fastq directory for saving fastqs mkdir -p /opt/analysis/fastq From 8f5e9a7a8a27201361b5a6b623aac1efaea02e66 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Tue, 2 Nov 2021 16:20:39 -0400 Subject: [PATCH 011/189] Style code --- MIPTools.def | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/MIPTools.def b/MIPTools.def index 2a4db95..fe1e200 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -414,13 +414,13 @@ From: amd64/ubuntu:20.04 ## Demux App ## ################################################################# %apprun demux - # Parse options - set -e - set -u - while getopts s:p: opt; do - case $opt in - s) sample_list=$OPTARG;; - p) platform=$OPTARG;; + # Exit if something fails or if have unset object + set -eu + + while getopts "s:p:" opt; do + case ${opt} in + s) sample_list=${OPTARG} ;; + p) platform=${OPTARG} ;; ?) echo "Usage: singularity run --app demux \\" echo " -B /path_to_output_dir:/opt/analysis \\" echo " -B /path_to_base_resources:/opt/resources \\" @@ -428,32 +428,32 @@ From: amd64/ubuntu:20.04 echo " -p sequencing_platform (nextseq or miseq) \\" echo "The sample list file must be present in the output" echo "directory mounted to /opt/analysis." - exit 1;; + exit 1 ;; esac done # Define variables cd /opt/src template_dir="/opt/resources/templates/sample_sheet_templates/" - platform_template="$platform"_sample_sheet_template.csv - template="$template_dir$platform_template" + platform_template="${platform}"_sample_sheet_template.csv + template="${template_dir}${platform_template}" bc_dict="/opt/resources/barcode_dict.json" output_dir="/opt/analysis" - sample_list="/opt/analysis/$sample_list" + sample_list="/opt/analysis/${sample_list}" # Create a sample sheet for demultiplexing python -c 'import mip_functions as mip; mip.generate_sample_sheet( "'"$sample_list"'", "'"$bc_dict"'", "'"$template"'", "'"$platform"'", "'"$output_dir"'")' - # cd to where bcl files are. + # cd to where bcl files are cd /opt/analysis # Create a fastq directory for saving fastqs mkdir -p /opt/analysis/fastq # Copy sample list to fastq directory - scp $sample_list /opt/analysis/fastq/ + scp ${sample_list} /opt/analysis/fastq/ # Increase limit of open number of files. ulimit -Sn $(ulimit -Hn) From 5e9921e1f1edabe7af24a42e12ee8234b9bd783a Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Tue, 2 Nov 2021 18:10:18 -0400 Subject: [PATCH 012/189] Update changelog --- CHANGELOG.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e5a5af..ffa2f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,14 @@ ## MIPTools (development version) -- Automatically build and deploy container using Github Actions (@arisp99, #11). -- Fix build failure due to dependency changes (#7). +### App Changes -### Maintenance - -- Remove duplicated files. -- Improve bash errors. -- Make strings human readable (@arisp99, #5). +- New `bs_download` app replaces the `download` app. The new app improves the + method to download data from the Illumina BaseSpace Sequence Hub by using + command line tools (@arisp99, #13). +- The `demux` app no longer requires both a run directory and a output + directory. These directories have been combined so that fastq files are + output to the run directory. ### Documentation Overhaul @@ -17,6 +17,17 @@ - Improve clarity of README and add additional instructions on downloading or building the container. +### Bug Fixes + +- Fix build failure due to dependency changes (#7). + +### Maintenance + +- Automatically build and deploy container using Github Actions (@arisp99, #11). +- Remove duplicated files. +- Improve bash errors. +- Make strings human readable (@arisp99, #5). + ## MIPTools 1.0.0 - First major release. From 6ff787e739f10b64f8a224ee737471efb1aedb9d Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Wed, 3 Nov 2021 17:40:38 -0400 Subject: [PATCH 013/189] Improve docs for `demux_qc` app --- CHANGELOG.md | 1 + MIPTools.def | 53 +++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e5a5af..a7cb101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### Documentation Overhaul +- Add better documentation for the `demux_qc` app. - Add doc-strings to python functions. - Improve clarity of README and add additional instructions on downloading or building the container. diff --git a/MIPTools.def b/MIPTools.def index d3810f3..58193ff 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -430,19 +430,46 @@ From: amd64/ubuntu:20.04 ## Demux QC App ## ################################################################## %apprun demux_qc - # Parse options - set -e - set -u - while getopts p: opt; do - case $opt in - p) platform=$OPTARG;; - ?) echo "Usage: singularity run --app demux_qc\\" - echo " -B /path_to_base_resources:/opt/resources \\" - echo " -B /path_to_fastq_dir:/opt/analysis " - echo " mycontainer.sif -p sequencing_platform" - exit 1;; + # Exit if something fails or if have unset object + set -eu + + help() { + echo "Run quality control on demultiplexed data." + echo "" + echo "Usage:" + echo " singularity run [options] --app demux_qc "\ + "[app_options]" + echo "" + echo "Options:" + echo " See 'singularity run'." + echo "" + echo "App Options:" + echo " -p Required. The sequencing platform used. Either 'miseq'" + echo " or 'nextseq'." + echo " -h Print the help page." + echo "" + echo "Examples:" + echo " # Set paths" + echo " $ resource_dir=/bin/MIPTools/base_resources" + echo " $ fastq_dir=/work/usr/example" + echo "" + echo " # Run app" + echo " $ singularity run \\" + echo " -B \$resource_dir:/opt/resources"\ + "-B \$fastq_dir:/opt/analysis \\" + echo " --app demux_qc -p nextseq" + } + + # Argument handling + while getopts "p:h" opt; do + case ${opt} in + p) platform=${OPTARG} ;; + h) help + exit 1 ;; + *) help + exit 1 ;; esac done - # Run app - python /opt/src/demux_qc.py -p $platform + # Run python script + python /opt/src/demux_qc.py -p ${platform} From b43b73204a0ae654649b9550e9e96b501e4d12bb Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Wed, 3 Nov 2021 17:40:56 -0400 Subject: [PATCH 014/189] Style code --- src/demux_qc.py | 78 +++++++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/src/demux_qc.py b/src/demux_qc.py index c988ab1..efafba1 100644 --- a/src/demux_qc.py +++ b/src/demux_qc.py @@ -1,4 +1,3 @@ -"""Generate demultiplexing statistics after a sequencine run.""" import mip_functions as mip import pickle import os @@ -7,17 +6,21 @@ def main(platform, stats_dir): - """Generate demultiplexing statistics after a sequencine run.""" + """Generate demultiplexing statistics after a sequencing run.""" bc_dict = "/opt/resources/barcode_dict.json" + # load barcode dict to be passed to the header-primer conversion function with open(bc_dict, "rb") as infile: bc_dict = pickle.load(infile) - # fastq summary fies created for each lane contains raw read numbers - # and reads passing filter for tile and sample + + # fastq summary files created for each lane contains raw read numbers and + # reads passing filter for tile and sample fsums = [] + # demultiplexing summary files have the information with the most popular # unindexed sample barcodes. dfiles = [] + # scan the stats dir and extract information from the relevant files for f in os.scandir(stats_dir): if f.name.startswith("FastqSummary"): @@ -28,6 +31,7 @@ def main(platform, stats_dir): elif f.name.startswith("DemuxSummary"): lane = "Lane" + f.name.split(".")[0][-1] dfiles.append([f.path, lane]) + dsums = [] for d, l in dfiles: with open(d) as infile: @@ -39,7 +43,8 @@ def main(platform, stats_dir): dsums.append(counts) elif line.startswith("### Columns: Index_Sequence Hit_Count"): start = True - # create summary dataframe for read counts + + # create summary data frame for read counts fsums = pd.concat(fsums) # create per sample read count summary if sample sheet is provided @@ -53,31 +58,41 @@ def main(platform, stats_dir): parse_line = True if parse_line: sample_sheet_list.append(line.split(",")) - sample_sheet = pd.DataFrame(sample_sheet_list[1:], - columns=sample_sheet_list[0]) + sample_sheet = pd.DataFrame(sample_sheet_list[1:], columns=sample_sheet_list[0]) sample_sheet["Sample_ID"] = sample_sheet["Sample_ID"].astype(int) sample_sheet = sample_sheet.rename( - columns = {"Sample_ID": "SampleNumber", "Sample_Name": "Sample ID"}) + columns={"Sample_ID": "SampleNumber", "Sample_Name": "Sample ID"} + ) sample_sums = fsums.groupby(["SampleNumber", "Lane"], as_index=False)[ - ["NumberOfReadsRaw", "NumberOfReadsPF"]].sum() + ["NumberOfReadsRaw", "NumberOfReadsPF"] + ].sum() sample_sums = sample_sums.merge(sample_sheet) sample_sums.to_csv( - os.path.join(stats_dir, "PerSampleReadCounts.csv"), - index = False) + os.path.join(stats_dir, "PerSampleReadCounts.csv"), index=False + ) except IOError: pass + # Print out read number summary. - fsums = fsums[["NumberOfReadsRaw", "NumberOfReadsPF", "Lane"]].groupby( - "Lane").sum().reset_index() - print(("Total number of raw reads and reads passing filter were " - "{0[NumberOfReadsRaw]:,} and {0[NumberOfReadsPF]:,}, " - "respectively.").format( - fsums.sum() - )) + fsums = ( + fsums[["NumberOfReadsRaw", "NumberOfReadsPF", "Lane"]] + .groupby("Lane") + .sum() + .reset_index() + ) + print( + ( + "Total number of raw reads and reads passing filter were " + "{0[NumberOfReadsRaw]:,} and {0[NumberOfReadsPF]:,}, " + "respectively." + ).format(fsums.sum()) + ) fsums.to_csv(os.path.join(stats_dir, "ReadSummary.csv")) + # create a dataframe with unindexed read information dsums = pd.DataFrame(dsums, columns=["Header", "Read Count", "Lane"]) dsums["Read Count"] = dsums["Read Count"].astype(int) + # get primer indexes for corresponding headers dsums["Fw,Rev"] = dsums["Header"].apply( lambda a: mip.header_to_primer(bc_dict, a, platform) @@ -85,12 +100,15 @@ def main(platform, stats_dir): dsums["Fw"] = dsums["Fw,Rev"].apply(lambda a: a[0]) dsums["Rev"] = dsums["Fw,Rev"].apply(lambda a: a[1]) dsums["Index Difference"] = dsums["Rev"] - dsums["Fw"] + # separate 999 values which do not correspond to our indexes - caught = dsums.loc[(dsums["Fw"] != 999) - & (dsums["Rev"] != 999)] - print(("There were {:,} undetermined reads. {:,} of these belong to " - "possible primer pairs.").format(dsums["Read Count"].sum(), - caught["Read Count"].sum())) + caught = dsums.loc[(dsums["Fw"] != 999) & (dsums["Rev"] != 999)] + print( + ( + "There were {:,} undetermined reads. {:,} of these belong to " + "possible primer pairs." + ).format(dsums["Read Count"].sum(), caught["Read Count"].sum()) + ) dsums.to_csv(os.path.join(stats_dir, "UndeterminedIndexSummary.csv")) caught.to_csv(os.path.join(stats_dir, "UndeterminedPrimerSummary.csv")) @@ -98,18 +116,20 @@ def main(platform, stats_dir): if __name__ == "__main__": # Read input arguments parser = argparse.ArgumentParser( - description=""" Create a QC report following a bcl demultiplex operateon. - """) + description="Create a QC report following demultiplexing of data." + ) parser.add_argument( - "-p", "--platform", + "-p", + "--platform", help=("Sequencing platform."), required=True, - choices=["nextseq", "miseq"] + choices=["nextseq", "miseq"], ) parser.add_argument( - "-d", "--stats-dir", + "-d", + "--stats-dir", help=("Path to directory where demultiplexing stats are saved."), - default="/opt/analysis/Stats" + default="/opt/analysis/Stats", ) # parse arguments from command line From e51f3d25c8b31d3b9074c55546d9a3a05f2733a1 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Wed, 3 Nov 2021 17:44:11 -0400 Subject: [PATCH 015/189] Update `.gitignore` --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8716a74..cb8a825 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,3 @@ miptools/ build_logs/ src/__pycache__/ base_resources/.ipynb_checkpoints/ - -.pre-commit-config.yaml From 3849118eff6f8e2d1e180a7e4157b04dcba8f4a0 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Thu, 4 Nov 2021 12:22:14 -0400 Subject: [PATCH 016/189] Revert "Use basespace CLI to download run data" (#14) This PR will be merged later on for MIPTools v2.0.0 --- CHANGELOG.md | 25 ++---- MIPTools.def | 130 +++++++++++-------------------- base_resources/basespace.cfg | 2 - bin/BaseSpaceRunDownloader_v2.py | 91 ++++++++++++++++++++++ 4 files changed, 144 insertions(+), 104 deletions(-) delete mode 100644 base_resources/basespace.cfg create mode 100644 bin/BaseSpaceRunDownloader_v2.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 73753fa..a7cb101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,14 @@ ## MIPTools (development version) -### App Changes +- Automatically build and deploy container using Github Actions (@arisp99, #11). +- Fix build failure due to dependency changes (#7). -- New `bs_download` app replaces the `download` app. The new app improves the - method to download data from the Illumina BaseSpace Sequence Hub by using - command line tools (@arisp99, #13). -- The `demux` app no longer requires both a run directory and a output - directory. These directories have been combined so that fastq files are - output to the run directory. +### Maintenance + +- Remove duplicated files. +- Improve bash errors. +- Make strings human readable (@arisp99, #5). ### Documentation Overhaul @@ -18,17 +18,6 @@ - Improve clarity of README and add additional instructions on downloading or building the container. -### Bug Fixes - -- Fix build failure due to dependency changes (#7). - -### Maintenance - -- Automatically build and deploy container using Github Actions (@arisp99, #11). -- Remove duplicated files. -- Improve bash errors. -- Make strings human readable (@arisp99, #5). - ## MIPTools 1.0.0 - First major release. diff --git a/MIPTools.def b/MIPTools.def index b717e01..58193ff 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -186,10 +186,6 @@ From: amd64/ubuntu:20.04 # install parasight scp /opt/programs/parasight_v7.6/parasight.pl /opt/bin/parasight76.pl - # install basespace cli - BS_PATH="https://launch.basespace.illumina.com/CLI/latest/amd64-linux/bs" - wget $BS_PATH -O /opt/bin/bs - # add executable flag to executables chmod -R +xr /usr/bin chmod -R +xr /opt/bin @@ -338,122 +334,88 @@ From: amd64/ubuntu:20.04 . /opt/analysis/wrangle.sh ################################################################## -## Basespace Download App ## +## Download App ## ################################################################## -%apprun bs_download - # Exit if something fails +%apprun download + # Parse options set -e - - # Set default values for paths - output_path="/opt/analysis" - config_path="/opt/resources/basespace.cfg" - - help() { - echo "Download data from the Illumina BaseSpace Sequence Hub." - echo "" - echo "Usage:" - echo " singularity run [options] --app bs_download "\ - "[app_options]" - echo "" - echo "Options:" - echo " See 'singularity run'." - echo "" - echo "App Options:" - echo " -i Required. The run ID of the data to download." - echo " -o The path to the output directory." - echo " Default: '/opt/analysis'." - echo " -c The path to the authentication credentials file." - echo " This file is created by 'bs auth'. For additional" - echo " information see the help page for that command." - echo " Default: '/opt/resources/basespace.cfg'." - echo " -h Print the help page." - echo "" - echo "Examples:" - echo " # Set paths" - echo " $ resource_dir=/bin/MIPTools/base_resources" - echo " $ run_dir=/work/usr/example" - echo "" - echo " # Run app" - echo " $ singularity run \\" - echo " -B \$resource_dir:/opt/resources"\ - "-B \$run_dir:/opt/analysis \\" - echo " --app bs_download -i " - } - - # Argument handling using getopts - # Could alternatively use manual processing. This would allow for long - # form inputs. - while getopts "i:o:c:h" opt; do - case "${opt}" in - i) run_id=${OPTARG} ;; - o) ouput_path=${OPTARG} ;; - c) config_path=${OPTARG} ;; - h) help - exit 1 ;; - *) help - exit 1 ;; - esac + set -u + while getopts r: opt; do + case $opt in + r) run_id=$OPTARG;; + ?) echo "Usage: singularity run --app download \\" + echo " -B /path_to_output_dir:/opt/analysis \\" + echo " -B /path_to_base_resources:/opt/resources \\" + echo " mycontainer.sif -r my_Illumina_run_ID" + echo "An 'access_token.txt' file with a valid access token is " + echo "required. It must be present in base_resources directory." + echo "A data directory where the data will be downloaded to" + echo "must be mounted to /opt/data." + exit 1;; + esac done - - # Ensure run_id is specified - if [ ! $run_id ]; then - echo "Argument -i must be provided" - echo "$usage" >&2 - exit 1 - fi - # Read data from config file - # Remove whitespace from each line and export each line as a variable - export BASESPACE_API_SERVER=$(sed "1q;d" ${config_path} | sed "s/.*=.//g") - export BASESPACE_ACCESS_TOKEN=$(sed "2q;d" ${config_path} | sed "s/.*=.//g") + # Print to CLI + echo "Downloading NextSeq run $run_id from BaseSpace." + echo "Depending on the data size, this can take very long (up to 10 h)" + echo "It is recommended to run this app in a screen (GNU screen)." + echo "A message indicating the end of download will be printed when done." + echo "Check nohup.out file in your output directory for the download log." + + # cd and run app + # Use nohup to make command keep running even if get hangup signal + cd /opt/analysis + nohup python /opt/bin/BaseSpaceRunDownloader_v2.py \ + -r $run_id -a "$(cat /opt/resources/access_token.txt)" - # Download data - bs download run -i ${run_id} -o ${output_path} + # Print to CLI + echo "Download finished." ################################################################# ## Demux App ## ################################################################# %apprun demux - # Exit if something fails or if have unset object - set -eu - - while getopts "s:p:" opt; do - case ${opt} in - s) sample_list=${OPTARG} ;; - p) platform=${OPTARG} ;; + # Parse options + set -e + set -u + while getopts s:p: opt; do + case $opt in + s) sample_list=$OPTARG;; + p) platform=$OPTARG;; ?) echo "Usage: singularity run --app demux \\" + echo " -B /path_to_run_dir:/opt/data \\" echo " -B /path_to_output_dir:/opt/analysis \\" echo " -B /path_to_base_resources:/opt/resources \\" echo " mycontainer.sif -s sample_list_file \\" echo " -p sequencing_platform (nextseq or miseq) \\" echo "The sample list file must be present in the output" echo "directory mounted to /opt/analysis." - exit 1 ;; + exit 1;; esac done # Define variables cd /opt/src template_dir="/opt/resources/templates/sample_sheet_templates/" - platform_template="${platform}"_sample_sheet_template.csv - template="${template_dir}${platform_template}" + platform_template="$platform"_sample_sheet_template.csv + template="$template_dir$platform_template" bc_dict="/opt/resources/barcode_dict.json" output_dir="/opt/analysis" - sample_list="/opt/analysis/${sample_list}" + sample_list="/opt/analysis/$sample_list" # Create a sample sheet for demultiplexing python -c 'import mip_functions as mip; mip.generate_sample_sheet( "'"$sample_list"'", "'"$bc_dict"'", "'"$template"'", "'"$platform"'", "'"$output_dir"'")' - # cd to where bcl files are - cd /opt/analysis + # cd to where bcl files are. + cd /opt/data # Create a fastq directory for saving fastqs mkdir -p /opt/analysis/fastq # Copy sample list to fastq directory - scp ${sample_list} /opt/analysis/fastq/ + scp $sample_list /opt/analysis/fastq/ # Increase limit of open number of files. ulimit -Sn $(ulimit -Hn) diff --git a/base_resources/basespace.cfg b/base_resources/basespace.cfg deleted file mode 100644 index 34fa528..0000000 --- a/base_resources/basespace.cfg +++ /dev/null @@ -1,2 +0,0 @@ -apiServer = https://api.basespace.illumina.com -accessToken = diff --git a/bin/BaseSpaceRunDownloader_v2.py b/bin/BaseSpaceRunDownloader_v2.py new file mode 100644 index 0000000..9709a53 --- /dev/null +++ b/bin/BaseSpaceRunDownloader_v2.py @@ -0,0 +1,91 @@ +from urllib.request import Request, urlopen, URLError +import json +import math +import sys +import os +import socket +import optparse + + +def arg_parser(): + parser = optparse.OptionParser() + parser.add_option('-r', dest='runid', help='Run ID: required') + parser.add_option('-a', dest='accesstoken', help='Access Token: required') + (options, args) = parser.parse_args() + try: + if options.runid is None: + raise Exception + if options.accesstoken is None: + raise Exception + except Exception: + print("Usage: BaseSpaceRunDownloader_vN.py -r " + " -a ") + sys.exit() + return options + + +def restrequest(rawrequest): + request = Request(rawrequest) + try: + response = urlopen(request) + json_string = response.read() + json_obj = json.loads(json_string) + except URLError as e: + print('Got an error code:', e) + sys.exit() + return json_obj + + +def downloadrestrequest(rawrequest, path): + dirname = RunID + os.sep + os.path.dirname(path) + if dirname != '': + if not os.path.isdir(dirname): + os.makedirs(dirname) + request = (rawrequest) + outfile = open(RunID + os.sep + path, 'wb') + try: + response = urlopen(request, timeout=1) + outfile.write(response.read()) + outfile.close() + except URLError as e: + print('Got an error code:', e) + outfile.close() + downloadrestrequest(rawrequest, path) + except socket.error: + print('Got a socket error: retrying') + outfile.close() + downloadrestrequest(rawrequest, path) + + +options = arg_parser() +RunID = options.runid +AccessToken = options.accesstoken +request = ('http://api.basespace.illumina.com/v1pre3/runs/{}/files?' + 'access_token={}').format(RunID, AccessToken) +json_obj = restrequest(request) +totalCount = json_obj['Response']['TotalCount'] +noffsets = int(math.ceil(float(totalCount)/1000.0)) +hreflist = [] +pathlist = [] +filenamelist = [] + +for index in range(noffsets): + offset = 1000*index + request = ( + 'http://api.basespace.illumina.com/v1pre3/runs/{}/files' + '?access_token={}&limit=1000&Offset={}' + ).format(RunID, AccessToken, offset) + json_obj = restrequest(request) + nfiles = len(json_obj['Response']['Items']) + for fileindex in range(nfiles): + href = json_obj['Response']['Items'][fileindex]['Href'] + hreflist.append(href) + path = json_obj['Response']['Items'][fileindex]['Path'] + pathlist.append(path) + +for index in range(len(hreflist)): + request = ( + 'http://api.basespace.illumina.com/{}/content?access_token={}' + ).format(hreflist[index], AccessToken) + print('downloading {}'.format(pathlist[index])) + downloadrestrequest(request, pathlist[index]) From b3c76cf7c05b8826c0bd4cd2d9379266af29f576 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Thu, 4 Nov 2021 16:43:47 -0400 Subject: [PATCH 017/189] Add `-h` flag for the demux app Part of the effort in #17 --- MIPTools.def | 83 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 26 deletions(-) diff --git a/MIPTools.def b/MIPTools.def index 58193ff..7612bb6 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -375,47 +375,78 @@ From: amd64/ubuntu:20.04 ## Demux App ## ################################################################# %apprun demux - # Parse options - set -e - set -u - while getopts s:p: opt; do - case $opt in - s) sample_list=$OPTARG;; - p) platform=$OPTARG;; - ?) echo "Usage: singularity run --app demux \\" - echo " -B /path_to_run_dir:/opt/data \\" - echo " -B /path_to_output_dir:/opt/analysis \\" - echo " -B /path_to_base_resources:/opt/resources \\" - echo " mycontainer.sif -s sample_list_file \\" - echo " -p sequencing_platform (nextseq or miseq) \\" - echo "The sample list file must be present in the output" - echo "directory mounted to /opt/analysis." - exit 1;; + # Exit if something fails or if have unset object + set -eu + + help() { + echo "Demultiplex data. Generates per-sample fastq files from the raw" + echo "sequence data consisting of bcl files." + echo "" + echo "Usage:" + echo " singularity run [options] --app demux [app_options]" + echo "" + echo "Options:" + echo " See 'singularity run'." + echo "" + echo "App Options:" + echo " -s Required. The list of samples. Contains the samples used" + echo " in the study, the primers used, etc. This file must be " + echo " present in the directory mounted to '/opt/analysis'." + echo " -p Required. The sequencing platform used. Either 'miseq'" + echo " or 'nextseq'." + echo " -h Print the help page." + echo "" + echo "Examples:" + echo " # Set paths" + echo " $ resource_dir=/bin/MIPTools/base_resources" + echo " $ bcl_dir=/work/usr/downloaded" + echo " $ fastq_dir=/work/usr/fastq" + echo "" + echo " # Run app" + echo " $ singularity run \\" + echo " -B \${resource_dir}:/opt/resources \\" + echo " -B \${bcl_dir}:/opt/data \\" + echo " -B \${fastq_dir}:/opt/analysis \\" + echo " --app demux -s sample_list.tsv -p nextseq" + } + + while getopts "s:p:h" opt; do + case ${opt} in + s) sample_list=${OPTARG} ;; + p) platform=${OPTARG} ;; + h) help + exit 1 ;; + *) help + exit 1 ;; esac done # Define variables cd /opt/src template_dir="/opt/resources/templates/sample_sheet_templates/" - platform_template="$platform"_sample_sheet_template.csv - template="$template_dir$platform_template" + platform_template="${platform}"_sample_sheet_template.csv + template="${template_dir}${platform_template}" bc_dict="/opt/resources/barcode_dict.json" output_dir="/opt/analysis" - sample_list="/opt/analysis/$sample_list" + sample_list="/opt/analysis/${sample_list}" # Create a sample sheet for demultiplexing python -c 'import mip_functions as mip; mip.generate_sample_sheet( - "'"$sample_list"'", "'"$bc_dict"'", "'"$template"'", "'"$platform"'", - "'"$output_dir"'")' - - # cd to where bcl files are. + "'"${sample_list}"'", + "'"${bc_dict}"'", + "'"${template}"'", + "'"${platform}"'", + "'"${output_dir}"'" + )' + + # cd to where bcl files are cd /opt/data # Create a fastq directory for saving fastqs mkdir -p /opt/analysis/fastq # Copy sample list to fastq directory - scp $sample_list /opt/analysis/fastq/ + scp ${sample_list} /opt/analysis/fastq/ # Increase limit of open number of files. ulimit -Sn $(ulimit -Hn) @@ -455,8 +486,8 @@ From: amd64/ubuntu:20.04 echo "" echo " # Run app" echo " $ singularity run \\" - echo " -B \$resource_dir:/opt/resources"\ - "-B \$fastq_dir:/opt/analysis \\" + echo " -B \${resource_dir}:/opt/resources"\ + "-B \${fastq_dir}:/opt/analysis \\" echo " --app demux_qc -p nextseq" } From 1f1da3f41ce9c27af43c00f112ec86a868e45df8 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Thu, 4 Nov 2021 16:54:38 -0400 Subject: [PATCH 018/189] Add `-h` flag to the download app Part of the effort in #17 --- MIPTools.def | 63 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/MIPTools.def b/MIPTools.def index 7612bb6..b187981 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -337,27 +337,54 @@ From: amd64/ubuntu:20.04 ## Download App ## ################################################################## %apprun download - # Parse options - set -e - set -u - while getopts r: opt; do - case $opt in - r) run_id=$OPTARG;; - ?) echo "Usage: singularity run --app download \\" - echo " -B /path_to_output_dir:/opt/analysis \\" - echo " -B /path_to_base_resources:/opt/resources \\" - echo " mycontainer.sif -r my_Illumina_run_ID" - echo "An 'access_token.txt' file with a valid access token is " - echo "required. It must be present in base_resources directory." - echo "A data directory where the data will be downloaded to" - echo "must be mounted to /opt/data." - exit 1;; + # Exit if something fails or if have unset object + set -eu + + help() { + echo "Download data from the Illumina BaseSpace Sequence Hub." + echo "" + echo "Usage:" + echo " singularity run [options] --app download "\ + "[app_options]" + echo "" + echo "Options:" + echo " See 'singularity run'." + echo "" + echo "App Options:" + echo " -r Required. The run ID of the data to download." + echo " -h Print the help page." + echo "" + echo "Additional Details:" + echo " An 'access_token.txt' file with a valid access token is" + echo " required. It must be present in the 'base_resources' directory." + echo " A data directory where the data will be downloaded to must be" + echo " mounted to '/opt/data'." + echo "" + echo "Examples:" + echo " # Set paths" + echo " $ resource_dir=/bin/MIPTools/base_resources" + echo " $ output_dir=/work/usr/downloaded" + echo "" + echo " # Run app" + echo " $ singularity run \\" + echo " -B \${resource_dir}:/opt/resources"\ + "-B \${output_dir}:/opt/analysis \\" + echo " --app download -r " + } + + while getopts "r:h" opt; do + case ${opt} in + r) run_id=${OPTARG} ;; + h) help + exit 1 ;; + *) help + exit 1 ;; esac done # Print to CLI - echo "Downloading NextSeq run $run_id from BaseSpace." - echo "Depending on the data size, this can take very long (up to 10 h)" + echo "Downloading NextSeq run ${run_id} from BaseSpace." + echo "Depending on the data size, this can take very long (up to 10 h)." echo "It is recommended to run this app in a screen (GNU screen)." echo "A message indicating the end of download will be printed when done." echo "Check nohup.out file in your output directory for the download log." @@ -366,7 +393,7 @@ From: amd64/ubuntu:20.04 # Use nohup to make command keep running even if get hangup signal cd /opt/analysis nohup python /opt/bin/BaseSpaceRunDownloader_v2.py \ - -r $run_id -a "$(cat /opt/resources/access_token.txt)" + -r ${run_id} -a "$(cat /opt/resources/access_token.txt)" # Print to CLI echo "Download finished." From d478ac8d315266973b0af649e70139f7063dea64 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Thu, 4 Nov 2021 17:37:36 -0400 Subject: [PATCH 019/189] Add `-h` flag to wrangler app Part of the effort in #17 --- MIPTools.def | 96 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/MIPTools.def b/MIPTools.def index b187981..a46d7aa 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -276,9 +276,46 @@ From: amd64/ubuntu:20.04 ## Wrangler App ## ################################################################## %apprun wrangler - # Modify shell behavior - set -e - set -u + # Exit if something fails or if have unset object + set -eu + + help() { + echo "Run quality control on demultiplexed data." + echo "" + echo "Usage:" + echo " singularity run [options] --app wrangler "\ + "[app_options]" + echo "" + echo "Options:" + echo " See 'singularity run'." + echo "" + echo "App Options:" + echo " -c Number of available processors to use. Default: 1." + echo " -e Required. A unique ID given to each sequencing run by" + echo " the user." + echo " -h Print the help page." + echo " -k Keep intermediate files generated by MIPWrangler." + echo " -l Required. File providing a list of samples with " + echo " associated information." + echo " -m Minimum capture length for stitching excluding probe" + echo " arms." + echo " -n Starting number for MIP server. Default: 1." + echo " -p Required. Probe sets to be processed." + echo " -s Required. Sample sets to be processed." + echo " -w Absolute path to MIPWrangler run script. " + echo " Default: '/opt/bin/runMIPWranglerCurrent.sh'." + echo " -x Required. Probe set to be processed." + echo "" + echo "Examples:" + echo " $ singularity run --app wrangler \\" + echo " -e experiment_id -l sample_list.file -p probe_sets \\" + echo " -s sample_sets -x stitch_options" + echo "" + echo " $ singularity run --app wrangler \\" + echo " -c cpu_count -e experiment_id -l sample_list.file \\" + echo " -m min_capture_length -p probe_sets -s sample_sets \\" + echo " -x stitch_options -k" + } # Set defaults cluster_script="runMIPWranglerCurrent.sh" @@ -289,44 +326,31 @@ From: amd64/ubuntu:20.04 keep_files="" # Parse options - while getopts p:l:e:s:w:n:c:x:m:k OPT; do - case "$OPT" in - e) - experiment_id="$OPTARG";; - l) - sample_list="$OPTARG";; - p) - probe_sets="$OPTARG";; - s) - sample_sets="$OPTARG";; - w) - cluster_script="$OPTARG";; - n) - server_number="$OPTARG";; - c) - cpu_count="$OPTARG";; - x) - stitch_options="$OPTARG";; - k) - keep_files="-k";; - - m) - min_capture_length="$OPTARG";; - - *) - echo "Invalid option. Use 'wrangler \ - -e experiment_id -l sample_list.file -p probe_sets\ - -s sample_sets -w cluster_script -n server_number \ - -c cpu_count -x stitch_options' \ - -m min_capture_length [-k]" + while getopts "c:e:hkl:m:n:p:s:w:x:" opt; do + case "${opt}" in + c) cpu_count="${OPTARG}" ;; + e) experiment_id="${OPTARG}" ;; + h) help + exit 1 ;; + k) keep_files="-k" ;; + l) sample_list="${OPTARG}" ;; + m) min_capture_length="${OPTARG}" ;; + n) server_number="${OPTARG}" ;; + p) probe_sets="${OPTARG}" ;; + s) sample_sets="${OPTARG}" ;; + w) cluster_script="${OPTARG}" ;; + x) stitch_options="${OPTARG}" ;; + *) help + exit 1 ;; esac done # Create wrangler bash scripts using python python /opt/src/generate_wrangler_scripts.py \ - -e $experiment_id -l /opt/analysis/$sample_list \ - -p $probe_sets -s $sample_sets -w $cluster_script -n $server_number \ - -c $cpu_count -x $stitch_options -m $min_capture_length $keep_files + -c ${cpu_count} -e ${experiment_id} ${keep_files} \ + -l /opt/analysis/${sample_list} -m ${min_capture_length} \ + -n ${server_number} -p ${probe_sets} -s ${sample_sets} \ + -w ${cluster_script} -x ${stitch_options} # Run wrangler scripts. # The dot space is used to let the sourced script modify the current From c385952faa0e8465e49234b5974ae735b68701ea Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Sat, 6 Nov 2021 10:19:39 -0400 Subject: [PATCH 020/189] Add `-h` flag to the jupyter app Part of the effort in #17 --- MIPTools.def | 71 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/MIPTools.def b/MIPTools.def index a46d7aa..d5f2705 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -226,9 +226,8 @@ From: amd64/ubuntu:20.04 ## Jupyter App ## ################################################################# %apprun jupyter - # Modify shell behavior - set -e - set -u + # Exit if something fails or if have unset object + set -eu # Port forwarding setup nb_port=$(shuf -i 8000-9999 -n 1) @@ -236,16 +235,49 @@ From: amd64/ubuntu:20.04 server_user=$(whoami)@$(hostname -f) nb_dir=/opt + help() { + echo "Open an interactive Jupyter Notebook. The notebook can be used" + echo "for post-wrangler mapping and variant calling." + echo "" + echo "Usage:" + echo " singularity run [options] --app jupyter "\ + "[app_options]" + echo "" + echo "Options:" + echo " See 'singularity run'." + echo "" + echo "App Options:" + echo " -d The port to be used to load the Jupyter Notebook." + echo " -h Print the help page." + echo " -p The notebook directory." + echo "" + echo "Examples:" + echo " # Set paths" + echo " $ resource_dir=/bin/MIPTools/base_resources" + echo " $ project_resources=/work/usr/DR1_project_resources" + echo " $ species_resources=/work/usr/pf_species_resources" + echo " $ wrangler_dir=/work/usr/wrangler" + echo " $ variant_dir=/work/usr/variant" + echo "" + echo " # Run app" + echo " $ singularity run \\" + echo " -B \${resource_dir}:/opt/resources \\" + echo " -B \${project_resources}:/opt/project_resources \\" + echo " -B \${species_resources}:/opt/species_resources \\" + echo " -B \${wrangler_dir}:/opt/data \\" + echo " -B \${variant_dir}:/opt/analysis \\" + echo " --app jupyter " + } + # Parse options - while getopts p:d: OPT; do - case "$OPT" in - p) - nb_port="$OPTARG";; - d) - nb_dir="$OPTARG";; - *) - echo "Invalid option. Use -p to specify notebook port \ - -d to specify notebook directory." + while getopts "d:hp:" opt; do + case ${opt} in + d) nb_dir=${OPTARG} ;; + h) help + exit 1 ;; + p) nb_port=${OPTARG} ;; + *) help + exit 1 ;; esac done @@ -253,12 +285,11 @@ From: amd64/ubuntu:20.04 rsync /opt/resources/*.ipynb /opt/analysis --ignore-existing \ --ignore-missing-args - # Inform the user how to acess the notebook - port_fw="Use the following command if you are running this notebook from " - port_fw=$port_fw"a remote server. Ignore if using a local computer." - echo $port_fw - port_fw="ssh -N -f -L localhost:$nb_port:$server_ip:$nb_port $server_user" - echo $port_fw + # Inform the user how to access the notebook + echo "Use the following command if you are running this notebook from a" + echo "remote server. Ignore if using a local computer." + echo "ssh -f -N -L localhost:${nb_port}:${server_ip}:${nb_port}"\ + "${server_user}" # Setup juptyr notebook settings jupyter nbextension enable plotlywidget/extension @@ -269,8 +300,8 @@ From: amd64/ubuntu:20.04 jupyter nbextension enable spellchecker/main # Run notebook - jupyter notebook --notebook-dir=$nb_dir --ip=$server_ip \ - --port=$nb_port --no-browser + jupyter notebook --notebook-dir=${nb_dir} --ip=${server_ip} \ + --port=${nb_port} --no-browser ################################################################## ## Wrangler App ## From 600121ff75104e044e18350bdb3c96a97423feb2 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Sat, 6 Nov 2021 10:20:24 -0400 Subject: [PATCH 021/189] Style code Closes #17 --- MIPTools.def | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/MIPTools.def b/MIPTools.def index d5f2705..023999e 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -358,19 +358,19 @@ From: amd64/ubuntu:20.04 # Parse options while getopts "c:e:hkl:m:n:p:s:w:x:" opt; do - case "${opt}" in - c) cpu_count="${OPTARG}" ;; - e) experiment_id="${OPTARG}" ;; + case ${opt} in + c) cpu_count=${OPTARG} ;; + e) experiment_id=${OPTARG} ;; h) help exit 1 ;; - k) keep_files="-k" ;; - l) sample_list="${OPTARG}" ;; - m) min_capture_length="${OPTARG}" ;; - n) server_number="${OPTARG}" ;; - p) probe_sets="${OPTARG}" ;; - s) sample_sets="${OPTARG}" ;; - w) cluster_script="${OPTARG}" ;; - x) stitch_options="${OPTARG}" ;; + k) keep_files=-k ;; + l) sample_list=${OPTARG} ;; + m) min_capture_length=${OPTARG} ;; + n) server_number=${OPTARG} ;; + p) probe_sets=${OPTARG} ;; + s) sample_sets=${OPTARG} ;; + w) cluster_script=${OPTARG} ;; + x) stitch_options=${OPTARG} ;; *) help exit 1 ;; esac @@ -406,8 +406,8 @@ From: amd64/ubuntu:20.04 echo " See 'singularity run'." echo "" echo "App Options:" - echo " -r Required. The run ID of the data to download." echo " -h Print the help page." + echo " -r Required. The run ID of the data to download." echo "" echo "Additional Details:" echo " An 'access_token.txt' file with a valid access token is" @@ -427,11 +427,11 @@ From: amd64/ubuntu:20.04 echo " --app download -r " } - while getopts "r:h" opt; do + while getopts "hr:" opt; do case ${opt} in - r) run_id=${OPTARG} ;; h) help exit 1 ;; + r) run_id=${OPTARG} ;; *) help exit 1 ;; esac @@ -471,12 +471,12 @@ From: amd64/ubuntu:20.04 echo " See 'singularity run'." echo "" echo "App Options:" + echo " -h Print the help page." + echo " -p Required. The sequencing platform used. Either 'miseq'" + echo " or 'nextseq'." echo " -s Required. The list of samples. Contains the samples used" echo " in the study, the primers used, etc. This file must be " echo " present in the directory mounted to '/opt/analysis'." - echo " -p Required. The sequencing platform used. Either 'miseq'" - echo " or 'nextseq'." - echo " -h Print the help page." echo "" echo "Examples:" echo " # Set paths" @@ -492,12 +492,12 @@ From: amd64/ubuntu:20.04 echo " --app demux -s sample_list.tsv -p nextseq" } - while getopts "s:p:h" opt; do + while getopts "hp:s:" opt; do case ${opt} in - s) sample_list=${OPTARG} ;; - p) platform=${OPTARG} ;; h) help exit 1 ;; + p) platform=${OPTARG} ;; + s) sample_list=${OPTARG} ;; *) help exit 1 ;; esac @@ -557,9 +557,9 @@ From: amd64/ubuntu:20.04 echo " See 'singularity run'." echo "" echo "App Options:" + echo " -h Print the help page." echo " -p Required. The sequencing platform used. Either 'miseq'" echo " or 'nextseq'." - echo " -h Print the help page." echo "" echo "Examples:" echo " # Set paths" @@ -574,11 +574,11 @@ From: amd64/ubuntu:20.04 } # Argument handling - while getopts "p:h" opt; do + while getopts "hp:" opt; do case ${opt} in - p) platform=${OPTARG} ;; h) help exit 1 ;; + p) platform=${OPTARG} ;; *) help exit 1 ;; esac From cc84f66c64f9292c774aedc905b1d880eeea8d82 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Sat, 6 Nov 2021 14:48:11 -0400 Subject: [PATCH 022/189] Modify project version and add doc badge --- .gitignore | 3 +++ MIPTools.def | 2 +- README.md | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index cb8a825..07bfd30 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ miptools/ build_logs/ src/__pycache__/ base_resources/.ipynb_checkpoints/ +!docs/** +docs/.DS_store +docs/_build diff --git a/MIPTools.def b/MIPTools.def index 023999e..2e0b7d9 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -6,7 +6,7 @@ From: amd64/ubuntu:20.04 ################################################################## %labels Author Bailey Lab - Version v1.0.0.9000 + Version v0.4.0.9000 ################################################################## ## Post Section ## diff --git a/README.md b/README.md index 7c76d1d..3272157 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # MIPTools [![Build Singularity](https://github.com/bailey-lab/MIPTools/actions/workflows/build-container.yaml/badge.svg)](https://github.com/bailey-lab/MIPTools/actions/workflows/build-container.yaml) +[![Documentation Status](https://readthedocs.org/projects/miptools/badge/?version=latest)](https://miptools.readthedocs.io/en/latest/?badge=latest) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/bailey-lab/MIPTools) -![GitHub](https://img.shields.io/github/license/bailey-lab/MIPTools) +![License](https://img.shields.io/github/license/bailey-lab/MIPTools) MIPTools is a suite of computational tools that are used for molecular inversion probe design, data processing, and analysis. From 796563ebe9187aecab69ea063d0dc470624df0d2 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Sat, 6 Nov 2021 14:50:09 -0400 Subject: [PATCH 023/189] Add basic web documentation --- .readthedocs.yaml | 26 ++++++++ docs/Makefile | 20 ++++++ docs/conf.py | 50 ++++++++++++++ docs/index.rst | 20 ++++++ docs/installation.rst | 95 +++++++++++++++++++++++++++ docs/make.bat | 35 ++++++++++ docs/quick_start.rst | 149 ++++++++++++++++++++++++++++++++++++++++++ docs/requirements.txt | 4 ++ 8 files changed, 399 insertions(+) create mode 100644 .readthedocs.yaml create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/installation.rst create mode 100644 docs/make.bat create mode 100644 docs/quick_start.rst create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..f3d05ac --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,26 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-20.04 + tools: + python: "3.10" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +formats: + - pdf + - epub + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..f7428ac --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,50 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = "MIPTools" +copyright = "2021, Bailey Lab" +author = "Bailey Lab" + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ["myst_parser"] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "sphinx_rtd_theme" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..3905f56 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,20 @@ +====================== +MIPTools Documentation +====================== + +Welcome to the MIPTools User Guide! + +MIPTools is a suite of computational tools that are used for molecular +inversion probe design, data processing, and analysis. + +.. toctree:: + :maxdepth: 2 + + installation + quick_start + +Need Help? +---------- + +- See our `Github Issues Page + `_ diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..c67d0ee --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,95 @@ +.. _installation: + +============ +Installation +============ + +Dependencies +============ + +A working copy of Singularity is required: https://www.sylabs.io/docs/. +Singularity is best installed with **sudo**. While it is said to be possible to +install with unprivileged user with some features missing, MIPTools hasn't been +tested on such an installation. + +Singularity is available for most Linux systems. It is also possible to install +and use on Mac OS using virtual machines with a little bit of extra work. + +Note that the :code:`snap` package is a rapid way to install the go language +required by Singularity (e.g. on Ubuntu/Debian: :code:`sudo snap install go +--classic`). Install system dependencies + +Quick Start +=========== +The MIPTools container, built and ready to use, can be +downloaded from the [Sylabs Cloud](https://cloud.sylabs.io/). You can download +either the development version or the most recent stable release: + +.. code-block:: bash + + # Download the development version + # The development version is updated every two weeks + singularity pull library://apascha1/miptools/miptools:dev + + # Download the latest stable release + singularity pull library://apascha1/miptools/miptools:v1.0.0 + + +Note that these prebuilt versions do not include the :code:`bcl2fastq` software +due to its license. If you plan to use MIPTools to demultiplex bcl files, you +must build the container yourself. + +Install From Source +=================== +MIPTools can also be built from source code using the definition file provided +in this [GitHub repository](https://github.com/bailey-lab/MIPTools). + +The process can take about 10-30 minutes to build, depending on the number of +CPU cores available. By default, the build process will use 6 CPU cores. This +should pose no problems with most modern computers, but if the computer used +for building the container has less then 6 cpu cores available, change the +:code:`"CPU_COUNT=6"` value at the top of the :code:`MIPTools.def` file to a +suitable number before running the following code. On the other hand, if +you have access to more CPU power, by all means, use them by setting the +same parameter to a higher value. + +You must have **sudo** privelege to _build_ the image. You do not need sudo to +_use_ the image. So if you want to run the container on an environment without +sudo, either download a prebuilt image (see above) or build the container on +your own machine where you _do_ have sudo privilege and copy the image file to +the computer without sudo. Note that the Singularity program itself must have +been installed with sudo. + +If you plan to use MIPTools to demultiplex bcl files, you should download +:code:`bcl2fastq` separately. Currently, you can download it from +[here](https://support.illumina.com/downloads/bcl2fastq-conversion-software-v2-20.html), +but this may change in the future. You must download the file: :code:`bcl2fastq2 +Conversion Software v2.20 Installer (Linux rpm)` and place it in the +:code:`MIPTools/programs` directory. + +You can install the most recent release using the following: + +.. code-block:: bash + + # Install stable version v1.0.0 + git clone --b v1.0.0 https://github.com/bailey-lab/MIPTools.git + + +You can alternatively install the development version: + +.. code-block:: bash + + # Install dev version + git clone https://github.com/bailey-lab/MIPTools.git + +Next, simply build the container and you should be all set to get started using +MIPTools! + +.. code-block:: bash + + cd MIPTools + sudo singularity build miptools.sif MIPTools.def + +:code:`miptools.sif` is a single **portable** file which has all the programs +needed for MIP design, data analysis, and a lot more. More information +about the extra programs and their uses will be added over time. diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..153be5e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/quick_start.rst b/docs/quick_start.rst new file mode 100644 index 0000000..86dfc74 --- /dev/null +++ b/docs/quick_start.rst @@ -0,0 +1,149 @@ +.. _quick_start: + +=========== +Quick Start +=========== + +Although :code:`miptools.sif` contains all programs needed, it does not include +the data to be analyzed or other resources to be used. Every time we run +Singularity we will **bind** needed directories to the container. There are +three resources directories which are required for most operations. In addition +to those, some apps need a :code:`data_dir` and :code:`analysis_dir`. The +:code:`-B` option is used for each binding: + +.. code-block:: bash + + singularity some-command -B path_on_host:path_on_container path_to_container + +The path on the left side of the colon specifies where on *your* computer the +directory is and the right side is the location in the container where the +directory should be bound (mounted) to. You should only change the left side of +the column according to the location of the resource you are providing, and +should *never* change the path on the right side. Each binding is specified +with a separate :code:`-B` option. See below for examples. + +Directory Structure +=================== + +Three resource directories are required for most operations. These live outside +the container and must be **bound** to the container at run time with the +:code:`-B` option. In addition, a data directory and an analysis directory will +be used for most operations. + + - **base_resources:** Provided in the GitHub repository. It contains common + resources across projects. It should be bound to the container with + :code:`-B [path to base resources container]:/opt/resources`. This makes the + base_resources directory available to the container and it would be reached + at :code:`/opt/resources` path within the container. The + :code:`/opt/resources` part of this argument must not be altered. For + example, if my base resources are located in my computer at + :code:`/home/base`, I would bind it to the container with :code:`-B + /home/base:/opt/resources`. + + - **species_resources:** Contains resources shared by projects using the same + target species (Pf, human, etc.). Bind this to + :code:`/opt/species_resources` in the container. For example, if I am + working with *Plasmodium falciparum* sequences and I have the necessary + files in my computer at :code:`/home/pf3d/`, then the binding parameter is + :code:`-B /home/pf3d:/opt/species_resources`. The directory contains the + following contents: + + - *file_locations.tsv*: This file is required for all operations. It is a + tab separated text file showing where each required file will be + located in the container. Each line corresponds to one file. First + field states the species for the file, second field states what kind of + file it is and the last field is the absolute path to the file. + + For example, the line + *"pf fasta_genome /opt/species_resources/genomes/genome.fa"* would mean + that the fasta genome file for the species 'pf' will be found at + :code:'/opt/species_resources/genomes/genome.fa' within the container. + This also means that there is a file at + :code:`/home/pf3d/genomes/genome.fa` in my computer, assuming I bound + :code:`/home/pf3d` to :code:`/opt/species_resources` in the container. + + - *fasta file*: This file is required for all operations. Genome + reference sequence in fasta format. + + - *bowtie2_genome*: This file is required for probe design operations + only. It is the reference genome indexed using bowtie2. If this is not + available, it can be generated using MIPTools. + + - *bwa_genome*: This file is required for data analysis operations only. + It is the reference genome indexed using bwa. If this is not available, + it can be generated using MIPTools. + + - *snps*: This is an optional file. However, it is extremely useful in + probe designs to avoid probe arms landing on variant regions, etc. So + it should always be used except in rare cases where such a file is not + available for the target species. The format of the file is vcf. + Individual genotypes are not necessary (a.k.a. sites only vcf). The + only requirement is that the INFO field for each variant has a field + showing the population allele frequency of alternate alleles. By + default, AF field is used. The AF field lists the allele frequencies of + each alternate allele, and does not list the frequency of the reference + allele. Vcf files may have other INFO fields that include allele + frequency information. If such a field is to be used, there are two + settings in the design settings file (.rinfo file) that must be + modified. *allele_frequency_name* field must be set to the INFO field + name to be used; *af_start_index* may have to be set to a 1 (instead of + default 0) depending on whether the reference allele frequency is + provided in the new field. For example, if we want to use the 1000 + genomes vcf file, the allele frequencies are provided in the CAF field + and they include the reference allele. We would have to change the + *allele_frequency_name* field to *CAF* from the default *AF*; and set + *af_start_index* to 1 because the first alternate allele's frequency is + provided in the second place (following the reference allele). + + - *refgene*: RefGen style gene/gene prediction table in GenePred format. + These are available at http://genome.ucsc.edu under Tools/Table Browser + for most species. The fields in the file are "bin, name, chrom, strand, + txStart, txEnd, cdsStart, cdsEnd, exonCount, exonStarts, exonEnds, + score, name2, cdsStartStat, cdsEndStat, exonFrames". This file is + required for probe design operations if genic information is to be + used. For example, if probes need to be designed for exons of a gene, + or a gene name is given as design target. If a gene name will be + provided, it must match the **name2** column of the RefGen file. If you + are creating this file manually, the only fields necessary are: chrom, + strand, exonStarts, exonEnds and name2. All other fields can be set to + an arbitrary value (none, for example) but not left empty. The order of + columns must not be changed. + + Note: If you have gff3/gtf formatted files, they can be converted to + GenePred format using Jim Kent's programs [gff3ToGenePred] + (http://hgdownload.cse.ucsc.edu/admin/exe/linux.x86_64/gff3ToGenePred) + and + [gtfToGenePred](http://hgdownload.cse.ucsc.edu/admin/exe/linux.x86_64/gtfToGenePred). + + - *refgene_tabix*: RefGen file, sorted and indexed using tabix. File + requirement is the same as the refgene file. tabix is available within + the MIPTools container, so you don't have to install it yourself. + + + - **project_resources:** Contains project specific files. Bind this to + :code:`/opt/project_resources`. + + - **data_dir:** Contains data to be analyzed. Typically, nothing will be + written to this directory. Bind this directory to :code:`/opt/data`. + + - **analysis_dir:** Where analysis will be carried out and all output files + will be saved. Bind it to :code:`/opt/analysis` This is the only directory + that needs write permission as the output will be saved here. + +:code:`data_dir` and :code:`analysis_dir` will have different content for +different app operations. Also, one app's analysis directory may be the +next app's data directory in the pipeline. + +Resource Requirements +===================== + +Resources required vary widely depending on the project. Both designs and data +analysis can be parallelized, so the more CPUs you have the better. Plenty of +storage is also recommended. For designs on large target regions (>5kb), files +can take up 10 GB or more per region. Consider allocating > 5 GB RAM for a +large design region (multiply the RAM requirement by CPU number if +parallelizing). For a typical MIP data analysis involving ~1000 MIPs and ~1000 +samples, consider using at least 20 CPUs and 20 GB RAM to get the analysis done +within 10-12 h. You should expect ~200 GB disk space used for such an analysis +as well, although most files can be removed after processing steps to reduce +long term disk usage. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..63a8808 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +# Defining the exact version will make sure things don't break +sphinx==3.4.3 +sphinx_rtd_theme==0.5.1 +readthedocs-sphinx-search==0.1.0 \ No newline at end of file From 7b9c9e5e0d9e0c29c66fbe5c7288e343b3f80c67 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Sat, 6 Nov 2021 14:57:55 -0400 Subject: [PATCH 024/189] Remove extension Docs will be written in .rst and not .md --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index f7428ac..ec79d13 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["myst_parser"] +extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] From e9af3916911f782a4acafeac4b71ee234bb54783 Mon Sep 17 00:00:00 2001 From: Jeff Bailey Date: Sat, 6 Nov 2021 23:00:47 -0400 Subject: [PATCH 025/189] Update installation.rst --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index c67d0ee..bfa0f73 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -12,7 +12,7 @@ Singularity is best installed with **sudo**. While it is said to be possible to install with unprivileged user with some features missing, MIPTools hasn't been tested on such an installation. -Singularity is available for most Linux systems. It is also possible to install +Singularity is available for most Linux systems including high-performance clusters. It is also possible to install and use on Mac OS using virtual machines with a little bit of extra work. Note that the :code:`snap` package is a rapid way to install the go language From 888a3829d7183f00d61e4b10153dab3d07227b40 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 8 Nov 2021 12:19:41 -0500 Subject: [PATCH 026/189] Use Github Pages to deploy docs --- .github/workflows/deploy-docs.yaml | 58 ++++++++++++++++++++++++++++++ docs/conf.py | 2 +- docs/requirements.txt | 3 +- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/deploy-docs.yaml diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml new file mode 100644 index 0000000..011aff3 --- /dev/null +++ b/.github/workflows/deploy-docs.yaml @@ -0,0 +1,58 @@ +name: Deploy Docs + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - "docs/**" + +jobs: + deploy-docs: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: "3.8" + + - name: Upgrade pip + run: | + # install pip=>20.1 to use "pip cache dir" + python3 -m pip install --upgrade pip + + - name: Get pip cache dir + id: pip-cache + run: echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: python3 -m pip install -r docs/requirements.txt + + - name: Build docs + run: cd docs && make html + + - name: Upload artifact + uses: actions/upload-artifact@v2.2.4 + with: + name: html-docs + path: docs/_build/html/ + + - name: Deploy to gh-pages + uses: JamesIves/github-pages-deploy-action@4.1.5 + with: + branch: gh-pages + folder: docs/_build/html + commit-message: Update documentation from latest commit diff --git a/docs/conf.py b/docs/conf.py index ec79d13..e8077c6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [] +extensions = ["sphinx.ext.githubpages"] # add .nojekyll to gh-pages # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/requirements.txt b/docs/requirements.txt index 63a8808..b097243 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ # Defining the exact version will make sure things don't break sphinx==3.4.3 sphinx_rtd_theme==0.5.1 -readthedocs-sphinx-search==0.1.0 \ No newline at end of file +readthedocs-sphinx-search==0.1.0 +docutils==0.16 # see https://github.com/sphinx-doc/sphinx/issues/9049 \ No newline at end of file From c57f8b938e2de211a91b24d856a942b0e738de4e Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 8 Nov 2021 12:59:50 -0500 Subject: [PATCH 027/189] Allow for docs to be written in MyST --- docs/conf.py | 7 +++++-- docs/requirements.txt | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e8077c6..e64c10b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,10 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["sphinx.ext.githubpages"] # add .nojekyll to gh-pages +extensions = [ + "sphinx.ext.githubpages", # add .nojekyll to gh-pages + "myst_parser", # write docs using MyST (a flavor of markdown) +] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -35,7 +38,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "README.md"] # -- Options for HTML output ------------------------------------------------- diff --git a/docs/requirements.txt b/docs/requirements.txt index b097243..4ec7552 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,4 +2,5 @@ sphinx==3.4.3 sphinx_rtd_theme==0.5.1 readthedocs-sphinx-search==0.1.0 -docutils==0.16 # see https://github.com/sphinx-doc/sphinx/issues/9049 \ No newline at end of file +docutils==0.16 # see https://github.com/sphinx-doc/sphinx/issues/9049 +myst_parser==0.15.2 # write docs using MyST (a flavor of markdown) \ No newline at end of file From 3cd3c42294eefe3de4861a7c9669d4ab15914f0b Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 8 Nov 2021 13:00:25 -0500 Subject: [PATCH 028/189] Update README --- README.md | 2 +- docs/README.md | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 docs/README.md diff --git a/README.md b/README.md index 3272157..e1a7157 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # MIPTools [![Build Singularity](https://github.com/bailey-lab/MIPTools/actions/workflows/build-container.yaml/badge.svg)](https://github.com/bailey-lab/MIPTools/actions/workflows/build-container.yaml) -[![Documentation Status](https://readthedocs.org/projects/miptools/badge/?version=latest)](https://miptools.readthedocs.io/en/latest/?badge=latest) +[![Deploy Docs](https://github.com/bailey-lab/MIPTools/actions/workflows/deploy-docs.yaml/badge.svg)](https://github.com/bailey-lab/MIPTools/actions/workflows/deploy-docs.yaml) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/bailey-lab/MIPTools) ![License](https://img.shields.io/github/license/bailey-lab/MIPTools) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4ea1cb7 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,16 @@ +# MIPTools Documentation + +[![Deploy Docs](https://github.com/bailey-lab/MIPTools/actions/workflows/deploy-docs.yaml/badge.svg)](https://github.com/bailey-lab/MIPTools/actions/workflows/deploy-docs.yaml) +![GitHub release (latest +SemVer)](https://img.shields.io/github/v/release/bailey-lab/MIPTools) +![License](https://img.shields.io/github/license/bailey-lab/MIPTools) + +This folder contains the source code for the MIPTools documentation. Our +documentation can either be written in +[rst](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html) +or [MyST](https://myst-parser.readthedocs.io/en/latest/index.html) (a extension +of [markdown](https://www.markdownguide.org/)), both simple markup languages. +We use the documentation tool, +[sphinx](https://www.sphinx-doc.org/en/master/index.html), to build our +documentation, and host our final product on [Github +Pages](https://bailey-lab.github.io/MIPTools/). From a675a9d31714a5dd11fd8a9fa9cc295e4695fc61 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 8 Nov 2021 13:29:39 -0500 Subject: [PATCH 029/189] Add edit on github link --- docs/conf.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index e64c10b..9ededeb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,6 +20,7 @@ project = "MIPTools" copyright = "2021, Bailey Lab" author = "Bailey Lab" +version = "v0.4.0.9000" # -- General configuration --------------------------------------------------- @@ -51,3 +52,10 @@ # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] + +html_context = { + "display_github": True, + "github_user": "bailey-lab", + "github_repo": "MIPTools", + "github_version": "master/docs/", +} From 0aa72530acee5e7ca62285ebbc3d74ef159bb8f2 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 8 Nov 2021 13:31:10 -0500 Subject: [PATCH 030/189] Add copy button to code chunks --- docs/conf.py | 1 + docs/requirements.txt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 9ededeb..8b2ff34 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,6 +31,7 @@ extensions = [ "sphinx.ext.githubpages", # add .nojekyll to gh-pages "myst_parser", # write docs using MyST (a flavor of markdown) + "sphinx_copybutton", # add copy button to code chunks ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/requirements.txt b/docs/requirements.txt index 4ec7552..88a04ce 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,4 +3,5 @@ sphinx==3.4.3 sphinx_rtd_theme==0.5.1 readthedocs-sphinx-search==0.1.0 docutils==0.16 # see https://github.com/sphinx-doc/sphinx/issues/9049 -myst_parser==0.15.2 # write docs using MyST (a flavor of markdown) \ No newline at end of file +myst_parser==0.15.2 # write docs using MyST (a flavor of markdown) +sphinx_copybutton==0.4.0 # copy button \ No newline at end of file From 9447637c9bdfe8f52480a42f407584624255a9bb Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 8 Nov 2021 13:32:01 -0500 Subject: [PATCH 031/189] Update changelog --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7cb101..3abd6b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,17 @@ ### Documentation Overhaul +- Generate online documentation using + [Sphinx](https://www.sphinx-doc.org/en/master/index.html) and [Github + Pages](https://pages.github.com/). +- Add better documentation for the `jupyter` app. +- Add better documentation for the `wrangler` app. +- Add better documentation for the `download` app. - Add better documentation for the `demux_qc` app. - Add doc-strings to python functions. - Improve clarity of README and add additional instructions on downloading or building the container. -## MIPTools 1.0.0 +## MIPTools 0.4.0 -- First major release. +- Latest stable build. From 83e3bd234782242a43ab8573003890d6b5627b32 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 8 Nov 2021 13:33:55 -0500 Subject: [PATCH 032/189] Fix docs workflow --- .github/workflows/deploy-docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml index 011aff3..5e39921 100644 --- a/.github/workflows/deploy-docs.yaml +++ b/.github/workflows/deploy-docs.yaml @@ -3,7 +3,7 @@ name: Deploy Docs on: workflow_dispatch: push: - branches: [main] + branches: [main, master] paths: - "docs/**" From 4d3fe4429d1baecd0bb7c41f246b7e0d98b84ecd Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Wed, 10 Nov 2021 15:33:13 -0500 Subject: [PATCH 033/189] Add issue templates --- .github/ISSUE_TEMPLATE/bug-report.md | 27 +++++++++++++++++ .github/ISSUE_TEMPLATE/config.yaml | 5 ++++ .github/ISSUE_TEMPLATE/feature-request.md | 35 +++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..ea0470a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,27 @@ +--- +name: Bug Report +about: Report an error or unexpected behavior +title: "" +labels: bug +assignees: "" +--- + +### Bug Description + + + +### Expected Behavior + + + +### Reprex + + diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 0000000..e2c9c12 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: 💬 MIPTools Google Group + url: https://groups.google.com/g/miptools + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..cd8f618 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,35 @@ +--- +name: Feature Request +about: Suggest an idea or feature +title: "" +labels: "feature :sparkles:" +assignees: "" +--- + +### Related Problem + + + +### Solution Requested + + + + From 98d810ab77d861a82d54590db2c1ec580a433f67 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Wed, 10 Nov 2021 15:35:25 -0500 Subject: [PATCH 034/189] Update issue templates --- .github/ISSUE_TEMPLATE/{config.yaml => config.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{config.yaml => config.yml} (100%) diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/config.yaml rename to .github/ISSUE_TEMPLATE/config.yml From b403d6a98148d1eb7c072b5c68095a9e8e8cd1b3 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Wed, 10 Nov 2021 15:36:55 -0500 Subject: [PATCH 035/189] Update issue templates --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- .github/ISSUE_TEMPLATE/feature-request.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index ea0470a..f90f68a 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,6 +1,6 @@ --- name: Bug Report -about: Report an error or unexpected behavior +about: Report an error or unexpected behavior. title: "" labels: bug assignees: "" diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index cd8f618..5d8a59e 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -1,6 +1,6 @@ --- name: Feature Request -about: Suggest an idea or feature +about: Suggest an idea or feature. title: "" labels: "feature :sparkles:" assignees: "" From e0b3d79fe57749c10c557f02dbcbf03fbd2ec94b Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 15 Nov 2021 11:15:49 -0500 Subject: [PATCH 036/189] Update deploy docs workflow --- .github/workflows/deploy-docs.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml index 5e39921..0e9d369 100644 --- a/.github/workflows/deploy-docs.yaml +++ b/.github/workflows/deploy-docs.yaml @@ -6,6 +6,10 @@ on: branches: [main, master] paths: - "docs/**" + pull_request: + branches: [main, master] + paths: + - "docs/**" jobs: deploy-docs: @@ -55,4 +59,4 @@ jobs: with: branch: gh-pages folder: docs/_build/html - commit-message: Update documentation from latest commit + commit-message: Update docs site From 2bdd8010a64deebe20b5825160608273a4b107f1 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Wed, 15 Dec 2021 17:09:04 -0500 Subject: [PATCH 037/189] Change frequency of reminder --- .github/workflows/notify-maintainers.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/notify-maintainers.yaml b/.github/workflows/notify-maintainers.yaml index 1d62b96..a4159c8 100644 --- a/.github/workflows/notify-maintainers.yaml +++ b/.github/workflows/notify-maintainers.yaml @@ -1,7 +1,7 @@ name: Notify Maintainers on: schedule: - - cron: 0 12 1,15 * * + - cron: 0 12 1 * * workflow_dispatch: From 73cf66b072d44a6f627f3605828f917b99d58c5a Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Wed, 15 Dec 2021 17:09:49 -0500 Subject: [PATCH 038/189] Don't deploy to `gh-pages` on PRs --- .github/workflows/deploy-docs.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml index 0e9d369..e440697 100644 --- a/.github/workflows/deploy-docs.yaml +++ b/.github/workflows/deploy-docs.yaml @@ -55,6 +55,7 @@ jobs: path: docs/_build/html/ - name: Deploy to gh-pages + if: github.event_name != 'pull_request' uses: JamesIves/github-pages-deploy-action@4.1.5 with: branch: gh-pages From 0354b792a4d90217f9f8bd7662af759df33ad8bb Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Sun, 6 Feb 2022 16:57:26 -0500 Subject: [PATCH 039/189] Update license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 27ca53b..d87bf8b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Ozkan Aydemir +Copyright (c) 2022 Bailey Lab Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From fb6ab0213b6a2a3a1190cd3e2cf0678711b3301b Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Sun, 6 Feb 2022 17:05:23 -0500 Subject: [PATCH 040/189] Remove dev definition file --- MIPTools-dev.def | 338 ----------------------------------------------- 1 file changed, 338 deletions(-) delete mode 100644 MIPTools-dev.def diff --git a/MIPTools-dev.def b/MIPTools-dev.def deleted file mode 100644 index f9b32b5..0000000 --- a/MIPTools-dev.def +++ /dev/null @@ -1,338 +0,0 @@ -Bootstrap: docker -From: amd64/ubuntu:20.04 - -%post - # set number of cpus to use in build - CPU_COUNT=20 - # set build environment - export DEBIAN_FRONTEND=noninteractive \ - CONDA_DIR=/opt/conda \ - SHELL=/bin/bash \ - LANG=en_US.UTF-8 \ - LANGUAGE=en_US.UTF-8 \ - LC_ALL=en_US.UTF-8 \ - MINICONDA_VERSION=4.8.3 - export PATH=$CONDA_DIR/bin:$PATH - - # install system packages - apt-get update \ - && apt-get -yq dist-upgrade \ - && apt-get install -yq --no-install-recommends \ - wget \ - bzip2 \ - ca-certificates \ - sudo \ - locales \ - fonts-liberation \ - fonts-dejavu \ - git \ - build-essential \ - gcc \ - openssh-client \ - nano \ - libtbb-dev \ - libz-dev \ - libxrender1 \ - cmake \ - automake \ - autoconf \ - rsync \ - pigz \ - perl-tk \ - less \ - software-properties-common \ - libxext6 \ - libxrender1 \ - ghostscript \ - openjdk-11-jdk \ - liblzma-dev \ - libbz2-dev \ - libssl-dev \ - libcurl4-gnutls-dev \ - alien \ - unzip \ - tree \ - pandoc - - # set environment locale - echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen - echo "LANG=en_US.UTF-8" > /etc/locale.conf - echo "LC_ALL=en_US.UTF-8" >> /etc/environment - echo "LANGUAGE=en_US.UTF-8" >> /etc/environment - locale-gen en_US.UTF-8 - update-locale LANG=en_US.UTF-8 - - # install bcl2fastq, if the file is there - cd /opt/programs - unzip bcl2fastq2*.zip || true - alien bcl2fastq2*.rpm || true - dpkg -i bcl2fastq2*.deb || true - - # install msa2vcf - cd /opt/programs - git clone https://github.com/lindenb/jvarkit.git - cd jvarkit - ./gradlew msa2vcf - - # install conda - cd /tmp && \ - wget --quiet https://repo.continuum.io/miniconda/Miniconda3-py38_${MINICONDA_VERSION}-Linux-x86_64.sh && \ - echo "d63adf39f2c220950a063e0529d4ff74 *Miniconda3-py38_${MINICONDA_VERSION}-Linux-x86_64.sh" | md5sum -c - && \ - /bin/bash Miniconda3-py38_${MINICONDA_VERSION}-Linux-x86_64.sh -f -b -p $CONDA_DIR && \ - rm Miniconda3-py38_${MINICONDA_VERSION}-Linux-x86_64.sh && \ - $CONDA_DIR/bin/conda config --add channels defaults && \ - $CONDA_DIR/bin/conda config --add channels bioconda && \ - $CONDA_DIR/bin/conda config --add channels conda-forge && \ - $CONDA_DIR/bin/conda config --add channels r && \ - $CONDA_DIR/bin/conda config --system --set show_channel_urls true && \ - $CONDA_DIR/bin/conda install --quiet --yes conda="${MINICONDA_VERSION%.*}.*" && \ - conda clean -tipsy - - # install mamba - conda install mamba -c conda-forge - - # install conda packages using mamba - mamba install -qy\ - "gxx_linux-64" \ - "python" \ - "notebook" \ - "nbconvert" \ - "jupyter_contrib_nbextensions" \ - "xlrd" \ - "bcftools" \ - "samtools" \ - "vcftools" \ - "htslib" \ - "bwa" \ - "bowtie2" \ - "primer3" \ - "primer3-py" \ - "numpy" \ - "scipy" \ - "pysam" \ - "pandas" \ - "matplotlib" \ - "seaborn" \ - "scikit-learn" \ - "scandir" \ - "openpyxl" \ - "simplegeneric" \ - "matplotlib-venn" \ - "tblib" \ - "parallel" \ - "scikit-allel" \ - "bioconductor-dnacopy" \ - "basemap-data-hires" \ - "seqtk=1.3" \ - "freebayes" \ - "lastz" \ - "plotly" \ - "texlive-core" - - # install vt variant tool set - cd /opt/programs - git clone https://github.com/atks/vt.git - cd vt - git checkout 0.577 - make -j $CPU_COUNT - scp vt /opt/bin - - # install parasight - scp /opt/programs/parasight_v7.6/parasight.pl /opt/bin/parasight76.pl - - # add executable flag to executables - chmod -R +xr /usr/bin - chmod -R +xr /opt/bin - - - # create work and resources directories in /opt - mkdir /opt/work \ - /opt/project_resources \ - /opt/species_resources \ - /opt/data \ - /opt/analysis \ - /opt/host_species \ - /opt/extras - - -%files - programs /opt - bin /opt - src /opt - base_resources/ /opt/resources - -%environment - path=/opt/bin:/opt/conda/bin:/opt/programs/MIPWrangler/bin: - path=$path/opt/programs/elucidator/bin:/opt/programs/gatk: - path=$path$PATH - export PATH=$path - export XDG_RUNTIME_DIR="" - export DEBIAN_FRONTEND=noninteractive - export LANG=en_US.UTF-8 - export LANGUAduGE="en_US.UTF-8" - export LC_ALL="en_US.UTF-8" - -%apprun jupyter - set -e - set -u - nb_port=$(shuf -i 8000-9999 -n 1) - server_ip=$(hostname -i) - server_user=$(whoami)@$(hostname -f) - nb_dir=/opt - while getopts p:d: OPT; do - case "$OPT" in - p) - nb_port="$OPTARG";; - d) - nb_dir="$OPTARG";; - *) - echo "Invalid option. Use -p to specify notebook port \ - -d to specify notebook directory." - esac - done - rsync /opt/resources/*.ipynb /opt/analysis --ignore-existing \ - --ignore-missing-args - port_fw="Use the following command if you are running this notebook from " - port_fw=$port_fw"a remote server. Ignore if using a local computer." - echo $port_fw - port_fw="ssh -N -f -L localhost:$nb_port:$server_ip:$nb_port $server_user" - echo $port_fw - - jupyter nbextension enable plotlywidget/extension - jupyter nbextension enable toc2/main - jupyter nbextension enable codefolding/main - jupyter nbextension enable highlighter/highlighter - jupyter nbextension enable keyboard_shortcut_editor/main - jupyter nbextension enable spellchecker/main - - jupyter notebook --notebook-dir=$nb_dir --ip=$server_ip \ - --port=$nb_port --no-browser - -%apprun wrangler - set -e - set -u - # set defaults - cluster_script="runMIPWranglerCurrent.sh" - server_number=1 - cpu_count=1 - min_capture_length="none" - stitch_options="none" - keep_files="" - while getopts p:l:e:s:w:n:c:x:m:k OPT; do - case "$OPT" in - e) - experiment_id="$OPTARG";; - l) - sample_list="$OPTARG";; - p) - probe_sets="$OPTARG";; - s) - sample_sets="$OPTARG";; - w) - cluster_script="$OPTARG";; - n) - server_number="$OPTARG";; - c) - cpu_count="$OPTARG";; - x) - stitch_options="$OPTARG";; - k) - keep_files="-k";; - - m) - min_capture_length="$OPTARG";; - - *) - echo "Invalid option. Use 'wrangler \ - -e experiment_id -l sample_list.file -p probe_sets\ - -s sample_sets -w cluster_script -n server_number \ - -c cpu_count -x stitch_options' \ - -m min_capture_length [-k]" - esac - done - python /opt/src/generate_wrangler_scripts.py \ - -e $experiment_id -l /opt/analysis/$sample_list \ - -p $probe_sets -s $sample_sets -w $cluster_script -n $server_number \ - -c $cpu_count -x $stitch_options -m $min_capture_length $keep_files - . /opt/analysis/wrangle.sh - -%apprun download - set -e - set -u - while getopts r: opt; do - case $opt in - r) run_id=$OPTARG;; - ?) echo "Usage: singularity run --app download \\" - echo " -B /path_to_output_dir:/opt/analysis \\" - echo " -B /path_to_base_resources:/opt/resources \\" - echo " mycontainer.sif -r my_Illumina_run_ID" - echo "An 'access_token.txt' file with a valid access token is " - echo "required. It must be present in base_resources directory." - echo "A data directory where the data will be downloaded to" - echo "must be mounted to /opt/data." - exit 1;; - esac - done - echo "Downloading NextSeq run $run_id from BaseSpace." - echo "Depending on the data size, this can take very long (up to 10 h)" - echo "It is recommended to run this app in a screen (GNU screen)." - echo "A message indicating the end of download will be printed when done." - echo "Check nohup.out file in your output directory for the download log." - cd /opt/analysis - nohup python /opt/bin/BaseSpaceRunDownloader_v2.py \ - -r $run_id -a "$(cat /opt/resources/access_token.txt)" - echo "Download finished." - -%apprun demux - set -e - set -u - while getopts s:p: opt; do - case $opt in - s) sample_list=$OPTARG;; - p) platform=$OPTARG;; - ?) echo "Usage: singularity run --app demux \\" - echo " -B /path_to_run_dir:/opt/data \\" - echo " -B /path_to_output_dir:/opt/analysis \\" - echo " -B /path_to_base_resources:/opt/resources \\" - echo " mycontainer.sif -s sample_list_file \\" - echo " -p sequencing_platform (nextseq or miseq) \\" - echo "The sample list file must be present in the output" - echo "directory mounted to /opt/analysis." - exit 1;; - esac - done - # create a sample sheet for demultiplexing - cd /opt/src - template_dir="/opt/resources/templates/sample_sheet_templates/" - platform_template="$platform"_sample_sheet_template.csv - template="$template_dir$platform_template" - bc_dict="/opt/resources/barcode_dict.json" - output_dir="/opt/analysis" - sample_list="/opt/analysis/$sample_list" - python -c 'import mip_functions as mip; mip.generate_sample_sheet( - "'"$sample_list"'", "'"$bc_dict"'", "'"$template"'", "'"$platform"'", - "'"$output_dir"'")' - # cd to where bcl files are. - cd /opt/data - # create a fastq directory for saving fastqs - mkdir -p /opt/analysis/fastq - # increase limit of open number of files. - ulimit -Sn $(ulimit -Hn) - nohup bcl2fastq -o /opt/analysis/fastq \ - --sample-sheet /opt/analysis/SampleSheet.csv \ - --no-lane-splitting - -%apprun demux_qc - set -e - set -u - while getopts p: opt; do - case $opt in - p) platform=$OPTARG;; - ?) echo "Usage: singularity run --app demux_qc\\" - echo " -B /path_to_base_resources:/opt/resources \\" - echo " -B /path_to_fastq_dir:/opt/analysis " - echo " mycontainer.sif -p sequencing_platform" - exit 1;; - esac - done - python /opt/src/demux_qc.py -p $platform From 776e3558ae53c1e4bce9889ec06b56580a02caf6 Mon Sep 17 00:00:00 2001 From: Ozkan Aydemir Date: Mon, 14 Feb 2022 00:30:53 -0500 Subject: [PATCH 041/189] Update to sample sheet preparation addressing some issues arising from the generate_sample_sheets function from the mip_functions module. That function is removed now and generating the final sample sheet from the various input files (capture plates, legacy sample sheets etc.) is handled by the sample_sheet_prep.py script. demux app is also updated to reflect this change. demux app now takes the samplesheet.csv files generated by the sample_sheet_prep.py script, and do not run the old generate_sampe_sheets function from within the mip_functions module. demux app also does not need the sequencing platform anymore. This commit also moves the barcode dictionary file from the base_resources root directory to sample_prep subdirectory for a more cohesive organization. The update changes how the sample sheets are generated so that the error arising when the first or last column of a sample file was empty (when sample_name was not the first column, for example). This update also changes the behaviour of sample_sheet_prep.py script so that when the inputs contain invalid samples, fields etc. an error is raised. The old behaviour was to print a warning message which could lead to missing samples if these warning messages are overlooked. The input files should not contain any invalid information so the new behaviour makes sure that the input information is good. --- MIPTools.def | 58 ++--- .../barcode_dict.pickle} | 0 src/demux_qc.py | 2 +- src/mip_functions.py | 99 -------- src/sample_sheet_prep.py | 220 +++++++++++++----- 5 files changed, 177 insertions(+), 202 deletions(-) rename base_resources/{barcode_dict.json => sample_prep/barcode_dict.pickle} (100%) diff --git a/MIPTools.def b/MIPTools.def index 2e0b7d9..94be180 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -237,7 +237,7 @@ From: amd64/ubuntu:20.04 help() { echo "Open an interactive Jupyter Notebook. The notebook can be used" - echo "for post-wrangler mapping and variant calling." + echo "for post-wrangler mapping and variant calling." echo "" echo "Usage:" echo " singularity run [options] --app jupyter "\ @@ -285,7 +285,7 @@ From: amd64/ubuntu:20.04 rsync /opt/resources/*.ipynb /opt/analysis --ignore-existing \ --ignore-missing-args - # Inform the user how to access the notebook + # Inform the user how to access the notebook echo "Use the following command if you are running this notebook from a" echo "remote server. Ignore if using a local computer." echo "ssh -f -N -L localhost:${nb_port}:${server_ip}:${nb_port}"\ @@ -347,7 +347,7 @@ From: amd64/ubuntu:20.04 echo " -m min_capture_length -p probe_sets -s sample_sets \\" echo " -x stitch_options -k" } - + # Set defaults cluster_script="runMIPWranglerCurrent.sh" server_number=1 @@ -443,14 +443,14 @@ From: amd64/ubuntu:20.04 echo "It is recommended to run this app in a screen (GNU screen)." echo "A message indicating the end of download will be printed when done." echo "Check nohup.out file in your output directory for the download log." - + # cd and run app # Use nohup to make command keep running even if get hangup signal cd /opt/analysis nohup python /opt/bin/BaseSpaceRunDownloader_v2.py \ -r ${run_id} -a "$(cat /opt/resources/access_token.txt)" - # Print to CLI + # Print to CLI echo "Download finished." ################################################################# @@ -461,7 +461,7 @@ From: amd64/ubuntu:20.04 set -eu help() { - echo "Demultiplex data. Generates per-sample fastq files from the raw" + echo "Demultiplex data. Generates per-sample fastq files from the raw" echo "sequence data consisting of bcl files." echo "" echo "Usage:" @@ -472,31 +472,28 @@ From: amd64/ubuntu:20.04 echo "" echo "App Options:" echo " -h Print the help page." - echo " -p Required. The sequencing platform used. Either 'miseq'" - echo " or 'nextseq'." - echo " -s Required. The list of samples. Contains the samples used" - echo " in the study, the primers used, etc. This file must be " - echo " present in the directory mounted to '/opt/analysis'." + echo " -s Required. Sample sheet for demultiplexing. " + echo " This file must be present in the directory mounted to " + echo " '/opt/analysis'." echo "" echo "Examples:" echo " # Set paths" echo " $ resource_dir=/bin/MIPTools/base_resources" echo " $ bcl_dir=/work/usr/downloaded" - echo " $ fastq_dir=/work/usr/fastq" + echo " $ fastq_root_dir=/work/usr/" echo "" echo " # Run app" echo " $ singularity run \\" echo " -B \${resource_dir}:/opt/resources \\" echo " -B \${bcl_dir}:/opt/data \\" - echo " -B \${fastq_dir}:/opt/analysis \\" - echo " --app demux -s sample_list.tsv -p nextseq" + echo " -B \${fastq_root_dir}:/opt/analysis \\" + echo " --app demux -s SampleSheet.csv" } - while getopts "hp:s:" opt; do + while getopts "hs:" opt; do case ${opt} in h) help exit 1 ;; - p) platform=${OPTARG} ;; s) sample_list=${OPTARG} ;; *) help exit 1 ;; @@ -504,39 +501,20 @@ From: amd64/ubuntu:20.04 done # Define variables - cd /opt/src - template_dir="/opt/resources/templates/sample_sheet_templates/" - platform_template="${platform}"_sample_sheet_template.csv - template="${template_dir}${platform_template}" - bc_dict="/opt/resources/barcode_dict.json" - output_dir="/opt/analysis" - sample_list="/opt/analysis/${sample_list}" - - # Create a sample sheet for demultiplexing - python -c 'import mip_functions as mip; mip.generate_sample_sheet( - "'"${sample_list}"'", - "'"${bc_dict}"'", - "'"${template}"'", - "'"${platform}"'", - "'"${output_dir}"'" - )' - + sample_sheet="/opt/analysis/${sample_list}" # cd to where bcl files are cd /opt/data - + # Create a fastq directory for saving fastqs mkdir -p /opt/analysis/fastq - - # Copy sample list to fastq directory - scp ${sample_list} /opt/analysis/fastq/ - + # Increase limit of open number of files. ulimit -Sn $(ulimit -Hn) - + # Run bcl2fastq # Use nohup to make command keep running even if get hangup signal nohup bcl2fastq -o /opt/analysis/fastq \ - --sample-sheet /opt/analysis/SampleSheet.csv \ + --sample-sheet ${sample_sheet} \ --no-lane-splitting ################################################################## diff --git a/base_resources/barcode_dict.json b/base_resources/sample_prep/barcode_dict.pickle similarity index 100% rename from base_resources/barcode_dict.json rename to base_resources/sample_prep/barcode_dict.pickle diff --git a/src/demux_qc.py b/src/demux_qc.py index efafba1..bd6f35f 100644 --- a/src/demux_qc.py +++ b/src/demux_qc.py @@ -7,7 +7,7 @@ def main(platform, stats_dir): """Generate demultiplexing statistics after a sequencing run.""" - bc_dict = "/opt/resources/barcode_dict.json" + bc_dict = "/opt/resources/sample_prep/barcode_dict.pickle" # load barcode dict to be passed to the header-primer conversion function with open(bc_dict, "rb") as infile: diff --git a/src/mip_functions.py b/src/mip_functions.py index 08148b6..6b7c263 100644 --- a/src/mip_functions.py +++ b/src/mip_functions.py @@ -9695,105 +9695,6 @@ def save_fasta_dict(fasta_dict, fasta_file, linewidth=60): outfile.write(fasta_seq[i: i + linewidth] + "\n") -def generate_sample_sheet(sample_list_file, - barcode_dict_file, - sample_sheet_template, - platform, - output_dir, - warnings=False): - """Create a sample sheet to be used by bcl2fasq file from sample list.""" - with open(barcode_dict_file, "rb") as in1: - barcode_dic = pickle.load(in1) - # read in sample information - sample_names = [] - sample_info = {} - with open(sample_list_file) as infile: - linenum = 0 - for line in infile: - newline = line.strip().split("\t") - # first line is the header with column names - if linenum == 0: - colnames = newline - linenum += 1 - else: - sample_dict = {colname: colvalue for colname, colvalue - in zip(colnames, newline)} - sample_set = sample_dict["sample_set"] - sample_name = sample_dict["sample_name"] - replicate_number = sample_dict["replicate"] - forward_index = sample_dict["fw"] - reverse_index = sample_dict["rev"] - sample_id = "-".join([sample_name, - sample_set, - replicate_number]) - if sample_id in sample_info: - print("Repeating sample name ", sample_id) - if not sample_id.replace("-", "").isalnum(): - print(("Sample IDs can only contain " - "alphanumeric characters and '-'. " - "{} has invalid characters.").format(sample_id)) - continue - # nextseq and miseq barcodes are handled differently - if platform == "nextseq": - sample_dict.update( - {"i7": barcode_dic[reverse_index]["index_sequence"], - "i5": barcode_dic[forward_index]["index_sequence"]}) - elif platform == "miseq": - sample_dict.update( - {"i7": barcode_dic[reverse_index]["index_sequence"], - "i5": barcode_dic[forward_index]["sequence"]}) - sample_dict["sample_index"] = linenum - linenum += 1 - sample_info[sample_id] = sample_dict - sample_names.append(sample_id) - # Check for samples sharing one or both barcodes. One barcode sharing is - # allowed but a warning can be printed if desired by setting the warning - # to True. If both barcodes are shared among two samples, those samples - # will be ignored and a message will be broadcast. - samples_sharing = [] - for s1 in sample_info: - for s2 in sample_info: - if s1 != s2: - if ((sample_info[s1]["fw"] == sample_info[s2]["fw"]) - and (sample_info[s1]["rev"] == sample_info[s2]["rev"])): - samples_sharing.append([s1, s2]) - elif warnings and ( - (sample_info[s1]["fw"] == sample_info[s2]["fw"]) - or (sample_info[s1]["rev"] == sample_info[s2]["rev"]) - ): - print("Samples %s and %s share a barcode" % (s1, s2)) - samples_sharing_set = [] - if len(samples_sharing) > 0: - for s in samples_sharing: - samples_sharing_set.extend(s) - samples_sharing_set = set(samples_sharing_set) - print("There are %d samples sharing the same barcode pair" - % len(samples_sharing_set)) - pd.DataFrame(samples_sharing).to_csv( - os.path.join(output_dir, "samples_sharing_barcodes.tsv"), - sep="\t" - ) - # create sample sheet - sample_sheet = os.path.join(output_dir, "SampleSheet.csv") - with open(sample_sheet_template) as infile, \ - open(sample_sheet, "w") as outfile: - outfile_list = infile.readlines() - outfile_list = [o.strip() for o in outfile_list] - for sample_id in sample_names: - if sample_id in samples_sharing_set: - continue - reverse_index = sample_info[sample_id]["rev"] - forward_index = sample_info[sample_id]["fw"] - sample_index = str(sample_info[sample_id]["sample_index"]) - outlist = [sample_index, sample_id, "", "", - "S" + reverse_index, - sample_info[sample_id]["i7"], - "N" + forward_index, - sample_info[sample_id]["i5"], "", ""] - outfile_list.append(",".join(outlist)) - outfile.write("\n".join(outfile_list)) - - def chromosome_converter(chrom, from_malariagen): """ Convert plasmodium chromosome names from standard (chr1, etc) to malariagen names (Pf3d7...) and vice versa. diff --git a/src/sample_sheet_prep.py b/src/sample_sheet_prep.py index cef8bc4..c44e9ac 100644 --- a/src/sample_sheet_prep.py +++ b/src/sample_sheet_prep.py @@ -2,17 +2,23 @@ import argparse import os import numpy as np +import subprocess +import pickle def sample_sheet_prep( capture_plates, sample_plates, legacy_sheets, output_file, - wdir="/opt/analysis", + working_directory="/opt/analysis", quadrants="/opt/resources/sample_prep/quadrants.csv", forward_plates="/opt/resources/sample_prep/forward_plates.csv", - reverse_plates="/opt/resources/sample_prep/reverse_plates.csv"): + reverse_plates="/opt/resources/sample_prep/reverse_plates.csv", + barcode_dictionary="/opt/resources/sample_prep/barcode_dict.pickle", + platform="nextseq", + template_dir="/opt/resources/templates/sample_sheet_templates/"): quad = pd.read_csv(quadrants) forward_plates = pd.read_csv(forward_plates) reverse_plates = pd.read_csv(reverse_plates) + wdir = working_directory if capture_plates is not None: capture_paths = [os.path.join(wdir, p) for p in capture_plates] capture_plates = [] @@ -21,9 +27,8 @@ def sample_sheet_prep( capture_plates.append(pd.read_table(p).rename( columns={"Library Prep": "library_prep"})) except IOError: - print(("Warning: Capture plate file {} does not exist in the " - "run directory {} and will not be used.").format( - p, wdir)) + raise Exception(("Error: Capture plate file {} does not exist " + "in the run directory {}").format(p, wdir)) if len(capture_plates) > 0: capture_plates = pd.concat(capture_plates, ignore_index=True, axis=0) @@ -33,9 +38,8 @@ def sample_sheet_prep( try: sample_plates.append(pd.read_table(p)) except IOError: - print(("Warning:Sample plate file {} does not exist in the" - " run directory {} and will not be used.").format( - p, wdir)) + raise Exception(("Error: Sample plate file {} does not " + "exist in the run directory {}").format(p, wdir)) if len(sample_plates) > 0: sample_plates = pd.concat(sample_plates, ignore_index=True, axis=0) @@ -45,12 +49,16 @@ def sample_sheet_prep( lambda a: int(a[1:])) plating_cols = ["sample_name", "sample_plate", "row", "column"] sample_plates = sample_plates.loc[:, plating_cols] + # if a sample plate is referenced in capture plates but + # sample plate information is not provided, raise an error. plates_without_samples = set( capture_plates["sample_plate"]).difference( sample_plates["sample_plate"]) if len(plates_without_samples) > 0: - print(("Warning: {} does not have corresponding sample " - "plates.").format(plates_without_samples)) + raise Exception(("Error: Sample plate(s) {} do not " + "exist.").format(plates_without_samples)) + # if a sample plate is provided but not referenced in the + # captures, print a warning. samples_without_plates = set( sample_plates["sample_plate"]).difference( capture_plates["sample_plate"]) @@ -64,9 +72,11 @@ def sample_sheet_prep( captures = captures.drop_duplicates() captures["replicate"] = np.nan else: - print("Warning: No valid sample plate was found.") + raise Exception("Error: sample plates {} were not found." + ).format(sample_plates) else: - print("Warning: No valid capture plate was found.") + raise Exception("Error: capture plates {} were not found." + ).format(capture_plates) else: capture_plates = [] @@ -78,31 +88,28 @@ def sample_sheet_prep( legacy_sheets.append(pd.read_table(p).rename( columns={"Library Prep": "library_prep"})) except IOError: - print(("Warning:Sample sheet file {} does not exist in the" - " run directory {} and will not be used.").format( - p, wdir)) - if len(legacy_sheets) > 0: - legacy_sheets = pd.concat(legacy_sheets, ignore_index=True, axis=0) - legacy_sheets = legacy_sheets.drop_duplicates() + raise Exception(("Error: Sample sheet file {} does not exist " + "in the run directory {}").format(p, wdir)) + legacy_sheets = pd.concat(legacy_sheets, ignore_index=True, axis=0) + legacy_sheets = legacy_sheets.drop_duplicates() else: legacy_sheets = [] if (len(capture_plates) > 0) and (len(legacy_sheets) > 0): - com = pd.concat([captures, legacy_sheets], axis=0, ignore_index=True, - sort=False) + sample_sheet = pd.concat([captures, legacy_sheets], axis=0, + ignore_index=True, sort=False) elif len(capture_plates) > 0: # "replicate" column is only available in 96 well format # this will need to be set to NaN value if no 96 well sample sheet # was used. captures["replicate"] = np.nan - com = captures + sample_sheet = captures elif len(legacy_sheets) > 0: - com = legacy_sheets + sample_sheet = legacy_sheets else: - print("At least one sample sheet must be provided.") - return + raise Exception("At least one sample sheet must be provided.") - com = com.drop_duplicates() + sample_sheet = sample_sheet.drop_duplicates() def assign_replicate(replicates): replicates = list(replicates) @@ -123,58 +130,137 @@ def assign_replicate(replicates): reps_used.add(rep) return pd.Series(replicates) try: - com["Replicate"] = com.groupby(["sample_name", "sample_set"])[ - "replicate"].transform(assign_replicate).astype(int) - com.drop("replicate", inplace=True, axis=1) - com.rename(columns={"Replicate": "replicate"}, inplace=True) + sample_sheet["Replicate"] = sample_sheet.groupby( + ["sample_name", "sample_set"])["replicate"].transform( + assign_replicate).astype(int) + sample_sheet.drop("replicate", inplace=True, axis=1) + sample_sheet.rename(columns={"Replicate": "replicate"}, inplace=True) except ValueError: - print("Error in assigning replicates. Please make sure " + raise Exception("Error in assigning replicates. Please make sure " "the 'sample_name' and 'sample_set' fields have " "valid, non-empty values in all provided files.") + + # load barcode dictionary file to add sample barcode sequences + with open(barcode_dictionary, "rb") as bc_file: + barcodes = pickle.load(bc_file) + + # create the sample id as the combination of sample name, sample set + # and replicate number + sample_sheet["sample_id"] = sample_sheet[ + ["sample_name", "sample_set", "replicate"]].apply( + lambda a: "-".join(map(str, a)), axis=1) + + # check sample ids for formatting. + sample_sheet["valid_sample_id"] = sample_sheet["sample_id"].map( + lambda a: a.replace("-", "").isalnum()) + invalid_samples = sample_sheet.loc[~sample_sheet["valid_sample_id"]] + if invalid_samples.shape[0] > 0: + invalid_samples_file = os.path.join(wdir, "invalid_samples.csv") + invalid_samples.to_csv(invalid_samples_file) + raise Exception(("Sample IDs can only contain alphanumeric characters" + " and '-'. There are samples with invalid characters. " + "Please correct the sample ids saved in {}")).format( + invalid_samples_file) + + # get i5 and i7 index (sample barcode) sequences for each sample. + # orientation of i5 sequences are different for miseq and nextseq + sample_sheet["i7"] = sample_sheet["rev"].map( + lambda a: barcodes[a]["index_sequence"]) + if platform == "nextseq": + sample_sheet["i5"] = sample_sheet["fw"].map( + lambda a: barcodes[a]["index_sequence"]) + elif platform == "miseq": + sample_sheet["i5"] = sample_sheet["fw"].map( + lambda a: barcodes[a]["sequence"]) + + # reset the index to make sure index starts from zero + sample_sheet.reset_index(inplace=True) + # Create barcode names. This naming scheme is following earlier notation + # used but probably any unique name would work. + sample_sheet["reverse_index"] = "S" + sample_sheet["rev"].astype(str) + sample_sheet["forward_index"] = "N" + sample_sheet["fw"].astype(str) + # there are 4 fields that can be left empty in the sample sheet but + # we still need to have those columns as empty strings + empty_cols = ["empty" + str(i) for i in range(4)] + for c in empty_cols: + sample_sheet[c] = "" + # check whether all required columns have valid values required_columns = ["sample_name", "sample_set", "probe_set", - "replicate", "fw", "rev", "library_prep"] - missing_values = com[required_columns].isnull().any() + "replicate", "fw", "rev", "library_prep", + "sample_id", "i5", "i7", "reverse_index", + "forward_index"] + missing_values = sample_sheet[required_columns].isnull().any() missing_values = missing_values.loc[missing_values].index.to_list() if len(missing_values) > 0: - print(("Error: Required column(s) {} cannot have missing values." - ).format(", ".join(missing_values))) + bad_sample_sheet = os.path.join(wdir, "bad_sample_sheet.csv") + sample_sheet.to_csv(bad_sample_sheet) + raise Exception(("Error: Required column(s) {} cannot have missing " + "values. Please inspect the file {}").format( + ", ".join(missing_values), bad_sample_sheet)) + + # create a small function to save files + def make_sample_sheet(s_sheet, pfix): + """Save sample sheet to temporary file and concatenate to template.""" + # select the columns needed in the final sample sheet + cols = ["sample_id", "empty0", "empty1", "reverse_index", "i7", + "forward_index", "i5", "empty2", "empty3"] + # save sample sheet to a temporary file + sample_sheet_tail = os.path.join(wdir, "temp_samples.csv") + s_sheet.loc[:, cols].to_csv(sample_sheet_tail, index=True, + header=False) + # the sample sheet we generated needs some lines from the template file + # we'll cat that file before the sample sheet tail just saved. + sample_sheet_head = os.path.join( + template_dir, platform + "_sample_sheet_template.csv") + + sample_sheet_file = os.path.join(wdir, pfix + "SampleSheet.csv") + with open(sample_sheet_file, "w") as final_sample_sheet: + res = subprocess.run(["cat", sample_sheet_head, sample_sheet_tail], + stdout=final_sample_sheet, + stderr=subprocess.PIPE) + if res.stderr == b"": + subprocess.run(["rm", sample_sheet_tail]) + else: + raise Exception(( + "Error creating final sample sheet file: {}").format( + res.stderr)) + # save the entire dataframe as _samples.tsv file + s_sheet.to_csv(os.path.join(wdir, pfix + output_file), + index=False, sep="\t") return - if com.shape[0] != (com.groupby(["fw", "rev"]).first().shape[0]): + + # check if there are non-unique primer pairs + if sample_sheet.shape[0] != ( + sample_sheet.groupby(["fw", "rev"]).first().shape[0]): size_file = os.path.join(wdir, "repeating_primers.csv") + # nonunique primer pairs may be allowed if they belong to + # different probe sets becouse the data can be separated based + # on the probe sequences. print(("There are repeating forward/reverse primer pairs.\n" "Sample sheet will be split based on the probe sets used.\n" "Inspect {} for repeating primer information.").format( size_file)) - c_size = com.groupby(["fw", "rev"]).size().sort_values( + c_size = sample_sheet.groupby(["fw", "rev"]).size().sort_values( ascending=False) c_size = c_size.loc[c_size > 1].reset_index() - com.merge(c_size).to_csv(size_file, index=False) - gb = com.groupby("probe_set") + sample_sheet.merge(c_size).to_csv(size_file, index=False) + + # create a separate sample sheet file for each probe set + gb = sample_sheet.groupby("probe_set") for group_key in gb.groups: g = gb.get_group(group_key) + # raise error if non-unique primers exist within probesets if g.shape[0] != (g.groupby(["fw", "rev"]).first().shape[0]): size_file = os.path.join(wdir, group_key + "_repeating.csv") - print(("There are repeating forward/reverse primer pairs " - "within probe set {}. Inspect {} and correct the " - "sample sheet before proceeding with demultiplexing." - ).format(group_key, size_file)) - g_size = g.groupby(["fw", "rev"]).size().sort_values( - ascending=False) - g_size = g_size.loc[g_size > 1].reset_index() - g.merge(g_size).to_csv(size_file, index=False) - g.to_csv(os.path.join(wdir, group_key + "_" + output_file), - index=False, sep="\t") + raise Exception(("There are repeating forward/reverse primer " + "pairs within probe set {}. Inspect {} and correct the " + "sample sheet.").format(group_key, size_file)) + make_sample_sheet(g, group_key + "_") else: - com.to_csv(os.path.join(wdir, output_file), index=False, sep="\t") - - for sample_id in com["sample_name"]: - if not sample_id.replace("-", "").isalnum(): - print(("Sample names can only contain " - "alphanumeric characters and '-'. " - "{} has invalid characters. " - "This sample will not be processed.").format(sample_id)) + # save a single sample sheet when all primer pairs are unique + make_sample_sheet(sample_sheet, "") if __name__ == "__main__": @@ -189,8 +275,8 @@ def assign_replicate(replicates): parser.add_argument("-s", "--sample-plates", help=("Sample plate file(s)."), nargs="*") - parser.add_argument("-t", "--sample-sheets", - help=("Finished sample sheet file(s)."), + parser.add_argument("-t", "--legacy-sheets", + help=("Legacy sample sheet file(s)."), nargs="*") parser.add_argument("-o", "--output-file", help=("Output file name."), @@ -210,9 +296,19 @@ def assign_replicate(replicates): help=("Reverse primer plate file."), default=("/opt/resources/sample_prep/" "reverse_plates.csv")) + parser.add_argument("-b", "--barcode-dictionary", + help="Path to sample barcode dictionary.", + default=("/opt/resources/sample_prep/" + "barcode_dict.pickle")) + parser.add_argument("-p", "--platform", + help="Sequencing platform", + default="nextseq", + choices=["nextseq", "miseq"]) + parser.add_argument("-d", "--template-dir", + help="Directory containing sample sheet headers.", + default=("/opt/resources/templates/" + "sample_sheet_templates/")) + args = vars(parser.parse_args()) - sample_sheet_prep(args["capture_plates"], args["sample_plates"], - args["sample_sheets"], args["output_file"], - args["working_directory"], args["quadrants"], - args["forward_plates"], args["reverse_plates"]) + sample_sheet_prep(**args) From a08e0fd46513f652fbcdc90a15b09ab60921a4b8 Mon Sep 17 00:00:00 2001 From: aydemiro Date: Mon, 14 Feb 2022 15:52:14 -0500 Subject: [PATCH 042/189] Update generate_wrangler_scripts.py When mip arms file is missing, it is created from the mip info dictionary. However, the newly created file is not loaded, leading to file missing error. This commit fixes that. --- src/generate_wrangler_scripts.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/generate_wrangler_scripts.py b/src/generate_wrangler_scripts.py index c1ed574..554370e 100644 --- a/src/generate_wrangler_scripts.py +++ b/src/generate_wrangler_scripts.py @@ -176,8 +176,13 @@ "/opt/project_resources/mip_ids/mip_info.json").format( arm_file, p_name)) try: + # generate the probe arm file from the mip info file probe_summary_generator.generate_mip_arms_file( p_name, probe_sets_file=mipset_table) + # load the arm file generated + with open(arm_file) as infile: + mip_arms_list.append(pd.read_table(infile)) + probes.update(temp_probes) except Exception as e: print(("MIP arm file generation for probe set {} " "failed due to {}.")).format(p_name, e) From 0ccd0ad22e4daf69e78ba8bd7c95b6b6009bec4b Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 14 Feb 2022 21:13:11 -0500 Subject: [PATCH 043/189] Install `magrittr` in definition file Resolves #7. --- MIPTools.def | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MIPTools.def b/MIPTools.def index 2e0b7d9..da1db26 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -163,6 +163,9 @@ From: amd64/ubuntu:20.04 make -j $CPU_COUNT scp vt /opt/bin + # install magrittr + Rscript -e 'install.packages("magrittr", repos = "https://cloud.r-project.org")' + # install RealMcCoil # Rscript -e 'devtools::install_github("OJWatson/McCOILR")' From 9e951770048b43979fa98c622c60a3379bcd7f8c Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 14 Feb 2022 21:57:45 -0500 Subject: [PATCH 044/189] Reinstall McCOILR Adresses #7. --- MIPTools.def | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIPTools.def b/MIPTools.def index da1db26..cc6d35d 100644 --- a/MIPTools.def +++ b/MIPTools.def @@ -167,7 +167,7 @@ From: amd64/ubuntu:20.04 Rscript -e 'install.packages("magrittr", repos = "https://cloud.r-project.org")' # install RealMcCoil - # Rscript -e 'devtools::install_github("OJWatson/McCOILR")' + Rscript -e 'devtools::install_github("OJWatson/McCOILR")' # install rehh Rscript -e 'install.packages("rehh", repos="https://cloud.r-project.org")' From 26b85ef79493df45b5d7d908a1570061964cb544 Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Mon, 21 Feb 2022 00:16:00 -0500 Subject: [PATCH 045/189] Fix version number Closes #24. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1a7157..050463c 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ either the development version or the most recent stable release: singularity pull library://apascha1/miptools/miptools:dev # Download the latest stable release -singularity pull library://apascha1/miptools/miptools:v1.0.0 +singularity pull library://apascha1/miptools/miptools:v0.4.0 ``` Note that these prebuilt versions do not include the `bcl2fastq` software due From 5007b89bce5a34ead5ba11e6f5b24a2d6ce5fb8e Mon Sep 17 00:00:00 2001 From: Aris Paschalidis Date: Tue, 22 Feb 2022 11:12:16 -0500 Subject: [PATCH 046/189] Remove miscellaneous audio file --- base_resources/finish.wav | Bin 327724 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 base_resources/finish.wav diff --git a/base_resources/finish.wav b/base_resources/finish.wav deleted file mode 100644 index c4831c0bd2a99f2e42d81b2b9d863bce7aa4c65a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 327724 zcmZ_X1^m55l|S%{g0XJ6Yh$;r-G!*ASlF!?m>4K3cDJr#D;5H_uHAu(-K}6CD4>{N zuax)ydC%uJ``jqS;OtnPTdJKXSod+l?9Gp|;w z?N+B*-QXRo)tTRWn$`BJGq3Km$3A#oHo~!6ShO{Cne_gNpVVY6HA`!K+-p*&HSbP zdr~#^X_M;Y^(lcEhSM8~4;WNY?qup*U`u(D+e@ye7wrD0)jU0Y)1&j0nb^_itr za>Ie&OjRnhbfNGE%5qm*kod}-Jh!x~na&qE&s^P$;|Xu?!e=`yB%9xrQ@U)@Puc3* zuX2zQgq7x#I;c@xHKw*yZqc9Mn(`-lsLN7PL)4}4t6y7bIn9^i$$jETone^V)-G`` zdg7zz)e=fUp*Z-&lzqCyIWc^#j{{?2>C-|FWi9nq>woNhaEiXtz4^Cv7@KW$;u2Kg z@@-NI{CK{YDL=zon_7W$%0D%mtsZt^tn#eAU3m^C6-WPA6M0QJ1|pD@wy21@ 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%0A2fif<6V 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