From a5ea07ab6df936d0e776459289a1420297c0316d Mon Sep 17 00:00:00 2001 From: martin1cifuentes Date: Sat, 6 Jun 2026 17:46:25 +0800 Subject: [PATCH 1/3] Add pVACbind run-planning preservation tests --- tests/test_pvacbind_run_orchestration.py | 288 +++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 tests/test_pvacbind_run_orchestration.py diff --git a/tests/test_pvacbind_run_orchestration.py b/tests/test_pvacbind_run_orchestration.py new file mode 100644 index 000000000..39fb7892e --- /dev/null +++ b/tests/test_pvacbind_run_orchestration.py @@ -0,0 +1,288 @@ +import argparse +import io +import os +import tempfile +import unittest +from contextlib import redirect_stdout +from unittest import mock + +import pvactools.tools.pvacbind.run as pvacbind_run + + +class PvacbindRunOrchestrationTests(unittest.TestCase): + def test_main_builds_class_specific_pipeline_handoffs_and_combined_report_request(self): + input_file = "/input.fa" + pipeline_calls = [] + executed_pipelines = [] + postprocessor_calls = [] + + class FakePipeline: + def __init__(self, **kwargs): + self.kwargs = kwargs + pipeline_calls.append(kwargs) + + def execute(self): + executed_pipelines.append(self.kwargs) + all_epitopes_path = os.path.join( + self.kwargs["output_dir"], + "{}.{}.all_epitopes.tsv".format( + self.kwargs["sample_name"], + self.kwargs["filename_addition"], + ), + ) + with open(all_epitopes_path, "w"): + pass + + class FakePostProcessor: + def __init__(self, **kwargs): + postprocessor_calls.append(kwargs) + + def execute(self): + pass + + with tempfile.TemporaryDirectory() as output_dir, \ + mock.patch.object(pvacbind_run, "PvacbindPipeline", FakePipeline), \ + mock.patch.object(pvacbind_run, "combine_reports") as combine_reports, \ + mock.patch.object(pvacbind_run, "PostProcessor", FakePostProcessor), \ + mock.patch.object(pvacbind_run, "change_permissions_recursive") as change_permissions_recursive, \ + mock.patch.object(pvacbind_run.platform, "system", return_value="Linux"): + pvacbind_run.main([ + input_file, + "Sample", + "HLA-A*02:01,DRB1*11:01", + "NetMHC", + "NNalign", + output_dir, + "-e1", "8,9", + "-e2", "15,16", + "--top-score-metric", "lowest", + "--top-score-metric2", "ic50,presentation_percentile", + "--binding-threshold", "123", + "--binding-percentile-threshold", "1.5", + "--presentation-percentile-threshold", "3.5", + "--immunogenicity-percentile-threshold", "4.5", + "--percentile-threshold-strategy", "exploratory", + "--allele-specific-binding-thresholds", + "--net-chop-method", "cterm", + "--net-chop-threshold", "0.7", + "--additional-report-columns", "sample_name", + "--fasta-size", "42", + "--iedb-retries", "7", + "--keep-tmp-files", + "--n-threads", "2", + "--run-reference-proteome-similarity", + "--blastp-path", "/blastp", + "--blastp-db", "refseq_protein", + "--problematic-amino-acids", "C:1,M", + "--peptide-fasta", "peptides.fa", + "--aggregate-inclusion-binding-threshold", "4321", + "--aggregate-inclusion-count-limit", "9", + "--netmhc-stab", + "--use-normalized-percentiles", + "--reference-scores-path", "/scores", + ]) + + self.assertEqual(len(pipeline_calls), 2) + self.assertEqual(executed_pipelines, pipeline_calls) + + class_i_arguments = pipeline_calls[0] + class_ii_arguments = pipeline_calls[1] + + expected_shared_arguments = { + "input_file": input_file, + "input_file_type": "fasta", + "sample_name": "Sample", + "top_score_metric": "lowest", + "top_score_metric2": ["ic50", "presentation_percentile"], + "binding_threshold": 123, + "binding_percentile_threshold": 1.5, + "immunogenicity_percentile_threshold": 4.5, + "presentation_percentile_threshold": 3.5, + "percentile_threshold_strategy": "exploratory", + "allele_specific_binding_thresholds": True, + "net_chop_fasta": input_file, + "net_chop_method": "cterm", + "net_chop_threshold": 0.7, + "additional_report_columns": "sample_name", + "fasta_size": 42, + "iedb_retries": 7, + "keep_tmp_files": True, + "n_threads": 2, + "species": "human", + "run_reference_proteome_similarity": True, + "blastp_path": "/blastp", + "blastp_db": "refseq_protein", + "problematic_amino_acids": ["C:1", "M"], + "run_post_processor": True, + "peptide_fasta": "peptides.fa", + "aggregate_inclusion_binding_threshold": 4321, + "aggregate_inclusion_count_limit": 9, + } + for key, expected_value in expected_shared_arguments.items(): + self.assertEqual(class_i_arguments[key], expected_value) + self.assertEqual(class_ii_arguments[key], expected_value) + + self.assertEqual(class_i_arguments["alleles"], ["HLA-A*02:01"]) + self.assertIsNone(class_i_arguments["iedb_executable"]) + self.assertEqual(class_i_arguments["epitope_lengths"], [8, 9]) + self.assertEqual(class_i_arguments["prediction_algorithms"], ["NetMHC"]) + self.assertEqual(class_i_arguments["output_dir"], os.path.join(os.path.abspath(output_dir), "MHC_Class_I")) + self.assertTrue(class_i_arguments["netmhc_stab"]) + self.assertEqual(class_i_arguments["filename_addition"], "MHC_I") + self.assertTrue(class_i_arguments["use_normalized_percentiles"]) + self.assertEqual(class_i_arguments["reference_scores_path"], "/scores") + + self.assertEqual(class_ii_arguments["alleles"], ["DRB1*11:01"]) + self.assertIsNone(class_ii_arguments["iedb_executable"]) + self.assertEqual(class_ii_arguments["epitope_lengths"], [15, 16]) + self.assertEqual(class_ii_arguments["prediction_algorithms"], ["NNalign"]) + self.assertEqual(class_ii_arguments["output_dir"], os.path.join(os.path.abspath(output_dir), "MHC_Class_II")) + self.assertFalse(class_ii_arguments["netmhc_stab"]) + self.assertEqual(class_ii_arguments["filename_addition"], "MHC_II") + self.assertNotIn("use_normalized_percentiles", class_ii_arguments) + self.assertNotIn("reference_scores_path", class_ii_arguments) + + combined_dir = os.path.join(os.path.abspath(output_dir), "combined") + class_i_file = os.path.join(os.path.abspath(output_dir), "MHC_Class_I", "Sample.MHC_I.all_epitopes.tsv") + class_ii_file = os.path.join(os.path.abspath(output_dir), "MHC_Class_II", "Sample.MHC_II.all_epitopes.tsv") + combined_file = os.path.join(combined_dir, "Sample.Combined.all_epitopes.tsv") + combine_reports.assert_called_once_with([class_i_file, class_ii_file], combined_file) + self.assertEqual(len(postprocessor_calls), 1) + self.assertEqual(postprocessor_calls[0]["input_file"], combined_file) + self.assertEqual( + postprocessor_calls[0]["filtered_report_file"], + os.path.join(combined_dir, "Sample.Combined.filtered.tsv"), + ) + self.assertEqual(postprocessor_calls[0]["filename_addition"], "Combined") + change_permissions_recursive.assert_called_once_with(os.path.abspath(output_dir), 0o755, 0o644) + + def test_main_skips_absent_class_work_and_does_not_request_combined_reports(self): + pipeline_calls = [] + + class FakePipeline: + def __init__(self, **kwargs): + self.kwargs = kwargs + pipeline_calls.append(kwargs) + + def execute(self): + all_epitopes_path = os.path.join( + self.kwargs["output_dir"], + "{}.{}.all_epitopes.tsv".format( + self.kwargs["sample_name"], + self.kwargs["filename_addition"], + ), + ) + with open(all_epitopes_path, "w"): + pass + + with tempfile.TemporaryDirectory() as output_dir, \ + mock.patch.object(pvacbind_run, "PvacbindPipeline", FakePipeline), \ + mock.patch.object(pvacbind_run, "combine_reports") as combine_reports, \ + mock.patch.object(pvacbind_run, "PostProcessor") as postprocessor, \ + mock.patch.object(pvacbind_run, "change_permissions_recursive"), \ + redirect_stdout(io.StringIO()) as stdout: + pvacbind_run.main([ + "/input.fa", + "Sample", + "HLA-A*02:01", + "NetMHC", + output_dir, + ]) + + self.assertEqual(len(pipeline_calls), 1) + self.assertEqual(pipeline_calls[0]["filename_addition"], "MHC_I") + combine_reports.assert_not_called() + postprocessor.assert_not_called() + self.assertIn( + "No MHC class II prediction algorithms chosen. Skipping MHC class II predictions.", + stdout.getvalue(), + ) + + def test_create_combined_reports_builds_postprocessor_handoff(self): + postprocessor_calls = [] + + class FakePostProcessor: + def __init__(self, **kwargs): + self.kwargs = kwargs + postprocessor_calls.append(kwargs) + + def execute(self): + pass + + with tempfile.TemporaryDirectory() as base_output_dir, \ + mock.patch.object(pvacbind_run, "combine_reports") as combine_reports, \ + mock.patch.object(pvacbind_run, "PostProcessor", FakePostProcessor): + os.makedirs(os.path.join(base_output_dir, "MHC_Class_I")) + os.makedirs(os.path.join(base_output_dir, "MHC_Class_II")) + class_i_file = os.path.join(base_output_dir, "MHC_Class_I", "Sample.MHC_I.all_epitopes.tsv") + class_ii_file = os.path.join(base_output_dir, "MHC_Class_II", "Sample.MHC_II.all_epitopes.tsv") + with open(class_i_file, "w"): + pass + with open(class_ii_file, "w"): + pass + + args = argparse.Namespace( + sample_name="Sample", + binding_threshold=500, + run_reference_proteome_similarity=True, + ) + pvacbind_run.create_combined_reports(base_output_dir, args) + + combined_dir = os.path.join(base_output_dir, "combined") + combined_all_epitopes = os.path.join(combined_dir, "Sample.Combined.all_epitopes.tsv") + combined_filtered = os.path.join(combined_dir, "Sample.Combined.filtered.tsv") + combine_reports.assert_called_once_with([class_i_file, class_ii_file], combined_all_epitopes) + + self.assertEqual(len(postprocessor_calls), 1) + postprocessor_kwargs = postprocessor_calls[0] + self.assertEqual(postprocessor_kwargs["input_file"], combined_all_epitopes) + self.assertEqual(postprocessor_kwargs["filtered_report_file"], combined_filtered) + self.assertFalse(postprocessor_kwargs["run_coverage_filter"]) + self.assertIsNone(postprocessor_kwargs["minimum_fold_change"]) + self.assertEqual(postprocessor_kwargs["file_type"], "pVACbind") + self.assertFalse(postprocessor_kwargs["run_transcript_support_level_filter"]) + self.assertFalse(postprocessor_kwargs["run_net_chop"]) + self.assertFalse(postprocessor_kwargs["run_netmhc_stab"]) + self.assertFalse(postprocessor_kwargs["run_manufacturability_metrics"]) + self.assertFalse(postprocessor_kwargs["run_reference_proteome_similarity"]) + self.assertEqual(postprocessor_kwargs["filename_addition"], "Combined") + + def test_create_combined_reports_aborts_when_class_report_is_missing(self): + with tempfile.TemporaryDirectory() as base_output_dir, \ + mock.patch.object(pvacbind_run, "combine_reports") as combine_reports, \ + mock.patch.object(pvacbind_run, "PostProcessor") as postprocessor, \ + redirect_stdout(io.StringIO()) as stdout: + os.makedirs(os.path.join(base_output_dir, "MHC_Class_I")) + class_i_file = os.path.join(base_output_dir, "MHC_Class_I", "Sample.MHC_I.all_epitopes.tsv") + with open(class_i_file, "w"): + pass + + pvacbind_run.create_combined_reports( + base_output_dir, + argparse.Namespace(sample_name="Sample"), + ) + + missing_file = os.path.join(base_output_dir, "MHC_Class_II", "Sample.MHC_II.all_epitopes.tsv") + self.assertIn("File {} doesn't exist. Aborting.".format(missing_file), stdout.getvalue()) + combine_reports.assert_not_called() + postprocessor.assert_not_called() + + def test_iedb_retry_limit_exits_before_prediction_evidence_is_consumed(self): + with tempfile.TemporaryDirectory() as output_dir, \ + mock.patch.object(pvacbind_run, "split_algorithms") as split_algorithms: + with self.assertRaises(SystemExit) as cm: + pvacbind_run.main([ + "/input.fa", + "Sample", + "HLA-A*02:01", + "NetMHC", + output_dir, + "--iedb-retries", "101", + ]) + + self.assertEqual(str(cm.exception), "The number of IEDB retries must be less than or equal to 100") + split_algorithms.assert_not_called() + + +if __name__ == "__main__": + unittest.main() From 08719acc68caf0d54be0ca3c7ac1f0252c9aadb0 Mon Sep 17 00:00:00 2001 From: martin1cifuentes Date: Sat, 6 Jun 2026 17:46:48 +0800 Subject: [PATCH 2/3] Extract pVACbind run-planning owners --- pvactools/tools/pvacbind/run_planning.py | 185 +++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 pvactools/tools/pvacbind/run_planning.py diff --git a/pvactools/tools/pvacbind/run_planning.py b/pvactools/tools/pvacbind/run_planning.py new file mode 100644 index 000000000..125da387b --- /dev/null +++ b/pvactools/tools/pvacbind/run_planning.py @@ -0,0 +1,185 @@ +import os +from dataclasses import dataclass + + +# pVACbind-local run planning, output layout, and handoff construction. +# Execution, parser validation, prediction classification, and report semantics live elsewhere. +MHC_CLASS_I_DIRECTORY = "MHC_Class_I" +MHC_CLASS_II_DIRECTORY = "MHC_Class_II" +COMBINED_DIRECTORY = "combined" + +MHC_CLASS_I_FILENAME_ADDITION = "MHC_I" +MHC_CLASS_II_FILENAME_ADDITION = "MHC_II" +COMBINED_FILENAME_ADDITION = "Combined" + + +@dataclass(frozen=True) +class PvacbindOutputLayout: + base_output_dir: str + sample_name: str + + @property + def class_i_output_dir(self): + return os.path.join(self.base_output_dir, MHC_CLASS_I_DIRECTORY) + + @property + def class_ii_output_dir(self): + return os.path.join(self.base_output_dir, MHC_CLASS_II_DIRECTORY) + + @property + def combined_output_dir(self): + return os.path.join(self.base_output_dir, COMBINED_DIRECTORY) + + @property + def class_i_all_epitopes_file(self): + return os.path.join( + self.class_i_output_dir, + "{}.{}.all_epitopes.tsv".format(self.sample_name, MHC_CLASS_I_FILENAME_ADDITION), + ) + + @property + def class_ii_all_epitopes_file(self): + return os.path.join( + self.class_ii_output_dir, + "{}.{}.all_epitopes.tsv".format(self.sample_name, MHC_CLASS_II_FILENAME_ADDITION), + ) + + @property + def combined_all_epitopes_file(self): + return os.path.join( + self.combined_output_dir, + "{}.{}.all_epitopes.tsv".format(self.sample_name, COMBINED_FILENAME_ADDITION), + ) + + @property + def combined_filtered_report_file(self): + return os.path.join( + self.combined_output_dir, + "{}.{}.filtered.tsv".format(self.sample_name, COMBINED_FILENAME_ADDITION), + ) + + +@dataclass(frozen=True) +class PvacbindRunPlan: + class_i_prediction_algorithms: list + class_ii_prediction_algorithms: list + class_i_alleles: list + class_ii_alleles: list + species: str + + def should_run_class_i(self): + return len(self.class_i_prediction_algorithms) > 0 and len(self.class_i_alleles) > 0 + + def should_run_class_ii(self): + return len(self.class_ii_prediction_algorithms) > 0 and len(self.class_ii_alleles) > 0 + + def should_create_combined_reports(self): + return self.should_run_class_i() and self.should_run_class_ii() + + def class_i_skip_message(self): + if self.should_run_class_i(): + return None + elif len(self.class_i_prediction_algorithms) == 0: + return "No MHC class I prediction algorithms chosen. Skipping MHC class I predictions." + elif len(self.class_i_alleles) == 0: + return "No MHC class I alleles chosen. Skipping MHC class I predictions." + return None + + def class_ii_skip_message(self): + if self.should_run_class_ii(): + return None + elif len(self.class_ii_prediction_algorithms) == 0: + return "No MHC class II prediction algorithms chosen. Skipping MHC class II predictions." + elif len(self.class_ii_alleles) == 0: + return "No MHC class II alleles chosen. Skipping MHC class II predictions." + return None + + +@dataclass(frozen=True) +class PvacbindPipelineHandoffBuilder: + args: object + run_plan: PvacbindRunPlan + output_layout: PvacbindOutputLayout + + def shared_arguments(self): + return { + 'input_file' : self.args.input_file, + 'input_file_type' : 'fasta', + 'sample_name' : self.args.sample_name, + 'top_score_metric' : self.args.top_score_metric, + 'top_score_metric2' : self.args.top_score_metric2, + 'binding_threshold' : self.args.binding_threshold, + 'binding_percentile_threshold': self.args.binding_percentile_threshold, + 'immunogenicity_percentile_threshold': self.args.immunogenicity_percentile_threshold, + 'presentation_percentile_threshold': self.args.presentation_percentile_threshold, + 'percentile_threshold_strategy': self.args.percentile_threshold_strategy, + 'allele_specific_binding_thresholds': self.args.allele_specific_binding_thresholds, + 'net_chop_fasta' : self.args.input_file, + 'net_chop_method' : self.args.net_chop_method, + 'net_chop_threshold' : self.args.net_chop_threshold, + 'additional_report_columns' : self.args.additional_report_columns, + 'fasta_size' : self.args.fasta_size, + 'iedb_retries' : self.args.iedb_retries, + 'keep_tmp_files' : self.args.keep_tmp_files, + 'n_threads' : self.args.n_threads, + 'species' : self.run_plan.species, + 'run_reference_proteome_similarity': self.args.run_reference_proteome_similarity, + 'blastp_path' : self.args.blastp_path, + 'blastp_db' : self.args.blastp_db, + 'problematic_amino_acids' : self.args.problematic_amino_acids, + 'run_post_processor' : True, + 'peptide_fasta' : self.args.peptide_fasta, + 'aggregate_inclusion_binding_threshold': self.args.aggregate_inclusion_binding_threshold, + 'aggregate_inclusion_count_limit': self.args.aggregate_inclusion_count_limit, + } + + def class_i_arguments(self, iedb_mhc_i_executable): + class_i_arguments = self.shared_arguments() + class_i_arguments['alleles'] = self.run_plan.class_i_alleles + class_i_arguments['iedb_executable'] = iedb_mhc_i_executable + class_i_arguments['epitope_lengths'] = self.args.class_i_epitope_length + class_i_arguments['prediction_algorithms'] = self.run_plan.class_i_prediction_algorithms + class_i_arguments['output_dir'] = self.output_layout.class_i_output_dir + class_i_arguments['netmhc_stab'] = self.args.netmhc_stab + class_i_arguments['filename_addition'] = MHC_CLASS_I_FILENAME_ADDITION + class_i_arguments['use_normalized_percentiles'] = self.args.use_normalized_percentiles + class_i_arguments['reference_scores_path'] = self.args.reference_scores_path + return class_i_arguments + + def class_ii_arguments(self, iedb_mhc_ii_executable): + class_ii_arguments = self.shared_arguments() + class_ii_arguments['alleles'] = self.run_plan.class_ii_alleles + class_ii_arguments['prediction_algorithms'] = self.run_plan.class_ii_prediction_algorithms + class_ii_arguments['iedb_executable'] = iedb_mhc_ii_executable + class_ii_arguments['epitope_lengths'] = self.args.class_ii_epitope_length + class_ii_arguments['output_dir'] = self.output_layout.class_ii_output_dir + class_ii_arguments['netmhc_stab'] = False + class_ii_arguments['filename_addition'] = MHC_CLASS_II_FILENAME_ADDITION + return class_ii_arguments + + +@dataclass(frozen=True) +class CombinedReportHandoff: + output_layout: PvacbindOutputLayout + + @property + def input_files(self): + return [ + self.output_layout.class_i_all_epitopes_file, + self.output_layout.class_ii_all_epitopes_file, + ] + + def post_processing_params(self, args): + post_processing_params = vars(args).copy() + post_processing_params['input_file'] = self.output_layout.combined_all_epitopes_file + post_processing_params['filtered_report_file'] = self.output_layout.combined_filtered_report_file + post_processing_params['run_coverage_filter'] = False + post_processing_params['minimum_fold_change'] = None + post_processing_params['file_type'] = 'pVACbind' + post_processing_params['run_transcript_support_level_filter'] = False + post_processing_params['run_net_chop'] = False + post_processing_params['run_netmhc_stab'] = False + post_processing_params['run_manufacturability_metrics'] = False + post_processing_params['run_reference_proteome_similarity'] = False + post_processing_params["filename_addition"] = COMBINED_FILENAME_ADDITION + return post_processing_params From d35e1cc5b3f5894a3c75053f3368d9095f204823 Mon Sep 17 00:00:00 2001 From: martin1cifuentes Date: Sat, 6 Jun 2026 17:47:06 +0800 Subject: [PATCH 3/3] Route pVACbind run coordination through run_planning --- pvactools/tools/pvacbind/run.py | 125 ++++++++++---------------------- 1 file changed, 37 insertions(+), 88 deletions(-) diff --git a/pvactools/tools/pvacbind/run.py b/pvactools/tools/pvacbind/run.py index d1df8daf3..23910918a 100644 --- a/pvactools/tools/pvacbind/run.py +++ b/pvactools/tools/pvacbind/run.py @@ -11,38 +11,30 @@ from pvactools.lib.post_processor import PostProcessor from pvactools.lib.run_utils import * from pvactools.lib.prediction_class_utils import * +from pvactools.tools.pvacbind.run_planning import ( + CombinedReportHandoff, + PvacbindOutputLayout, + PvacbindPipelineHandoffBuilder, + PvacbindRunPlan, +) def define_parser(): return PvacbindRunArgumentParser().parser def create_combined_reports(base_output_dir, args): - output_dir = os.path.join(base_output_dir, 'combined') - os.makedirs(output_dir, exist_ok=True) + output_layout = PvacbindOutputLayout(base_output_dir, args.sample_name) + combined_handoff = CombinedReportHandoff(output_layout) + os.makedirs(output_layout.combined_output_dir, exist_ok=True) - file1 = os.path.join(base_output_dir, 'MHC_Class_I', "{}.MHC_I.all_epitopes.tsv".format(args.sample_name)) - file2 = os.path.join(base_output_dir, 'MHC_Class_II', "{}.MHC_II.all_epitopes.tsv".format(args.sample_name)) - if not os.path.exists(file1): - print("File {} doesn't exist. Aborting.".format(file1)) + if not os.path.exists(output_layout.class_i_all_epitopes_file): + print("File {} doesn't exist. Aborting.".format(output_layout.class_i_all_epitopes_file)) return - if not os.path.exists(file2): - print("File {} doesn't exist. Aborting.".format(file2)) + if not os.path.exists(output_layout.class_ii_all_epitopes_file): + print("File {} doesn't exist. Aborting.".format(output_layout.class_ii_all_epitopes_file)) return - combined_output_file = os.path.join(output_dir, "{}.Combined.all_epitopes.tsv".format(args.sample_name)) - combine_reports([file1, file2], combined_output_file) - filtered_report_file = os.path.join(output_dir, "{}.Combined.filtered.tsv".format(args.sample_name)) - - post_processing_params = vars(args) - post_processing_params['input_file'] = combined_output_file - post_processing_params['filtered_report_file'] = filtered_report_file - post_processing_params['run_coverage_filter'] = False - post_processing_params['minimum_fold_change'] = None - post_processing_params['file_type'] = 'pVACbind' - post_processing_params['run_transcript_support_level_filter'] = False - post_processing_params['run_net_chop'] = False - post_processing_params['run_netmhc_stab'] = False - post_processing_params['run_manufacturability_metrics'] = False - post_processing_params['run_reference_proteome_similarity'] = False - post_processing_params["filename_addition"] = "Combined" + + combine_reports(combined_handoff.input_files, output_layout.combined_all_epitopes_file) + post_processing_params = combined_handoff.post_processing_params(args) PostProcessor(**post_processing_params).execute() @@ -56,7 +48,6 @@ def main(args_input = sys.argv[1:]): if args.n_threads > 1 and platform.system() == "Darwin": raise Exception("Multithreading is not supported on MacOS") - input_file_type = 'fasta' base_output_dir = os.path.abspath(args.output_dir) if (args.netmhciipan_version == '4.0' and args.iedb_install_directory is not None): @@ -66,39 +57,17 @@ def main(args_input = sys.argv[1:]): (class_i_prediction_algorithms, class_ii_prediction_algorithms) = split_algorithms(args.prediction_algorithms) alleles = combine_class_ii_alleles(args.allele) (class_i_alleles, class_ii_alleles, species) = split_alleles(alleles) - - shared_arguments = { - 'input_file' : args.input_file, - 'input_file_type' : input_file_type, - 'sample_name' : args.sample_name, - 'top_score_metric' : args.top_score_metric, - 'top_score_metric2' : args.top_score_metric2, - 'binding_threshold' : args.binding_threshold, - 'binding_percentile_threshold': args.binding_percentile_threshold, - 'immunogenicity_percentile_threshold': args.immunogenicity_percentile_threshold, - 'presentation_percentile_threshold': args.presentation_percentile_threshold, - 'percentile_threshold_strategy': args.percentile_threshold_strategy, - 'allele_specific_binding_thresholds': args.allele_specific_binding_thresholds, - 'net_chop_fasta' : args.input_file, - 'net_chop_method' : args.net_chop_method, - 'net_chop_threshold' : args.net_chop_threshold, - 'additional_report_columns' : args.additional_report_columns, - 'fasta_size' : args.fasta_size, - 'iedb_retries' : args.iedb_retries, - 'keep_tmp_files' : args.keep_tmp_files, - 'n_threads' : args.n_threads, - 'species' : species, - 'run_reference_proteome_similarity': args.run_reference_proteome_similarity, - 'blastp_path' : args.blastp_path, - 'blastp_db' : args.blastp_db, - 'problematic_amino_acids' : args.problematic_amino_acids, - 'run_post_processor' : True, - 'peptide_fasta' : args.peptide_fasta, - 'aggregate_inclusion_binding_threshold': args.aggregate_inclusion_binding_threshold, - 'aggregate_inclusion_count_limit': args.aggregate_inclusion_count_limit, - } - - if len(class_i_prediction_algorithms) > 0 and len(class_i_alleles) > 0: + output_layout = PvacbindOutputLayout(base_output_dir, args.sample_name) + run_plan = PvacbindRunPlan( + class_i_prediction_algorithms, + class_ii_prediction_algorithms, + class_i_alleles, + class_ii_alleles, + species, + ) + pipeline_handoffs = PvacbindPipelineHandoffBuilder(args, run_plan, output_layout) + + if run_plan.should_run_class_i(): if args.iedb_install_directory: iedb_mhc_i_executable = os.path.join(args.iedb_install_directory, 'mhc_i', 'src', 'predict_binding.py') if not os.path.exists(iedb_mhc_i_executable): @@ -112,27 +81,16 @@ def main(args_input = sys.argv[1:]): print("Executing MHC Class I predictions") - output_dir = os.path.join(base_output_dir, 'MHC_Class_I') + output_dir = output_layout.class_i_output_dir os.makedirs(output_dir, exist_ok=True) - class_i_arguments = shared_arguments.copy() - class_i_arguments['alleles'] = class_i_alleles - class_i_arguments['iedb_executable'] = iedb_mhc_i_executable - class_i_arguments['epitope_lengths'] = args.class_i_epitope_length - class_i_arguments['prediction_algorithms'] = class_i_prediction_algorithms - class_i_arguments['output_dir'] = output_dir - class_i_arguments['netmhc_stab'] = args.netmhc_stab - class_i_arguments['filename_addition'] = "MHC_I" - class_i_arguments['use_normalized_percentiles'] = args.use_normalized_percentiles - class_i_arguments['reference_scores_path'] = args.reference_scores_path + class_i_arguments = pipeline_handoffs.class_i_arguments(iedb_mhc_i_executable) pipeline = PvacbindPipeline(**class_i_arguments) pipeline.execute() - elif len(class_i_prediction_algorithms) == 0: - print("No MHC class I prediction algorithms chosen. Skipping MHC class I predictions.") - elif len(class_i_alleles) == 0: - print("No MHC class I alleles chosen. Skipping MHC class I predictions.") + else: + print(run_plan.class_i_skip_message()) - if len(class_ii_prediction_algorithms) > 0 and len(class_ii_alleles) > 0: + if run_plan.should_run_class_ii(): if args.iedb_install_directory: iedb_mhc_ii_executable = os.path.join(args.iedb_install_directory, 'mhc_ii', 'mhc_II_binding.py') if not os.path.exists(iedb_mhc_ii_executable): @@ -142,25 +100,16 @@ def main(args_input = sys.argv[1:]): print("Executing MHC Class II predictions") - output_dir = os.path.join(base_output_dir, 'MHC_Class_II') + output_dir = output_layout.class_ii_output_dir os.makedirs(output_dir, exist_ok=True) - class_ii_arguments = shared_arguments.copy() - class_ii_arguments['alleles'] = class_ii_alleles - class_ii_arguments['prediction_algorithms'] = class_ii_prediction_algorithms - class_ii_arguments['iedb_executable'] = iedb_mhc_ii_executable - class_ii_arguments['epitope_lengths'] = args.class_ii_epitope_length - class_ii_arguments['output_dir'] = output_dir - class_ii_arguments['netmhc_stab'] = False - class_ii_arguments['filename_addition'] = "MHC_II" + class_ii_arguments = pipeline_handoffs.class_ii_arguments(iedb_mhc_ii_executable) pipeline = PvacbindPipeline(**class_ii_arguments) pipeline.execute() - elif len(class_ii_prediction_algorithms) == 0: - print("No MHC class II prediction algorithms chosen. Skipping MHC class II predictions.") - elif len(class_ii_alleles) == 0: - print("No MHC class II alleles chosen. Skipping MHC class II predictions.") + else: + print(run_plan.class_ii_skip_message()) - if len(class_i_prediction_algorithms) > 0 and len(class_i_alleles) > 0 and len(class_ii_prediction_algorithms) > 0 and len(class_ii_alleles) > 0: + if run_plan.should_create_combined_reports(): print("Creating combined reports") create_combined_reports(base_output_dir, args)