Repository: AroneyS/binchicken (language: python)

Issue:
Switch to Fastp QC
- [x] Update Aviary
- [x] Add `--local-cores` argument
- [x] Add `--skip-qc` to Aviary commands/rules
- [x] Add read_qc rule that runs fastp QC when `--run-qc` is set
- [x] Set `--run-qc` to true when `--sra`
- [x] Test manual


Discussion / hints:

Fix patch:
diff --git a/ibis/config/template_coassemble.yaml b/ibis/config/template_coassemble.yaml
index abe8dc96..e64c2a26 100644
--- a/ibis/config/template_coassemble.yaml
+++ b/ibis/config/template_coassemble.yaml
@@ -5,6 +5,7 @@ genomes:
 taxa_of_interest: ""
 max_coassembly_size: 
 assemble_unmapped: false
+read_qc: false
 sra: false
 prodigal_meta: false
 single_assembly: false
diff --git a/ibis/ibis.py b/ibis/ibis.py
index d2e1cbf0..e3cd8519 100755
--- a/ibis/ibis.py
+++ b/ibis/ibis.py
@@ -91,7 +91,7 @@ def read_list(path):
         return [line.strip() for line in f]
 
 def run_workflow(config, workflow, output_dir, cores=16, dryrun=False,
-                 profile=None, cluster_retries=None,
+                 profile=None, local_cores=1, cluster_retries=None,
                  snakemake_args="", conda_frontend="mamba", conda_prefix=None):
     load_configfile(config)
 
@@ -99,7 +99,7 @@ def run_workflow(config, workflow, output_dir, cores=16, dryrun=False,
         "snakemake --snakefile {snakefile} --configfile '{config}' --directory {output_dir} "
         "{jobs} --rerun-incomplete --keep-going --nolock "
         "{snakemake_args} "
-        "{profile} {retries} --use-conda {conda_frontend} {conda_prefix} "
+        "{profile} {local} {retries} --use-conda {conda_frontend} {conda_prefix} "
         "{dryrun} "
     ).format(
         snakefile=os.path.join(os.path.dirname(__file__), "workflow", workflow),
@@ -107,6 +107,7 @@ def run_workflow(config, workflow, output_dir, cores=16, dryrun=False,
         output_dir=output_dir,
         jobs=f"--cores {cores}" if cores is not None else "--cores 1",
         profile="" if not profile else f"--profile {profile}",
+        local=f"--local-cores {local_cores}",
         retries="" if (cluster_retries is None) else f"--retries {cluster_retries}",
         conda_frontend=f"--conda-frontend {conda_frontend}" if conda_frontend is not None else "",
         conda_prefix=f"--conda-prefix {conda_prefix}" if conda_prefix is not None else "",
@@ -144,6 +145,7 @@ def download_sra(args):
         cores = args.cores,
         dryrun = args.dryrun,
         profile = args.snakemake_profile,
+        local_cores = args.local_cores,
         cluster_retries = args.cluster_retries,
         conda_prefix = args.conda_prefix,
         snakemake_args = target_rule + " " + args.snakemake_args if args.snakemake_args else target_rule,
@@ -446,6 +448,7 @@ def coassemble(args):
         "prodigal_meta": args.prodigal_meta,
         # Coassembly config
         "assemble_unmapped": args.assemble_unmapped,
+        "run_qc": args.run_qc,
         "unmapping_min_appraised": args.unmapping_min_appraised,
         "unmapping_max_identity": args.unmapping_max_identity,
         "unmapping_max_alignment": args.unmapping_max_alignment,
@@ -473,6 +476,7 @@ def coassemble(args):
         cores = args.cores,
         dryrun = args.dryrun,
         profile = args.snakemake_profile,
+        local_cores = args.local_cores,
         cluster_retries = args.cluster_retries,
         conda_prefix = args.conda_prefix,
         snakemake_args = args.snakemake_args,
@@ -552,6 +556,7 @@ def evaluate(args):
         cores = args.cores,
         dryrun = args.dryrun,
         profile = args.snakemake_profile,
+        local_cores = args.local_cores,
         cluster_retries = args.cluster_retries,
         conda_prefix = args.conda_prefix,
         snakemake_args = args.snakemake_args,
@@ -610,6 +615,7 @@ def update(args):
 
     if args.sra:
         args.forward, args.reverse = download_sra(args)
+        args.run_qc = True
 
     if args.run_aviary:
         args.snakemake_args = "aviary_combine --rerun-triggers mtime " + args.snakemake_args if args.snakemake_args else "aviary_combine --rerun-triggers mtime"
@@ -776,6 +782,7 @@ def build(args):
     args.aviary_checkm2_dir = "."
     args.aviary_cores = None
     args.assemble_unmapped = True
+    args.run_qc = True
     args.coassemblies = None
     args.singlem_metapackage = "."
 
@@ -912,6 +919,7 @@ def add_general_snakemake_options(argument_group, required_conda_prefix=False):
         argument_group.add_argument("--snakemake-profile", default="",
                                     help="Snakemake profile (see https://snakemake.readthedocs.io/en/stable/executing/cli.html#profiles).\n"
                                          "Can be used to submit rules as jobs to cluster engine (see https://snakemake.readthedocs.io/en/stable/executing/cluster.html).")
+        argument_group.add_argument("--local-cores", type=int, help="Maximum number of cores to use on localrules when running in cluster mode", default=1)
         argument_group.add_argument("--cluster-retries", help="Number of times to retry a failed job when using cluster submission (see `--snakemake-profile`).", default=0)
         argument_group.add_argument("--snakemake-args", help="Additional commands to be supplied to snakemake in the form of a space-prefixed single string e.g. \" --quiet\"", default="")
 
@@ -987,6 +995,7 @@ def add_coassemble_arguments(parser):
         # Coassembly options
         coassemble_coassembly = parser.add_argument_group("Coassembly options")
         coassemble_coassembly.add_argument("--assemble-unmapped", action="store_true", help="Only assemble reads that do not map to reference genomes")
+        coassemble_coassembly.add_argument("--run-qc", action="store_true", help="Run Fastp QC on reads. Requires unmapping.")
         coassemble_coassembly.add_argument("--unmapping-min-appraised", type=int, help="Minimum fraction of sequences binned to justify unmapping [default: 0.1]", default=0.1)
         coassemble_coassembly.add_argument("--unmapping-max-identity", type=float, help="Maximum sequence identity of mapped sequences kept for coassembly [default: 99%]", default=99)
         coassemble_coassembly.add_argument("--unmapping-max-alignment", type=float, help="Maximum percent alignment of mapped sequences kept for coassembly [default: 99%]", default=99)
@@ -1028,12 +1037,13 @@ def add_coassemble_arguments(parser):
     # Base arguments
     update_base = update_parser.add_argument_group("Input arguments")
     add_base_arguments(update_base)
-    update_base.add_argument("--sra", action="store_true", help="Download reads from SRA (read argument still required)")
+    update_base.add_argument("--sra", action="store_true", help="Download reads from SRA (read argument still required). Also sets --run-qc.")
     # Coassembly options
     update_coassembly = update_parser.add_argument_group("Coassembly options")
     add_coassemble_output_arguments(update_coassembly)
     update_coassembly.add_argument("--coassemblies", nargs='+', help="Choose specific coassemblies from elusive clusters (e.g. coassembly_0)")
     update_coassembly.add_argument("--assemble-unmapped", action="store_true", help="Only assemble reads that do not map to reference genomes")
+    update_coassembly.add_argument("--run-qc", action="store_true", help="Run Fastp QC on reads. Requires unmapping.")
     update_coassembly.add_argument("--unmapping-min-appraised", type=float, help="Minimum fraction of sequences binned to justify unmapping [default: 0.1]", default=0.1)
     update_coassembly.add_argument("--unmapping-max-identity", type=float, help="Maximum sequence identity of mapped sequences kept for coassembly [default: 99%]", default=99)
     update_coassembly.add_argument("--unmapping-max-alignment", type=float, help="Maximum percent alignment of mapped sequences kept for coassembly [default: 99%]", default=99)
@@ -1098,6 +1108,8 @@ def coassemble_argument_verification(args):
             raise Exception("Input SingleM query (--sample-query) requires SingleM otu tables (--sample-singlem) for coverage")
         if args.assemble_unmapped and args.single_assembly:
             raise Exception("Assemble unmapped is incompatible with single-sample assembly")
+        if args.run_qc and not args.assemble_unmapped:
+            raise Exception("Run QC requires unmapping (--assemble-unmapped)")
         if args.assemble_unmapped and not args.genomes and not args.genomes_list:
             raise Exception("Reference genomes must be provided to assemble unmapped reads")
         if not args.singlem_metapackage and not os.environ['SINGLEM_METAPACKAGE_PATH'] and not args.sample_query and not args.sample_query_list:
diff --git a/ibis/workflow/coassemble.smk b/ibis/workflow/coassemble.smk
index 30eb5a95..a21d6d79 100644
--- a/ibis/workflow/coassemble.smk
+++ b/ibis/workflow/coassemble.smk
@@ -406,6 +406,42 @@ rule mock_download_sra:
 #####################################
 ### Map reads to matching genomes ###
 #####################################
+rule qc_reads:
+    input:
+        reads_1 = lambda wildcards: config["reads_1"][wildcards.read],
+        reads_2 = lambda wildcards: config["reads_2"][wildcards.read],
+    output:
+        reads_1 = output_dir + "/qc/{read}_1.fastq.gz",
+        reads_2 = output_dir + "/qc/{read}_2.fastq.gz",
+        json = output_dir + "/qc/{read}.json",
+        html = output_dir + "/qc/{read}.html",
+    group: "unmapping"
+    params:
+        quality_cutoff = 15,
+        unqualified_percent_limit = 40,
+        min_length = 100,
+    threads: 16
+    resources:
+        mem_mb=125*1000,
+        runtime = "4h",
+    log:
+        logs_dir + "/mapping/{read}_qc.log"
+    conda:
+        "env/fastp.yml"
+    shell:
+        "fastp "
+        "-i {input.reads_1} "
+        "-I {input.reads_2} "
+        "-o {output.reads_1} "
+        "-O {output.reads_2} "
+        "-j {output.json} "
+        "-h {output.html} "
+        "-w {threads} "
+        "-q {params.quality_cutoff} "
+        "-u {params.unqualified_percent_limit} "
+        "-l {params.min_length} "
+        "&> {log}"
+
 rule collect_genomes:
     input:
         appraise_binned = output_dir + "/appraise/binned.otu_table.tsv",
@@ -421,8 +457,8 @@ rule collect_genomes:
 
 rule map_reads:
     input:
-        reads_1 = lambda wildcards: config["reads_1"][wildcards.read],
-        reads_2 = lambda wildcards: config["reads_2"][wildcards.read],
+        reads_1 = lambda wildcards: config["reads_1"][wildcards.read] if not config["run_qc"] else output_dir + "/qc/{read}_1.fastq.gz",
+        reads_2 = lambda wildcards: config["reads_2"][wildcards.read] if not config["run_qc"] else output_dir + "/qc/{read}_2.fastq.gz",
         genomes = output_dir + "/mapping/{read}_reference.fna",
     output:
         dir = temp(directory(output_dir + "/mapping/{read}_coverm")),
@@ -573,6 +609,7 @@ rule aviary_assemble:
         "-n {threads} "
         "-t {threads} "
         "-m {resources.mem_gb} "
+        "--skip-qc "
         "{resources.assembler} "
         "{params.dryrun} "
         "&> {log} "
@@ -619,6 +656,7 @@ rule aviary_recover:
         "-n {threads} "
         "-t {threads} "
         "-m {resources.mem_gb} "
+        "--skip-qc "
         "{params.snakemake_profile} "
         "{params.cluster_retries} "
         "{params.dryrun} "
diff --git a/ibis/workflow/env/aviary.yml b/ibis/workflow/env/aviary.yml
index 7cc6391f..73400979 100644
--- a/ibis/workflow/env/aviary.yml
+++ b/ibis/workflow/env/aviary.yml
@@ -15,4 +15,4 @@ dependencies:
   - pip
   - git
   - pip:
-    - git+https://github.com/AroneyS/aviary.git@7e88bb5ba43c2d842c7eec9430b1aa368bf595cd
+    - git+https://github.com/rhysnewell/aviary.git@022d290
diff --git a/ibis/workflow/env/fastp.yml b/ibis/workflow/env/fastp.yml
new file mode 100644
index 00000000..ce775c38
--- /dev/null
+++ b/ibis/workflow/env/fastp.yml
@@ -0,0 +1,5 @@
+channels:
+  - conda-forge
+  - bioconda
+dependencies:
+  - fastp=0.23.*
diff --git a/ibis/workflow/scripts/aviary_commands.py b/ibis/workflow/scripts/aviary_commands.py
index c46d49b6..3f33271d 100755
--- a/ibis/workflow/scripts/aviary_commands.py
+++ b/ibis/workflow/scripts/aviary_commands.py
@@ -36,7 +36,7 @@ def pipeline(coassemblies, reads_1, reads_2, output_dir, threads, memory, fast=F
                 pl.lit(threads),
                 pl.lit(" -m "),
                 pl.lit(memory),
-                pl.lit(" &> "),
+                pl.lit(" --skip-qc &> "),
                 pl.lit(output_dir),
                 pl.lit("/coassemble/logs/"),
                 pl.col("coassembly"),
@@ -63,7 +63,7 @@ def pipeline(coassemblies, reads_1, reads_2, output_dir, threads, memory, fast=F
                 pl.lit(threads//2),
                 pl.lit(" -m "),
                 pl.lit(memory//2),
-                pl.lit(" &> "),
+                pl.lit(" --skip-qc &> "),
                 pl.lit(output_dir),
                 pl.lit("/coassemble/logs/"),
                 pl.col("coassembly"),


Test patch:
diff --git a/test/test_aviary_commands.py b/test/test_aviary_commands.py
index 58a8cb5a..8ae2ae33 100644
--- a/test/test_aviary_commands.py
+++ b/test/test_aviary_commands.py
@@ -57,14 +57,14 @@ def test_aviary_commands(self):
                 f"-1 1_1.fq.gz 2_1.fq.gz 3_1.fq.gz "
                 f"-2 1_2.fq.gz 2_2.fq.gz 3_2.fq.gz "
                 f"--output {output_dir}/coassemble/coassembly_0/assemble "
-                f"-n {threads} -t {threads} -m {memory} "
+                f"-n {threads} -t {threads} -m {memory} --skip-qc "
                 f"&> {output_dir}/coassemble/logs/coassembly_0_assemble.log ",
 
                 f"aviary recover --assembly {output_dir}/coassemble/coassembly_0/assemble/assembly/final_contigs.fasta "
                 f"-1 1_1.fq.gz 2_1.fq.gz 3_1.fq.gz 4_1.fq.gz 5_1.fq.gz 6_1.fq.gz "
                 f"-2 1_2.fq.gz 2_2.fq.gz 3_2.fq.gz 4_2.fq.gz 5_2.fq.gz 6_2.fq.gz "
                 f"--output {output_dir}/coassemble/coassembly_0/recover "
-                f"-n {threads//2} -t {threads//2} -m {memory//2} "
+                f"-n {threads//2} -t {threads//2} -m {memory//2} --skip-qc "
                 f"&> {output_dir}/coassemble/logs/coassembly_0_recover.log ",
             ],
             [
@@ -72,14 +72,14 @@ def test_aviary_commands(self):
                 f"-1 4_1.fq.gz 5_1.fq.gz 6_1.fq.gz "
                 f"-2 4_2.fq.gz 5_2.fq.gz 6_2.fq.gz "
                 f"--output {output_dir}/coassemble/coassembly_2/assemble "
-                f"-n {threads} -t {threads} -m {memory} "
+                f"-n {threads} -t {threads} -m {memory} --skip-qc "
                 f"&> {output_dir}/coassemble/logs/coassembly_2_assemble.log ",
 
                 f"aviary recover --assembly {output_dir}/coassemble/coassembly_2/assemble/assembly/final_contigs.fasta "
                 f"-1 4_1.fq.gz 5_1.fq.gz 6_1.fq.gz "
                 f"-2 4_2.fq.gz 5_2.fq.gz 6_2.fq.gz "
                 f"--output {output_dir}/coassemble/coassembly_2/recover "
-                f"-n {threads//2} -t {threads//2} -m {memory//2} "
+                f"-n {threads//2} -t {threads//2} -m {memory//2} --skip-qc "
                 f"&> {output_dir}/coassemble/logs/coassembly_2_recover.log ",
             ],
         ], schema=COMMANDS_COLUMNS, orient="row")
@@ -103,7 +103,7 @@ def test_aviary_commands_fast(self):
                 f"-1 1_1.fq.gz 2_1.fq.gz 3_1.fq.gz "
                 f"-2 1_2.fq.gz 2_2.fq.gz 3_2.fq.gz "
                 f"--output {output_dir}/coassemble/coassembly_0/assemble "
-                f"-n {threads} -t {threads} -m {memory} "
+                f"-n {threads} -t {threads} -m {memory} --skip-qc "
                 f"&> {output_dir}/coassemble/logs/coassembly_0_assemble.log ",
 
                 f"aviary recover --assembly {output_dir}/coassemble/coassembly_0/assemble/assembly/final_contigs.fasta "
@@ -114,7 +114,7 @@ def test_aviary_commands_fast(self):
                 f"--skip-binners maxbin concoct rosella "
                 f"--skip-abundances "
                 f"--refinery-max-iterations 0 "
-                f"-n {threads//2} -t {threads//2} -m {memory//2} "
+                f"-n {threads//2} -t {threads//2} -m {memory//2} --skip-qc "
                 f"&> {output_dir}/coassemble/logs/coassembly_0_recover.log ",
             ],
             [
@@ -122,7 +122,7 @@ def test_aviary_commands_fast(self):
                 f"-1 4_1.fq.gz 5_1.fq.gz 6_1.fq.gz "
                 f"-2 4_2.fq.gz 5_2.fq.gz 6_2.fq.gz "
                 f"--output {output_dir}/coassemble/coassembly_2/assemble "
-                f"-n {threads} -t {threads} -m {memory} "
+                f"-n {threads} -t {threads} -m {memory} --skip-qc "
                 f"&> {output_dir}/coassemble/logs/coassembly_2_assemble.log ",
 
                 f"aviary recover --assembly {output_dir}/coassemble/coassembly_2/assemble/assembly/final_contigs.fasta "
@@ -133,7 +133,7 @@ def test_aviary_commands_fast(self):
                 f"--skip-binners maxbin concoct rosella "
                 f"--skip-abundances "
                 f"--refinery-max-iterations 0 "
-                f"-n {threads//2} -t {threads//2} -m {memory//2} "
+                f"-n {threads//2} -t {threads//2} -m {memory//2} --skip-qc "
                 f"&> {output_dir}/coassemble/logs/coassembly_2_recover.log ",
             ],
         ], schema=COMMANDS_COLUMNS, orient="row")
diff --git a/test/test_build.py b/test/test_build.py
index 1e1a1bfa..acc06df3 100644
--- a/test/test_build.py
+++ b/test/test_build.py
@@ -52,6 +52,7 @@ def test_build(self):
             self.assertFalse("ibis/workflow/env/kingfisher.yml will be created." in output)
             self.assertFalse("ibis/workflow/env/prodigal.yml will be created." in output)
             self.assertFalse("ibis/workflow/env/aviary.yml will be created." in output)
+            self.assertFalse("ibis/workflow/env/fastp.yml will be created." in output)
 
             # Dryrun evaluate
             cmd = (
@@ -70,6 +71,7 @@ def test_build(self):
             self.assertFalse("ibis/workflow/env/kingfisher.yml will be created." in output)
             self.assertFalse("ibis/workflow/env/prodigal.yml will be created." in output)
             self.assertFalse("ibis/workflow/env/aviary.yml will be created." in output)
+            self.assertFalse("ibis/workflow/env/fastp.yml will be created." in output)
 
             # Dryrun update
             cmd = (
@@ -98,6 +100,7 @@ def test_build(self):
             self.assertFalse("ibis/workflow/env/kingfisher.yml will be created." in output)
             self.assertFalse("ibis/workflow/env/prodigal.yml will be created." in output)
             self.assertFalse("ibis/workflow/env/aviary.yml will be created." in output)
+            self.assertFalse("ibis/workflow/env/fastp.yml will be created." in output)
 
             # Dryrun iterate
             cmd = (
@@ -118,6 +121,7 @@ def test_build(self):
             self.assertFalse("ibis/workflow/env/kingfisher.yml will be created." in output)
             self.assertFalse("ibis/workflow/env/prodigal.yml will be created." in output)
             self.assertFalse("ibis/workflow/env/aviary.yml will be created." in output)
+            self.assertFalse("ibis/workflow/env/fastp.yml will be created." in output)
 
 
 if __name__ == '__main__':
diff --git a/test/test_coassemble.py b/test/test_coassemble.py
index 23c93a11..7fb81df6 100644
--- a/test/test_coassemble.py
+++ b/test/test_coassemble.py
@@ -158,7 +158,7 @@ def test_coassemble(self):
                         os.path.join(test_dir, "coassemble", "mapping", "sample_1_unmapped.2.fq.gz"),
                         os.path.join(test_dir, "coassemble", "mapping", "sample_2_unmapped.2.fq.gz"),
                         "--output", os.path.join(test_dir, "coassemble", "coassemble", "coassembly_0", "assemble"),
-                        "-n 64 -t 64 -m 500 &>",
+                        "-n 64 -t 64 -m 500 --skip-qc &>",
                         os.path.join(test_dir, "coassemble", "coassemble", "logs", "coassembly_0_assemble.log"),
                         ""
                     ]),
@@ -184,7 +184,7 @@ def test_coassemble(self):
                         os.path.join(test_dir, "coassemble", "mapping", "sample_3_unmapped.2.fq.gz"),
                         "--output", os.path.join(test_dir, "coassemble", "coassemble", "coassembly_0", "recover"),
                         "--workflow recover_mags_no_singlem --skip-binners maxbin concoct rosella --skip-abundances --refinery-max-iterations 0 "
-                        "-n 32 -t 32 -m 250 &>",
+                        "-n 32 -t 32 -m 250 --skip-qc &>",
                         os.path.join(test_dir, "coassemble", "coassemble", "logs", "coassembly_0_recover.log"),
                         ""
                     ]),
@@ -288,7 +288,7 @@ def test_coassemble_taxa_of_interest(self):
                         os.path.join(os.path.abspath(path_to_data), "sample_1.2.fq"),
                         os.path.join(os.path.abspath(path_to_data), "sample_3.2.fq"),
                         "--output", os.path.join(test_dir, "coassemble", "coassemble", "coassembly_0", "assemble"),
-                        "-n 64 -t 64 -m 500 &>",
+                        "-n 64 -t 64 -m 500 --skip-qc &>",
                         os.path.join(test_dir, "coassemble", "coassemble", "logs", "coassembly_0_assemble.log"),
                         ""
                     ]),
@@ -311,7 +311,7 @@ def test_coassemble_taxa_of_interest(self):
                         os.path.join(os.path.abspath(path_to_data), "sample_1.2.fq"),
                         os.path.join(os.path.abspath(path_to_data), "sample_3.2.fq"),
                         "--output", os.path.join(test_dir, "coassemble", "coassemble", "coassembly_0", "recover"),
-                        "-n 32 -t 32 -m 250 &>",
+                        "-n 32 -t 32 -m 250 --skip-qc &>",
                         os.path.join(test_dir, "coassemble", "coassemble", "logs", "coassembly_0_recover.log"),
                         ""
                     ]),
@@ -710,7 +710,7 @@ def test_coassemble_no_mapping(self):
                         os.path.join(os.path.abspath(path_to_data), "sample_1.2.fq"),
                         os.path.join(os.path.abspath(path_to_data), "sample_2.2.fq"),
                         "--output", os.path.join(test_dir, "coassemble", "coassemble", "coassembly_0", "assemble"),
-                        "-n 64 -t 64 -m 500 &>",
+                        "-n 64 -t 64 -m 500 --skip-qc &>",
                         os.path.join(test_dir, "coassemble", "coassemble", "logs", "coassembly_0_assemble.log"),
                         ""
                     ]),
@@ -736,7 +736,7 @@ def test_coassemble_no_mapping(self):
                         os.path.join(os.path.abspath(path_to_data), "sample_3.2.fq"),
                         "--output", os.path.join(test_dir, "coassemble", "coassemble", "coassembly_0", "recover"),
                         "--workflow recover_mags_no_singlem --skip-binners maxbin concoct rosella --skip-abundances --refinery-max-iterations 0 "
-                        "-n 32 -t 32 -m 250 &>",
+                        "-n 32 -t 32 -m 250 --skip-qc &>",
                         os.path.join(test_dir, "coassemble", "coassemble", "logs", "coassembly_0_recover.log"),
                         ""
                     ]),
@@ -823,6 +823,7 @@ def test_coassemble_singlem_inputs(self):
             self.assertTrue("count_bp_reads" not in output)
             self.assertTrue("target_elusive" in output)
             self.assertTrue("cluster_graph" in output)
+            self.assertTrue("qc_reads" not in output)
             self.assertTrue("collect_genomes" not in output)
             self.assertTrue("map_reads" not in output)
             self.assertTrue("finish_mapping" not in output)
@@ -860,6 +861,7 @@ def test_coassemble_run_aviary(self):
             self.assertTrue("count_bp_reads" not in output)
             self.assertTrue("target_elusive" in output)
             self.assertTrue("cluster_graph" in output)
+            self.assertTrue("qc_reads" not in output)
             self.assertTrue("collect_genomes" not in output)
             self.assertTrue("map_reads" not in output)
             self.assertTrue("finish_mapping" not in output)
@@ -901,6 +903,50 @@ def test_coassemble_run_aviary_unmapped(self):
             self.assertTrue("count_bp_reads" in output)
             self.assertTrue("target_elusive" in output)
             self.assertTrue("cluster_graph" in output)
+            self.assertTrue("qc_reads" not in output)
+            self.assertTrue("collect_genomes" in output)
+            self.assertTrue("map_reads" in output)
+            self.assertTrue("finish_mapping" in output)
+            self.assertTrue("aviary_commands" not in output)
+            self.assertTrue("aviary_assemble" not in output)
+            self.assertTrue("aviary_recover" not in output)
+            self.assertTrue("aviary_combine" in output)
+
+    def test_coassemble_run_aviary_unmapped_qc(self):
+        with in_tempdir():
+            cmd = (
+                f"ibis coassemble "
+                f"--assemble-unmapped "
+                f"--run-qc "
+                f"--run-aviary "
+                f"--aviary-gtdbtk-dir gtdb_release "
+                f"--aviary-checkm2-dir CheckM2_database "
+                f"--forward {SAMPLE_READS_FORWARD} "
+                f"--reverse {SAMPLE_READS_REVERSE} "
+                f"--genomes {GENOMES} "
+                f"--genome-transcripts {GENOME_TRANSCRIPTS} "
+                f"--sample-singlem {SAMPLE_SINGLEM} "
+                f"--sample-read-size {SAMPLE_READ_SIZE} "
+                f"--genome-singlem {GENOME_SINGLEM} "
+                f"--singlem-metapackage {METAPACKAGE} "
+                f"--output test "
+                f"--conda-prefix {path_to_conda} "
+                f"--dryrun "
+                f"--snakemake-args \" --quiet\" "
+            )
+            output = extern.run(cmd)
+
+            self.assertTrue("singlem_pipe_reads" not in output)
+            self.assertTrue("genome_transcripts" not in output)
+            self.assertTrue("singlem_pipe_genomes" not in output)
+            self.assertTrue("singlem_summarise_genomes" not in output)
+            self.assertTrue("singlem_appraise" in output)
+            self.assertTrue("query_processing" not in output)
+            self.assertTrue("single_assembly" not in output)
+            self.assertTrue("count_bp_reads" in output)
+            self.assertTrue("target_elusive" in output)
+            self.assertTrue("cluster_graph" in output)
+            self.assertTrue("qc_reads" in output)
             self.assertTrue("collect_genomes" in output)
             self.assertTrue("map_reads" in output)
             self.assertTrue("finish_mapping" in output)
@@ -943,6 +989,7 @@ def test_coassemble_files_of_paths(self):
             self.assertTrue("count_bp_reads" in output)
             self.assertTrue("target_elusive" in output)
             self.assertTrue("cluster_graph" in output)
+            self.assertTrue("qc_reads" not in output)
             self.assertTrue("collect_genomes" in output)
             self.assertTrue("map_reads" in output)
             self.assertTrue("finish_mapping" in output)
@@ -989,6 +1036,7 @@ def test_coassemble_singlem_inputs_files_of_paths(self):
             self.assertTrue("count_bp_reads" not in output)
             self.assertTrue("target_elusive" in output)
             self.assertTrue("cluster_graph" in output)
+            self.assertTrue("qc_reads" not in output)
             self.assertTrue("collect_genomes" not in output)
             self.assertTrue("map_reads" not in output)
             self.assertTrue("finish_mapping" not in output)
@@ -1024,6 +1072,7 @@ def test_coassemble_singlem_inputs_dir(self):
             self.assertTrue("count_bp_reads" not in output)
             self.assertTrue("target_elusive" in output)
             self.assertTrue("cluster_graph" in output)
+            self.assertTrue("qc_reads" not in output)
             self.assertTrue("collect_genomes" not in output)
             self.assertTrue("map_reads" not in output)
             self.assertTrue("finish_mapping" not in output)
diff --git a/test/test_manual.py b/test/test_manual.py
index 341ed398..abf7aeca 100644
--- a/test/test_manual.py
+++ b/test/test_manual.py
@@ -99,6 +99,7 @@ def test_update_aviary_run_real(self):
             f"--coassemble-summary {os.path.join(MOCK_COASSEMBLE, 'summary.tsv')} "
             f"--output {output_dir} "
             f"--conda-prefix {path_to_conda} "
+            f"--local-cores 5 "
             f"--snakemake-profile mqsub-lyra "
             f"--cluster-retries 1 "
         )
diff --git a/test/test_update.py b/test/test_update.py
index 5b9406b6..0a2d29c0 100644
--- a/test/test_update.py
+++ b/test/test_update.py
@@ -95,7 +95,7 @@ def test_update(self):
                         os.path.join(test_dir, "coassemble", "mapping", "sample_1_unmapped.2.fq.gz"),
                         os.path.join(test_dir, "coassemble", "mapping", "sample_2_unmapped.2.fq.gz"),
                         "--output", os.path.join(test_dir, "coassemble", "coassemble", "coassembly_0", "assemble"),
-                        "-n 64 -t 64 -m 500 &>",
+                        "-n 64 -t 64 -m 500 --skip-qc &>",
                         os.path.join(test_dir, "coassemble", "coassemble", "logs", "coassembly_0_assemble.log"),
                         ""
                     ]),
@@ -121,7 +121,7 @@ def test_update(self):
                         os.path.join(test_dir, "coassemble", "mapping", "sample_3_unmapped.2.fq.gz"),
                         "--output", os.path.join(test_dir, "coassemble", "coassemble", "coassembly_0", "recover"),
                         "--workflow recover_mags_no_singlem --skip-binners maxbin concoct rosella --skip-abundances --refinery-max-iterations 0 "
-                        "-n 32 -t 32 -m 250 &>",
+                        "-n 32 -t 32 -m 250 --skip-qc &>",
                         os.path.join(test_dir, "coassemble", "coassemble", "logs", "coassembly_0_recover.log"),
                         ""
                     ]),
@@ -136,6 +136,7 @@ def test_update_specified_files(self):
             cmd = (
                 f"ibis update "
                 f"--assemble-unmapped "
+                f"--run-qc "
                 f"--forward {SAMPLE_READS_FORWARD} "
                 f"--reverse {SAMPLE_READS_REVERSE} "
                 f"--genomes {GENOMES} "
@@ -162,6 +163,7 @@ def test_update_specified_files(self):
             self.assertTrue("count_bp_reads" not in output)
             self.assertTrue("target_elusive" not in output)
             self.assertTrue("cluster_graph" not in output)
+            self.assertTrue("qc_reads" in output)
             self.assertTrue("collect_genomes" in output)
             self.assertTrue("map_reads" in output)
             self.assertTrue("finish_mapping" in output)
@@ -233,6 +235,7 @@ def test_update_sra_download(self):
             self.assertTrue("count_bp_reads" not in output)
             self.assertTrue("target_elusive" not in output)
             self.assertTrue("cluster_graph" not in output)
+            self.assertTrue("qc_reads" in output)
             self.assertTrue("collect_genomes" in output)
             self.assertTrue("map_reads" in output)
             self.assertTrue("finish_mapping" in output)
@@ -244,6 +247,7 @@ def test_update_sra_download_mock(self):
                 f"ibis update "
                 f"--forward SRR3309137 SRR8334323 SRR8334324 "
                 f"--sra "
+                f"--assemble-unmapped "
                 f"--genomes {GENOMES} "
                 f"--coassemble-unbinned {MOCK_UNBINNED_SRA} "
                 f"--coassemble-binned {MOCK_BINNED_SRA} "
@@ -277,22 +281,22 @@ def test_update_sra_download_mock(self):
                 file = f.readline().decode()
                 self.assertTrue("@SEQ_ID.1" in file)
 
-            sra_f2_path = os.path.join("test", "coassemble", "sra", "SRR8334324_1.fastq.gz")
+            sra_f2_path = os.path.join("test", "coassemble", "qc", "SRR8334323_1.fastq.gz")
             self.assertTrue(os.path.exists(sra_f2_path))
             with gzip.open(sra_f2_path) as f:
                 file = f.readline().decode()
-                self.assertTrue("@SEQ_ID.1" in file)
+                self.assertTrue("@SEQ_ID.1" not in file)
 
             recover_path = os.path.join("test", "coassemble", "commands", "recover_commands.sh")
             self.assertTrue(os.path.exists(recover_path))
             with open(recover_path) as f:
                 file = f.readline()
-                self.assertTrue("sra/SRR3309137_1.fastq.gz" in file)
-                self.assertTrue("sra/SRR3309137_2.fastq.gz" in file)
-                self.assertTrue("sra/SRR8334323_1.fastq.gz" in file)
-                self.assertTrue("sra/SRR8334323_2.fastq.gz" in file)
-                self.assertTrue("sra/SRR8334324_1.fastq.gz" in file)
-                self.assertTrue("sra/SRR8334324_2.fastq.gz" in file)
+                self.assertTrue("mapping/SRR3309137_unmapped.1.fq.gz" in file)
+                self.assertTrue("mapping/SRR3309137_unmapped.2.fq.gz" in file)
+                self.assertTrue("mapping/SRR8334323_unmapped.1.fq.gz" in file)
+                self.assertTrue("mapping/SRR8334323_unmapped.2.fq.gz" in file)
+                self.assertTrue("mapping/SRR8334324_unmapped.1.fq.gz" in file)
+                self.assertTrue("mapping/SRR8334324_unmapped.2.fq.gz" in file)
 
     def test_update_sra_download_mock_filter_single(self):
         with in_tempdir():
@@ -396,6 +400,7 @@ def test_update_aviary_run(self):
             self.assertTrue("count_bp_reads" not in output)
             self.assertTrue("target_elusive" not in output)
             self.assertTrue("cluster_graph" not in output)
+            self.assertTrue("qc_reads" in output)
             self.assertTrue("collect_genomes" in output)
             self.assertTrue("map_reads" in output)
             self.assertTrue("finish_mapping" in output)
@@ -419,7 +424,10 @@ def test_update_aviary_dryrun(self):
                 f"--conda-prefix {path_to_conda} "
                 f"--snakemake-args \" --config aviary_dryrun=True\" "
             )
-            extern.run(cmd)
+            try:
+                extern.run(cmd)
+            except:
+                import pdb; pdb.set_trace()
 
             config_path = os.path.join("test", "config.yaml")
             self.assertTrue(os.path.exists(config_path))